@straiffi/archon 1.7.2 → 1.7.4

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 (176) hide show
  1. package/dist/client/assets/ActivityView-BgMz3NNT.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-CNqWDS3E.js +1 -0
  3. package/dist/client/assets/TestsDialog-Cll7xJe0.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
  7. package/dist/client/assets/channel-CqNHF3FD.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.js → chunk-WU5MYG2G-DrVJWKVR.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DvF75yLZ.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.js → gitGraphDiagram-PVQCEYII-CY0tHL5N.js} +1 -1
  33. package/dist/client/assets/index-CJRgmWwY.js +273 -0
  34. package/dist/client/assets/index-mRkJWu70.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-gm51u2Nn.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
  39. package/dist/client/assets/{line-Bert22-_.js → line-PHHbm9rj.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-CLxAkW69.js} +1 -1
  41. package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-CebvSmx9.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.js → stateDiagram-AJRCARHV-B995qt1S.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BnCBVqcd.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +472 -39
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +1311 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +228 -71
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentRuns.js +205 -38
  62. package/dist/server/lib/agentRuns.js.map +1 -1
  63. package/dist/server/lib/attachments.js +24 -0
  64. package/dist/server/lib/attachments.js.map +1 -1
  65. package/dist/server/lib/automationProgression.js +464 -0
  66. package/dist/server/lib/automationProgression.js.map +1 -0
  67. package/dist/server/lib/automationReconciliation.js +498 -0
  68. package/dist/server/lib/automationReconciliation.js.map +1 -0
  69. package/dist/server/lib/automationStageDetails.js +232 -0
  70. package/dist/server/lib/automationStageDetails.js.map +1 -0
  71. package/dist/server/lib/buildOpenQuestionOperations.js +238 -0
  72. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
  73. package/dist/server/lib/buildOpenQuestions.js +44 -0
  74. package/dist/server/lib/buildOpenQuestions.js.map +1 -1
  75. package/dist/server/lib/buildSequences.js +130 -46
  76. package/dist/server/lib/buildSequences.js.map +1 -1
  77. package/dist/server/lib/bundleReviewOperations.js +91 -9
  78. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  79. package/dist/server/lib/chatOperations.js +67 -12
  80. package/dist/server/lib/chatOperations.js.map +1 -1
  81. package/dist/server/lib/executionReconciliation.js +518 -0
  82. package/dist/server/lib/executionReconciliation.js.map +1 -0
  83. package/dist/server/lib/executionRetry.js +694 -0
  84. package/dist/server/lib/executionRetry.js.map +1 -0
  85. package/dist/server/lib/executionRetryContext.js +489 -0
  86. package/dist/server/lib/executionRetryContext.js.map +1 -0
  87. package/dist/server/lib/executionRetryPredicates.js +74 -0
  88. package/dist/server/lib/executionRetryPredicates.js.map +1 -0
  89. package/dist/server/lib/executionRunRetention.js +141 -0
  90. package/dist/server/lib/executionRunRetention.js.map +1 -0
  91. package/dist/server/lib/executionRuns.js +1367 -0
  92. package/dist/server/lib/executionRuns.js.map +1 -0
  93. package/dist/server/lib/executionTimeline.js +45 -0
  94. package/dist/server/lib/executionTimeline.js.map +1 -0
  95. package/dist/server/lib/git.js +385 -6
  96. package/dist/server/lib/git.js.map +1 -1
  97. package/dist/server/lib/integrations/github.js +43 -0
  98. package/dist/server/lib/integrations/github.js.map +1 -1
  99. package/dist/server/lib/integrations/githubActions.js +1 -0
  100. package/dist/server/lib/integrations/githubActions.js.map +1 -1
  101. package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
  102. package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
  103. package/dist/server/lib/projectMemorySelections.js +7 -3
  104. package/dist/server/lib/projectMemorySelections.js.map +1 -1
  105. package/dist/server/lib/shutdown.js +2 -2
  106. package/dist/server/lib/shutdown.js.map +1 -1
  107. package/dist/server/lib/specArtifact.js +3 -0
  108. package/dist/server/lib/specArtifact.js.map +1 -1
  109. package/dist/server/lib/specFormatting.js +1 -1
  110. package/dist/server/lib/specFormatting.js.map +1 -1
  111. package/dist/server/lib/specOperations.js +78 -12
  112. package/dist/server/lib/specOperations.js.map +1 -1
  113. package/dist/server/lib/specReviewContext.js +54 -18
  114. package/dist/server/lib/specReviewContext.js.map +1 -1
  115. package/dist/server/lib/specReviewDraft.js +3 -2
  116. package/dist/server/lib/specReviewDraft.js.map +1 -1
  117. package/dist/server/lib/specReviewOperations.js +74 -9
  118. package/dist/server/lib/specReviewOperations.js.map +1 -1
  119. package/dist/server/lib/specReviewValidation.js +217 -0
  120. package/dist/server/lib/specReviewValidation.js.map +1 -0
  121. package/dist/server/lib/specReviews.js +30 -2
  122. package/dist/server/lib/specReviews.js.map +1 -1
  123. package/dist/server/lib/specs.js +18 -1
  124. package/dist/server/lib/specs.js.map +1 -1
  125. package/dist/server/lib/stageSnapshots.js +197 -0
  126. package/dist/server/lib/stageSnapshots.js.map +1 -0
  127. package/dist/server/lib/testSessions.js +160 -18
  128. package/dist/server/lib/testSessions.js.map +1 -1
  129. package/dist/server/lib/ticketAutomation.js +80 -4
  130. package/dist/server/lib/ticketAutomation.js.map +1 -1
  131. package/dist/server/lib/ticketAutomationCommit.js +466 -91
  132. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  133. package/dist/server/lib/ticketFollowUpOperations.js +270 -29
  134. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  135. package/dist/server/lib/ticketMessages.js +16 -0
  136. package/dist/server/lib/ticketMessages.js.map +1 -1
  137. package/dist/server/lib/ticketRunOperations.js +51 -6
  138. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  139. package/dist/server/lib/ticketUndo.js +77 -168
  140. package/dist/server/lib/ticketUndo.js.map +1 -1
  141. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  142. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  143. package/dist/server/lib/tickets.js +41 -27
  144. package/dist/server/lib/tickets.js.map +1 -1
  145. package/dist/server/lib/transcriptRuns.js +3 -3
  146. package/dist/server/lib/transcriptRuns.js.map +1 -1
  147. package/dist/server/lib/turnExecutionErrors.js +12 -0
  148. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  149. package/dist/server/lib/types.js +58 -1
  150. package/dist/server/lib/types.js.map +1 -1
  151. package/dist/server/workers/build.js +596 -184
  152. package/dist/server/workers/build.js.map +1 -1
  153. package/dist/server/workers/chat.js +269 -132
  154. package/dist/server/workers/chat.js.map +1 -1
  155. package/dist/server/workers/followUp.js +731 -203
  156. package/dist/server/workers/followUp.js.map +1 -1
  157. package/dist/server/workers/plan.js +546 -188
  158. package/dist/server/workers/plan.js.map +1 -1
  159. package/dist/server/workers/planFollowUp.js +240 -75
  160. package/dist/server/workers/planFollowUp.js.map +1 -1
  161. package/dist/server/workers/review.js +790 -297
  162. package/dist/server/workers/review.js.map +1 -1
  163. package/dist/server/workers/specReview.js +212 -46
  164. package/dist/server/workers/specReview.js.map +1 -1
  165. package/dist/server/workers/specSession.js +434 -254
  166. package/dist/server/workers/specSession.js.map +1 -1
  167. package/package.json +1 -1
  168. package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
  169. package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
  170. package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
  171. package/dist/client/assets/channel-Dd5Len0X.js +0 -1
  172. package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
  173. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
  174. package/dist/client/assets/index-BHq6-Eir.js +0 -270
  175. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  176. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C8yO6POT.js +0 -1
