@straiffi/archon 1.7.3 → 1.7.5
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.
- package/dist/client/assets/ActivityView-CT9iCPyc.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-BA6xyQeX.js +1 -0
- package/dist/client/assets/TestsDialog-D2qrW5Ug.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BzzGotac.js → architectureDiagram-3BPJPVTR-R6v7w0Nl.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-DIcqLHKg.js → blockDiagram-GPEHLZMM-CHlBlnJq.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-Hc1Sqmhs.js → c4Diagram-AAUBKEIU-BvZKqUc6.js} +1 -1
- package/dist/client/assets/channel-Ckv8cPSf.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-CWSwjsgk.js → chunk-2J33WTMH-2gsIkBeO.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-BnX3DljN.js → chunk-3OPIFGDE-CcqPBjqx.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-EyyJlI-d.js → chunk-5ZQYHXKU-DX_wK5Rp.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-Ci6CfxWj.js → chunk-727SXJPM-B9WUxzJL.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-RKZNsJTm.js → chunk-AQP2D5EJ-BG0s75_H.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-DBf0Euoj.js → chunk-CSCIHK7Q-CyNcvvqW.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-BmLQI_2S.js → chunk-KSCS5N6A-CWVy_Wi0.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-DKwVAb8G.js → chunk-L5ZTLDWV-3dsXRB0X.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-axotKkbz.js → chunk-LZXEDZCA-B09QSB9z.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-BBlJ4nXC.js → chunk-ND2GUHAM-BxzAdxTN.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-BKJGoZZt.js → chunk-NZK2D7GU-D4ZfFwbB.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-C-Ghigo1.js → chunk-O5CBEL6O-BxQULuKS.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-CCzVWYcd.js → chunk-WU5MYG2G-CEJwp0Ry.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-B-N-arsi.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-PTX7YrlL.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-rwIdPcjI.js → dagre-BM42HDAG-Dx_V7OMy.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-DJWxOQsg.js → diagram-2AECGRRQ-Cc3VTg9P.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-C-mQeFtd.js → diagram-5GNKFQAL-mJ1xhs2G.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BAu6u4sR.js → diagram-KO2AKTUF-CLyT9ac8.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-U6CbvBn0.js → diagram-LMA3HP47-CVr47yij.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-DNTwFcs1.js → diagram-OG6HWLK6-Dp5TJ8lM.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DaHX6Xw5.js → erDiagram-TEJ5UH35-BBxwyu0r.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-B9hKqL2l.js → flowDiagram-I6XJVG4X-CuFYzVQ-.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-BQ1HPHVO.js → ganttDiagram-6RSMTGT7-aRQarcQY.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BBRcsmMK.js → gitGraphDiagram-PVQCEYII-B3mHSkBv.js} +1 -1
- package/dist/client/assets/index-BJGHMaMz.css +2 -0
- package/dist/client/assets/index-Oz0S7bcT.js +273 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-DUoNiSGC.js → infoDiagram-5YYISTIA-BSyryQJ9.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DJN1y2w8.js → ishikawaDiagram-YF4QCWOH-BAGbrtSG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-BxLLMT70.js → journeyDiagram-JHISSGLW-CDTht6k6.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BdCPistJ.js → kanban-definition-UN3LZRKU-BDtuMp0H.js} +1 -1
- package/dist/client/assets/{line-4mIgXsZd.js → line-n0aNicmA.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-Pu1QGWJ-.js → mermaid-parser.core-CdE4vZ4k.js} +1 -1
- package/dist/client/assets/{mermaid.core-DnH9DrgZ.js → mermaid.core-C546CQj1.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-DqE4ba3C.js → mindmap-definition-RKZ34NQL-BwO779am.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-Crjy9ejf.js → pieDiagram-4H26LBE5-DU1k71kn.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BUSZ2VB7.js → quadrantDiagram-W4KKPZXB-CvM5lyQg.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-CBhFKEoV.js → requirementDiagram-4Y6WPE33-DDBdSq0B.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CgdbY41v.js → sankeyDiagram-5OEKKPKP-ELM2mu81.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-hkp_H7AE.js → sequenceDiagram-3UESZ5HK-D7uF-nRR.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-C6x45RWp.js → stateDiagram-AJRCARHV-IhaGRCbG.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-ceHFQMIT.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-CjnZWttQ.js → timeline-definition-PNZ67QCA-zHJGFvZ8.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-DU5ZyYju.js → vennDiagram-CIIHVFJN-EG1HFfD7.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CaCEnCB0.js → wardleyDiagram-YWT4CUSO-DdQevxXg.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-Ce3xa37U.js → xychartDiagram-2RQKCTM6-BjihNG0E.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +283 -0
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +390 -117
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/attachments.js +40 -0
- package/dist/server/lib/attachments.js.map +1 -1
- package/dist/server/lib/buildOpenQuestionOperations.js +17 -2
- package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -1
- package/dist/server/lib/buildSequences.js +25 -0
- package/dist/server/lib/buildSequences.js.map +1 -1
- package/dist/server/lib/bundleReviewOperations.js +9 -0
- package/dist/server/lib/bundleReviewOperations.js.map +1 -1
- package/dist/server/lib/chatOperations.js +40 -16
- package/dist/server/lib/chatOperations.js.map +1 -1
- package/dist/server/lib/executionArtifactCleanupState.js +12 -0
- package/dist/server/lib/executionArtifactCleanupState.js.map +1 -0
- package/dist/server/lib/executionArtifactRetention.js +405 -0
- package/dist/server/lib/executionArtifactRetention.js.map +1 -0
- package/dist/server/lib/executionArtifacts.js +705 -0
- package/dist/server/lib/executionArtifacts.js.map +1 -0
- package/dist/server/lib/executionRetry.js +702 -0
- package/dist/server/lib/executionRetry.js.map +1 -0
- package/dist/server/lib/executionRetryContext.js +510 -0
- package/dist/server/lib/executionRetryContext.js.map +1 -0
- package/dist/server/lib/executionRetryPredicates.js +74 -0
- package/dist/server/lib/executionRetryPredicates.js.map +1 -0
- package/dist/server/lib/executionRuns.js +203 -40
- package/dist/server/lib/executionRuns.js.map +1 -1
- package/dist/server/lib/executionTimeline.js +73 -0
- package/dist/server/lib/executionTimeline.js.map +1 -0
- package/dist/server/lib/lightweightPrompt.js +2 -2
- package/dist/server/lib/lightweightPrompt.js.map +1 -1
- package/dist/server/lib/projects.js +16 -3
- package/dist/server/lib/projects.js.map +1 -1
- package/dist/server/lib/pullRequestContent.js +148 -25
- package/dist/server/lib/pullRequestContent.js.map +1 -1
- package/dist/server/lib/queue.js +1 -0
- package/dist/server/lib/queue.js.map +1 -1
- package/dist/server/lib/runtimePaths.js +7 -0
- package/dist/server/lib/runtimePaths.js.map +1 -1
- package/dist/server/lib/specOperations.js +51 -16
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +10 -4
- package/dist/server/lib/specReviewContext.js.map +1 -1
- package/dist/server/lib/specReviewOperations.js +33 -5
- package/dist/server/lib/specReviewOperations.js.map +1 -1
- package/dist/server/lib/testSessions.js +2 -5
- package/dist/server/lib/testSessions.js.map +1 -1
- package/dist/server/lib/ticketAutomationCommit.js +11 -4
- package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +74 -27
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketMessages.js +16 -0
- package/dist/server/lib/ticketMessages.js.map +1 -1
- package/dist/server/lib/types.js +12 -0
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/build.js +1 -1
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/chat.js +26 -20
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +118 -15
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +23 -4
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +12 -9
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +2 -2
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +2 -2
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +29 -22
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/ActivityView-DTEenQas.js +0 -1
- package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +0 -1
- package/dist/client/assets/TestsDialog-cBaoffCs.js +0 -4
- package/dist/client/assets/channel-Cym17waa.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +0 -1
- package/dist/client/assets/index-CEMaJCPS.js +0 -273
- package/dist/client/assets/index-lWftq65U.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +0 -1
|
@@ -0,0 +1,702 @@
|
|
|
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, hasLaterSpecReviewForSnapshot, 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
|
+
if (hasLaterSpecReviewForSnapshot({
|
|
273
|
+
projectId: project.id,
|
|
274
|
+
sourceRunId: source.id,
|
|
275
|
+
sessionId: session.id,
|
|
276
|
+
snapshotId: snapshot.id,
|
|
277
|
+
})) {
|
|
278
|
+
return deny('attempt_superseded', 409, 'A newer Spec Review attempt already exists for this snapshot.');
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
eligibility: { eligible: true, reason: null },
|
|
282
|
+
source,
|
|
283
|
+
project,
|
|
284
|
+
ticket: null,
|
|
285
|
+
bundleId: null,
|
|
286
|
+
triggerMessageId: null,
|
|
287
|
+
instruction: null,
|
|
288
|
+
followUpMode: null,
|
|
289
|
+
sessionId: session.id,
|
|
290
|
+
priorSnapshotId: snapshot.id,
|
|
291
|
+
transcriptBoundaryMessageId: boundaryId,
|
|
292
|
+
runConfig: runConfig ?? undefined,
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
const resolveTestRetry = (project, source) => {
|
|
296
|
+
const details = getTestExecutionDetails(project.id, source.id);
|
|
297
|
+
if (!details?.command_id) {
|
|
298
|
+
return deny('retry_input_unavailable', 409, 'The original test command is no longer identifiable.');
|
|
299
|
+
}
|
|
300
|
+
const command = findProjectTestCommand(project, details.command_id);
|
|
301
|
+
const selection = parseExecutionRetryTestSelection(details.selection);
|
|
302
|
+
if (!command || !selection) {
|
|
303
|
+
return deny('retry_input_unavailable', 409, 'The original test input is no longer available.');
|
|
304
|
+
}
|
|
305
|
+
const workspace = resolveTestWorkspace(project, details);
|
|
306
|
+
if (!workspace) {
|
|
307
|
+
return deny('retry_workspace_unavailable', 409, 'The original test workspace is no longer available.');
|
|
308
|
+
}
|
|
309
|
+
if (!isTestSelectionSupported(command, selection, workspace.cwd)) {
|
|
310
|
+
return deny('retry_input_unavailable', 409, 'The original test selection is no longer supported.');
|
|
311
|
+
}
|
|
312
|
+
if (selection.kind !== 'all' && !doesTestFileExist(workspace.cwd, selection.path, command)) {
|
|
313
|
+
return deny('retry_input_unavailable', 409, 'The selected test file no longer exists.');
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
eligibility: { eligible: true, reason: null },
|
|
317
|
+
source,
|
|
318
|
+
project,
|
|
319
|
+
ticket: details.ticket_id ? getTicket(details.ticket_id) : null,
|
|
320
|
+
bundleId: workspace.target_bundle_id,
|
|
321
|
+
triggerMessageId: null,
|
|
322
|
+
instruction: null,
|
|
323
|
+
followUpMode: null,
|
|
324
|
+
test: { command, selection, workspace, ticketId: details.ticket_id },
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
export const resolveExecutionRetry = (projectId, runId) => {
|
|
328
|
+
const project = getProjectById(projectId);
|
|
329
|
+
const source = project ? getExecutionRun(project.id, runId) : null;
|
|
330
|
+
if (!project || !source) {
|
|
331
|
+
return deny('retry_target_missing', 404, 'Execution run not found');
|
|
332
|
+
}
|
|
333
|
+
const links = listExecutionRunLinks(project.id, source.id);
|
|
334
|
+
const eligibility = resolveExecutionRetryEligibilityFromContext(createExecutionRetryResolutionContext({ project, runs: [source], links }), source.id);
|
|
335
|
+
if (!eligibility.eligible) {
|
|
336
|
+
return deny(eligibility.reason, eligibility.reason === 'retry_target_missing' ? 404 : 409, 'This execution is not eligible for retry.');
|
|
337
|
+
}
|
|
338
|
+
if (!RETRYABLE_EXECUTION_KINDS.has(source.kind)) {
|
|
339
|
+
return deny('unsupported_kind', 409, 'This execution kind cannot be retried through the generic retry API.');
|
|
340
|
+
}
|
|
341
|
+
if (!isRetryableExecutionStatus(source.kind, source.status)) {
|
|
342
|
+
return deny('status_not_retryable', 409, 'Only failed, cancelled, or interrupted executions can be retried.');
|
|
343
|
+
}
|
|
344
|
+
const directChild = getDirectRetryChild(project.id, source.id);
|
|
345
|
+
if (directChild) {
|
|
346
|
+
return (directChild.kind === 'test' ? RETRYABLE_TEST_STATUSES : RETRYABLE_AGENT_STATUSES).has(directChild.status)
|
|
347
|
+
? deny('retry_source_superseded', 409, 'Retry the latest replacement attempt instead.')
|
|
348
|
+
: deny('retry_already_created', 409, 'A retry has already been created for this execution.');
|
|
349
|
+
}
|
|
350
|
+
if (source.kind === 'test') {
|
|
351
|
+
return resolveTestRetry(project, source);
|
|
352
|
+
}
|
|
353
|
+
if (source.kind === 'chat_turn' || source.kind === 'spec_turn') {
|
|
354
|
+
return resolveSessionRetry(project, source);
|
|
355
|
+
}
|
|
356
|
+
if (source.kind === 'spec_review') {
|
|
357
|
+
return resolveSpecReviewRetry(project, source);
|
|
358
|
+
}
|
|
359
|
+
const ticket = resolvePrimaryTicket(project.id, source);
|
|
360
|
+
if (!ticket) {
|
|
361
|
+
return deny('retry_target_missing', 404, 'The execution ticket no longer exists.');
|
|
362
|
+
}
|
|
363
|
+
if (isLaterRunSupersededKind(source.kind) && hasLaterExecutionRunForPrimaryEntity({
|
|
364
|
+
projectId: project.id,
|
|
365
|
+
sourceRunId: source.id,
|
|
366
|
+
kind: source.kind,
|
|
367
|
+
entityType: 'ticket',
|
|
368
|
+
entityId: ticket.id,
|
|
369
|
+
})) {
|
|
370
|
+
return deny('attempt_superseded', 409, 'A newer ticket execution has already advanced this workflow.');
|
|
371
|
+
}
|
|
372
|
+
const planTarget = source.kind === 'plan' ? resolvePlanTarget(project.id, source) : undefined;
|
|
373
|
+
const bundleId = source.kind === 'plan' ? planTarget?.bundleId : resolveBundleId(project.id, source, ticket);
|
|
374
|
+
const bundle = bundleId ? getBundle(bundleId, project.id) : null;
|
|
375
|
+
if ((source.kind === 'plan' && !planTarget) || bundleId === undefined || (bundleId && !bundle)) {
|
|
376
|
+
return deny('retry_target_missing', 404, 'The execution bundle no longer exists.');
|
|
377
|
+
}
|
|
378
|
+
if (hasAutomationOwnership(source, bundleId)) {
|
|
379
|
+
return deny('automation_resume_required', 409, 'This execution is owned by automation and must be resumed through automation recovery.');
|
|
380
|
+
}
|
|
381
|
+
if (ticket.agent_status === 'running') {
|
|
382
|
+
return deny('retry_target_busy', 409, 'The execution ticket is already running.');
|
|
383
|
+
}
|
|
384
|
+
const effectiveProject = getEffectiveProject(ticket);
|
|
385
|
+
if (!effectiveProject || effectiveProject.id !== project.id || !existsSync(project.repo_path)) {
|
|
386
|
+
return deny('retry_workspace_unavailable', 409, 'The execution workspace is unavailable.');
|
|
387
|
+
}
|
|
388
|
+
if (source.kind === 'plan' && bundle && bundle.kind !== 'project_root' && !resolveExistingWorktreePath(bundle.branch, project)) {
|
|
389
|
+
return deny('retry_workspace_unavailable', 409, 'The preserved Plan worktree is unavailable.');
|
|
390
|
+
}
|
|
391
|
+
if (source.kind === 'plan' && ticket.state !== 'plan') {
|
|
392
|
+
return deny('retry_state_incompatible', 409, 'The ticket is no longer in Plan.');
|
|
393
|
+
}
|
|
394
|
+
if (source.kind === 'plan'
|
|
395
|
+
&& shouldBlockPlanForBundleMutationById(bundleId, planTarget?.branch, bundle?.branch)
|
|
396
|
+
&& getBundlePlanMutationBlockerById(bundleId)) {
|
|
397
|
+
return deny('retry_target_busy', 409, 'The bundle is busy with other domain work.');
|
|
398
|
+
}
|
|
399
|
+
if (source.kind === 'build') {
|
|
400
|
+
if (ticket.state !== 'build') {
|
|
401
|
+
return deny('retry_state_incompatible', 409, 'The ticket is no longer in Build.');
|
|
402
|
+
}
|
|
403
|
+
if (getBundleBuildBlockerResponse(ticket)) {
|
|
404
|
+
return deny('retry_target_busy', 409, 'The bundle is busy with other domain work.');
|
|
405
|
+
}
|
|
406
|
+
const buildEligibility = prepareTicketForBuild(ticket.id, { updateState: false });
|
|
407
|
+
if (!buildEligibility.ok) {
|
|
408
|
+
return deny('retry_state_incompatible', buildEligibility.status, buildEligibility.error);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (source.kind === 'review') {
|
|
412
|
+
const reviewError = getTicketReviewRetryEligibilityError(ticket);
|
|
413
|
+
if (reviewError) {
|
|
414
|
+
return deny('retry_state_incompatible', 409, reviewError);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (source.kind === 'build_follow_up' && ticket.state === 'plan') {
|
|
418
|
+
return deny('retry_state_incompatible', 409, 'The ticket can no longer accept a Build follow-up.');
|
|
419
|
+
}
|
|
420
|
+
if (source.kind === 'plan_follow_up' && ticket.state !== 'plan') {
|
|
421
|
+
return deny('retry_state_incompatible', 409, 'The ticket can no longer accept a Plan follow-up.');
|
|
422
|
+
}
|
|
423
|
+
if ((source.kind === 'review' || source.kind === 'build_follow_up')
|
|
424
|
+
&& !resolveExistingWorktreePath(resolveTicketBranch(ticket) ?? '', project)) {
|
|
425
|
+
return deny('retry_workspace_unavailable', 409, 'The execution worktree is unavailable.');
|
|
426
|
+
}
|
|
427
|
+
const trigger = source.kind === 'build_follow_up' || source.kind === 'plan_follow_up'
|
|
428
|
+
? resolveTrigger(source, ticket)
|
|
429
|
+
: null;
|
|
430
|
+
if ((source.kind === 'build_follow_up' || source.kind === 'plan_follow_up') && !trigger) {
|
|
431
|
+
return deny('retry_input_unavailable', 409, 'The exact follow-up trigger is unavailable.');
|
|
432
|
+
}
|
|
433
|
+
const followUpMode = source.kind === 'build_follow_up'
|
|
434
|
+
? source.metadata?.follow_up_mode
|
|
435
|
+
: null;
|
|
436
|
+
if (source.kind === 'build_follow_up' && followUpMode !== 'plan' && followUpMode !== 'build') {
|
|
437
|
+
return deny('retry_input_unavailable', 409, 'The Build follow-up mode was not preserved for this attempt.');
|
|
438
|
+
}
|
|
439
|
+
const runConfig = source.kind === 'build_follow_up' || source.kind === 'plan_follow_up'
|
|
440
|
+
? resolveExecutionRetryRunConfig(source)
|
|
441
|
+
: undefined;
|
|
442
|
+
if ((source.kind === 'build_follow_up' || source.kind === 'plan_follow_up') && !runConfig) {
|
|
443
|
+
return deny('retry_input_unavailable', 409, 'The original follow-up provider configuration is unavailable.');
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
eligibility: { eligible: true, reason: null },
|
|
447
|
+
source,
|
|
448
|
+
project,
|
|
449
|
+
ticket,
|
|
450
|
+
bundleId,
|
|
451
|
+
triggerMessageId: trigger?.id ?? null,
|
|
452
|
+
instruction: trigger?.instruction ?? null,
|
|
453
|
+
followUpMode: followUpMode,
|
|
454
|
+
planBranch: planTarget?.branch,
|
|
455
|
+
runConfig: runConfig ?? undefined,
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
const createdRetryResponse = (source) => {
|
|
459
|
+
const replacement = getDirectRetryChild(source.project_id, source.id);
|
|
460
|
+
return replacement ? {
|
|
461
|
+
execution_run_id: replacement.id,
|
|
462
|
+
retry_of_run_id: source.id,
|
|
463
|
+
attempt: replacement.attempt,
|
|
464
|
+
} : null;
|
|
465
|
+
};
|
|
466
|
+
const retryResponse = (source, replacementRunId) => {
|
|
467
|
+
const replacement = createdRetryResponse(source);
|
|
468
|
+
if (!replacement && !replacementRunId) {
|
|
469
|
+
throw new Error('Retry scheduling did not create a replacement execution');
|
|
470
|
+
}
|
|
471
|
+
return replacement ?? {
|
|
472
|
+
execution_run_id: replacementRunId,
|
|
473
|
+
retry_of_run_id: source.id,
|
|
474
|
+
attempt: source.attempt + 1,
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
export const dispatchExecutionRetry = async (resolution, broadcaster, dispatchTest) => {
|
|
478
|
+
if ('error' in resolution) {
|
|
479
|
+
return { ok: false, status: resolution.error.status, error: resolution.error.message, reason: resolution.eligibility.reason };
|
|
480
|
+
}
|
|
481
|
+
const { source, ticket } = resolution;
|
|
482
|
+
if (source.kind === 'test' && resolution.test) {
|
|
483
|
+
try {
|
|
484
|
+
const replacementRunId = await dispatchTest(resolution.test);
|
|
485
|
+
return { ok: true, value: retryResponse(source, replacementRunId) };
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
const replacement = createdRetryResponse(source);
|
|
489
|
+
if (replacement) {
|
|
490
|
+
return { ok: true, value: replacement };
|
|
491
|
+
}
|
|
492
|
+
return { ok: false, status: 409, error: error instanceof Error ? error.message : String(error), reason: 'retry_input_unavailable' };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if ((source.kind === 'chat_turn' || source.kind === 'spec_turn') && resolution.sessionId && resolution.runConfig) {
|
|
496
|
+
const operation = source.kind === 'chat_turn'
|
|
497
|
+
? submitProjectChatSessionMessage({
|
|
498
|
+
projectId: source.project_id,
|
|
499
|
+
chatSessionId: resolution.sessionId,
|
|
500
|
+
payload: { instruction: resolution.instruction },
|
|
501
|
+
broadcaster,
|
|
502
|
+
initiation: { initiator: 'user', retrySourceRunId: source.id },
|
|
503
|
+
existingTriggerMessageId: resolution.triggerMessageId ?? undefined,
|
|
504
|
+
existingAttachments: resolution.attachments,
|
|
505
|
+
providerContinuation: 'fresh',
|
|
506
|
+
conversationContext: resolution.conversationContext,
|
|
507
|
+
retryRunConfig: resolution.runConfig,
|
|
508
|
+
turnMode: resolution.sessionMode,
|
|
509
|
+
})
|
|
510
|
+
: submitProjectSpecSessionMessage({
|
|
511
|
+
projectId: source.project_id,
|
|
512
|
+
specSessionId: resolution.sessionId,
|
|
513
|
+
payload: { instruction: resolution.instruction },
|
|
514
|
+
broadcaster,
|
|
515
|
+
initiation: { initiator: 'user', retrySourceRunId: source.id },
|
|
516
|
+
existingTriggerMessageId: resolution.triggerMessageId ?? undefined,
|
|
517
|
+
existingAttachments: resolution.attachments,
|
|
518
|
+
providerContinuation: 'fresh',
|
|
519
|
+
conversationContext: resolution.conversationContext,
|
|
520
|
+
priorSnapshotId: resolution.priorSnapshotId,
|
|
521
|
+
retryRunConfig: resolution.runConfig,
|
|
522
|
+
turnMode: resolution.sessionMode,
|
|
523
|
+
});
|
|
524
|
+
if (!operation.ok) {
|
|
525
|
+
const replacement = createdRetryResponse(source);
|
|
526
|
+
if (replacement) {
|
|
527
|
+
return { ok: true, value: replacement };
|
|
528
|
+
}
|
|
529
|
+
return { ok: false, status: operation.status, error: operation.error, reason: 'retry_target_busy' };
|
|
530
|
+
}
|
|
531
|
+
return { ok: true, value: retryResponse(source) };
|
|
532
|
+
}
|
|
533
|
+
if (source.kind === 'spec_review' && resolution.sessionId && resolution.priorSnapshotId && resolution.runConfig) {
|
|
534
|
+
const operation = startProjectSpecReview({
|
|
535
|
+
projectId: source.project_id,
|
|
536
|
+
specSessionId: resolution.sessionId,
|
|
537
|
+
snapshotId: resolution.priorSnapshotId,
|
|
538
|
+
payload: undefined,
|
|
539
|
+
broadcaster,
|
|
540
|
+
retrySourceRunId: source.id,
|
|
541
|
+
retryRunConfig: resolution.runConfig,
|
|
542
|
+
transcriptBoundaryMessageId: resolution.transcriptBoundaryMessageId,
|
|
543
|
+
});
|
|
544
|
+
if (!operation.ok) {
|
|
545
|
+
const replacement = createdRetryResponse(source);
|
|
546
|
+
if (replacement) {
|
|
547
|
+
return { ok: true, value: replacement };
|
|
548
|
+
}
|
|
549
|
+
return { ok: false, status: operation.status, error: operation.error, reason: 'retry_target_busy' };
|
|
550
|
+
}
|
|
551
|
+
return { ok: true, value: retryResponse(source) };
|
|
552
|
+
}
|
|
553
|
+
if (!ticket) {
|
|
554
|
+
return { ok: false, status: 404, error: 'Retry target not found', reason: 'retry_target_missing' };
|
|
555
|
+
}
|
|
556
|
+
const initiation = { initiator: 'user', retrySourceRunId: source.id };
|
|
557
|
+
let schedulingError = null;
|
|
558
|
+
if (source.kind === 'plan') {
|
|
559
|
+
const scheduling = startPlan(ticket, broadcaster, {
|
|
560
|
+
branch: resolution.planBranch,
|
|
561
|
+
targetBundleId: resolution.bundleId,
|
|
562
|
+
initiation,
|
|
563
|
+
providerContinuation: 'fresh',
|
|
564
|
+
});
|
|
565
|
+
if (!scheduling.accepted) {
|
|
566
|
+
schedulingError = { status: scheduling.reason === 'queue_failed' ? 503 : 409, error: scheduling.error };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else if (source.kind === 'build') {
|
|
570
|
+
if (resolution.bundleId && hasActiveSequenceForBundle(source.project_id, resolution.bundleId)) {
|
|
571
|
+
try {
|
|
572
|
+
withExecutionRunTransaction(transaction => {
|
|
573
|
+
const updated = db.prepare(`
|
|
574
|
+
UPDATE tickets
|
|
575
|
+
SET agent_status = NULL, agent_log = NULL, streaming_response = NULL,
|
|
576
|
+
build_completed_at = NULL, run_setup_status = NULL, updated_at = CURRENT_TIMESTAMP
|
|
577
|
+
WHERE id = ? AND project_id = ? AND agent_status IS NOT 'running' AND state = 'build'
|
|
578
|
+
`).run(ticket.id, source.project_id);
|
|
579
|
+
if (updated.changes !== 1) {
|
|
580
|
+
throw new Error('Ticket is already running or no longer in Build');
|
|
581
|
+
}
|
|
582
|
+
const acceptedTicket = getTicket(ticket.id);
|
|
583
|
+
if (!acceptedTicket || !acceptedTicket.worktree_bundle_id) {
|
|
584
|
+
throw new Error('Retry target not found');
|
|
585
|
+
}
|
|
586
|
+
const sequenceId = enqueueBundledBuild({
|
|
587
|
+
ticketId: acceptedTicket.id,
|
|
588
|
+
projectId: source.project_id,
|
|
589
|
+
worktreeBundleId: acceptedTicket.worktree_bundle_id,
|
|
590
|
+
activate: false,
|
|
591
|
+
}).sequenceId;
|
|
592
|
+
transaction.createExecutionRun({
|
|
593
|
+
projectId: source.project_id,
|
|
594
|
+
kind: 'build',
|
|
595
|
+
domainStatus: 'queued',
|
|
596
|
+
sourceReference: { kind: 'ticket', id: acceptedTicket.id, label: acceptedTicket.title },
|
|
597
|
+
links: [
|
|
598
|
+
{ entityType: 'bundle', entityId: acceptedTicket.worktree_bundle_id, relationship: 'context' },
|
|
599
|
+
{ entityType: 'build_sequence', entityId: sequenceId, relationship: 'context' },
|
|
600
|
+
],
|
|
601
|
+
initiatedBy: 'user',
|
|
602
|
+
retryOfRunId: source.id,
|
|
603
|
+
eventType: 'retry_started',
|
|
604
|
+
});
|
|
605
|
+
}, broadcaster);
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
schedulingError = { status: 409, error: error instanceof Error ? error.message : String(error) };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
const scheduling = startBuild(ticket, broadcaster, {
|
|
613
|
+
resume: false,
|
|
614
|
+
initiation,
|
|
615
|
+
accept: () => {
|
|
616
|
+
const updated = db.prepare(`
|
|
617
|
+
UPDATE tickets
|
|
618
|
+
SET agent_status = NULL, agent_log = NULL, streaming_response = NULL,
|
|
619
|
+
build_completed_at = NULL, run_setup_status = NULL, updated_at = CURRENT_TIMESTAMP
|
|
620
|
+
WHERE id = ? AND project_id = ? AND agent_status IS NOT 'running' AND state = 'build'
|
|
621
|
+
`).run(ticket.id, source.project_id);
|
|
622
|
+
if (updated.changes !== 1) {
|
|
623
|
+
throw new Error('Ticket is already running or no longer in Build');
|
|
624
|
+
}
|
|
625
|
+
const acceptedTicket = getTicket(ticket.id);
|
|
626
|
+
if (!acceptedTicket) {
|
|
627
|
+
throw new Error('Retry target not found');
|
|
628
|
+
}
|
|
629
|
+
const sequenceId = acceptedTicket.worktree_bundle_id
|
|
630
|
+
? enqueueBundledBuild({
|
|
631
|
+
ticketId: acceptedTicket.id,
|
|
632
|
+
projectId: source.project_id,
|
|
633
|
+
worktreeBundleId: acceptedTicket.worktree_bundle_id,
|
|
634
|
+
activate: true,
|
|
635
|
+
}).sequenceId
|
|
636
|
+
: null;
|
|
637
|
+
return {
|
|
638
|
+
ticket: acceptedTicket,
|
|
639
|
+
sequenceId,
|
|
640
|
+
...(sequenceId ? { compensate: () => compensateBuildSequenceAcceptance(sequenceId, [acceptedTicket.id]) } : {}),
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
if (!scheduling.accepted) {
|
|
645
|
+
schedulingError = { status: scheduling.reason === 'queue_failed' ? 503 : 409, error: scheduling.error };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else if (source.kind === 'review') {
|
|
650
|
+
const result = rerunTicketReviewOperation(ticket, broadcaster, { initiation });
|
|
651
|
+
if (!result.ok) {
|
|
652
|
+
schedulingError = result;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
else if (source.kind === 'build_follow_up') {
|
|
656
|
+
const result = submitTicketFollowUp({
|
|
657
|
+
ticketId: ticket.id,
|
|
658
|
+
requestedProjectId: source.project_id,
|
|
659
|
+
payload: { instruction: resolution.instruction, mode: resolution.followUpMode },
|
|
660
|
+
broadcaster,
|
|
661
|
+
initiation,
|
|
662
|
+
existingTriggerMessageId: resolution.triggerMessageId ?? undefined,
|
|
663
|
+
providerContinuation: 'fresh',
|
|
664
|
+
retryRunConfig: resolution.runConfig,
|
|
665
|
+
});
|
|
666
|
+
if (!result.ok) {
|
|
667
|
+
schedulingError = result;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
else if (source.kind === 'plan_follow_up') {
|
|
671
|
+
const result = submitPlanTicketFollowUp({
|
|
672
|
+
ticketId: ticket.id,
|
|
673
|
+
requestedProjectId: source.project_id,
|
|
674
|
+
payload: { instruction: resolution.instruction },
|
|
675
|
+
broadcaster,
|
|
676
|
+
initiation,
|
|
677
|
+
existingTriggerMessageId: resolution.triggerMessageId ?? undefined,
|
|
678
|
+
providerContinuation: 'fresh',
|
|
679
|
+
retryRunConfig: resolution.runConfig,
|
|
680
|
+
});
|
|
681
|
+
if (!result.ok) {
|
|
682
|
+
schedulingError = result;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (schedulingError) {
|
|
686
|
+
const replacement = createdRetryResponse(source);
|
|
687
|
+
if (replacement) {
|
|
688
|
+
return { ok: true, value: replacement };
|
|
689
|
+
}
|
|
690
|
+
return { ok: false, status: schedulingError.status, error: schedulingError.error, reason: 'retry_target_busy' };
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
return { ok: true, value: retryResponse(source) };
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
return { ok: false, status: 409, error: error instanceof Error ? error.message : String(error), reason: 'retry_already_created' };
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
export const getExecutionRetryEligibility = (projectId, runId, context) => (context
|
|
700
|
+
? resolveExecutionRetryEligibilityFromContext(context, runId)
|
|
701
|
+
: resolveExecutionRetry(projectId, runId).eligibility);
|
|
702
|
+
//# sourceMappingURL=executionRetry.js.map
|