@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,917 @@
1
+ import { and, asc, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
2
+ import { agents, assets, companies, companyMemberships, goals, heartbeatRuns, issueAttachments, issueLabels, issueComments, issues, labels, projectWorkspaces, projects, } from "@paperclipai/db";
3
+ import { extractProjectMentionIds } from "@paperclipai/shared";
4
+ import { conflict, notFound, unprocessable } from "../errors.js";
5
+ const ALL_ISSUE_STATUSES = ["backlog", "todo", "in_progress", "in_review", "blocked", "done", "cancelled"];
6
+ function assertTransition(from, to) {
7
+ if (from === to)
8
+ return;
9
+ if (!ALL_ISSUE_STATUSES.includes(to)) {
10
+ throw conflict(`Unknown issue status: ${to}`);
11
+ }
12
+ }
13
+ function applyStatusSideEffects(status, patch) {
14
+ if (!status)
15
+ return patch;
16
+ if (status === "in_progress" && !patch.startedAt) {
17
+ patch.startedAt = new Date();
18
+ }
19
+ if (status === "done") {
20
+ patch.completedAt = new Date();
21
+ }
22
+ if (status === "cancelled") {
23
+ patch.cancelledAt = new Date();
24
+ }
25
+ return patch;
26
+ }
27
+ function sameRunLock(checkoutRunId, actorRunId) {
28
+ if (actorRunId)
29
+ return checkoutRunId === actorRunId;
30
+ return checkoutRunId == null;
31
+ }
32
+ const TERMINAL_HEARTBEAT_RUN_STATUSES = new Set(["succeeded", "failed", "cancelled", "timed_out"]);
33
+ function escapeLikePattern(value) {
34
+ return value.replace(/[\\%_]/g, "\\$&");
35
+ }
36
+ async function labelMapForIssues(dbOrTx, issueIds) {
37
+ const map = new Map();
38
+ if (issueIds.length === 0)
39
+ return map;
40
+ const rows = await dbOrTx
41
+ .select({
42
+ issueId: issueLabels.issueId,
43
+ label: labels,
44
+ })
45
+ .from(issueLabels)
46
+ .innerJoin(labels, eq(issueLabels.labelId, labels.id))
47
+ .where(inArray(issueLabels.issueId, issueIds))
48
+ .orderBy(asc(labels.name), asc(labels.id));
49
+ for (const row of rows) {
50
+ const existing = map.get(row.issueId);
51
+ if (existing)
52
+ existing.push(row.label);
53
+ else
54
+ map.set(row.issueId, [row.label]);
55
+ }
56
+ return map;
57
+ }
58
+ async function withIssueLabels(dbOrTx, rows) {
59
+ if (rows.length === 0)
60
+ return [];
61
+ const labelsByIssueId = await labelMapForIssues(dbOrTx, rows.map((row) => row.id));
62
+ return rows.map((row) => {
63
+ const issueLabels = labelsByIssueId.get(row.id) ?? [];
64
+ return {
65
+ ...row,
66
+ labels: issueLabels,
67
+ labelIds: issueLabels.map((label) => label.id),
68
+ };
69
+ });
70
+ }
71
+ const ACTIVE_RUN_STATUSES = ["queued", "running"];
72
+ async function activeRunMapForIssues(dbOrTx, issueRows) {
73
+ const map = new Map();
74
+ const runIds = issueRows
75
+ .map((row) => row.executionRunId)
76
+ .filter((id) => id != null);
77
+ if (runIds.length === 0)
78
+ return map;
79
+ const rows = await dbOrTx
80
+ .select({
81
+ id: heartbeatRuns.id,
82
+ status: heartbeatRuns.status,
83
+ agentId: heartbeatRuns.agentId,
84
+ invocationSource: heartbeatRuns.invocationSource,
85
+ triggerDetail: heartbeatRuns.triggerDetail,
86
+ startedAt: heartbeatRuns.startedAt,
87
+ finishedAt: heartbeatRuns.finishedAt,
88
+ createdAt: heartbeatRuns.createdAt,
89
+ })
90
+ .from(heartbeatRuns)
91
+ .where(and(inArray(heartbeatRuns.id, runIds), inArray(heartbeatRuns.status, ACTIVE_RUN_STATUSES)));
92
+ for (const row of rows) {
93
+ map.set(row.id, row);
94
+ }
95
+ return map;
96
+ }
97
+ function withActiveRuns(issueRows, runMap) {
98
+ return issueRows.map((row) => ({
99
+ ...row,
100
+ activeRun: row.executionRunId ? (runMap.get(row.executionRunId) ?? null) : null,
101
+ }));
102
+ }
103
+ export function issueService(db) {
104
+ async function assertAssignableAgent(companyId, agentId) {
105
+ const assignee = await db
106
+ .select({
107
+ id: agents.id,
108
+ companyId: agents.companyId,
109
+ status: agents.status,
110
+ })
111
+ .from(agents)
112
+ .where(eq(agents.id, agentId))
113
+ .then((rows) => rows[0] ?? null);
114
+ if (!assignee)
115
+ throw notFound("Assignee agent not found");
116
+ if (assignee.companyId !== companyId) {
117
+ throw unprocessable("Assignee must belong to same company");
118
+ }
119
+ if (assignee.status === "pending_approval") {
120
+ throw conflict("Cannot assign work to pending approval agents");
121
+ }
122
+ if (assignee.status === "terminated") {
123
+ throw conflict("Cannot assign work to terminated agents");
124
+ }
125
+ }
126
+ async function assertAssignableUser(companyId, userId) {
127
+ const membership = await db
128
+ .select({ id: companyMemberships.id })
129
+ .from(companyMemberships)
130
+ .where(and(eq(companyMemberships.companyId, companyId), eq(companyMemberships.principalType, "user"), eq(companyMemberships.principalId, userId), eq(companyMemberships.status, "active")))
131
+ .then((rows) => rows[0] ?? null);
132
+ if (!membership) {
133
+ throw notFound("Assignee user not found");
134
+ }
135
+ }
136
+ async function assertValidLabelIds(companyId, labelIds, dbOrTx = db) {
137
+ if (labelIds.length === 0)
138
+ return;
139
+ const existing = await dbOrTx
140
+ .select({ id: labels.id })
141
+ .from(labels)
142
+ .where(and(eq(labels.companyId, companyId), inArray(labels.id, labelIds)));
143
+ if (existing.length !== new Set(labelIds).size) {
144
+ throw unprocessable("One or more labels are invalid for this company");
145
+ }
146
+ }
147
+ async function syncIssueLabels(issueId, companyId, labelIds, dbOrTx = db) {
148
+ const deduped = [...new Set(labelIds)];
149
+ await assertValidLabelIds(companyId, deduped, dbOrTx);
150
+ await dbOrTx.delete(issueLabels).where(eq(issueLabels.issueId, issueId));
151
+ if (deduped.length === 0)
152
+ return;
153
+ await dbOrTx.insert(issueLabels).values(deduped.map((labelId) => ({
154
+ issueId,
155
+ labelId,
156
+ companyId,
157
+ })));
158
+ }
159
+ async function isTerminalOrMissingHeartbeatRun(runId) {
160
+ const run = await db
161
+ .select({ status: heartbeatRuns.status })
162
+ .from(heartbeatRuns)
163
+ .where(eq(heartbeatRuns.id, runId))
164
+ .then((rows) => rows[0] ?? null);
165
+ if (!run)
166
+ return true;
167
+ return TERMINAL_HEARTBEAT_RUN_STATUSES.has(run.status);
168
+ }
169
+ async function adoptStaleCheckoutRun(input) {
170
+ const stale = await isTerminalOrMissingHeartbeatRun(input.expectedCheckoutRunId);
171
+ if (!stale)
172
+ return null;
173
+ const now = new Date();
174
+ const adopted = await db
175
+ .update(issues)
176
+ .set({
177
+ checkoutRunId: input.actorRunId,
178
+ executionRunId: input.actorRunId,
179
+ executionLockedAt: now,
180
+ updatedAt: now,
181
+ })
182
+ .where(and(eq(issues.id, input.issueId), eq(issues.status, "in_progress"), eq(issues.assigneeAgentId, input.actorAgentId), eq(issues.checkoutRunId, input.expectedCheckoutRunId)))
183
+ .returning({
184
+ id: issues.id,
185
+ status: issues.status,
186
+ assigneeAgentId: issues.assigneeAgentId,
187
+ checkoutRunId: issues.checkoutRunId,
188
+ executionRunId: issues.executionRunId,
189
+ })
190
+ .then((rows) => rows[0] ?? null);
191
+ return adopted;
192
+ }
193
+ return {
194
+ list: async (companyId, filters) => {
195
+ const conditions = [eq(issues.companyId, companyId)];
196
+ const rawSearch = filters?.q?.trim() ?? "";
197
+ const hasSearch = rawSearch.length > 0;
198
+ const escapedSearch = hasSearch ? escapeLikePattern(rawSearch) : "";
199
+ const startsWithPattern = `${escapedSearch}%`;
200
+ const containsPattern = `%${escapedSearch}%`;
201
+ const titleStartsWithMatch = sql `${issues.title} ILIKE ${startsWithPattern} ESCAPE '\\'`;
202
+ const titleContainsMatch = sql `${issues.title} ILIKE ${containsPattern} ESCAPE '\\'`;
203
+ const identifierStartsWithMatch = sql `${issues.identifier} ILIKE ${startsWithPattern} ESCAPE '\\'`;
204
+ const identifierContainsMatch = sql `${issues.identifier} ILIKE ${containsPattern} ESCAPE '\\'`;
205
+ const descriptionContainsMatch = sql `${issues.description} ILIKE ${containsPattern} ESCAPE '\\'`;
206
+ const commentContainsMatch = sql `
207
+ EXISTS (
208
+ SELECT 1
209
+ FROM ${issueComments}
210
+ WHERE ${issueComments.issueId} = ${issues.id}
211
+ AND ${issueComments.companyId} = ${companyId}
212
+ AND ${issueComments.body} ILIKE ${containsPattern} ESCAPE '\\'
213
+ )
214
+ `;
215
+ if (filters?.status) {
216
+ const statuses = filters.status.split(",").map((s) => s.trim());
217
+ conditions.push(statuses.length === 1 ? eq(issues.status, statuses[0]) : inArray(issues.status, statuses));
218
+ }
219
+ if (filters?.assigneeAgentId) {
220
+ conditions.push(eq(issues.assigneeAgentId, filters.assigneeAgentId));
221
+ }
222
+ if (filters?.assigneeUserId) {
223
+ conditions.push(eq(issues.assigneeUserId, filters.assigneeUserId));
224
+ }
225
+ if (filters?.projectId)
226
+ conditions.push(eq(issues.projectId, filters.projectId));
227
+ if (filters?.labelId) {
228
+ const labeledIssueIds = await db
229
+ .select({ issueId: issueLabels.issueId })
230
+ .from(issueLabels)
231
+ .where(and(eq(issueLabels.companyId, companyId), eq(issueLabels.labelId, filters.labelId)));
232
+ if (labeledIssueIds.length === 0)
233
+ return [];
234
+ conditions.push(inArray(issues.id, labeledIssueIds.map((row) => row.issueId)));
235
+ }
236
+ if (hasSearch) {
237
+ conditions.push(or(titleContainsMatch, identifierContainsMatch, descriptionContainsMatch, commentContainsMatch));
238
+ }
239
+ conditions.push(isNull(issues.hiddenAt));
240
+ const priorityOrder = sql `CASE ${issues.priority} WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END`;
241
+ const searchOrder = sql `
242
+ CASE
243
+ WHEN ${titleStartsWithMatch} THEN 0
244
+ WHEN ${titleContainsMatch} THEN 1
245
+ WHEN ${identifierStartsWithMatch} THEN 2
246
+ WHEN ${identifierContainsMatch} THEN 3
247
+ WHEN ${descriptionContainsMatch} THEN 4
248
+ WHEN ${commentContainsMatch} THEN 5
249
+ ELSE 6
250
+ END
251
+ `;
252
+ const rows = await db
253
+ .select()
254
+ .from(issues)
255
+ .where(and(...conditions))
256
+ .orderBy(hasSearch ? asc(searchOrder) : asc(priorityOrder), asc(priorityOrder), desc(issues.updatedAt));
257
+ const withLabels = await withIssueLabels(db, rows);
258
+ const runMap = await activeRunMapForIssues(db, withLabels);
259
+ return withActiveRuns(withLabels, runMap);
260
+ },
261
+ getById: async (id) => {
262
+ const row = await db
263
+ .select()
264
+ .from(issues)
265
+ .where(eq(issues.id, id))
266
+ .then((rows) => rows[0] ?? null);
267
+ if (!row)
268
+ return null;
269
+ const [enriched] = await withIssueLabels(db, [row]);
270
+ return enriched;
271
+ },
272
+ getByIdentifier: async (identifier) => {
273
+ const row = await db
274
+ .select()
275
+ .from(issues)
276
+ .where(eq(issues.identifier, identifier.toUpperCase()))
277
+ .then((rows) => rows[0] ?? null);
278
+ if (!row)
279
+ return null;
280
+ const [enriched] = await withIssueLabels(db, [row]);
281
+ return enriched;
282
+ },
283
+ create: async (companyId, data) => {
284
+ const { labelIds: inputLabelIds, ...issueData } = data;
285
+ if (data.assigneeAgentId && data.assigneeUserId) {
286
+ throw unprocessable("Issue can only have one assignee");
287
+ }
288
+ if (data.assigneeAgentId) {
289
+ await assertAssignableAgent(companyId, data.assigneeAgentId);
290
+ }
291
+ if (data.assigneeUserId) {
292
+ await assertAssignableUser(companyId, data.assigneeUserId);
293
+ }
294
+ if (data.status === "in_progress" && !data.assigneeAgentId && !data.assigneeUserId) {
295
+ throw unprocessable("in_progress issues require an assignee");
296
+ }
297
+ return db.transaction(async (tx) => {
298
+ const [company] = await tx
299
+ .update(companies)
300
+ .set({ issueCounter: sql `${companies.issueCounter} + 1` })
301
+ .where(eq(companies.id, companyId))
302
+ .returning({ issueCounter: companies.issueCounter, issuePrefix: companies.issuePrefix });
303
+ const issueNumber = company.issueCounter;
304
+ const identifier = `${company.issuePrefix}-${issueNumber}`;
305
+ const values = { ...issueData, companyId, issueNumber, identifier };
306
+ if (values.status === "in_progress" && !values.startedAt) {
307
+ values.startedAt = new Date();
308
+ }
309
+ if (values.status === "done") {
310
+ values.completedAt = new Date();
311
+ }
312
+ if (values.status === "cancelled") {
313
+ values.cancelledAt = new Date();
314
+ }
315
+ const [issue] = await tx.insert(issues).values(values).returning();
316
+ if (inputLabelIds) {
317
+ await syncIssueLabels(issue.id, companyId, inputLabelIds, tx);
318
+ }
319
+ const [enriched] = await withIssueLabels(tx, [issue]);
320
+ return enriched;
321
+ });
322
+ },
323
+ update: async (id, data) => {
324
+ const existing = await db
325
+ .select()
326
+ .from(issues)
327
+ .where(eq(issues.id, id))
328
+ .then((rows) => rows[0] ?? null);
329
+ if (!existing)
330
+ return null;
331
+ const { labelIds: nextLabelIds, ...issueData } = data;
332
+ if (issueData.status) {
333
+ assertTransition(existing.status, issueData.status);
334
+ }
335
+ const patch = {
336
+ ...issueData,
337
+ updatedAt: new Date(),
338
+ };
339
+ const nextAssigneeAgentId = issueData.assigneeAgentId !== undefined ? issueData.assigneeAgentId : existing.assigneeAgentId;
340
+ const nextAssigneeUserId = issueData.assigneeUserId !== undefined ? issueData.assigneeUserId : existing.assigneeUserId;
341
+ if (nextAssigneeAgentId && nextAssigneeUserId) {
342
+ throw unprocessable("Issue can only have one assignee");
343
+ }
344
+ if (patch.status === "in_progress" && !nextAssigneeAgentId && !nextAssigneeUserId) {
345
+ throw unprocessable("in_progress issues require an assignee");
346
+ }
347
+ if (issueData.assigneeAgentId) {
348
+ await assertAssignableAgent(existing.companyId, issueData.assigneeAgentId);
349
+ }
350
+ if (issueData.assigneeUserId) {
351
+ await assertAssignableUser(existing.companyId, issueData.assigneeUserId);
352
+ }
353
+ applyStatusSideEffects(issueData.status, patch);
354
+ if (issueData.status && issueData.status !== "done") {
355
+ patch.completedAt = null;
356
+ }
357
+ if (issueData.status && issueData.status !== "cancelled") {
358
+ patch.cancelledAt = null;
359
+ }
360
+ if (issueData.status && issueData.status !== "in_progress") {
361
+ patch.checkoutRunId = null;
362
+ }
363
+ if ((issueData.assigneeAgentId !== undefined && issueData.assigneeAgentId !== existing.assigneeAgentId) ||
364
+ (issueData.assigneeUserId !== undefined && issueData.assigneeUserId !== existing.assigneeUserId)) {
365
+ patch.checkoutRunId = null;
366
+ }
367
+ return db.transaction(async (tx) => {
368
+ const updated = await tx
369
+ .update(issues)
370
+ .set(patch)
371
+ .where(eq(issues.id, id))
372
+ .returning()
373
+ .then((rows) => rows[0] ?? null);
374
+ if (!updated)
375
+ return null;
376
+ if (nextLabelIds !== undefined) {
377
+ await syncIssueLabels(updated.id, existing.companyId, nextLabelIds, tx);
378
+ }
379
+ const [enriched] = await withIssueLabels(tx, [updated]);
380
+ return enriched;
381
+ });
382
+ },
383
+ remove: (id) => db.transaction(async (tx) => {
384
+ const attachmentAssetIds = await tx
385
+ .select({ assetId: issueAttachments.assetId })
386
+ .from(issueAttachments)
387
+ .where(eq(issueAttachments.issueId, id));
388
+ const removedIssue = await tx
389
+ .delete(issues)
390
+ .where(eq(issues.id, id))
391
+ .returning()
392
+ .then((rows) => rows[0] ?? null);
393
+ if (removedIssue && attachmentAssetIds.length > 0) {
394
+ await tx
395
+ .delete(assets)
396
+ .where(inArray(assets.id, attachmentAssetIds.map((row) => row.assetId)));
397
+ }
398
+ if (!removedIssue)
399
+ return null;
400
+ const [enriched] = await withIssueLabels(tx, [removedIssue]);
401
+ return enriched;
402
+ }),
403
+ checkout: async (id, agentId, expectedStatuses, checkoutRunId) => {
404
+ const issueCompany = await db
405
+ .select({ companyId: issues.companyId })
406
+ .from(issues)
407
+ .where(eq(issues.id, id))
408
+ .then((rows) => rows[0] ?? null);
409
+ if (!issueCompany)
410
+ throw notFound("Issue not found");
411
+ await assertAssignableAgent(issueCompany.companyId, agentId);
412
+ const now = new Date();
413
+ const sameRunAssigneeCondition = checkoutRunId
414
+ ? and(eq(issues.assigneeAgentId, agentId), or(isNull(issues.checkoutRunId), eq(issues.checkoutRunId, checkoutRunId)))
415
+ : and(eq(issues.assigneeAgentId, agentId), isNull(issues.checkoutRunId));
416
+ const executionLockCondition = checkoutRunId
417
+ ? or(isNull(issues.executionRunId), eq(issues.executionRunId, checkoutRunId))
418
+ : isNull(issues.executionRunId);
419
+ const updated = await db
420
+ .update(issues)
421
+ .set({
422
+ assigneeAgentId: agentId,
423
+ assigneeUserId: null,
424
+ checkoutRunId,
425
+ executionRunId: checkoutRunId,
426
+ status: "in_progress",
427
+ startedAt: now,
428
+ updatedAt: now,
429
+ })
430
+ .where(and(eq(issues.id, id), inArray(issues.status, expectedStatuses), or(isNull(issues.assigneeAgentId), sameRunAssigneeCondition), executionLockCondition))
431
+ .returning()
432
+ .then((rows) => rows[0] ?? null);
433
+ if (updated) {
434
+ const [enriched] = await withIssueLabels(db, [updated]);
435
+ return enriched;
436
+ }
437
+ const current = await db
438
+ .select({
439
+ id: issues.id,
440
+ status: issues.status,
441
+ assigneeAgentId: issues.assigneeAgentId,
442
+ checkoutRunId: issues.checkoutRunId,
443
+ executionRunId: issues.executionRunId,
444
+ })
445
+ .from(issues)
446
+ .where(eq(issues.id, id))
447
+ .then((rows) => rows[0] ?? null);
448
+ if (!current)
449
+ throw notFound("Issue not found");
450
+ if (current.assigneeAgentId === agentId &&
451
+ current.status === "in_progress" &&
452
+ current.checkoutRunId == null &&
453
+ (current.executionRunId == null || current.executionRunId === checkoutRunId) &&
454
+ checkoutRunId) {
455
+ const adopted = await db
456
+ .update(issues)
457
+ .set({
458
+ checkoutRunId,
459
+ executionRunId: checkoutRunId,
460
+ updatedAt: new Date(),
461
+ })
462
+ .where(and(eq(issues.id, id), eq(issues.status, "in_progress"), eq(issues.assigneeAgentId, agentId), isNull(issues.checkoutRunId), or(isNull(issues.executionRunId), eq(issues.executionRunId, checkoutRunId))))
463
+ .returning()
464
+ .then((rows) => rows[0] ?? null);
465
+ if (adopted)
466
+ return adopted;
467
+ }
468
+ if (checkoutRunId &&
469
+ current.assigneeAgentId === agentId &&
470
+ current.status === "in_progress" &&
471
+ current.checkoutRunId &&
472
+ current.checkoutRunId !== checkoutRunId) {
473
+ const adopted = await adoptStaleCheckoutRun({
474
+ issueId: id,
475
+ actorAgentId: agentId,
476
+ actorRunId: checkoutRunId,
477
+ expectedCheckoutRunId: current.checkoutRunId,
478
+ });
479
+ if (adopted) {
480
+ const row = await db.select().from(issues).where(eq(issues.id, id)).then((rows) => rows[0]);
481
+ const [enriched] = await withIssueLabels(db, [row]);
482
+ return enriched;
483
+ }
484
+ }
485
+ // If this run already owns it and it's in_progress, return it (no self-409)
486
+ if (current.assigneeAgentId === agentId &&
487
+ current.status === "in_progress" &&
488
+ sameRunLock(current.checkoutRunId, checkoutRunId)) {
489
+ const row = await db.select().from(issues).where(eq(issues.id, id)).then((rows) => rows[0]);
490
+ const [enriched] = await withIssueLabels(db, [row]);
491
+ return enriched;
492
+ }
493
+ throw conflict("Issue checkout conflict", {
494
+ issueId: current.id,
495
+ status: current.status,
496
+ assigneeAgentId: current.assigneeAgentId,
497
+ checkoutRunId: current.checkoutRunId,
498
+ executionRunId: current.executionRunId,
499
+ });
500
+ },
501
+ assertCheckoutOwner: async (id, actorAgentId, actorRunId) => {
502
+ const current = await db
503
+ .select({
504
+ id: issues.id,
505
+ status: issues.status,
506
+ assigneeAgentId: issues.assigneeAgentId,
507
+ checkoutRunId: issues.checkoutRunId,
508
+ })
509
+ .from(issues)
510
+ .where(eq(issues.id, id))
511
+ .then((rows) => rows[0] ?? null);
512
+ if (!current)
513
+ throw notFound("Issue not found");
514
+ if (current.status === "in_progress" &&
515
+ current.assigneeAgentId === actorAgentId &&
516
+ sameRunLock(current.checkoutRunId, actorRunId)) {
517
+ return { ...current, adoptedFromRunId: null };
518
+ }
519
+ if (actorRunId &&
520
+ current.status === "in_progress" &&
521
+ current.assigneeAgentId === actorAgentId &&
522
+ current.checkoutRunId &&
523
+ current.checkoutRunId !== actorRunId) {
524
+ const adopted = await adoptStaleCheckoutRun({
525
+ issueId: id,
526
+ actorAgentId,
527
+ actorRunId,
528
+ expectedCheckoutRunId: current.checkoutRunId,
529
+ });
530
+ if (adopted) {
531
+ return {
532
+ ...adopted,
533
+ adoptedFromRunId: current.checkoutRunId,
534
+ };
535
+ }
536
+ }
537
+ throw conflict("Issue run ownership conflict", {
538
+ issueId: current.id,
539
+ status: current.status,
540
+ assigneeAgentId: current.assigneeAgentId,
541
+ checkoutRunId: current.checkoutRunId,
542
+ actorAgentId,
543
+ actorRunId,
544
+ });
545
+ },
546
+ release: async (id, actorAgentId, actorRunId) => {
547
+ const existing = await db
548
+ .select()
549
+ .from(issues)
550
+ .where(eq(issues.id, id))
551
+ .then((rows) => rows[0] ?? null);
552
+ if (!existing)
553
+ return null;
554
+ if (actorAgentId && existing.assigneeAgentId && existing.assigneeAgentId !== actorAgentId) {
555
+ throw conflict("Only assignee can release issue");
556
+ }
557
+ if (actorAgentId &&
558
+ existing.status === "in_progress" &&
559
+ existing.assigneeAgentId === actorAgentId &&
560
+ existing.checkoutRunId &&
561
+ !sameRunLock(existing.checkoutRunId, actorRunId ?? null)) {
562
+ throw conflict("Only checkout run can release issue", {
563
+ issueId: existing.id,
564
+ assigneeAgentId: existing.assigneeAgentId,
565
+ checkoutRunId: existing.checkoutRunId,
566
+ actorRunId: actorRunId ?? null,
567
+ });
568
+ }
569
+ const updated = await db
570
+ .update(issues)
571
+ .set({
572
+ status: "todo",
573
+ assigneeAgentId: null,
574
+ checkoutRunId: null,
575
+ updatedAt: new Date(),
576
+ })
577
+ .where(eq(issues.id, id))
578
+ .returning()
579
+ .then((rows) => rows[0] ?? null);
580
+ if (!updated)
581
+ return null;
582
+ const [enriched] = await withIssueLabels(db, [updated]);
583
+ return enriched;
584
+ },
585
+ listLabels: (companyId) => db.select().from(labels).where(eq(labels.companyId, companyId)).orderBy(asc(labels.name), asc(labels.id)),
586
+ getLabelById: (id) => db
587
+ .select()
588
+ .from(labels)
589
+ .where(eq(labels.id, id))
590
+ .then((rows) => rows[0] ?? null),
591
+ createLabel: async (companyId, data) => {
592
+ const [created] = await db
593
+ .insert(labels)
594
+ .values({
595
+ companyId,
596
+ name: data.name.trim(),
597
+ color: data.color,
598
+ })
599
+ .returning();
600
+ return created;
601
+ },
602
+ deleteLabel: async (id) => db
603
+ .delete(labels)
604
+ .where(eq(labels.id, id))
605
+ .returning()
606
+ .then((rows) => rows[0] ?? null),
607
+ listComments: (issueId) => db
608
+ .select()
609
+ .from(issueComments)
610
+ .where(eq(issueComments.issueId, issueId))
611
+ .orderBy(desc(issueComments.createdAt)),
612
+ addComment: async (issueId, body, actor) => {
613
+ const issue = await db
614
+ .select({ companyId: issues.companyId })
615
+ .from(issues)
616
+ .where(eq(issues.id, issueId))
617
+ .then((rows) => rows[0] ?? null);
618
+ if (!issue)
619
+ throw notFound("Issue not found");
620
+ const [comment] = await db
621
+ .insert(issueComments)
622
+ .values({
623
+ companyId: issue.companyId,
624
+ issueId,
625
+ authorAgentId: actor.agentId ?? null,
626
+ authorUserId: actor.userId ?? null,
627
+ body,
628
+ })
629
+ .returning();
630
+ // Update issue's updatedAt so comment activity is reflected in recency sorting
631
+ await db
632
+ .update(issues)
633
+ .set({ updatedAt: new Date() })
634
+ .where(eq(issues.id, issueId));
635
+ return comment;
636
+ },
637
+ createAttachment: async (input) => {
638
+ const issue = await db
639
+ .select({ id: issues.id, companyId: issues.companyId })
640
+ .from(issues)
641
+ .where(eq(issues.id, input.issueId))
642
+ .then((rows) => rows[0] ?? null);
643
+ if (!issue)
644
+ throw notFound("Issue not found");
645
+ if (input.issueCommentId) {
646
+ const comment = await db
647
+ .select({ id: issueComments.id, companyId: issueComments.companyId, issueId: issueComments.issueId })
648
+ .from(issueComments)
649
+ .where(eq(issueComments.id, input.issueCommentId))
650
+ .then((rows) => rows[0] ?? null);
651
+ if (!comment)
652
+ throw notFound("Issue comment not found");
653
+ if (comment.companyId !== issue.companyId || comment.issueId !== issue.id) {
654
+ throw unprocessable("Attachment comment must belong to same issue and company");
655
+ }
656
+ }
657
+ return db.transaction(async (tx) => {
658
+ const [asset] = await tx
659
+ .insert(assets)
660
+ .values({
661
+ companyId: issue.companyId,
662
+ provider: input.provider,
663
+ objectKey: input.objectKey,
664
+ contentType: input.contentType,
665
+ byteSize: input.byteSize,
666
+ sha256: input.sha256,
667
+ originalFilename: input.originalFilename ?? null,
668
+ createdByAgentId: input.createdByAgentId ?? null,
669
+ createdByUserId: input.createdByUserId ?? null,
670
+ })
671
+ .returning();
672
+ const [attachment] = await tx
673
+ .insert(issueAttachments)
674
+ .values({
675
+ companyId: issue.companyId,
676
+ issueId: issue.id,
677
+ assetId: asset.id,
678
+ issueCommentId: input.issueCommentId ?? null,
679
+ })
680
+ .returning();
681
+ return {
682
+ id: attachment.id,
683
+ companyId: attachment.companyId,
684
+ issueId: attachment.issueId,
685
+ issueCommentId: attachment.issueCommentId,
686
+ assetId: attachment.assetId,
687
+ provider: asset.provider,
688
+ objectKey: asset.objectKey,
689
+ contentType: asset.contentType,
690
+ byteSize: asset.byteSize,
691
+ sha256: asset.sha256,
692
+ originalFilename: asset.originalFilename,
693
+ createdByAgentId: asset.createdByAgentId,
694
+ createdByUserId: asset.createdByUserId,
695
+ createdAt: attachment.createdAt,
696
+ updatedAt: attachment.updatedAt,
697
+ };
698
+ });
699
+ },
700
+ listAttachments: async (issueId) => db
701
+ .select({
702
+ id: issueAttachments.id,
703
+ companyId: issueAttachments.companyId,
704
+ issueId: issueAttachments.issueId,
705
+ issueCommentId: issueAttachments.issueCommentId,
706
+ assetId: issueAttachments.assetId,
707
+ provider: assets.provider,
708
+ objectKey: assets.objectKey,
709
+ contentType: assets.contentType,
710
+ byteSize: assets.byteSize,
711
+ sha256: assets.sha256,
712
+ originalFilename: assets.originalFilename,
713
+ createdByAgentId: assets.createdByAgentId,
714
+ createdByUserId: assets.createdByUserId,
715
+ createdAt: issueAttachments.createdAt,
716
+ updatedAt: issueAttachments.updatedAt,
717
+ })
718
+ .from(issueAttachments)
719
+ .innerJoin(assets, eq(issueAttachments.assetId, assets.id))
720
+ .where(eq(issueAttachments.issueId, issueId))
721
+ .orderBy(desc(issueAttachments.createdAt)),
722
+ getAttachmentById: async (id) => db
723
+ .select({
724
+ id: issueAttachments.id,
725
+ companyId: issueAttachments.companyId,
726
+ issueId: issueAttachments.issueId,
727
+ issueCommentId: issueAttachments.issueCommentId,
728
+ assetId: issueAttachments.assetId,
729
+ provider: assets.provider,
730
+ objectKey: assets.objectKey,
731
+ contentType: assets.contentType,
732
+ byteSize: assets.byteSize,
733
+ sha256: assets.sha256,
734
+ originalFilename: assets.originalFilename,
735
+ createdByAgentId: assets.createdByAgentId,
736
+ createdByUserId: assets.createdByUserId,
737
+ createdAt: issueAttachments.createdAt,
738
+ updatedAt: issueAttachments.updatedAt,
739
+ })
740
+ .from(issueAttachments)
741
+ .innerJoin(assets, eq(issueAttachments.assetId, assets.id))
742
+ .where(eq(issueAttachments.id, id))
743
+ .then((rows) => rows[0] ?? null),
744
+ removeAttachment: async (id) => db.transaction(async (tx) => {
745
+ const existing = await tx
746
+ .select({
747
+ id: issueAttachments.id,
748
+ companyId: issueAttachments.companyId,
749
+ issueId: issueAttachments.issueId,
750
+ issueCommentId: issueAttachments.issueCommentId,
751
+ assetId: issueAttachments.assetId,
752
+ provider: assets.provider,
753
+ objectKey: assets.objectKey,
754
+ contentType: assets.contentType,
755
+ byteSize: assets.byteSize,
756
+ sha256: assets.sha256,
757
+ originalFilename: assets.originalFilename,
758
+ createdByAgentId: assets.createdByAgentId,
759
+ createdByUserId: assets.createdByUserId,
760
+ createdAt: issueAttachments.createdAt,
761
+ updatedAt: issueAttachments.updatedAt,
762
+ })
763
+ .from(issueAttachments)
764
+ .innerJoin(assets, eq(issueAttachments.assetId, assets.id))
765
+ .where(eq(issueAttachments.id, id))
766
+ .then((rows) => rows[0] ?? null);
767
+ if (!existing)
768
+ return null;
769
+ await tx.delete(issueAttachments).where(eq(issueAttachments.id, id));
770
+ await tx.delete(assets).where(eq(assets.id, existing.assetId));
771
+ return existing;
772
+ }),
773
+ findMentionedAgents: async (companyId, body) => {
774
+ const re = /\B@([^\s@,!?.]+)/g;
775
+ const tokens = new Set();
776
+ let m;
777
+ while ((m = re.exec(body)) !== null)
778
+ tokens.add(m[1].toLowerCase());
779
+ if (tokens.size === 0)
780
+ return [];
781
+ const rows = await db.select({ id: agents.id, name: agents.name })
782
+ .from(agents).where(eq(agents.companyId, companyId));
783
+ return rows.filter(a => tokens.has(a.name.toLowerCase())).map(a => a.id);
784
+ },
785
+ findMentionedProjectIds: async (issueId) => {
786
+ const issue = await db
787
+ .select({
788
+ companyId: issues.companyId,
789
+ title: issues.title,
790
+ description: issues.description,
791
+ })
792
+ .from(issues)
793
+ .where(eq(issues.id, issueId))
794
+ .then((rows) => rows[0] ?? null);
795
+ if (!issue)
796
+ return [];
797
+ const comments = await db
798
+ .select({ body: issueComments.body })
799
+ .from(issueComments)
800
+ .where(eq(issueComments.issueId, issueId));
801
+ const mentionedIds = new Set();
802
+ for (const source of [
803
+ issue.title,
804
+ issue.description ?? "",
805
+ ...comments.map((comment) => comment.body),
806
+ ]) {
807
+ for (const projectId of extractProjectMentionIds(source)) {
808
+ mentionedIds.add(projectId);
809
+ }
810
+ }
811
+ if (mentionedIds.size === 0)
812
+ return [];
813
+ const rows = await db
814
+ .select({ id: projects.id })
815
+ .from(projects)
816
+ .where(and(eq(projects.companyId, issue.companyId), inArray(projects.id, [...mentionedIds])));
817
+ const valid = new Set(rows.map((row) => row.id));
818
+ return [...mentionedIds].filter((projectId) => valid.has(projectId));
819
+ },
820
+ getAncestors: async (issueId) => {
821
+ const raw = [];
822
+ const visited = new Set([issueId]);
823
+ const start = await db.select().from(issues).where(eq(issues.id, issueId)).then(r => r[0] ?? null);
824
+ let currentId = start?.parentId ?? null;
825
+ while (currentId && !visited.has(currentId) && raw.length < 50) {
826
+ visited.add(currentId);
827
+ const parent = await db.select({
828
+ id: issues.id, identifier: issues.identifier, title: issues.title, description: issues.description,
829
+ status: issues.status, priority: issues.priority,
830
+ assigneeAgentId: issues.assigneeAgentId, projectId: issues.projectId,
831
+ goalId: issues.goalId, parentId: issues.parentId,
832
+ }).from(issues).where(eq(issues.id, currentId)).then(r => r[0] ?? null);
833
+ if (!parent)
834
+ break;
835
+ raw.push({
836
+ id: parent.id, identifier: parent.identifier ?? null, title: parent.title, description: parent.description ?? null,
837
+ status: parent.status, priority: parent.priority,
838
+ assigneeAgentId: parent.assigneeAgentId ?? null,
839
+ projectId: parent.projectId ?? null, goalId: parent.goalId ?? null,
840
+ });
841
+ currentId = parent.parentId ?? null;
842
+ }
843
+ // Batch-fetch referenced projects and goals
844
+ const projectIds = [...new Set(raw.map(a => a.projectId).filter((id) => id != null))];
845
+ const goalIds = [...new Set(raw.map(a => a.goalId).filter((id) => id != null))];
846
+ const projectMap = new Map();
847
+ const goalMap = new Map();
848
+ if (projectIds.length > 0) {
849
+ const workspaceRows = await db
850
+ .select()
851
+ .from(projectWorkspaces)
852
+ .where(inArray(projectWorkspaces.projectId, projectIds))
853
+ .orderBy(desc(projectWorkspaces.isPrimary), asc(projectWorkspaces.createdAt), asc(projectWorkspaces.id));
854
+ const workspaceMap = new Map();
855
+ for (const workspace of workspaceRows) {
856
+ const existing = workspaceMap.get(workspace.projectId);
857
+ if (existing)
858
+ existing.push(workspace);
859
+ else
860
+ workspaceMap.set(workspace.projectId, [workspace]);
861
+ }
862
+ const rows = await db.select({
863
+ id: projects.id, name: projects.name, description: projects.description,
864
+ status: projects.status, goalId: projects.goalId,
865
+ }).from(projects).where(inArray(projects.id, projectIds));
866
+ for (const r of rows) {
867
+ const projectWorkspaceRows = workspaceMap.get(r.id) ?? [];
868
+ const workspaces = projectWorkspaceRows.map((workspace) => ({
869
+ id: workspace.id,
870
+ companyId: workspace.companyId,
871
+ projectId: workspace.projectId,
872
+ name: workspace.name,
873
+ cwd: workspace.cwd,
874
+ repoUrl: workspace.repoUrl ?? null,
875
+ repoRef: workspace.repoRef ?? null,
876
+ metadata: workspace.metadata ?? null,
877
+ isPrimary: workspace.isPrimary,
878
+ createdAt: workspace.createdAt,
879
+ updatedAt: workspace.updatedAt,
880
+ }));
881
+ const primaryWorkspace = workspaces.find((workspace) => workspace.isPrimary) ?? workspaces[0] ?? null;
882
+ projectMap.set(r.id, {
883
+ ...r,
884
+ workspaces,
885
+ primaryWorkspace,
886
+ });
887
+ // Also collect goalIds from projects
888
+ if (r.goalId && !goalIds.includes(r.goalId))
889
+ goalIds.push(r.goalId);
890
+ }
891
+ }
892
+ if (goalIds.length > 0) {
893
+ const rows = await db.select({
894
+ id: goals.id, title: goals.title, description: goals.description,
895
+ level: goals.level, status: goals.status,
896
+ }).from(goals).where(inArray(goals.id, goalIds));
897
+ for (const r of rows)
898
+ goalMap.set(r.id, r);
899
+ }
900
+ return raw.map(a => ({
901
+ ...a,
902
+ project: a.projectId ? projectMap.get(a.projectId) ?? null : null,
903
+ goal: a.goalId ? goalMap.get(a.goalId) ?? null : null,
904
+ }));
905
+ },
906
+ staleCount: async (companyId, minutes = 60) => {
907
+ const cutoff = new Date(Date.now() - minutes * 60 * 1000);
908
+ const result = await db
909
+ .select({ count: sql `count(*)` })
910
+ .from(issues)
911
+ .where(and(eq(issues.companyId, companyId), eq(issues.status, "in_progress"), isNull(issues.hiddenAt), sql `${issues.startedAt} < ${cutoff.toISOString()}`))
912
+ .then((rows) => rows[0]);
913
+ return Number(result?.count ?? 0);
914
+ },
915
+ };
916
+ }
917
+ //# sourceMappingURL=issues.js.map