@@ -0,0 +1,1367 @@
1
+ import { randomUUID } from 'crypto';
2
+ import db from '../db.js';
3
+ import { EXECUTION_RUN_KINDS, EXECUTION_RUN_LINK_ENTITY_TYPES } from './types.js';
4
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
5
+ const ACTIVE_STATUSES = new Set(['queued', 'running', 'waiting']);
6
+ const MAX_METADATA_BYTES = 16 * 1024;
7
+ const MAX_EVENT_MESSAGE_LENGTH = 4_000;
8
+ const MAX_FAILURE_MESSAGE_LENGTH = 4_000;
9
+ const MAX_JOBS_SNAPSHOT_BYTES = 64 * 1024;
10
+ const MAX_TEST_OUTPUT_LENGTH = 256 * 1024;
11
+ const APPROVED_EXECUTION_RUN_KINDS = new Set(EXECUTION_RUN_KINDS);
12
+ const EXECUTION_RUN_INITIATORS = new Set(['user', 'automation', 'recovery', 'system']);
13
+ const EXECUTION_RUN_LINK_RELATIONSHIPS = new Set(['primary', 'context', 'result', 'trigger']);
14
+ const EXECUTION_RUN_LINK_ENTITY_TYPE_SET = new Set(EXECUTION_RUN_LINK_ENTITY_TYPES);
15
+ const GENERIC_RETRY_DENIED_KINDS = new Set([
16
+ 'automation_commit',
17
+ 'automation_push',
18
+ 'automation_pull_request',
19
+ ]);
20
+ export class GitHubActionsDispatchConflictError extends Error {
21
+ executionRunId;
22
+ constructor(executionRunId) {
23
+ super('A dispatch for this project, workflow, and ref is still being resolved.');
24
+ this.name = 'GitHubActionsDispatchConflictError';
25
+ this.executionRunId = executionRunId;
26
+ }
27
+ }
28
+ const getRunStatement = db.prepare('SELECT * FROM execution_runs WHERE id = ? AND project_id = ?');
29
+ const getRunByIdStatement = db.prepare('SELECT * FROM execution_runs WHERE id = ?');
30
+ const projectExistsStatement = db.prepare('SELECT 1 FROM projects WHERE id = ?');
31
+ const insertRunStatement = db.prepare(`
32
+ INSERT INTO execution_runs (
33
+ id, project_id, kind, status, domain_status, source_kind, source_id, source_label, source_url,
34
+ correlation_id, initiated_by, metadata_schema_version, metadata_json, parent_run_id,
35
+ retry_of_run_id, attempt, created_at, updated_at
36
+ ) VALUES (?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
37
+ `);
38
+ const insertEventStatement = db.prepare(`
39
+ INSERT INTO lifecycle_events (
40
+ id, project_id, execution_run_id, correlation_id, sequence, event_type, from_status,
41
+ to_status, domain_status, message, metadata_schema_version, metadata_json, created_at
42
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
43
+ `);
44
+ const nextEventSequenceStatement = db.prepare(`
45
+ SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence
46
+ FROM lifecycle_events
47
+ WHERE execution_run_id = ?
48
+ `);
49
+ const insertRunLinkStatement = db.prepare(`
50
+ INSERT INTO execution_run_links (
51
+ execution_run_id, project_id, entity_type, entity_id, relationship,
52
+ label_snapshot, url_snapshot, created_at
53
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
54
+ `);
55
+ const getRunLinksStatement = db.prepare(`
56
+ SELECT * FROM execution_run_links
57
+ WHERE execution_run_id = ? AND project_id = ?
58
+ ORDER BY created_at ASC, entity_type ASC, entity_id ASC
59
+ `);
60
+ const getRunLinkStatement = db.prepare(`
61
+ SELECT * FROM execution_run_links
62
+ WHERE execution_run_id = ? AND project_id = ? AND entity_type = ? AND entity_id = ? AND relationship = ?
63
+ `);
64
+ const getDirectRetryChildStatement = db.prepare('SELECT id FROM execution_runs WHERE retry_of_run_id = ? LIMIT 1');
65
+ const hasLaterRunForPrimaryEntityStatement = db.prepare(`
66
+ SELECT 1
67
+ FROM execution_runs candidate
68
+ INNER JOIN execution_run_links candidate_primary
69
+ ON candidate_primary.execution_run_id = candidate.id
70
+ AND candidate_primary.project_id = candidate.project_id
71
+ AND candidate_primary.relationship = 'primary'
72
+ INNER JOIN execution_runs source ON source.id = ? AND source.project_id = ?
73
+ WHERE candidate.project_id = ?
74
+ AND candidate.kind = ?
75
+ AND candidate_primary.entity_type = ?
76
+ AND candidate_primary.entity_id = ?
77
+ AND candidate.rowid > source.rowid
78
+ AND candidate.retry_of_run_id IS NOT source.id
79
+ LIMIT 1
80
+ `);
81
+ const getEventStatement = db.prepare('SELECT * FROM lifecycle_events WHERE id = ? AND project_id = ?');
82
+ const insertTestDetailsStatement = db.prepare(`
83
+ INSERT INTO test_execution_details (
84
+ execution_run_id, command_id, command_name, command_runner, command_snapshot,
85
+ active_command, target_kind, target_bundle_id, target_label, ticket_id,
86
+ selection_json, summary_json, exit_code, duration_ms, output, output_truncated,
87
+ output_retention_state, output_expired_at, output_byte_count
88
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'retained', NULL, ?)
89
+ `);
90
+ const insertGitHubActionsDetailsStatement = db.prepare(`
91
+ INSERT INTO github_actions_run_details (
92
+ execution_run_id, workflow_id, workflow_name, ref, input_names_json, input_fingerprint,
93
+ resolution_attempts, provider_error_count, provider_run_id, provider_status,
94
+ conclusion, html_url, head_sha, head_branch, provider_created_at, provider_updated_at,
95
+ provider_started_at, dispatched_at, last_synced_at, jobs_json,
96
+ jobs_retention_state, jobs_expired_at, jobs_count
97
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'retained', NULL, ?)
98
+ `);
99
+ const getTestTargetBundleStatement = db.prepare(`
100
+ SELECT id, kind
101
+ FROM worktree_bundles
102
+ WHERE id = ? AND project_id = ?
103
+ `);
104
+ const getTestTargetTicketStatement = db.prepare(`
105
+ SELECT id, worktree_bundle_id
106
+ FROM tickets
107
+ WHERE id = ? AND project_id = ?
108
+ `);
109
+ const updateRunStatement = db.prepare(`
110
+ UPDATE execution_runs
111
+ SET status = ?, domain_status = ?, failure_code = ?, failure_message = ?,
112
+ failure_details_json = ?, started_at = ?, completed_at = ?, updated_at = ?
113
+ WHERE id = ? AND project_id = ?
114
+ `);
115
+ const assertBoundedString = (value, name, maxLength) => {
116
+ if (value !== null && value !== undefined && value.length > maxLength) {
117
+ throw new Error(`${name} exceeds ${maxLength} characters`);
118
+ }
119
+ };
120
+ const normalizeBoundedMessage = (value, maxLength) => {
121
+ if (value === null || value === undefined) {
122
+ return null;
123
+ }
124
+ if (value.length <= maxLength) {
125
+ return value;
126
+ }
127
+ const truncated = value.slice(0, maxLength);
128
+ const lastCodeUnit = truncated.charCodeAt(truncated.length - 1);
129
+ return lastCodeUnit >= 0xD800 && lastCodeUnit <= 0xDBFF
130
+ ? truncated.slice(0, -1)
131
+ : truncated;
132
+ };
133
+ const serializeBoundedObject = (value, name) => {
134
+ if (value === null || value === undefined) {
135
+ return null;
136
+ }
137
+ let serialized;
138
+ try {
139
+ serialized = JSON.stringify(value);
140
+ }
141
+ catch {
142
+ throw new Error(`${name} must be JSON serializable`);
143
+ }
144
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_METADATA_BYTES) {
145
+ throw new Error(`${name} exceeds ${MAX_METADATA_BYTES} bytes`);
146
+ }
147
+ return serialized;
148
+ };
149
+ const parseStoredObject = (value) => {
150
+ if (!value) {
151
+ return null;
152
+ }
153
+ try {
154
+ const parsed = JSON.parse(value);
155
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
156
+ return parsed;
157
+ }
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ return null;
163
+ };
164
+ const parseStoredArray = (value) => {
165
+ if (!value) {
166
+ return [];
167
+ }
168
+ try {
169
+ const parsed = JSON.parse(value);
170
+ return Array.isArray(parsed) ? parsed : [];
171
+ }
172
+ catch {
173
+ return [];
174
+ }
175
+ };
176
+ const serializeJobsSnapshot = (jobs) => {
177
+ if (jobs === null || jobs === undefined) {
178
+ return null;
179
+ }
180
+ const bounded = jobs.slice(0, 50).map(job => ({
181
+ ...job,
182
+ steps: job.steps.slice(0, 50),
183
+ }));
184
+ while (bounded.length > 0) {
185
+ const serialized = JSON.stringify(bounded);
186
+ if (Buffer.byteLength(serialized, 'utf8') <= MAX_JOBS_SNAPSHOT_BYTES) {
187
+ return serialized;
188
+ }
189
+ bounded.pop();
190
+ }
191
+ return '[]';
192
+ };
193
+ const boundTestOutput = (output, alreadyTruncated = false) => {
194
+ if (output.length <= MAX_TEST_OUTPUT_LENGTH) {
195
+ return { output, outputTruncated: alreadyTruncated };
196
+ }
197
+ return {
198
+ output: output.slice(-MAX_TEST_OUTPUT_LENGTH),
199
+ outputTruncated: true,
200
+ };
201
+ };
202
+ const SOURCE_TABLES = {
203
+ ticket: 'tickets',
204
+ project: 'projects',
205
+ bundle: 'worktree_bundles',
206
+ chat_session: 'chat_sessions',
207
+ spec_session: 'spec_sessions',
208
+ };
209
+ const loadSourceAvailability = (rows) => {
210
+ const availability = new Map();
211
+ for (const [sourceKind, table] of Object.entries(SOURCE_TABLES)) {
212
+ const sourceRows = rows.filter(row => row.source_kind === sourceKind && row.source_id);
213
+ const ids = [...new Set(sourceRows.map(row => row.source_id))];
214
+ if (ids.length === 0) {
215
+ continue;
216
+ }
217
+ const projectIds = [...new Set(sourceRows.map(row => row.project_id))];
218
+ const records = db.prepare(`
219
+ SELECT id, ${sourceKind === 'project' ? 'id' : 'project_id'} AS project_id
220
+ FROM ${table}
221
+ WHERE id IN (${ids.map(() => '?').join(', ')})
222
+ AND ${sourceKind === 'project' ? 'id' : 'project_id'} IN (${projectIds.map(() => '?').join(', ')})
223
+ `).all(...ids, ...projectIds);
224
+ const availableKeys = new Set(records.map(record => `${record.project_id}:${sourceKind}:${record.id}`));
225
+ for (const row of sourceRows) {
226
+ availability.set(`${row.project_id}:${sourceKind}:${row.source_id}`, availableKeys.has(`${row.project_id}:${sourceKind}:${row.source_id}`));
227
+ }
228
+ }
229
+ return availability;
230
+ };
231
+ const safeSnapshotUrl = (value) => {
232
+ if (!value) {
233
+ return null;
234
+ }
235
+ try {
236
+ const url = new URL(value);
237
+ return url.protocol === 'http:' || url.protocol === 'https:' ? value : null;
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ };
243
+ const serializeRun = (row, sourceAvailability) => ({
244
+ id: row.id,
245
+ project_id: row.project_id,
246
+ kind: row.kind,
247
+ status: row.status,
248
+ domain_status: row.domain_status,
249
+ source_reference: row.source_kind && row.source_id
250
+ ? {
251
+ kind: row.source_kind,
252
+ id: row.source_id,
253
+ label: row.source_label,
254
+ available: sourceAvailability?.get(`${row.project_id}:${row.source_kind}:${row.source_id}`) ?? loadSourceAvailability([row]).get(`${row.project_id}:${row.source_kind}:${row.source_id}`) ?? Boolean(safeSnapshotUrl(row.source_url)),
255
+ url: safeSnapshotUrl(row.source_url),
256
+ }
257
+ : null,
258
+ correlation_id: row.correlation_id,
259
+ initiated_by: row.initiated_by,
260
+ metadata_schema_version: row.metadata_schema_version,
261
+ metadata: parseStoredObject(row.metadata_json),
262
+ parent_run_id: row.parent_run_id,
263
+ retry_of_run_id: row.retry_of_run_id,
264
+ attempt: row.attempt,
265
+ failure: row.failure_message
266
+ ? {
267
+ code: row.failure_code,
268
+ message: row.failure_message,
269
+ details: parseStoredObject(row.failure_details_json),
270
+ }
271
+ : null,
272
+ created_at: row.created_at,
273
+ started_at: row.started_at,
274
+ completed_at: row.completed_at,
275
+ updated_at: row.updated_at,
276
+ });
277
+ const serializeEvent = (row) => ({
278
+ ...row,
279
+ metadata: parseStoredObject(row.metadata_json),
280
+ });
281
+ const PUBLIC_EVENT_METADATA_KEYS = new Set([
282
+ 'candidate_run_ids',
283
+ 'conclusion',
284
+ 'input_names',
285
+ 'provider_run_id',
286
+ 'ref',
287
+ 'resolution_attempts',
288
+ 'trigger',
289
+ 'workflow_id',
290
+ ]);
291
+ export const toCanonicalLifecycleEvent = (event) => {
292
+ const metadata = event.metadata
293
+ ? Object.fromEntries(Object.entries(event.metadata).filter(([key]) => PUBLIC_EVENT_METADATA_KEYS.has(key)))
294
+ : null;
295
+ return {
296
+ ...event,
297
+ metadata: metadata && Object.keys(metadata).length > 0 ? metadata : null,
298
+ };
299
+ };
300
+ const encodeCursor = (cursor) => Buffer.from(JSON.stringify({
301
+ version: 1,
302
+ created_at: cursor.created_at,
303
+ id: cursor.id,
304
+ }), 'utf8').toString('base64url');
305
+ const decodeCursor = (value) => {
306
+ try {
307
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
308
+ if (parsed.version !== 1
309
+ || typeof parsed.created_at !== 'string'
310
+ || parsed.created_at.length === 0
311
+ || parsed.created_at.length > 64
312
+ || typeof parsed.id !== 'string'
313
+ || parsed.id.length === 0
314
+ || parsed.id.length > 128) {
315
+ throw new Error('Invalid execution run cursor');
316
+ }
317
+ return { created_at: parsed.created_at, id: parsed.id };
318
+ }
319
+ catch (error) {
320
+ if (error instanceof Error && error.message === 'Invalid execution run cursor') {
321
+ throw error;
322
+ }
323
+ throw new Error('Invalid execution run cursor');
324
+ }
325
+ };
326
+ const encodeLifecycleEventCursor = (cursor) => Buffer.from(JSON.stringify({
327
+ version: 1,
328
+ created_at: cursor.created_at,
329
+ execution_run_id: cursor.execution_run_id,
330
+ sequence: cursor.sequence,
331
+ id: cursor.id,
332
+ }), 'utf8').toString('base64url');
333
+ const decodeLifecycleEventCursor = (value) => {
334
+ try {
335
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
336
+ if (parsed.version !== 1
337
+ || typeof parsed.created_at !== 'string'
338
+ || parsed.created_at.length === 0
339
+ || parsed.created_at.length > 64
340
+ || typeof parsed.execution_run_id !== 'string'
341
+ || parsed.execution_run_id.length === 0
342
+ || parsed.execution_run_id.length > 128
343
+ || !Number.isSafeInteger(parsed.sequence)
344
+ || parsed.sequence < 1
345
+ || typeof parsed.id !== 'string'
346
+ || parsed.id.length === 0
347
+ || parsed.id.length > 128) {
348
+ throw new Error('Invalid lifecycle event cursor');
349
+ }
350
+ return {
351
+ created_at: parsed.created_at,
352
+ execution_run_id: parsed.execution_run_id,
353
+ sequence: parsed.sequence,
354
+ id: parsed.id,
355
+ };
356
+ }
357
+ catch (error) {
358
+ if (error instanceof Error && error.message === 'Invalid lifecycle event cursor') {
359
+ throw error;
360
+ }
361
+ throw new Error('Invalid lifecycle event cursor');
362
+ }
363
+ };
364
+ const resolveLinkLabel = (projectId, link) => {
365
+ if (link.label) {
366
+ return link.label;
367
+ }
368
+ if (link.entityType === 'ticket') {
369
+ const row = db.prepare('SELECT title FROM tickets WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
370
+ return row?.title ?? null;
371
+ }
372
+ if (link.entityType === 'project') {
373
+ const row = db.prepare('SELECT name FROM projects WHERE id = ?').get(link.entityId);
374
+ return row?.name ?? null;
375
+ }
376
+ if (link.entityType === 'bundle') {
377
+ const row = db.prepare('SELECT name FROM worktree_bundles WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
378
+ return row?.name ?? null;
379
+ }
380
+ if (link.entityType === 'chat_session') {
381
+ const row = db.prepare('SELECT title FROM chat_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
382
+ return row?.title ?? null;
383
+ }
384
+ if (link.entityType === 'spec_session') {
385
+ const row = db.prepare('SELECT title FROM spec_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
386
+ return row?.title ?? null;
387
+ }
388
+ if (link.entityType === 'bundle_pull_request') {
389
+ const row = db.prepare(`
390
+ SELECT pr_number FROM bundle_pull_requests
391
+ WHERE id = ? AND project_id = ?
392
+ `).get(link.entityId, projectId);
393
+ return row ? `Pull request #${row.pr_number}` : null;
394
+ }
395
+ return null;
396
+ };
397
+ const resolveLinkOwner = (projectId, link) => {
398
+ switch (link.entityType) {
399
+ case 'project':
400
+ return link.entityId === projectId && projectExistsStatement.get(link.entityId)
401
+ ? { parentEntityType: null, parentEntityId: null }
402
+ : null;
403
+ case 'ticket':
404
+ return db.prepare('SELECT 1 FROM tickets WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
405
+ ? { parentEntityType: null, parentEntityId: null }
406
+ : null;
407
+ case 'bundle':
408
+ return db.prepare('SELECT 1 FROM worktree_bundles WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
409
+ ? { parentEntityType: null, parentEntityId: null }
410
+ : null;
411
+ case 'chat_session':
412
+ return db.prepare('SELECT 1 FROM chat_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
413
+ ? { parentEntityType: null, parentEntityId: null }
414
+ : null;
415
+ case 'spec_session':
416
+ return db.prepare('SELECT 1 FROM spec_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
417
+ ? { parentEntityType: null, parentEntityId: null }
418
+ : null;
419
+ case 'spec_snapshot': {
420
+ const row = db.prepare(`
421
+ SELECT snapshot.spec_session_id AS parent_id FROM spec_snapshots snapshot
422
+ JOIN spec_sessions session ON session.id = snapshot.spec_session_id
423
+ WHERE snapshot.id = ? AND session.project_id = ?
424
+ `).get(link.entityId, projectId);
425
+ return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
426
+ }
427
+ case 'review_run': {
428
+ const row = db.prepare(`
429
+ SELECT review.ticket_id AS parent_id FROM review_runs review
430
+ JOIN tickets ticket ON ticket.id = review.ticket_id
431
+ WHERE review.id = ? AND ticket.project_id = ?
432
+ `).get(link.entityId, projectId);
433
+ return row ? { parentEntityType: 'ticket', parentEntityId: row.parent_id } : null;
434
+ }
435
+ case 'spec_review_run': {
436
+ const row = db.prepare(`
437
+ SELECT review.spec_session_id AS parent_id FROM spec_review_runs review
438
+ JOIN spec_sessions session ON session.id = review.spec_session_id
439
+ JOIN spec_snapshots snapshot
440
+ ON snapshot.id = review.spec_snapshot_id AND snapshot.spec_session_id = review.spec_session_id
441
+ WHERE review.id = ? AND review.project_id = ? AND session.project_id = ?
442
+ `).get(link.entityId, projectId, projectId);
443
+ return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
444
+ }
445
+ case 'build_sequence':
446
+ return db.prepare('SELECT 1 FROM build_sequences WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
447
+ ? { parentEntityType: null, parentEntityId: null }
448
+ : null;
449
+ case 'github_workflow':
450
+ return projectExistsStatement.get(projectId)
451
+ ? { parentEntityType: null, parentEntityId: null }
452
+ : null;
453
+ case 'ticket_message': {
454
+ const row = db.prepare(`
455
+ SELECT message.ticket_id AS parent_id
456
+ FROM ticket_messages message
457
+ JOIN tickets ticket ON ticket.id = message.ticket_id
458
+ WHERE message.id = ? AND ticket.project_id = ?
459
+ `).get(link.entityId, projectId);
460
+ return row ? { parentEntityType: 'ticket', parentEntityId: row.parent_id } : null;
461
+ }
462
+ case 'chat_message': {
463
+ const row = db.prepare(`
464
+ SELECT message.chat_session_id AS parent_id
465
+ FROM chat_messages message
466
+ JOIN chat_sessions session ON session.id = message.chat_session_id
467
+ WHERE message.id = ? AND session.project_id = ?
468
+ `).get(link.entityId, projectId);
469
+ return row ? { parentEntityType: 'chat_session', parentEntityId: row.parent_id } : null;
470
+ }
471
+ case 'spec_message': {
472
+ const row = db.prepare(`
473
+ SELECT message.spec_session_id AS parent_id
474
+ FROM spec_messages message
475
+ JOIN spec_sessions session ON session.id = message.spec_session_id
476
+ WHERE message.id = ? AND session.project_id = ?
477
+ `).get(link.entityId, projectId);
478
+ return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
479
+ }
480
+ case 'bundle_pull_request': {
481
+ const row = db.prepare(`
482
+ SELECT pull_request.bundle_id AS parent_id
483
+ FROM bundle_pull_requests pull_request
484
+ JOIN worktree_bundles bundle
485
+ ON bundle.id = pull_request.bundle_id AND bundle.project_id = pull_request.project_id
486
+ WHERE pull_request.id = ? AND pull_request.project_id = ?
487
+ `).get(link.entityId, projectId);
488
+ return row ? { parentEntityType: 'bundle', parentEntityId: row.parent_id } : null;
489
+ }
490
+ }
491
+ };
492
+ const assertLinkOwnership = (projectId, link) => {
493
+ if (!EXECUTION_RUN_LINK_ENTITY_TYPE_SET.has(link.entityType)) {
494
+ throw new Error(`Unsupported execution run link entity type: ${link.entityType}`);
495
+ }
496
+ const owner = resolveLinkOwner(projectId, link);
497
+ if (!owner) {
498
+ throw new Error(`${link.entityType} ${link.entityId} does not belong to project ${projectId}`);
499
+ }
500
+ if (link.expectedParent && (owner.parentEntityType !== link.expectedParent.entityType
501
+ || owner.parentEntityId !== link.expectedParent.entityId)) {
502
+ throw new Error(`${link.entityType} ${link.entityId} does not belong to expected ${link.expectedParent.entityType} ${link.expectedParent.entityId}`);
503
+ }
504
+ };
505
+ const validateExecutionRunLink = (projectId, link) => {
506
+ if (!EXECUTION_RUN_LINK_RELATIONSHIPS.has(link.relationship)) {
507
+ throw new Error(`Unsupported execution run link relationship: ${link.relationship}`);
508
+ }
509
+ if (!EXECUTION_RUN_LINK_ENTITY_TYPE_SET.has(link.entityType)) {
510
+ throw new Error(`Unsupported execution run link entity type: ${link.entityType}`);
511
+ }
512
+ if (link.entityType.trim().length === 0 || link.entityId.trim().length === 0) {
513
+ throw new Error('Execution run links require an entity type and ID');
514
+ }
515
+ assertBoundedString(link.entityType, 'Link entity type', 100);
516
+ assertBoundedString(link.entityId, 'Link entity ID', 500);
517
+ assertBoundedString(link.url, 'Link URL', 2_000);
518
+ assertLinkOwnership(projectId, link);
519
+ const label = resolveLinkLabel(projectId, link);
520
+ assertBoundedString(label, 'Link label', 500);
521
+ return { ...link, label };
522
+ };
523
+ const normalizeInitialLinks = (input) => {
524
+ const links = [...(input.links ?? [])];
525
+ if (input.sourceReference) {
526
+ links.push({
527
+ entityType: input.sourceReference.kind,
528
+ entityId: input.sourceReference.id,
529
+ relationship: 'primary',
530
+ label: input.sourceReference.label,
531
+ url: input.sourceReference.url,
532
+ });
533
+ }
534
+ if (links.filter(link => link.relationship === 'primary').length > 1) {
535
+ throw new Error('Execution runs may have only one primary link');
536
+ }
537
+ return links;
538
+ };
539
+ const resolveCorrelationId = (parent) => {
540
+ if (parent) {
541
+ return parent.correlation_id;
542
+ }
543
+ return randomUUID();
544
+ };
545
+ const assertRetryEligible = (kind, retrySource, allowAutomationSideEffect) => {
546
+ if (!TERMINAL_STATUSES.has(retrySource.status)) {
547
+ throw new Error('Retry source execution run must be terminal');
548
+ }
549
+ if (kind === 'github_actions') {
550
+ throw new Error('GitHub Actions execution runs cannot be retried directly');
551
+ }
552
+ if (GENERIC_RETRY_DENIED_KINDS.has(kind) && !allowAutomationSideEffect) {
553
+ throw new Error(`${kind} execution runs cannot be retried through the generic retry API`);
554
+ }
555
+ if (kind !== 'test' && retrySource.status === 'completed') {
556
+ throw new Error('Completed execution runs are not eligible for retry');
557
+ }
558
+ if (hasDirectRetryChild(retrySource.id)) {
559
+ throw new Error(`Execution run ${retrySource.id} already has a direct retry child`);
560
+ }
561
+ };
562
+ const assertTestTarget = (projectId, details) => {
563
+ let expectedTargetKind = 'repo_root';
564
+ let expectedBundleId = null;
565
+ if (details.ticketId) {
566
+ const ticket = getTestTargetTicketStatement.get(details.ticketId, projectId);
567
+ if (!ticket) {
568
+ throw new Error(`Ticket ${details.ticketId} does not belong to project ${projectId}`);
569
+ }
570
+ if (ticket.worktree_bundle_id) {
571
+ const bundle = getTestTargetBundleStatement.get(ticket.worktree_bundle_id, projectId);
572
+ if (!bundle) {
573
+ throw new Error(`Ticket ${details.ticketId} bundle target does not belong to project ${projectId}`);
574
+ }
575
+ if (bundle.kind !== 'project_root') {
576
+ expectedTargetKind = 'bundle';
577
+ expectedBundleId = bundle.id;
578
+ }
579
+ }
580
+ }
581
+ else if (details.targetKind === 'bundle') {
582
+ const bundle = getTestTargetBundleStatement.get(details.targetBundleId, projectId);
583
+ if (!bundle || bundle.kind === 'project_root') {
584
+ throw new Error(`Bundle ${details.targetBundleId} does not belong to project ${projectId}`);
585
+ }
586
+ expectedTargetKind = 'bundle';
587
+ expectedBundleId = bundle.id;
588
+ }
589
+ if (details.targetKind !== expectedTargetKind || (details.targetBundleId ?? null) !== expectedBundleId) {
590
+ throw new Error('Test execution target does not match its ticket or bundle context');
591
+ }
592
+ };
593
+ export const hasDirectRetryChild = (runId) => Boolean(getDirectRetryChildStatement.get(runId));
594
+ export const getDirectRetryChild = (projectId, runId) => {
595
+ const child = getDirectRetryChildStatement.get(runId);
596
+ return child ? getExecutionRun(projectId, child.id) : null;
597
+ };
598
+ export const hasLaterExecutionRunForPrimaryEntity = ({ projectId, sourceRunId, kind, entityType, entityId, }) => Boolean(hasLaterRunForPrimaryEntityStatement.get(sourceRunId, projectId, projectId, kind, entityType, entityId));
599
+ const insertTestDetails = (runId, details) => {
600
+ const boundedOutput = boundTestOutput(details.output ?? '', details.outputTruncated);
601
+ insertTestDetailsStatement.run(runId, details.commandId ?? null, details.commandName ?? null, details.commandRunner ?? null, details.commandSnapshot ?? null, details.activeCommand ?? null, details.targetKind ?? null, details.targetBundleId ?? null, details.targetLabel ?? null, details.ticketId ?? null, serializeBoundedObject(details.selection, 'Test selection'), serializeBoundedObject(details.summary, 'Test summary'), details.exitCode ?? null, details.durationMs ?? null, boundedOutput.output, boundedOutput.outputTruncated ? 1 : 0, Buffer.byteLength(boundedOutput.output, 'utf8'));
602
+ };
603
+ const insertGitHubActionsDetails = (runId, details) => {
604
+ const jobsJson = serializeJobsSnapshot(details.jobs);
605
+ insertGitHubActionsDetailsStatement.run(runId, details.workflowId, details.workflowName ?? null, details.ref ?? null, details.inputNames ? JSON.stringify([...new Set(details.inputNames)].sort()) : null, details.inputFingerprint ?? null, details.resolutionAttempts ?? 0, details.providerErrorCount ?? 0, details.providerRunId ?? null, details.providerStatus ?? null, details.conclusion ?? null, details.htmlUrl ?? null, details.headSha ?? null, details.headBranch ?? null, details.providerCreatedAt ?? null, details.providerUpdatedAt ?? null, details.providerStartedAt ?? null, details.dispatchedAt ?? null, details.lastSyncedAt ?? null, jobsJson, details.jobs?.length ?? 0);
606
+ };
607
+ const getRunRow = (projectId, runId) => {
608
+ const row = getRunStatement.get(runId, projectId);
609
+ if (!row) {
610
+ throw new Error(`Execution run ${runId} was not found in project ${projectId}`);
611
+ }
612
+ return row;
613
+ };
614
+ const getRelatedRun = (projectId, runId, relation) => {
615
+ const row = getRunByIdStatement.get(runId);
616
+ if (!row) {
617
+ throw new Error(`${relation} execution run ${runId} was not found`);
618
+ }
619
+ if (row.project_id !== projectId) {
620
+ throw new Error(`${relation} execution run must belong to project ${projectId}`);
621
+ }
622
+ return row;
623
+ };
624
+ const assertNoLineageCycle = (runId, relatedRun, column) => {
625
+ const visited = new Set();
626
+ let current = relatedRun;
627
+ while (current) {
628
+ if (current.id === runId) {
629
+ throw new Error(`Execution run ${runId} would create a lineage cycle`);
630
+ }
631
+ if (visited.has(current.id)) {
632
+ throw new Error(`Existing execution run lineage contains a cycle at ${current.id}`);
633
+ }
634
+ visited.add(current.id);
635
+ const nextId = current[column];
636
+ current = nextId ? getRunByIdStatement.get(nextId) : undefined;
637
+ }
638
+ };
639
+ const createEvent = (input) => {
640
+ assertBoundedString(input.eventType, 'Lifecycle event type', 100);
641
+ const message = normalizeBoundedMessage(input.message, MAX_EVENT_MESSAGE_LENGTH);
642
+ const schemaVersion = input.metadataSchemaVersion ?? 1;
643
+ if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1) {
644
+ throw new Error('Lifecycle event metadata schema version must be a positive integer');
645
+ }
646
+ const id = input.id ?? randomUUID();
647
+ const run = getRunRow(input.projectId, input.runId);
648
+ const sequence = nextEventSequenceStatement.get(input.runId).sequence;
649
+ insertEventStatement.run(id, input.projectId, input.runId, run.correlation_id, sequence, input.eventType, input.fromStatus, input.toStatus, input.domainStatus, message, schemaVersion, input.metadataJson, input.createdAt);
650
+ const row = getEventStatement.get(id, input.projectId);
651
+ return serializeEvent(row);
652
+ };
653
+ const emitCommittedNotifications = (notifications) => {
654
+ for (const notification of notifications) {
655
+ try {
656
+ if (notification.event === 'execution-run:updated') {
657
+ notification.emitter?.emit(notification.event, notification.payload);
658
+ }
659
+ else {
660
+ notification.emitter?.emit(notification.event, toCanonicalLifecycleEvent(notification.payload));
661
+ }
662
+ }
663
+ catch {
664
+ // Realtime delivery is best-effort after the database transaction commits.
665
+ }
666
+ }
667
+ };
668
+ const enqueueRunAndEvent = (notifications, run, event, emitter) => {
669
+ notifications.push({ event: 'execution-run:updated', payload: run, emitter }, { event: 'lifecycle-event:created', payload: event, emitter });
670
+ };
671
+ const createExecutionRunInTransaction = (input, allowAutomationSideEffectRetry = false) => {
672
+ if (!projectExistsStatement.get(input.projectId)) {
673
+ throw new Error(`Project ${input.projectId} was not found`);
674
+ }
675
+ const id = input.id ?? randomUUID();
676
+ assertBoundedString(id, 'Execution run ID', 128);
677
+ if (!APPROVED_EXECUTION_RUN_KINDS.has(input.kind)) {
678
+ throw new Error(`Unsupported execution run kind: ${input.kind}`);
679
+ }
680
+ const initiatedBy = input.initiatedBy ?? 'user';
681
+ if (!EXECUTION_RUN_INITIATORS.has(initiatedBy)) {
682
+ throw new Error(`Unsupported execution run initiator: ${initiatedBy}`);
683
+ }
684
+ assertBoundedString(input.domainStatus, 'Domain status', 200);
685
+ const links = normalizeInitialLinks(input).map(link => validateExecutionRunLink(input.projectId, link));
686
+ const primaryLink = links.find(link => link.relationship === 'primary') ?? null;
687
+ const metadataSchemaVersion = input.metadataSchemaVersion ?? 1;
688
+ if (!Number.isSafeInteger(metadataSchemaVersion) || metadataSchemaVersion < 1) {
689
+ throw new Error('Execution run metadata schema version must be a positive integer');
690
+ }
691
+ const metadataJson = serializeBoundedObject(input.metadata, 'Execution run metadata');
692
+ let retrySource = null;
693
+ if (input.retryOfRunId) {
694
+ retrySource = getRelatedRun(input.projectId, input.retryOfRunId, 'Retry source');
695
+ if (retrySource.kind !== input.kind) {
696
+ throw new Error('Retry source execution run must have the same kind');
697
+ }
698
+ assertNoLineageCycle(id, retrySource, 'retry_of_run_id');
699
+ if (allowAutomationSideEffectRetry && !GENERIC_RETRY_DENIED_KINDS.has(input.kind)) {
700
+ throw new Error('Dedicated automation retry creation only supports automation side-effect runs');
701
+ }
702
+ assertRetryEligible(input.kind, retrySource, allowAutomationSideEffectRetry);
703
+ }
704
+ let expectedParentId = retrySource?.parent_run_id ?? null;
705
+ if (retrySource && input.parentRunId !== undefined && input.parentRunId !== expectedParentId) {
706
+ const recoveryParent = allowAutomationSideEffectRetry && typeof input.parentRunId === 'string'
707
+ ? getRelatedRun(input.projectId, input.parentRunId, 'Recovery parent')
708
+ : null;
709
+ if (recoveryParent?.kind !== 'automation_progression'
710
+ || recoveryParent.initiated_by !== 'recovery'
711
+ || recoveryParent.parent_run_id !== retrySource.id
712
+ || !ACTIVE_STATUSES.has(recoveryParent.status)) {
713
+ throw new Error('Retry parent execution run must match the retry source parent');
714
+ }
715
+ expectedParentId = recoveryParent.id;
716
+ }
717
+ const parentRunId = retrySource ? expectedParentId : input.parentRunId ?? null;
718
+ let parent = null;
719
+ if (parentRunId) {
720
+ parent = getRelatedRun(input.projectId, parentRunId, 'Parent');
721
+ assertNoLineageCycle(id, parent, 'parent_run_id');
722
+ }
723
+ const correlationId = resolveCorrelationId(parent);
724
+ if (input.kind !== 'test' && input.testDetails) {
725
+ throw new Error('Test execution details require a test execution run');
726
+ }
727
+ if (input.kind !== 'github_actions' && input.githubActionsDetails) {
728
+ throw new Error('GitHub Actions details require a github_actions execution run');
729
+ }
730
+ if (input.kind === 'github_actions' && !input.githubActionsDetails) {
731
+ throw new Error('GitHub Actions execution runs require domain details');
732
+ }
733
+ if (input.kind === 'test') {
734
+ assertTestTarget(input.projectId, input.testDetails);
735
+ }
736
+ if (input.kind === 'github_actions') {
737
+ const details = input.githubActionsDetails;
738
+ if (!Number.isSafeInteger(details.workflowId) || details.workflowId < 1) {
739
+ throw new Error('GitHub Actions execution runs require a positive workflow ID');
740
+ }
741
+ const conflict = db.prepare(`
742
+ SELECT er.id
743
+ FROM execution_runs er
744
+ JOIN github_actions_run_details ga ON ga.execution_run_id = er.id
745
+ WHERE er.project_id = ?
746
+ AND er.kind = 'github_actions'
747
+ AND er.status IN ('queued', 'waiting')
748
+ AND ga.provider_run_id IS NULL
749
+ AND ga.workflow_id = ?
750
+ AND ga.ref = ?
751
+ LIMIT 1
752
+ `).get(input.projectId, details.workflowId, details.ref ?? null);
753
+ if (conflict) {
754
+ throw new GitHubActionsDispatchConflictError(conflict.id);
755
+ }
756
+ }
757
+ const now = new Date().toISOString();
758
+ insertRunStatement.run(id, input.projectId, input.kind, input.domainStatus ?? null, primaryLink?.entityType ?? null, primaryLink?.entityId ?? null, primaryLink?.label ?? null, primaryLink?.url ?? null, correlationId, initiatedBy, metadataSchemaVersion, metadataJson, parent?.id ?? null, retrySource?.id ?? null, retrySource ? retrySource.attempt + 1 : 1, now, now);
759
+ for (const link of links) {
760
+ insertRunLinkStatement.run(id, input.projectId, link.entityType, link.entityId, link.relationship, link.label, link.url ?? null, now);
761
+ }
762
+ if (input.kind === 'test') {
763
+ insertTestDetails(id, input.testDetails);
764
+ }
765
+ if (input.kind === 'github_actions') {
766
+ insertGitHubActionsDetails(id, input.githubActionsDetails);
767
+ }
768
+ const event = createEvent({
769
+ projectId: input.projectId,
770
+ runId: id,
771
+ eventType: input.eventType ?? 'created',
772
+ fromStatus: null,
773
+ toStatus: 'queued',
774
+ domainStatus: input.domainStatus ?? null,
775
+ message: input.eventMessage,
776
+ metadataJson: serializeBoundedObject(input.eventMetadata, 'Lifecycle event metadata'),
777
+ createdAt: now,
778
+ });
779
+ const retryEvents = retrySource
780
+ ? [
781
+ createEvent({
782
+ projectId: input.projectId,
783
+ runId: retrySource.id,
784
+ eventType: 'retry_requested',
785
+ fromStatus: retrySource.status,
786
+ toStatus: retrySource.status,
787
+ domainStatus: retrySource.domain_status,
788
+ metadataJson: serializeBoundedObject({ replacement_run_id: id }, 'Lifecycle event metadata'),
789
+ createdAt: now,
790
+ }),
791
+ ...(event.event_type === 'retry_started'
792
+ ? []
793
+ : [createEvent({
794
+ projectId: input.projectId,
795
+ runId: id,
796
+ eventType: 'retry_started',
797
+ fromStatus: 'queued',
798
+ toStatus: 'queued',
799
+ domainStatus: input.domainStatus ?? null,
800
+ metadataJson: serializeBoundedObject({ retry_of_run_id: retrySource.id }, 'Lifecycle event metadata'),
801
+ createdAt: now,
802
+ })]),
803
+ ]
804
+ : [];
805
+ return { run: serializeRun(getRunRow(input.projectId, id)), event, retryEvents };
806
+ };
807
+ const insertRunLink = (input) => {
808
+ if (input.link.relationship === 'primary') {
809
+ throw new Error('Primary execution run links can only be set at run creation');
810
+ }
811
+ const link = validateExecutionRunLink(input.projectId, input.link);
812
+ const existing = getRunLinkStatement.get(input.runId, input.projectId, link.entityType, link.entityId, link.relationship);
813
+ if (existing) {
814
+ throw new Error(`Execution run link already exists for ${link.entityType} ${link.entityId} as ${link.relationship}`);
815
+ }
816
+ const now = new Date().toISOString();
817
+ insertRunLinkStatement.run(input.runId, input.projectId, link.entityType, link.entityId, link.relationship, link.label, link.url ?? null, now);
818
+ return getRunLinkStatement.get(input.runId, input.projectId, link.entityType, link.entityId, link.relationship);
819
+ };
820
+ const addExecutionRunLinkInTransaction = (input) => {
821
+ getRunRow(input.projectId, input.runId);
822
+ return insertRunLink(input);
823
+ };
824
+ const addExecutionRunLinksInTransaction = (input) => {
825
+ getRunRow(input.projectId, input.runId);
826
+ return input.links.map(link => insertRunLink({
827
+ projectId: input.projectId,
828
+ runId: input.runId,
829
+ link,
830
+ }));
831
+ };
832
+ const transitionExecutionRunInTransaction = (input, allowAmbiguousAutomationReconciliation = false) => {
833
+ const current = getRunRow(input.projectId, input.runId);
834
+ const domainStatus = input.domainStatus === undefined ? current.domain_status : input.domainStatus;
835
+ const failure = input.failure
836
+ ? { ...input.failure, message: normalizeBoundedMessage(input.failure.message, MAX_FAILURE_MESSAGE_LENGTH) ?? '' }
837
+ : null;
838
+ assertBoundedString(domainStatus, 'Domain status', 200);
839
+ assertBoundedString(failure?.code, 'Failure code', 200);
840
+ const failureDetailsJson = serializeBoundedObject(failure?.details, 'Failure details');
841
+ const isAmbiguousAutomationReconciliation = allowAmbiguousAutomationReconciliation
842
+ && current.status === 'failed'
843
+ && current.domain_status === 'ambiguous'
844
+ && GENERIC_RETRY_DENIED_KINDS.has(current.kind)
845
+ && (input.status === 'completed' || input.status === 'interrupted');
846
+ if (TERMINAL_STATUSES.has(current.status) && !isAmbiguousAutomationReconciliation) {
847
+ const terminalDataAgrees = current.status === input.status
848
+ && current.domain_status === domainStatus
849
+ && current.failure_code === (failure?.code ?? null)
850
+ && current.failure_message === (failure?.message ?? null)
851
+ && current.failure_details_json === failureDetailsJson
852
+ && (input.completedAt === undefined || current.completed_at === input.completedAt);
853
+ if (!terminalDataAgrees) {
854
+ throw new Error(`Execution run ${input.runId} is already terminal with conflicting data`);
855
+ }
856
+ return { run: serializeRun(current), event: null };
857
+ }
858
+ if (current.status === input.status) {
859
+ throw new Error(`Execution run ${input.runId} is already ${input.status}`);
860
+ }
861
+ if (ACTIVE_STATUSES.has(input.status)) {
862
+ const activeTransitionAllowed = (current.status === 'queued' && (input.status === 'running' || input.status === 'waiting'))
863
+ || (current.status === 'running' && input.status === 'waiting')
864
+ || (current.status === 'waiting' && input.status === 'running');
865
+ if (!activeTransitionAllowed) {
866
+ throw new Error(`Illegal execution run transition from ${current.status} to ${input.status}`);
867
+ }
868
+ if (failure) {
869
+ throw new Error('Active execution runs cannot have failure information');
870
+ }
871
+ }
872
+ const now = new Date().toISOString();
873
+ const completedAt = TERMINAL_STATUSES.has(input.status) ? input.completedAt ?? now : null;
874
+ const startedAt = input.status === 'running' ? current.started_at ?? now : current.started_at;
875
+ updateRunStatement.run(input.status, domainStatus, failure?.code ?? null, failure?.message ?? null, failureDetailsJson, startedAt, completedAt, now, input.runId, input.projectId);
876
+ const event = createEvent({
877
+ projectId: input.projectId,
878
+ runId: input.runId,
879
+ eventType: input.eventType ?? 'status_changed',
880
+ fromStatus: current.status,
881
+ toStatus: input.status,
882
+ domainStatus,
883
+ message: input.eventMessage,
884
+ metadataJson: serializeBoundedObject(input.eventMetadata, 'Lifecycle event metadata'),
885
+ createdAt: now,
886
+ });
887
+ return { run: serializeRun(getRunRow(input.projectId, input.runId)), event };
888
+ };
889
+ const appendLifecycleEventInTransaction = (input) => {
890
+ const run = getRunRow(input.projectId, input.runId);
891
+ const event = createEvent({
892
+ projectId: input.projectId,
893
+ runId: input.runId,
894
+ eventType: input.eventType,
895
+ fromStatus: run.status,
896
+ toStatus: run.status,
897
+ domainStatus: input.domainStatus === undefined ? run.domain_status : input.domainStatus,
898
+ message: input.message,
899
+ metadataSchemaVersion: input.metadataSchemaVersion,
900
+ metadataJson: serializeBoundedObject(input.metadata, 'Lifecycle event metadata'),
901
+ createdAt: new Date().toISOString(),
902
+ });
903
+ return { run: serializeRun(run), event };
904
+ };
905
+ const isPromiseLike = (value) => {
906
+ if ((typeof value !== 'object' || value === null) && typeof value !== 'function') {
907
+ return false;
908
+ }
909
+ return typeof value.then === 'function';
910
+ };
911
+ const executionTransactionNotificationScopes = [];
912
+ export const withExecutionRunTransaction = (callback, emitter) => {
913
+ const notifications = [];
914
+ const runTransaction = db.transaction(() => {
915
+ const transaction = {
916
+ createExecutionRun: input => {
917
+ const result = createExecutionRunInTransaction(input);
918
+ enqueueRunAndEvent(notifications, result.run, result.event, emitter);
919
+ for (const retryEvent of result.retryEvents) {
920
+ notifications.push({ event: 'lifecycle-event:created', payload: retryEvent, emitter });
921
+ }
922
+ return result.run;
923
+ },
924
+ createAutomationRetryExecutionRun: input => {
925
+ const result = createExecutionRunInTransaction(input, true);
926
+ enqueueRunAndEvent(notifications, result.run, result.event, emitter);
927
+ for (const retryEvent of result.retryEvents) {
928
+ notifications.push({ event: 'lifecycle-event:created', payload: retryEvent, emitter });
929
+ }
930
+ return result.run;
931
+ },
932
+ transitionExecutionRun: input => {
933
+ const result = transitionExecutionRunInTransaction(input);
934
+ if (result.event) {
935
+ enqueueRunAndEvent(notifications, result.run, result.event, emitter);
936
+ }
937
+ return result.run;
938
+ },
939
+ reconcileAmbiguousAutomationExecutionRun: input => {
940
+ const result = transitionExecutionRunInTransaction(input, true);
941
+ if (result.event) {
942
+ enqueueRunAndEvent(notifications, result.run, result.event, emitter);
943
+ }
944
+ return result.run;
945
+ },
946
+ appendLifecycleEvent: input => {
947
+ const result = appendLifecycleEventInTransaction(input);
948
+ notifications.push({ event: 'lifecycle-event:created', payload: result.event, emitter });
949
+ return result.event;
950
+ },
951
+ addExecutionRunLink: input => {
952
+ const link = addExecutionRunLinkInTransaction(input);
953
+ return { ...link, url_snapshot: safeSnapshotUrl(link.url_snapshot) };
954
+ },
955
+ addExecutionRunLinks: input => addExecutionRunLinksInTransaction(input).map(link => ({
956
+ ...link,
957
+ url_snapshot: safeSnapshotUrl(link.url_snapshot),
958
+ })),
959
+ };
960
+ const result = callback(transaction);
961
+ if (isPromiseLike(result)) {
962
+ throw new Error('Execution run transactions require a synchronous callback');
963
+ }
964
+ return result;
965
+ });
966
+ const parentNotifications = executionTransactionNotificationScopes.at(-1);
967
+ executionTransactionNotificationScopes.push(notifications);
968
+ try {
969
+ const result = runTransaction();
970
+ executionTransactionNotificationScopes.pop();
971
+ if (parentNotifications) {
972
+ parentNotifications.push(...notifications);
973
+ }
974
+ else {
975
+ emitCommittedNotifications(notifications);
976
+ }
977
+ return result;
978
+ }
979
+ catch (error) {
980
+ executionTransactionNotificationScopes.pop();
981
+ throw error;
982
+ }
983
+ };
984
+ export const createExecutionRun = (input, emitter) => (withExecutionRunTransaction(transaction => transaction.createExecutionRun(input), emitter));
985
+ export const addExecutionRunLink = (input) => (withExecutionRunTransaction(transaction => transaction.addExecutionRunLink(input)));
986
+ export const addExecutionRunLinks = (input) => (withExecutionRunTransaction(transaction => transaction.addExecutionRunLinks(input)));
987
+ export const transitionExecutionRun = (input, emitter) => (withExecutionRunTransaction(transaction => transaction.transitionExecutionRun(input), emitter));
988
+ export const appendLifecycleEvent = (input, emitter) => {
989
+ return withExecutionRunTransaction(transaction => transaction.appendLifecycleEvent(input), emitter);
990
+ };
991
+ export const getExecutionRun = (projectId, runId) => {
992
+ const row = getRunStatement.get(runId, projectId);
993
+ return row ? serializeRun(row) : null;
994
+ };
995
+ export const getExecutionRunsByIds = (projectId, runIds) => {
996
+ const ids = [...new Set(runIds)];
997
+ if (ids.length === 0) {
998
+ return [];
999
+ }
1000
+ const rows = db.prepare(`
1001
+ SELECT * FROM execution_runs
1002
+ WHERE project_id = ? AND id IN (${ids.map(() => '?').join(', ')})
1003
+ `).all(projectId, ...ids);
1004
+ const sourceAvailability = loadSourceAvailability(rows);
1005
+ return rows.map(row => serializeRun(row, sourceAvailability));
1006
+ };
1007
+ export const getQueuedSpecReviewExecutionRun = (projectId, snapshotId) => {
1008
+ const row = db.prepare(`
1009
+ SELECT execution_runs.id
1010
+ FROM execution_runs
1011
+ INNER JOIN execution_run_links snapshot_link
1012
+ ON snapshot_link.execution_run_id = execution_runs.id
1013
+ AND snapshot_link.project_id = execution_runs.project_id
1014
+ AND snapshot_link.entity_type = 'spec_snapshot'
1015
+ AND snapshot_link.relationship = 'context'
1016
+ WHERE execution_runs.project_id = ?
1017
+ AND execution_runs.kind = 'spec_review'
1018
+ AND execution_runs.status = 'queued'
1019
+ AND snapshot_link.entity_id = ?
1020
+ ORDER BY execution_runs.created_at DESC, execution_runs.id DESC
1021
+ LIMIT 1
1022
+ `).get(projectId, snapshotId);
1023
+ return row ? getExecutionRun(projectId, row.id) : null;
1024
+ };
1025
+ export const getQueuedBuildExecutionRun = (projectId, ticketId, sequenceId) => {
1026
+ const row = db.prepare(`
1027
+ SELECT execution_runs.id
1028
+ FROM execution_runs
1029
+ INNER JOIN execution_run_links ticket_link
1030
+ ON ticket_link.execution_run_id = execution_runs.id
1031
+ AND ticket_link.project_id = execution_runs.project_id
1032
+ AND ticket_link.entity_type = 'ticket'
1033
+ AND ticket_link.relationship = 'primary'
1034
+ INNER JOIN execution_run_links sequence_link
1035
+ ON sequence_link.execution_run_id = execution_runs.id
1036
+ AND sequence_link.project_id = execution_runs.project_id
1037
+ AND sequence_link.entity_type = 'build_sequence'
1038
+ AND sequence_link.relationship = 'context'
1039
+ WHERE execution_runs.project_id = ?
1040
+ AND execution_runs.kind = 'build'
1041
+ AND execution_runs.status = 'queued'
1042
+ AND ticket_link.entity_id = ?
1043
+ AND sequence_link.entity_id = ?
1044
+ ORDER BY execution_runs.created_at ASC, execution_runs.id ASC
1045
+ LIMIT 1
1046
+ `).get(projectId, ticketId, sequenceId);
1047
+ return row ? getExecutionRun(projectId, row.id) : null;
1048
+ };
1049
+ export const hasActiveBuildExecutionRunForTicket = (projectId, ticketId) => {
1050
+ return Boolean(db.prepare(`
1051
+ SELECT 1
1052
+ FROM execution_runs
1053
+ INNER JOIN execution_run_links ticket_link
1054
+ ON ticket_link.execution_run_id = execution_runs.id
1055
+ AND ticket_link.project_id = execution_runs.project_id
1056
+ AND ticket_link.entity_type = 'ticket'
1057
+ AND ticket_link.relationship = 'primary'
1058
+ WHERE execution_runs.project_id = ?
1059
+ AND execution_runs.kind = 'build'
1060
+ AND execution_runs.status IN ('queued', 'running', 'waiting')
1061
+ AND ticket_link.entity_id = ?
1062
+ LIMIT 1
1063
+ `).get(projectId, ticketId));
1064
+ };
1065
+ export const listExecutionRunLinks = (projectId, runId) => {
1066
+ getRunRow(projectId, runId);
1067
+ const links = getRunLinksStatement.all(runId, projectId);
1068
+ return links.map(link => ({ ...link, url_snapshot: safeSnapshotUrl(link.url_snapshot) }));
1069
+ };
1070
+ export const listExecutionRunLinksForRuns = (projectId, runIds) => {
1071
+ const ids = [...new Set(runIds)];
1072
+ if (ids.length === 0) {
1073
+ return [];
1074
+ }
1075
+ const links = db.prepare(`
1076
+ SELECT * FROM execution_run_links
1077
+ WHERE project_id = ? AND execution_run_id IN (${ids.map(() => '?').join(', ')})
1078
+ ORDER BY execution_run_id ASC, created_at ASC, entity_type ASC, entity_id ASC, relationship ASC
1079
+ `).all(projectId, ...ids);
1080
+ return links.map(link => ({ ...link, url_snapshot: safeSnapshotUrl(link.url_snapshot) }));
1081
+ };
1082
+ const testDetailsSelect = `
1083
+ SELECT td.*, er.project_id, er.status AS execution_status
1084
+ FROM test_execution_details td
1085
+ JOIN execution_runs er ON er.id = td.execution_run_id
1086
+ `;
1087
+ const serializeTestDetails = (row) => ({
1088
+ execution_run_id: row.execution_run_id,
1089
+ project_id: row.project_id,
1090
+ execution_status: row.execution_status,
1091
+ command_id: row.command_id,
1092
+ command_name: row.command_name,
1093
+ command_runner: row.command_runner,
1094
+ command_snapshot: row.command_snapshot,
1095
+ active_command: row.active_command,
1096
+ target_kind: row.target_kind,
1097
+ target_bundle_id: row.target_bundle_id,
1098
+ target_label: row.target_label,
1099
+ ticket_id: row.ticket_id,
1100
+ selection: parseStoredObject(row.selection_json),
1101
+ summary: parseStoredObject(row.summary_json),
1102
+ exit_code: row.exit_code,
1103
+ duration_ms: row.duration_ms,
1104
+ output: row.output,
1105
+ output_truncated: row.output_truncated === 1,
1106
+ output_retention_state: row.output_retention_state,
1107
+ output_expired_at: row.output_expired_at,
1108
+ output_byte_count: row.output_byte_count ?? Buffer.byteLength(row.output, 'utf8'),
1109
+ });
1110
+ export const getTestExecutionDetails = (projectId, runId) => {
1111
+ const row = db.prepare(`${testDetailsSelect} WHERE er.project_id = ? AND er.id = ?`)
1112
+ .get(projectId, runId);
1113
+ return row ? serializeTestDetails(row) : null;
1114
+ };
1115
+ export const updateTestExecutionDetails = (input) => {
1116
+ const current = getTestExecutionDetails(input.projectId, input.runId);
1117
+ if (!current) {
1118
+ throw new Error(`Test execution run ${input.runId} was not found in project ${input.projectId}`);
1119
+ }
1120
+ const boundedOutput = input.output === undefined
1121
+ ? { output: current.output, outputTruncated: current.output_truncated }
1122
+ : boundTestOutput(input.output, input.outputTruncated ?? current.output_truncated);
1123
+ db.prepare(`
1124
+ UPDATE test_execution_details
1125
+ SET summary_json = ?, exit_code = ?, duration_ms = ?, output = ?, output_truncated = ?,
1126
+ output_retention_state = ?, output_expired_at = ?, output_byte_count = ?
1127
+ WHERE execution_run_id = ?
1128
+ `).run(input.summary === undefined ? serializeBoundedObject(current.summary, 'Test summary') : serializeBoundedObject(input.summary, 'Test summary'), input.exitCode === undefined ? current.exit_code : input.exitCode, input.durationMs === undefined ? current.duration_ms : input.durationMs, boundedOutput.output, boundedOutput.outputTruncated ? 1 : 0, input.output === undefined ? current.output_retention_state : 'retained', input.output === undefined ? current.output_expired_at : null, input.output === undefined ? current.output_byte_count : Buffer.byteLength(boundedOutput.output, 'utf8'), input.runId);
1129
+ return getTestExecutionDetails(input.projectId, input.runId);
1130
+ };
1131
+ export const reconcileInterruptedTestRuns = (emitter) => {
1132
+ const rows = db.prepare(`
1133
+ SELECT project_id, id
1134
+ FROM execution_runs
1135
+ WHERE kind = 'test' AND status IN ('queued', 'running', 'waiting')
1136
+ `).all();
1137
+ for (const row of rows) {
1138
+ transitionExecutionRun({
1139
+ projectId: row.project_id,
1140
+ runId: row.id,
1141
+ status: 'interrupted',
1142
+ domainStatus: 'interrupted',
1143
+ failure: {
1144
+ code: 'server_restarted',
1145
+ message: 'The server restarted while this local test run was active.',
1146
+ },
1147
+ eventType: 'test_run_interrupted',
1148
+ }, emitter);
1149
+ }
1150
+ return rows.length;
1151
+ };
1152
+ const serializeGitHubActionsDetails = (row) => ({
1153
+ execution_run_id: row.execution_run_id,
1154
+ project_id: row.project_id,
1155
+ execution_status: row.execution_status,
1156
+ domain_status: row.domain_status,
1157
+ failure: row.failure_message ? {
1158
+ code: row.failure_code,
1159
+ message: row.failure_message,
1160
+ details: parseStoredObject(row.failure_details_json),
1161
+ } : null,
1162
+ workflow_id: row.workflow_id,
1163
+ workflow_name: row.workflow_name,
1164
+ ref: row.ref,
1165
+ input_names: parseStoredArray(row.input_names_json).filter(value => typeof value === 'string'),
1166
+ input_fingerprint: row.input_fingerprint,
1167
+ resolution_attempts: row.resolution_attempts,
1168
+ provider_error_count: row.provider_error_count,
1169
+ provider_run_id: row.provider_run_id,
1170
+ provider_status: row.provider_status,
1171
+ conclusion: row.conclusion,
1172
+ html_url: row.html_url,
1173
+ head_sha: row.head_sha,
1174
+ head_branch: row.head_branch,
1175
+ provider_created_at: row.provider_created_at,
1176
+ provider_updated_at: row.provider_updated_at,
1177
+ provider_started_at: row.provider_started_at,
1178
+ dispatched_at: row.dispatched_at,
1179
+ last_synced_at: row.last_synced_at,
1180
+ jobs: parseStoredArray(row.jobs_json),
1181
+ jobs_retention_state: row.jobs_retention_state,
1182
+ jobs_expired_at: row.jobs_expired_at,
1183
+ jobs_count: row.jobs_count ?? parseStoredArray(row.jobs_json).length,
1184
+ });
1185
+ const githubActionsDetailsSelect = `
1186
+ SELECT ga.*, er.project_id, er.status AS execution_status, er.domain_status,
1187
+ er.failure_code, er.failure_message, er.failure_details_json
1188
+ FROM github_actions_run_details ga
1189
+ JOIN execution_runs er ON er.id = ga.execution_run_id
1190
+ `;
1191
+ export const getGitHubActionsExecutionDetails = (projectId, runId) => {
1192
+ const row = db.prepare(`${githubActionsDetailsSelect} WHERE er.project_id = ? AND er.id = ?`)
1193
+ .get(projectId, runId);
1194
+ return row ? serializeGitHubActionsDetails(row) : null;
1195
+ };
1196
+ export const listGitHubActionsExecutionDetails = (projectId, statuses) => {
1197
+ const clauses = ["er.kind = 'github_actions'"];
1198
+ const parameters = [];
1199
+ if (projectId) {
1200
+ clauses.push('er.project_id = ?');
1201
+ parameters.push(projectId);
1202
+ }
1203
+ if (statuses && statuses.length > 0) {
1204
+ clauses.push(`er.status IN (${statuses.map(() => '?').join(', ')})`);
1205
+ parameters.push(...statuses);
1206
+ }
1207
+ const rows = db.prepare(`${githubActionsDetailsSelect} WHERE ${clauses.join(' AND ')} ORDER BY er.created_at DESC, er.id DESC`)
1208
+ .all(...parameters);
1209
+ return rows.map(serializeGitHubActionsDetails);
1210
+ };
1211
+ export const updateGitHubActionsRunDetails = (input) => {
1212
+ const current = getGitHubActionsExecutionDetails(input.projectId, input.runId);
1213
+ if (!current) {
1214
+ throw new Error(`GitHub Actions execution run ${input.runId} was not found in project ${input.projectId}`);
1215
+ }
1216
+ const jobsJson = input.jobs === undefined ? serializeJobsSnapshot(current.jobs) : serializeJobsSnapshot(input.jobs);
1217
+ db.prepare(`
1218
+ UPDATE github_actions_run_details
1219
+ SET workflow_name = ?, resolution_attempts = ?, provider_error_count = ?, provider_run_id = ?, provider_status = ?,
1220
+ conclusion = ?, html_url = ?, head_sha = ?, head_branch = ?, provider_created_at = ?,
1221
+ provider_updated_at = ?, provider_started_at = ?, last_synced_at = ?, jobs_json = ?,
1222
+ jobs_retention_state = ?, jobs_expired_at = ?, jobs_count = ?
1223
+ WHERE execution_run_id = ?
1224
+ `).run(input.workflowName === undefined ? current.workflow_name : input.workflowName, input.resolutionAttempts ?? current.resolution_attempts, input.providerErrorCount ?? current.provider_error_count, input.providerRunId === undefined ? current.provider_run_id : input.providerRunId, input.providerStatus === undefined ? current.provider_status : input.providerStatus, input.conclusion === undefined ? current.conclusion : input.conclusion, input.htmlUrl === undefined ? current.html_url : input.htmlUrl, input.headSha === undefined ? current.head_sha : input.headSha, input.headBranch === undefined ? current.head_branch : input.headBranch, input.providerCreatedAt === undefined ? current.provider_created_at : input.providerCreatedAt, input.providerUpdatedAt === undefined ? current.provider_updated_at : input.providerUpdatedAt, input.providerStartedAt === undefined ? current.provider_started_at : input.providerStartedAt, input.lastSyncedAt === undefined ? current.last_synced_at : input.lastSyncedAt, jobsJson, input.jobs === undefined ? current.jobs_retention_state : 'retained', input.jobs === undefined ? current.jobs_expired_at : null, input.jobs === undefined ? current.jobs_count : input.jobs?.length ?? 0, input.runId);
1225
+ return getGitHubActionsExecutionDetails(input.projectId, input.runId);
1226
+ };
1227
+ export const toGitHubActionsRun = (details) => {
1228
+ if (details.provider_run_id === null) {
1229
+ return null;
1230
+ }
1231
+ return {
1232
+ id: details.provider_run_id,
1233
+ name: details.workflow_name,
1234
+ workflow_id: details.workflow_id,
1235
+ status: details.provider_status === 'queued' || details.provider_status === 'in_progress' || details.provider_status === 'completed'
1236
+ ? details.provider_status
1237
+ : 'unknown',
1238
+ conclusion: details.conclusion,
1239
+ html_url: details.html_url,
1240
+ head_branch: details.head_branch,
1241
+ created_at: details.provider_created_at,
1242
+ updated_at: details.provider_updated_at,
1243
+ run_started_at: details.provider_started_at,
1244
+ jobs: details.jobs,
1245
+ };
1246
+ };
1247
+ export const listExecutionRuns = (input) => {
1248
+ const limit = input.limit ?? 50;
1249
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
1250
+ throw new Error('Execution run page limit must be between 1 and 200');
1251
+ }
1252
+ const statuses = [...new Set(input.statuses ?? [])];
1253
+ const cursor = input.cursor ? decodeCursor(input.cursor) : null;
1254
+ const clauses = ['er.project_id = ?'];
1255
+ const parameters = [input.projectId];
1256
+ if (input.kind) {
1257
+ clauses.push('er.kind = ?');
1258
+ parameters.push(input.kind);
1259
+ }
1260
+ if (statuses.length > 0) {
1261
+ clauses.push(`er.status IN (${statuses.map(() => '?').join(', ')})`);
1262
+ parameters.push(...statuses);
1263
+ }
1264
+ if (cursor) {
1265
+ clauses.push('(er.created_at < ? OR (er.created_at = ? AND er.id < ?))');
1266
+ parameters.push(cursor.created_at, cursor.created_at, cursor.id);
1267
+ }
1268
+ if (input.scopeEntity) {
1269
+ clauses.push(`EXISTS (
1270
+ SELECT 1 FROM execution_run_links scope_link
1271
+ WHERE scope_link.execution_run_id = er.id
1272
+ AND scope_link.project_id = er.project_id
1273
+ AND scope_link.entity_type = ?
1274
+ AND scope_link.entity_id = ?
1275
+ )`);
1276
+ parameters.push(input.scopeEntity.type, input.scopeEntity.id);
1277
+ }
1278
+ const rows = db.prepare(`
1279
+ SELECT er.* FROM execution_runs er
1280
+ WHERE ${clauses.join(' AND ')}
1281
+ ORDER BY er.created_at DESC, er.id DESC
1282
+ LIMIT ?
1283
+ `).all(...parameters, limit + 1);
1284
+ const pageRows = rows.slice(0, limit);
1285
+ const sourceAvailability = loadSourceAvailability(pageRows);
1286
+ const lastRow = pageRows.at(-1);
1287
+ return {
1288
+ runs: pageRows.map(row => serializeRun(row, sourceAvailability)),
1289
+ next_cursor: rows.length > limit && lastRow
1290
+ ? encodeCursor({ created_at: lastRow.created_at, id: lastRow.id })
1291
+ : null,
1292
+ };
1293
+ };
1294
+ export const listLifecycleEvents = (projectId, runId) => {
1295
+ getRunRow(projectId, runId);
1296
+ const rows = db.prepare(`
1297
+ SELECT * FROM lifecycle_events
1298
+ WHERE project_id = ? AND execution_run_id = ?
1299
+ ORDER BY sequence ASC
1300
+ `).all(projectId, runId);
1301
+ return rows.map(serializeEvent);
1302
+ };
1303
+ export const listLatestLifecycleEventsForRuns = (projectId, runIds) => {
1304
+ const ids = [...new Set(runIds)];
1305
+ if (ids.length === 0) {
1306
+ return [];
1307
+ }
1308
+ const rows = db.prepare(`
1309
+ SELECT * FROM (
1310
+ SELECT lifecycle_events.*,
1311
+ ROW_NUMBER() OVER (PARTITION BY execution_run_id ORDER BY sequence DESC, id DESC) AS timeline_rank
1312
+ FROM lifecycle_events
1313
+ WHERE project_id = ? AND execution_run_id IN (${ids.map(() => '?').join(', ')})
1314
+ )
1315
+ WHERE timeline_rank = 1
1316
+ `).all(projectId, ...ids);
1317
+ return rows.map(({ timeline_rank: _timelineRank, ...row }) => serializeEvent(row));
1318
+ };
1319
+ export const listProjectLifecycleEvents = (input) => {
1320
+ const limit = input.limit ?? 50;
1321
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
1322
+ throw new Error('Lifecycle event page limit must be between 1 and 200');
1323
+ }
1324
+ const statuses = [...new Set(input.statuses ?? [])];
1325
+ const cursor = input.cursor ? decodeLifecycleEventCursor(input.cursor) : null;
1326
+ const clauses = ['le.project_id = ?'];
1327
+ const parameters = [input.projectId];
1328
+ if (input.kind) {
1329
+ clauses.push('er.kind = ?');
1330
+ parameters.push(input.kind);
1331
+ }
1332
+ if (statuses.length > 0) {
1333
+ clauses.push(`er.status IN (${statuses.map(() => '?').join(', ')})`);
1334
+ parameters.push(...statuses);
1335
+ }
1336
+ if (cursor) {
1337
+ clauses.push(`(
1338
+ le.created_at < ?
1339
+ OR (le.created_at = ? AND le.execution_run_id < ?)
1340
+ OR (le.created_at = ? AND le.execution_run_id = ? AND le.sequence < ?)
1341
+ OR (le.created_at = ? AND le.execution_run_id = ? AND le.sequence = ? AND le.id < ?)
1342
+ )`);
1343
+ parameters.push(cursor.created_at, cursor.created_at, cursor.execution_run_id, cursor.created_at, cursor.execution_run_id, cursor.sequence, cursor.created_at, cursor.execution_run_id, cursor.sequence, cursor.id);
1344
+ }
1345
+ const rows = db.prepare(`
1346
+ SELECT le.*
1347
+ FROM lifecycle_events le
1348
+ JOIN execution_runs er ON er.id = le.execution_run_id AND er.project_id = le.project_id
1349
+ WHERE ${clauses.join(' AND ')}
1350
+ ORDER BY le.created_at DESC, le.execution_run_id DESC, le.sequence DESC, le.id DESC
1351
+ LIMIT ?
1352
+ `).all(...parameters, limit + 1);
1353
+ const pageRows = rows.slice(0, limit);
1354
+ const lastRow = pageRows.at(-1);
1355
+ return {
1356
+ events: pageRows.map(serializeEvent),
1357
+ next_cursor: rows.length > limit && lastRow
1358
+ ? encodeLifecycleEventCursor({
1359
+ created_at: lastRow.created_at,
1360
+ execution_run_id: lastRow.execution_run_id,
1361
+ sequence: lastRow.sequence,
1362
+ id: lastRow.id,
1363
+ })
1364
+ : null,
1365
+ };
1366
+ };
1367
+ //# sourceMappingURL=executionRuns.js.map