@straiffi/archon 1.7.3 → 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 (116) 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-BzzGotac.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DIcqLHKg.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-Hc1Sqmhs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
  7. package/dist/client/assets/channel-CqNHF3FD.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-CWSwjsgk.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-BnX3DljN.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-EyyJlI-d.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-Ci6CfxWj.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-RKZNsJTm.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DBf0Euoj.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-BmLQI_2S.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-DKwVAb8G.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-axotKkbz.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-BBlJ4nXC.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-BKJGoZZt.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-C-Ghigo1.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-CCzVWYcd.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-rwIdPcjI.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DJWxOQsg.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-C-mQeFtd.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-BAu6u4sR.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-U6CbvBn0.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-DNTwFcs1.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DaHX6Xw5.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-B9hKqL2l.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-BQ1HPHVO.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BBRcsmMK.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-DUoNiSGC.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DJN1y2w8.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BxLLMT70.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BdCPistJ.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
  39. package/dist/client/assets/{line-4mIgXsZd.js → line-PHHbm9rj.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-Pu1QGWJ-.js → mermaid-parser.core-CLxAkW69.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-DnH9DrgZ.js → mermaid.core-CebvSmx9.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DqE4ba3C.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-Crjy9ejf.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BUSZ2VB7.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CBhFKEoV.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CgdbY41v.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-hkp_H7AE.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-C6x45RWp.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-CjnZWttQ.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-DU5ZyYju.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CaCEnCB0.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-Ce3xa37U.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +1 -0
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +145 -88
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/attachments.js +24 -0
  60. package/dist/server/lib/attachments.js.map +1 -1
  61. package/dist/server/lib/buildOpenQuestionOperations.js +17 -2
  62. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -1
  63. package/dist/server/lib/bundleReviewOperations.js +9 -0
  64. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  65. package/dist/server/lib/chatOperations.js +40 -16
  66. package/dist/server/lib/chatOperations.js.map +1 -1
  67. package/dist/server/lib/executionRetry.js +694 -0
  68. package/dist/server/lib/executionRetry.js.map +1 -0
  69. package/dist/server/lib/executionRetryContext.js +489 -0
  70. package/dist/server/lib/executionRetryContext.js.map +1 -0
  71. package/dist/server/lib/executionRetryPredicates.js +74 -0
  72. package/dist/server/lib/executionRetryPredicates.js.map +1 -0
  73. package/dist/server/lib/executionRuns.js +141 -38
  74. package/dist/server/lib/executionRuns.js.map +1 -1
  75. package/dist/server/lib/executionTimeline.js +45 -0
  76. package/dist/server/lib/executionTimeline.js.map +1 -0
  77. package/dist/server/lib/specOperations.js +51 -16
  78. package/dist/server/lib/specOperations.js.map +1 -1
  79. package/dist/server/lib/specReviewContext.js +10 -4
  80. package/dist/server/lib/specReviewContext.js.map +1 -1
  81. package/dist/server/lib/specReviewOperations.js +33 -5
  82. package/dist/server/lib/specReviewOperations.js.map +1 -1
  83. package/dist/server/lib/testSessions.js +2 -5
  84. package/dist/server/lib/testSessions.js.map +1 -1
  85. package/dist/server/lib/ticketFollowUpOperations.js +74 -27
  86. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  87. package/dist/server/lib/ticketMessages.js +16 -0
  88. package/dist/server/lib/ticketMessages.js.map +1 -1
  89. package/dist/server/lib/types.js +1 -0
  90. package/dist/server/lib/types.js.map +1 -1
  91. package/dist/server/workers/build.js +1 -1
  92. package/dist/server/workers/build.js.map +1 -1
  93. package/dist/server/workers/chat.js +26 -20
  94. package/dist/server/workers/chat.js.map +1 -1
  95. package/dist/server/workers/followUp.js +24 -9
  96. package/dist/server/workers/followUp.js.map +1 -1
  97. package/dist/server/workers/plan.js +23 -4
  98. package/dist/server/workers/plan.js.map +1 -1
  99. package/dist/server/workers/planFollowUp.js +12 -9
  100. package/dist/server/workers/planFollowUp.js.map +1 -1
  101. package/dist/server/workers/review.js +2 -2
  102. package/dist/server/workers/review.js.map +1 -1
  103. package/dist/server/workers/specReview.js +2 -2
  104. package/dist/server/workers/specReview.js.map +1 -1
  105. package/dist/server/workers/specSession.js +29 -22
  106. package/dist/server/workers/specSession.js.map +1 -1
  107. package/package.json +1 -1
  108. package/dist/client/assets/ActivityView-DTEenQas.js +0 -1
  109. package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +0 -1
  110. package/dist/client/assets/TestsDialog-cBaoffCs.js +0 -4
  111. package/dist/client/assets/channel-Cym17waa.js +0 -1
  112. package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +0 -1
  113. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +0 -1
  114. package/dist/client/assets/index-CEMaJCPS.js +0 -273
  115. package/dist/client/assets/index-lWftq65U.css +0 -2
  116. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +0 -1
@@ -0,0 +1,694 @@
1
+ import { existsSync } from 'fs';
2
+ import db from '../db.js';
3
+ import { startBuild } from '../workers/build.js';
4
+ import { startPlan } from '../workers/plan.js';
5
+ import { listChatAttachmentRowsForMessage, listSpecAttachmentRowsForMessage } from './attachments.js';
6
+ import { prepareTicketForBuild } from './buildFlow.js';
7
+ import { compensateBuildSequenceAcceptance, enqueueBundledBuild, hasActiveSequenceForBundle } from './buildSequences.js';
8
+ import { getTicketReviewRetryEligibilityError, rerunTicketReviewOperation } from './bundleReviewOperations.js';
9
+ import { getBundlePlanMutationBlockerById, shouldBlockPlanForBundleMutationById } from './bundleActivity.js';
10
+ import { submitProjectChatSessionMessage } from './chatOperations.js';
11
+ import { getChatMessage } from './chatMessages.js';
12
+ import { doesChatSessionTargetMatchProjectTarget, resolveChatSessionTarget } from './chatTargets.js';
13
+ import { getChatSession } from './chats.js';
14
+ import { getDirectRetryChild, getExecutionRun, getTestExecutionDetails, hasLaterExecutionRunForPrimaryEntity, listExecutionRunLinks, withExecutionRunTransaction } from './executionRuns.js';
15
+ import { buildExecutionRetryContext, createExecutionRetryResolutionContext, resolveExecutionRetryEligibilityFromContext } from './executionRetryContext.js';
16
+ import { hasExecutionRetryAutomationOwnership, isLaterRunSupersededKind, isRetryableExecutionStatus, parseExecutionRetryTestSelection, RETRYABLE_AGENT_STATUSES, RETRYABLE_EXECUTION_KINDS, RETRYABLE_TEST_STATUSES, resolveExecutionRetryRunConfig, resolveExecutionRetrySessionMode, } from './executionRetryPredicates.js';
17
+ import { getProjectBranch, resolveExistingWorktreePath } from './git.js';
18
+ import { getEffectiveProject, getProjectById } from './projects.js';
19
+ import { getBundleAutomationState } from './ticketAutomation.js';
20
+ import { submitPlanTicketFollowUp, submitTicketFollowUp } from './ticketFollowUpOperations.js';
21
+ import { getTicketMessage } from './ticketMessages.js';
22
+ import { getBundle, getTicket, resolveTicketBranch } from './tickets.js';
23
+ import { doesTestFileExist, findProjectTestCommand, isTestSelectionSupported } from './testSessions.js';
24
+ import { getBundleBuildBlockerResponse } from './ticketWorkflowOperations.js';
25
+ import { submitProjectSpecSessionMessage } from './specOperations.js';
26
+ import { getSpecMessage } from './specMessages.js';
27
+ import { startProjectSpecReview } from './specReviewOperations.js';
28
+ import { getSpecSession, getSpecSnapshot } from './specs.js';
29
+ const deny = (reason, status, message) => ({
30
+ eligibility: { eligible: false, reason },
31
+ error: { status, message },
32
+ });
33
+ const resolveTicketWorkspace = (ticket, project) => {
34
+ const branch = resolveTicketBranch(ticket);
35
+ if (!branch) {
36
+ return null;
37
+ }
38
+ if (ticket.worktree_bundle_id) {
39
+ const bundle = getBundle(ticket.worktree_bundle_id, project.id);
40
+ if (!bundle) {
41
+ return null;
42
+ }
43
+ if (bundle.kind === 'project_root') {
44
+ const branchState = getProjectBranch(project, { includeDiffStats: false });
45
+ return branchState.status === 'ready' && branchState.branch
46
+ ? { project, cwd: project.repo_path, target_kind: 'repo_root', target_bundle_id: null, target_label: branchState.branch }
47
+ : null;
48
+ }
49
+ else {
50
+ const cwd = resolveExistingWorktreePath(bundle.branch, project);
51
+ return cwd
52
+ ? { project, cwd, target_kind: 'bundle', target_bundle_id: bundle.id, target_label: bundle.name || bundle.branch }
53
+ : null;
54
+ }
55
+ }
56
+ const cwd = resolveExistingWorktreePath(branch, project);
57
+ return cwd
58
+ ? { project, cwd, target_kind: 'repo_root', target_bundle_id: null, target_label: branch }
59
+ : null;
60
+ };
61
+ const resolveTestWorkspace = (project, details) => {
62
+ if (details.ticket_id) {
63
+ const ticket = getTicket(details.ticket_id);
64
+ const workspace = ticket?.project_id === project.id ? resolveTicketWorkspace(ticket, project) : null;
65
+ return workspace
66
+ && workspace.target_kind === details.target_kind
67
+ && workspace.target_bundle_id === details.target_bundle_id
68
+ ? workspace
69
+ : null;
70
+ }
71
+ if (details.target_kind === 'bundle' && details.target_bundle_id) {
72
+ const bundle = getBundle(details.target_bundle_id, project.id);
73
+ if (!bundle || bundle.kind === 'project_root') {
74
+ return null;
75
+ }
76
+ const cwd = resolveExistingWorktreePath(bundle.branch, project);
77
+ return cwd
78
+ ? { project, cwd, target_kind: 'bundle', target_bundle_id: bundle.id, target_label: bundle.name || bundle.branch }
79
+ : null;
80
+ }
81
+ if (details.target_kind !== 'repo_root') {
82
+ return null;
83
+ }
84
+ const branchState = getProjectBranch(project, { includeDiffStats: false });
85
+ return branchState.status === 'ready' && branchState.branch
86
+ ? { project, cwd: project.repo_path, target_kind: 'repo_root', target_bundle_id: null, target_label: 'Repo root' }
87
+ : null;
88
+ };
89
+ const resolvePrimaryTicket = (projectId, source) => {
90
+ const links = listExecutionRunLinks(projectId, source.id);
91
+ const primaryLinks = links.filter(link => link.relationship === 'primary' && link.entity_type === 'ticket');
92
+ if (primaryLinks.length !== 1) {
93
+ return null;
94
+ }
95
+ const ticket = getTicket(primaryLinks[0].entity_id);
96
+ return ticket?.project_id === projectId ? ticket : null;
97
+ };
98
+ const resolveBundleId = (projectId, source, ticket) => {
99
+ const bundleLinks = listExecutionRunLinks(projectId, source.id)
100
+ .filter(link => link.relationship === 'context' && link.entity_type === 'bundle');
101
+ if (bundleLinks.length > 1) {
102
+ return undefined;
103
+ }
104
+ const bundleId = bundleLinks[0]?.entity_id ?? ticket.worktree_bundle_id ?? null;
105
+ if (bundleId !== (ticket.worktree_bundle_id ?? null)) {
106
+ return undefined;
107
+ }
108
+ return bundleId;
109
+ };
110
+ const resolvePlanTarget = (projectId, source) => {
111
+ const bundleId = source.metadata?.plan_target_bundle_id;
112
+ const branch = source.metadata?.plan_target_branch;
113
+ if (!Object.prototype.hasOwnProperty.call(source.metadata ?? {}, 'plan_target_bundle_id')
114
+ || !Object.prototype.hasOwnProperty.call(source.metadata ?? {}, 'plan_target_branch')
115
+ || (typeof bundleId !== 'string' && bundleId !== null)
116
+ || (typeof branch !== 'string' && branch !== null)) {
117
+ return null;
118
+ }
119
+ const bundleLinks = listExecutionRunLinks(projectId, source.id)
120
+ .filter(link => link.relationship === 'context' && link.entity_type === 'bundle');
121
+ if (bundleId === null) {
122
+ return bundleLinks.length === 0 ? { bundleId, branch } : null;
123
+ }
124
+ const bundle = getBundle(bundleId, projectId);
125
+ return bundleLinks.length === 1 && bundleLinks[0].entity_id === bundleId && bundle?.branch === branch
126
+ ? { bundleId, branch }
127
+ : null;
128
+ };
129
+ const hasAutomationOwnership = (source, bundleId) => {
130
+ const parent = source.parent_run_id ? getExecutionRun(source.project_id, source.parent_run_id) : null;
131
+ const state = bundleId ? getBundleAutomationState(source.project_id, bundleId) : null;
132
+ return hasExecutionRetryAutomationOwnership({
133
+ source,
134
+ parentKind: parent?.kind ?? null,
135
+ automationPausedStageRunId: state?.automation_paused_stage_run_id ?? null,
136
+ });
137
+ };
138
+ const resolveTrigger = (source, ticket) => {
139
+ const triggerLinks = listExecutionRunLinks(source.project_id, source.id)
140
+ .filter(link => link.relationship === 'trigger' && link.entity_type === 'ticket_message');
141
+ if (triggerLinks.length !== 1) {
142
+ return null;
143
+ }
144
+ const message = getTicketMessage(triggerLinks[0].entity_id);
145
+ const expectedKind = source.kind === 'plan_follow_up' ? 'planning_follow_up' : 'follow_up';
146
+ if (!message
147
+ || message.ticket_id !== ticket.id
148
+ || message.role !== 'user'
149
+ || message.kind !== expectedKind
150
+ || !message.content.trim()) {
151
+ return null;
152
+ }
153
+ return { id: message.id, instruction: message.content.trim() };
154
+ };
155
+ const resolveSessionRetry = (project, source) => {
156
+ const isChat = source.kind === 'chat_turn';
157
+ const primaryType = isChat ? 'chat_session' : 'spec_session';
158
+ const triggerType = isChat ? 'chat_message' : 'spec_message';
159
+ const links = listExecutionRunLinks(project.id, source.id);
160
+ const primary = links.filter(link => link.relationship === 'primary' && link.entity_type === primaryType);
161
+ const triggers = links.filter(link => link.relationship === 'trigger' && link.entity_type === triggerType);
162
+ if (primary.length !== 1 || triggers.length !== 1) {
163
+ return deny('retry_input_unavailable', 409, 'The exact session trigger is unavailable.');
164
+ }
165
+ const session = isChat
166
+ ? getChatSession(primary[0].entity_id, { includeMessages: true })
167
+ : getSpecSession(primary[0].entity_id, { includeMessages: true });
168
+ const message = isChat ? getChatMessage(triggers[0].entity_id) : getSpecMessage(triggers[0].entity_id);
169
+ const messageBelongsToSession = isChat
170
+ ? message !== null && 'chat_session_id' in message && message.chat_session_id === session?.id
171
+ : message !== null && 'spec_session_id' in message && message.spec_session_id === session?.id;
172
+ if (!session
173
+ || session.project_id !== project.id
174
+ || session.agent_status === 'running'
175
+ || !message
176
+ || !messageBelongsToSession
177
+ || message.role !== 'user'
178
+ || message.kind !== 'prompt') {
179
+ return deny(session?.agent_status === 'running' ? 'retry_target_busy' : 'retry_input_unavailable', 409, 'The session retry input is unavailable.');
180
+ }
181
+ const attachments = isChat
182
+ ? listChatAttachmentRowsForMessage(session.id, message.id)
183
+ : listSpecAttachmentRowsForMessage(session.id, message.id);
184
+ const expectedAttachmentCount = source.metadata?.retry_attachment_count;
185
+ if (!Number.isInteger(expectedAttachmentCount)
186
+ || expectedAttachmentCount !== attachments.length
187
+ || (!message.content.trim() && attachments.length === 0)
188
+ || attachments.some(attachment => !existsSync(attachment.storage_path))) {
189
+ return deny('retry_input_unavailable', 409, 'The session retry input is unavailable.');
190
+ }
191
+ const replay = buildExecutionRetryContext({ messages: session.messages ?? [], triggerMessageId: message.id });
192
+ if (!replay.ok) {
193
+ if (replay.hasLaterUserTurn) {
194
+ return deny('attempt_superseded', 409, 'A newer session turn has already advanced the conversation.');
195
+ }
196
+ return deny('retry_input_unavailable', 409, 'The pre-trigger conversation is unavailable.');
197
+ }
198
+ if (hasLaterExecutionRunForPrimaryEntity({
199
+ projectId: project.id,
200
+ sourceRunId: source.id,
201
+ kind: isChat ? 'chat_turn' : 'spec_turn',
202
+ entityType: primaryType,
203
+ entityId: session.id,
204
+ })) {
205
+ return deny('attempt_superseded', 409, 'A newer session turn has already advanced the conversation.');
206
+ }
207
+ const runConfig = resolveExecutionRetryRunConfig(source);
208
+ if (!runConfig) {
209
+ return deny('retry_input_unavailable', 409, 'The original provider configuration is unavailable.');
210
+ }
211
+ const sessionMode = resolveExecutionRetrySessionMode(source);
212
+ if (!sessionMode) {
213
+ return deny('retry_input_unavailable', 409, 'The original session mode is unavailable.');
214
+ }
215
+ if (isChat) {
216
+ const target = resolveChatSessionTarget(session, project);
217
+ if (!target.target_available || !doesChatSessionTargetMatchProjectTarget(session, project)) {
218
+ return deny('retry_workspace_unavailable', 409, 'The Chat workspace is unavailable.');
219
+ }
220
+ }
221
+ else if (!existsSync(project.repo_path)) {
222
+ return deny('retry_workspace_unavailable', 409, 'The Spec workspace is unavailable.');
223
+ }
224
+ let priorSnapshotId;
225
+ if (!isChat) {
226
+ const priorSnapshotLinks = links.filter(link => link.relationship === 'context' && link.entity_type === 'spec_snapshot');
227
+ const resultSnapshotLinks = links.filter(link => link.relationship === 'result' && link.entity_type === 'spec_snapshot');
228
+ if (priorSnapshotLinks.length > 1 || resultSnapshotLinks.length > 0) {
229
+ return deny('retry_input_unavailable', 409, 'The prior Spec snapshot is unavailable.');
230
+ }
231
+ priorSnapshotId = priorSnapshotLinks[0]?.entity_id ?? null;
232
+ if (priorSnapshotId && getSpecSnapshot(priorSnapshotId)?.spec_session_id !== session.id) {
233
+ return deny('retry_input_unavailable', 409, 'The prior Spec snapshot is unavailable.');
234
+ }
235
+ }
236
+ return {
237
+ eligibility: { eligible: true, reason: null },
238
+ source,
239
+ project,
240
+ ticket: null,
241
+ bundleId: null,
242
+ triggerMessageId: message.id,
243
+ instruction: message.content.trim(),
244
+ followUpMode: null,
245
+ sessionId: session.id,
246
+ attachments,
247
+ conversationContext: replay.context,
248
+ priorSnapshotId,
249
+ runConfig: runConfig ?? undefined,
250
+ sessionMode: sessionMode,
251
+ };
252
+ };
253
+ const resolveSpecReviewRetry = (project, source) => {
254
+ const links = listExecutionRunLinks(project.id, source.id);
255
+ const primary = links.filter(link => link.relationship === 'primary' && link.entity_type === 'spec_session');
256
+ const snapshots = links.filter(link => link.relationship === 'context' && link.entity_type === 'spec_snapshot');
257
+ const runConfig = resolveExecutionRetryRunConfig(source);
258
+ const boundary = source.metadata?.retry_transcript_boundary_message_id;
259
+ if (primary.length !== 1 || snapshots.length !== 1 || !runConfig || typeof boundary !== 'string') {
260
+ return deny('retry_input_unavailable', 409, 'The Spec Review input is unavailable.');
261
+ }
262
+ const session = getSpecSession(primary[0].entity_id, { includeMessages: true });
263
+ const snapshot = getSpecSnapshot(snapshots[0].entity_id);
264
+ const boundaryId = boundary === 'none' ? null : boundary;
265
+ if (!session
266
+ || session.project_id !== project.id
267
+ || !snapshot
268
+ || snapshot.spec_session_id !== session.id
269
+ || (boundaryId && !session.messages?.some(message => message.id === boundaryId))) {
270
+ return deny('retry_input_unavailable', 409, 'The Spec Review input is unavailable.');
271
+ }
272
+ return {
273
+ eligibility: { eligible: true, reason: null },
274
+ source,
275
+ project,
276
+ ticket: null,
277
+ bundleId: null,
278
+ triggerMessageId: null,
279
+ instruction: null,
280
+ followUpMode: null,
281
+ sessionId: session.id,
282
+ priorSnapshotId: snapshot.id,
283
+ transcriptBoundaryMessageId: boundaryId,
284
+ runConfig: runConfig ?? undefined,
285
+ };
286
+ };
287
+ const resolveTestRetry = (project, source) => {
288
+ const details = getTestExecutionDetails(project.id, source.id);
289
+ if (!details?.command_id) {
290
+ return deny('retry_input_unavailable', 409, 'The original test command is no longer identifiable.');
291
+ }
292
+ const command = findProjectTestCommand(project, details.command_id);
293
+ const selection = parseExecutionRetryTestSelection(details.selection);
294
+ if (!command || !selection) {
295
+ return deny('retry_input_unavailable', 409, 'The original test input is no longer available.');
296
+ }
297
+ const workspace = resolveTestWorkspace(project, details);
298
+ if (!workspace) {
299
+ return deny('retry_workspace_unavailable', 409, 'The original test workspace is no longer available.');
300
+ }
301
+ if (!isTestSelectionSupported(command, selection, workspace.cwd)) {
302
+ return deny('retry_input_unavailable', 409, 'The original test selection is no longer supported.');
303
+ }
304
+ if (selection.kind !== 'all' && !doesTestFileExist(workspace.cwd, selection.path, command)) {
305
+ return deny('retry_input_unavailable', 409, 'The selected test file no longer exists.');
306
+ }
307
+ return {
308
+ eligibility: { eligible: true, reason: null },
309
+ source,
310
+ project,
311
+ ticket: details.ticket_id ? getTicket(details.ticket_id) : null,
312
+ bundleId: workspace.target_bundle_id,
313
+ triggerMessageId: null,
314
+ instruction: null,
315
+ followUpMode: null,
316
+ test: { command, selection, workspace, ticketId: details.ticket_id },
317
+ };
318
+ };
319
+ export const resolveExecutionRetry = (projectId, runId) => {
320
+ const project = getProjectById(projectId);
321
+ const source = project ? getExecutionRun(project.id, runId) : null;
322
+ if (!project || !source) {
323
+ return deny('retry_target_missing', 404, 'Execution run not found');
324
+ }
325
+ const links = listExecutionRunLinks(project.id, source.id);
326
+ const eligibility = resolveExecutionRetryEligibilityFromContext(createExecutionRetryResolutionContext({ project, runs: [source], links }), source.id);
327
+ if (!eligibility.eligible) {
328
+ return deny(eligibility.reason, eligibility.reason === 'retry_target_missing' ? 404 : 409, 'This execution is not eligible for retry.');
329
+ }
330
+ if (!RETRYABLE_EXECUTION_KINDS.has(source.kind)) {
331
+ return deny('unsupported_kind', 409, 'This execution kind cannot be retried through the generic retry API.');
332
+ }
333
+ if (!isRetryableExecutionStatus(source.kind, source.status)) {
334
+ return deny('status_not_retryable', 409, 'Only failed, cancelled, or interrupted executions can be retried.');
335
+ }
336
+ const directChild = getDirectRetryChild(project.id, source.id);
337
+ if (directChild) {
338
+ return (directChild.kind === 'test' ? RETRYABLE_TEST_STATUSES : RETRYABLE_AGENT_STATUSES).has(directChild.status)
339
+ ? deny('retry_source_superseded', 409, 'Retry the latest replacement attempt instead.')
340
+ : deny('retry_already_created', 409, 'A retry has already been created for this execution.');
341
+ }
342
+ if (source.kind === 'test') {
343
+ return resolveTestRetry(project, source);
344
+ }
345
+ if (source.kind === 'chat_turn' || source.kind === 'spec_turn') {
346
+ return resolveSessionRetry(project, source);
347
+ }
348
+ if (source.kind === 'spec_review') {
349
+ return resolveSpecReviewRetry(project, source);
350
+ }
351
+ const ticket = resolvePrimaryTicket(project.id, source);
352
+ if (!ticket) {
353
+ return deny('retry_target_missing', 404, 'The execution ticket no longer exists.');
354
+ }
355
+ if (isLaterRunSupersededKind(source.kind) && hasLaterExecutionRunForPrimaryEntity({
356
+ projectId: project.id,
357
+ sourceRunId: source.id,
358
+ kind: source.kind,
359
+ entityType: 'ticket',
360
+ entityId: ticket.id,
361
+ })) {
362
+ return deny('attempt_superseded', 409, 'A newer ticket execution has already advanced this workflow.');
363
+ }
364
+ const planTarget = source.kind === 'plan' ? resolvePlanTarget(project.id, source) : undefined;
365
+ const bundleId = source.kind === 'plan' ? planTarget?.bundleId : resolveBundleId(project.id, source, ticket);
366
+ const bundle = bundleId ? getBundle(bundleId, project.id) : null;
367
+ if ((source.kind === 'plan' && !planTarget) || bundleId === undefined || (bundleId && !bundle)) {
368
+ return deny('retry_target_missing', 404, 'The execution bundle no longer exists.');
369
+ }
370
+ if (hasAutomationOwnership(source, bundleId)) {
371
+ return deny('automation_resume_required', 409, 'This execution is owned by automation and must be resumed through automation recovery.');
372
+ }
373
+ if (ticket.agent_status === 'running') {
374
+ return deny('retry_target_busy', 409, 'The execution ticket is already running.');
375
+ }
376
+ const effectiveProject = getEffectiveProject(ticket);
377
+ if (!effectiveProject || effectiveProject.id !== project.id || !existsSync(project.repo_path)) {
378
+ return deny('retry_workspace_unavailable', 409, 'The execution workspace is unavailable.');
379
+ }
380
+ if (source.kind === 'plan' && bundle && bundle.kind !== 'project_root' && !resolveExistingWorktreePath(bundle.branch, project)) {
381
+ return deny('retry_workspace_unavailable', 409, 'The preserved Plan worktree is unavailable.');
382
+ }
383
+ if (source.kind === 'plan' && ticket.state !== 'plan') {
384
+ return deny('retry_state_incompatible', 409, 'The ticket is no longer in Plan.');
385
+ }
386
+ if (source.kind === 'plan'
387
+ && shouldBlockPlanForBundleMutationById(bundleId, planTarget?.branch, bundle?.branch)
388
+ && getBundlePlanMutationBlockerById(bundleId)) {
389
+ return deny('retry_target_busy', 409, 'The bundle is busy with other domain work.');
390
+ }
391
+ if (source.kind === 'build') {
392
+ if (ticket.state !== 'build') {
393
+ return deny('retry_state_incompatible', 409, 'The ticket is no longer in Build.');
394
+ }
395
+ if (getBundleBuildBlockerResponse(ticket)) {
396
+ return deny('retry_target_busy', 409, 'The bundle is busy with other domain work.');
397
+ }
398
+ const buildEligibility = prepareTicketForBuild(ticket.id, { updateState: false });
399
+ if (!buildEligibility.ok) {
400
+ return deny('retry_state_incompatible', buildEligibility.status, buildEligibility.error);
401
+ }
402
+ }
403
+ if (source.kind === 'review') {
404
+ const reviewError = getTicketReviewRetryEligibilityError(ticket);
405
+ if (reviewError) {
406
+ return deny('retry_state_incompatible', 409, reviewError);
407
+ }
408
+ }
409
+ if (source.kind === 'build_follow_up' && ticket.state === 'plan') {
410
+ return deny('retry_state_incompatible', 409, 'The ticket can no longer accept a Build follow-up.');
411
+ }
412
+ if (source.kind === 'plan_follow_up' && ticket.state !== 'plan') {
413
+ return deny('retry_state_incompatible', 409, 'The ticket can no longer accept a Plan follow-up.');
414
+ }
415
+ if ((source.kind === 'review' || source.kind === 'build_follow_up')
416
+ && !resolveExistingWorktreePath(resolveTicketBranch(ticket) ?? '', project)) {
417
+ return deny('retry_workspace_unavailable', 409, 'The execution worktree is unavailable.');
418
+ }
419
+ const trigger = source.kind === 'build_follow_up' || source.kind === 'plan_follow_up'
420
+ ? resolveTrigger(source, ticket)
421
+ : null;
422
+ if ((source.kind === 'build_follow_up' || source.kind === 'plan_follow_up') && !trigger) {
423
+ return deny('retry_input_unavailable', 409, 'The exact follow-up trigger is unavailable.');
424
+ }
425
+ const followUpMode = source.kind === 'build_follow_up'
426
+ ? source.metadata?.follow_up_mode
427
+ : null;
428
+ if (source.kind === 'build_follow_up' && followUpMode !== 'plan' && followUpMode !== 'build') {
429
+ return deny('retry_input_unavailable', 409, 'The Build follow-up mode was not preserved for this attempt.');
430
+ }
431
+ const runConfig = source.kind === 'build_follow_up' || source.kind === 'plan_follow_up'
432
+ ? resolveExecutionRetryRunConfig(source)
433
+ : undefined;
434
+ if ((source.kind === 'build_follow_up' || source.kind === 'plan_follow_up') && !runConfig) {
435
+ return deny('retry_input_unavailable', 409, 'The original follow-up provider configuration is unavailable.');
436
+ }
437
+ return {
438
+ eligibility: { eligible: true, reason: null },
439
+ source,
440
+ project,
441
+ ticket,
442
+ bundleId,
443
+ triggerMessageId: trigger?.id ?? null,
444
+ instruction: trigger?.instruction ?? null,
445
+ followUpMode: followUpMode,
446
+ planBranch: planTarget?.branch,
447
+ runConfig: runConfig ?? undefined,
448
+ };
449
+ };
450
+ const createdRetryResponse = (source) => {
451
+ const replacement = getDirectRetryChild(source.project_id, source.id);
452
+ return replacement ? {
453
+ execution_run_id: replacement.id,
454
+ retry_of_run_id: source.id,
455
+ attempt: replacement.attempt,
456
+ } : null;
457
+ };
458
+ const retryResponse = (source, replacementRunId) => {
459
+ const replacement = createdRetryResponse(source);
460
+ if (!replacement && !replacementRunId) {
461
+ throw new Error('Retry scheduling did not create a replacement execution');
462
+ }
463
+ return replacement ?? {
464
+ execution_run_id: replacementRunId,
465
+ retry_of_run_id: source.id,
466
+ attempt: source.attempt + 1,
467
+ };
468
+ };
469
+ export const dispatchExecutionRetry = async (resolution, broadcaster, dispatchTest) => {
470
+ if ('error' in resolution) {
471
+ return { ok: false, status: resolution.error.status, error: resolution.error.message, reason: resolution.eligibility.reason };
472
+ }
473
+ const { source, ticket } = resolution;
474
+ if (source.kind === 'test' && resolution.test) {
475
+ try {
476
+ const replacementRunId = await dispatchTest(resolution.test);
477
+ return { ok: true, value: retryResponse(source, replacementRunId) };
478
+ }
479
+ catch (error) {
480
+ const replacement = createdRetryResponse(source);
481
+ if (replacement) {
482
+ return { ok: true, value: replacement };
483
+ }
484
+ return { ok: false, status: 409, error: error instanceof Error ? error.message : String(error), reason: 'retry_input_unavailable' };
485
+ }
486
+ }
487
+ if ((source.kind === 'chat_turn' || source.kind === 'spec_turn') && resolution.sessionId && resolution.runConfig) {
488
+ const operation = source.kind === 'chat_turn'
489
+ ? submitProjectChatSessionMessage({
490
+ projectId: source.project_id,
491
+ chatSessionId: resolution.sessionId,
492
+ payload: { instruction: resolution.instruction },
493
+ broadcaster,
494
+ initiation: { initiator: 'user', retrySourceRunId: source.id },
495
+ existingTriggerMessageId: resolution.triggerMessageId ?? undefined,
496
+ existingAttachments: resolution.attachments,
497
+ providerContinuation: 'fresh',
498
+ conversationContext: resolution.conversationContext,
499
+ retryRunConfig: resolution.runConfig,
500
+ turnMode: resolution.sessionMode,
501
+ })
502
+ : submitProjectSpecSessionMessage({
503
+ projectId: source.project_id,
504
+ specSessionId: resolution.sessionId,
505
+ payload: { instruction: resolution.instruction },
506
+ broadcaster,
507
+ initiation: { initiator: 'user', retrySourceRunId: source.id },
508
+ existingTriggerMessageId: resolution.triggerMessageId ?? undefined,
509
+ existingAttachments: resolution.attachments,
510
+ providerContinuation: 'fresh',
511
+ conversationContext: resolution.conversationContext,
512
+ priorSnapshotId: resolution.priorSnapshotId,
513
+ retryRunConfig: resolution.runConfig,
514
+ turnMode: resolution.sessionMode,
515
+ });
516
+ if (!operation.ok) {
517
+ const replacement = createdRetryResponse(source);
518
+ if (replacement) {
519
+ return { ok: true, value: replacement };
520
+ }
521
+ return { ok: false, status: operation.status, error: operation.error, reason: 'retry_target_busy' };
522
+ }
523
+ return { ok: true, value: retryResponse(source) };
524
+ }
525
+ if (source.kind === 'spec_review' && resolution.sessionId && resolution.priorSnapshotId && resolution.runConfig) {
526
+ const operation = startProjectSpecReview({
527
+ projectId: source.project_id,
528
+ specSessionId: resolution.sessionId,
529
+ snapshotId: resolution.priorSnapshotId,
530
+ payload: undefined,
531
+ broadcaster,
532
+ retrySourceRunId: source.id,
533
+ retryRunConfig: resolution.runConfig,
534
+ transcriptBoundaryMessageId: resolution.transcriptBoundaryMessageId,
535
+ });
536
+ if (!operation.ok) {
537
+ const replacement = createdRetryResponse(source);
538
+ if (replacement) {
539
+ return { ok: true, value: replacement };
540
+ }
541
+ return { ok: false, status: operation.status, error: operation.error, reason: 'retry_target_busy' };
542
+ }
543
+ return { ok: true, value: retryResponse(source) };
544
+ }
545
+ if (!ticket) {
546
+ return { ok: false, status: 404, error: 'Retry target not found', reason: 'retry_target_missing' };
547
+ }
548
+ const initiation = { initiator: 'user', retrySourceRunId: source.id };
549
+ let schedulingError = null;
550
+ if (source.kind === 'plan') {
551
+ const scheduling = startPlan(ticket, broadcaster, {
552
+ branch: resolution.planBranch,
553
+ targetBundleId: resolution.bundleId,
554
+ initiation,
555
+ providerContinuation: 'fresh',
556
+ });
557
+ if (!scheduling.accepted) {
558
+ schedulingError = { status: scheduling.reason === 'queue_failed' ? 503 : 409, error: scheduling.error };
559
+ }
560
+ }
561
+ else if (source.kind === 'build') {
562
+ if (resolution.bundleId && hasActiveSequenceForBundle(source.project_id, resolution.bundleId)) {
563
+ try {
564
+ withExecutionRunTransaction(transaction => {
565
+ const updated = db.prepare(`
566
+ UPDATE tickets
567
+ SET agent_status = NULL, agent_log = NULL, streaming_response = NULL,
568
+ build_completed_at = NULL, run_setup_status = NULL, updated_at = CURRENT_TIMESTAMP
569
+ WHERE id = ? AND project_id = ? AND agent_status IS NOT 'running' AND state = 'build'
570
+ `).run(ticket.id, source.project_id);
571
+ if (updated.changes !== 1) {
572
+ throw new Error('Ticket is already running or no longer in Build');
573
+ }
574
+ const acceptedTicket = getTicket(ticket.id);
575
+ if (!acceptedTicket || !acceptedTicket.worktree_bundle_id) {
576
+ throw new Error('Retry target not found');
577
+ }
578
+ const sequenceId = enqueueBundledBuild({
579
+ ticketId: acceptedTicket.id,
580
+ projectId: source.project_id,
581
+ worktreeBundleId: acceptedTicket.worktree_bundle_id,
582
+ activate: false,
583
+ }).sequenceId;
584
+ transaction.createExecutionRun({
585
+ projectId: source.project_id,
586
+ kind: 'build',
587
+ domainStatus: 'queued',
588
+ sourceReference: { kind: 'ticket', id: acceptedTicket.id, label: acceptedTicket.title },
589
+ links: [
590
+ { entityType: 'bundle', entityId: acceptedTicket.worktree_bundle_id, relationship: 'context' },
591
+ { entityType: 'build_sequence', entityId: sequenceId, relationship: 'context' },
592
+ ],
593
+ initiatedBy: 'user',
594
+ retryOfRunId: source.id,
595
+ eventType: 'retry_started',
596
+ });
597
+ }, broadcaster);
598
+ }
599
+ catch (error) {
600
+ schedulingError = { status: 409, error: error instanceof Error ? error.message : String(error) };
601
+ }
602
+ }
603
+ else {
604
+ const scheduling = startBuild(ticket, broadcaster, {
605
+ resume: false,
606
+ initiation,
607
+ accept: () => {
608
+ const updated = db.prepare(`
609
+ UPDATE tickets
610
+ SET agent_status = NULL, agent_log = NULL, streaming_response = NULL,
611
+ build_completed_at = NULL, run_setup_status = NULL, updated_at = CURRENT_TIMESTAMP
612
+ WHERE id = ? AND project_id = ? AND agent_status IS NOT 'running' AND state = 'build'
613
+ `).run(ticket.id, source.project_id);
614
+ if (updated.changes !== 1) {
615
+ throw new Error('Ticket is already running or no longer in Build');
616
+ }
617
+ const acceptedTicket = getTicket(ticket.id);
618
+ if (!acceptedTicket) {
619
+ throw new Error('Retry target not found');
620
+ }
621
+ const sequenceId = acceptedTicket.worktree_bundle_id
622
+ ? enqueueBundledBuild({
623
+ ticketId: acceptedTicket.id,
624
+ projectId: source.project_id,
625
+ worktreeBundleId: acceptedTicket.worktree_bundle_id,
626
+ activate: true,
627
+ }).sequenceId
628
+ : null;
629
+ return {
630
+ ticket: acceptedTicket,
631
+ sequenceId,
632
+ ...(sequenceId ? { compensate: () => compensateBuildSequenceAcceptance(sequenceId, [acceptedTicket.id]) } : {}),
633
+ };
634
+ },
635
+ });
636
+ if (!scheduling.accepted) {
637
+ schedulingError = { status: scheduling.reason === 'queue_failed' ? 503 : 409, error: scheduling.error };
638
+ }
639
+ }
640
+ }
641
+ else if (source.kind === 'review') {
642
+ const result = rerunTicketReviewOperation(ticket, broadcaster, { initiation });
643
+ if (!result.ok) {
644
+ schedulingError = result;
645
+ }
646
+ }
647
+ else if (source.kind === 'build_follow_up') {
648
+ const result = submitTicketFollowUp({
649
+ ticketId: ticket.id,
650
+ requestedProjectId: source.project_id,
651
+ payload: { instruction: resolution.instruction, mode: resolution.followUpMode },
652
+ broadcaster,
653
+ initiation,
654
+ existingTriggerMessageId: resolution.triggerMessageId ?? undefined,
655
+ providerContinuation: 'fresh',
656
+ retryRunConfig: resolution.runConfig,
657
+ });
658
+ if (!result.ok) {
659
+ schedulingError = result;
660
+ }
661
+ }
662
+ else if (source.kind === 'plan_follow_up') {
663
+ const result = submitPlanTicketFollowUp({
664
+ ticketId: ticket.id,
665
+ requestedProjectId: source.project_id,
666
+ payload: { instruction: resolution.instruction },
667
+ broadcaster,
668
+ initiation,
669
+ existingTriggerMessageId: resolution.triggerMessageId ?? undefined,
670
+ providerContinuation: 'fresh',
671
+ retryRunConfig: resolution.runConfig,
672
+ });
673
+ if (!result.ok) {
674
+ schedulingError = result;
675
+ }
676
+ }
677
+ if (schedulingError) {
678
+ const replacement = createdRetryResponse(source);
679
+ if (replacement) {
680
+ return { ok: true, value: replacement };
681
+ }
682
+ return { ok: false, status: schedulingError.status, error: schedulingError.error, reason: 'retry_target_busy' };
683
+ }
684
+ try {
685
+ return { ok: true, value: retryResponse(source) };
686
+ }
687
+ catch (error) {
688
+ return { ok: false, status: 409, error: error instanceof Error ? error.message : String(error), reason: 'retry_already_created' };
689
+ }
690
+ };
691
+ export const getExecutionRetryEligibility = (projectId, runId, context) => (context
692
+ ? resolveExecutionRetryEligibilityFromContext(context, runId)
693
+ : resolveExecutionRetry(projectId, runId).eligibility);
694
+ //# sourceMappingURL=executionRetry.js.map