@straiffi/archon 1.10.9 → 1.10.10
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-CRkM5TeN.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-KFUiR00H.js +1 -0
- package/dist/client/assets/VerificationDialog-BIDttgiW.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BAIGV9Mj.js → architectureDiagram-3BPJPVTR-u0PSZlNP.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-B-5v5jSZ.js → blockDiagram-GPEHLZMM-DFXLHRIk.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-C6wU415o.js → c4Diagram-AAUBKEIU-Dvic8tCt.js} +1 -1
- package/dist/client/assets/channel-Dhv_uSKx.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-COmvX2tP.js → chunk-2J33WTMH-t5CxhRXJ.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-BlANptdY.js → chunk-3OPIFGDE-YHT1dP8Q.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-ETyA2_GU.js → chunk-5ZQYHXKU-DcxuI7lb.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-D5-DudwF.js → chunk-727SXJPM-D1F6DmrT.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-k1AJ4sA3.js → chunk-AQP2D5EJ-BIx6Ohbk.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-jfksXwY9.js → chunk-CSCIHK7Q-B355h6HA.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-CHj0FDj9.js → chunk-KSCS5N6A-CLHGYOLQ.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-1ODGxXhG.js → chunk-L5ZTLDWV-DFLYcMx8.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-Bgh9X2W0.js → chunk-LZXEDZCA-DtljFujX.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-DDkb3sp4.js → chunk-ND2GUHAM-CIuUQ86K.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-B6B8PZ_-.js → chunk-NZK2D7GU-CIwqlxek.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-DhiUIFgj.js → chunk-O5CBEL6O-W42BEPu4.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-Dzd0fbP3.js → chunk-WU5MYG2G-C_zuaNxV.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-9XKvSSLq.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Dbr5_49Y.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-DoNDMCXI.js → dagre-BM42HDAG-UKfIUJTP.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-ETDXSa5q.js → diagram-2AECGRRQ-Xn6yojqq.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CgTUep7e.js → diagram-5GNKFQAL-BzpY1ZEl.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BKTx2iRe.js → diagram-KO2AKTUF-Dyrvzvsg.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-9iEiScHT.js → diagram-LMA3HP47-AqImKwAr.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-UIZRh7Qa.js → diagram-OG6HWLK6-CaQQ46mi.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-BpNJmJva.js → erDiagram-TEJ5UH35-Dkw4Aa5u.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-C-rK0XZ1.js → flowDiagram-I6XJVG4X-CJTuGX9h.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-DWyfPvv_.js → ganttDiagram-6RSMTGT7-CidgOdS-.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-x71bzdc_.js → gitGraphDiagram-PVQCEYII-Bj02mIQj.js} +1 -1
- package/dist/client/assets/index-ClRB9YWJ.css +2 -0
- package/dist/client/assets/index-DBMdsTvy.js +276 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-CbRus0Xu.js → infoDiagram-5YYISTIA-GwJWfo_r.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DvaMHrgN.js → ishikawaDiagram-YF4QCWOH-rbrlWbUn.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-BQBu4YGn.js → journeyDiagram-JHISSGLW-BLi_uzQM.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-8rU-U7L1.js → kanban-definition-UN3LZRKU-ynlywm-_.js} +1 -1
- package/dist/client/assets/{line-CSZnwjLA.js → line-BNyTZBB4.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-CTOl2slE.js → mermaid-parser.core-CzUOsZAD.js} +1 -1
- package/dist/client/assets/{mermaid.core-B9gr0Vqf.js → mermaid.core-K0QR6SzN.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-But1_PWQ.js → mindmap-definition-RKZ34NQL-BKcgqXm0.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-CnlVmfSG.js → pieDiagram-4H26LBE5-Bjx0lvDI.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Dww1ZGuR.js → quadrantDiagram-W4KKPZXB-CmUlgyUv.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-CNf_loe8.js → requirementDiagram-4Y6WPE33-Dj2mzg1y.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBhi37Jj.js → sankeyDiagram-5OEKKPKP-DC--H6qf.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B64hb4E4.js → sequenceDiagram-3UESZ5HK-BkNrt6Ra.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-DcS6oB-S.js → stateDiagram-AJRCARHV-BuxxRu1Y.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Cyo85GPN.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-Dq-4tXfh.js → timeline-definition-PNZ67QCA-Db3pCVSp.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CeDklSE_.js → vennDiagram-CIIHVFJN-BgBSGThQ.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-D2cJISMm.js → wardleyDiagram-YWT4CUSO-BJhPdjYS.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-CBF-tTpF.js → xychartDiagram-2RQKCTM6-BY4Ra0kX.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +341 -4
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +172 -249
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agentRuns.js +14 -1
- package/dist/server/lib/agentRuns.js.map +1 -1
- package/dist/server/lib/automationLoopProjection.js +55 -12
- package/dist/server/lib/automationLoopProjection.js.map +1 -1
- package/dist/server/lib/automationProgression.js +2 -5
- package/dist/server/lib/automationProgression.js.map +1 -1
- package/dist/server/lib/automationResumeDecision.js +19 -1
- package/dist/server/lib/automationResumeDecision.js.map +1 -1
- package/dist/server/lib/buildControlEligibility.js +4 -0
- package/dist/server/lib/buildControlEligibility.js.map +1 -0
- package/dist/server/lib/buildExecutionAdmission.js +76 -0
- package/dist/server/lib/buildExecutionAdmission.js.map +1 -0
- package/dist/server/lib/buildSequenceResume.js +79 -0
- package/dist/server/lib/buildSequenceResume.js.map +1 -0
- package/dist/server/lib/buildWorkload.js +11 -0
- package/dist/server/lib/buildWorkload.js.map +1 -0
- package/dist/server/lib/checkAutoFixDetails.js +36 -1
- package/dist/server/lib/checkAutoFixDetails.js.map +1 -1
- package/dist/server/lib/executionRetry.js +13 -1
- package/dist/server/lib/executionRetry.js.map +1 -1
- package/dist/server/lib/executionRunAuthority.js +2 -2
- package/dist/server/lib/executionRunAuthority.js.map +1 -1
- package/dist/server/lib/executionTargets.js +7 -4
- package/dist/server/lib/executionTargets.js.map +1 -1
- package/dist/server/lib/executionTimeline.js +8 -13
- package/dist/server/lib/executionTimeline.js.map +1 -1
- package/dist/server/lib/executionTimelineHierarchy.js +83 -0
- package/dist/server/lib/executionTimelineHierarchy.js.map +1 -0
- package/dist/server/lib/failedBuildSequenceRecovery.js +133 -0
- package/dist/server/lib/failedBuildSequenceRecovery.js.map +1 -0
- package/dist/server/lib/hostRunSession.js +62 -0
- package/dist/server/lib/hostRunSession.js.map +1 -0
- package/dist/server/lib/hostServiceRecovery.js +159 -0
- package/dist/server/lib/hostServiceRecovery.js.map +1 -0
- package/dist/server/lib/hostServiceRunProvider.js +32 -0
- package/dist/server/lib/hostServiceRunProvider.js.map +1 -0
- package/dist/server/lib/mobileAccess.js +6 -2
- package/dist/server/lib/mobileAccess.js.map +1 -1
- package/dist/server/lib/projectAutoConfig.js +5 -2
- package/dist/server/lib/projectAutoConfig.js.map +1 -1
- package/dist/server/lib/projectTargets.js +8 -2
- package/dist/server/lib/projectTargets.js.map +1 -1
- package/dist/server/lib/projects.js +25 -1
- package/dist/server/lib/projects.js.map +1 -1
- package/dist/server/lib/reviewFindingReconciliation.js +9 -2
- package/dist/server/lib/reviewFindingReconciliation.js.map +1 -1
- package/dist/server/lib/reviewFindings.js +82 -21
- package/dist/server/lib/reviewFindings.js.map +1 -1
- package/dist/server/lib/reviewRepairAssessmentRecords.js +41 -0
- package/dist/server/lib/reviewRepairAssessmentRecords.js.map +1 -0
- package/dist/server/lib/reviewRepairCompletion.js +78 -18
- package/dist/server/lib/reviewRepairCompletion.js.map +1 -1
- package/dist/server/lib/reviewRepairIterationRecords.js +4 -2
- package/dist/server/lib/reviewRepairIterationRecords.js.map +1 -1
- package/dist/server/lib/reviewRepairIterations.js +8 -2
- package/dist/server/lib/reviewRepairIterations.js.map +1 -1
- package/dist/server/lib/reviewRepairProgress.js +177 -0
- package/dist/server/lib/reviewRepairProgress.js.map +1 -0
- package/dist/server/lib/reviewRepairReconciliation.js +119 -140
- package/dist/server/lib/reviewRepairReconciliation.js.map +1 -1
- package/dist/server/lib/reviewRepairResumeOverride.js +55 -0
- package/dist/server/lib/reviewRepairResumeOverride.js.map +1 -0
- package/dist/server/lib/run.js +523 -504
- package/dist/server/lib/run.js.map +1 -1
- package/dist/server/lib/runProcessIdentity.js +71 -0
- package/dist/server/lib/runProcessIdentity.js.map +1 -0
- package/dist/server/lib/runProcesses.js +261 -0
- package/dist/server/lib/runProcesses.js.map +1 -0
- package/dist/server/lib/runServices.js +221 -0
- package/dist/server/lib/runServices.js.map +1 -0
- package/dist/server/lib/runSessionLifecycle.js +68 -0
- package/dist/server/lib/runSessionLifecycle.js.map +1 -0
- package/dist/server/lib/runStatus.js +7 -0
- package/dist/server/lib/runStatus.js.map +1 -0
- package/dist/server/lib/serviceRuns.js +273 -16
- package/dist/server/lib/serviceRuns.js.map +1 -1
- package/dist/server/lib/ticketAutomation.js +4 -27
- package/dist/server/lib/ticketAutomation.js.map +1 -1
- package/dist/server/lib/ticketRunOperations.js +103 -34
- package/dist/server/lib/ticketRunOperations.js.map +1 -1
- package/dist/server/lib/tickets.js +0 -13
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/build.js +44 -69
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/plan.js +0 -7
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/review.js +95 -146
- package/dist/server/workers/review.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/ActivityView-CKvbDXeA.js +0 -1
- package/dist/client/assets/GitHubActionsDialog-Dzo0vtak.js +0 -1
- package/dist/client/assets/VerificationDialog-BJnOitrJ.js +0 -4
- package/dist/client/assets/channel-5mIy7nm1.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BiQ-X2Sg.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CF-wCYW-.js +0 -1
- package/dist/client/assets/index-CIAO5aE_.css +0 -2
- package/dist/client/assets/index-nCGyalrn.js +0 -276
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-PWeeL0tR.js +0 -1
- package/dist/server/lib/automationDeliveryDecision.js +0 -34
- package/dist/server/lib/automationDeliveryDecision.js.map +0 -1
package/dist/server/lib/run.js
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { assertHostSessionRequest, createHostRunSession, createRunConfigSnapshot, finishHostService, queueHostServices, serviceIdentity } from "./hostRunSession.js";
|
|
3
|
+
import { isProcessAlive, killManagedProcess, runCommand, runWorkspaceSetup, stopRunProcesses, trackExecutionHandle, waitForProcessesToExit } from "./runProcesses.js";
|
|
4
|
+
import { createRunServiceLauncher } from "./runServices.js";
|
|
5
|
+
import { withHostServiceRecovery } from "./hostServiceRunProvider.js";
|
|
6
|
+
import { createHostSessionSettler, finalizeDurableRun } from "./runSessionLifecycle.js";
|
|
7
|
+
import { existsSync } from "fs";
|
|
4
8
|
import { resolveWorktreePath } from "./git.js";
|
|
5
9
|
import { getProjectById, getProjectRunConfig } from "./projects.js";
|
|
6
10
|
import { resolveHelperModelConfig } from "./agentModelConfig.js";
|
|
7
11
|
import config from "./config.js";
|
|
8
|
-
import {
|
|
9
|
-
import { deactivatePreviewProxyForProject,
|
|
10
|
-
import {
|
|
11
|
-
import { redactRepositorySecrets } from "./projectMemoryScan.js";
|
|
12
|
+
import { createIdlePreviewStatus, createRunPreviewStatus, isPreviewService, } from "./preview.js";
|
|
13
|
+
import { deactivatePreviewProxyForProject, } from "./previewProxy.js";
|
|
14
|
+
import { resolveShell } from "./shell.js";
|
|
12
15
|
import { isTicketRunSetupReady } from "./ticketDependencies.js";
|
|
13
16
|
import { listTicketsInRunContext, resolveTicketBranch, resolveTicketContextKey, resolveTicketTool, } from "./tickets.js";
|
|
14
17
|
import { createWorkspaceProcessEnvironment } from "./workspaceProcessEnvironment.js";
|
|
15
18
|
import { resolveWorkloadExecutionEnvironment } from "./buildExecutionEnvironment.js";
|
|
16
|
-
import { classifyTicketExecutionTarget, resolveExecutionTarget, } from "./executionTargets.js";
|
|
17
|
-
import {
|
|
18
|
-
import { transitionExecutionRun } from "./executionRuns.js";
|
|
19
|
+
import { classifyTicketExecutionTarget, resolveExecutionTarget, resolveExecutionTargetLocation, } from "./executionTargets.js";
|
|
20
|
+
import { createServiceRun, markServiceRunAllocationStarted, hasActiveProjectServiceRun, recordServiceRunPlacement, getRecoverableHostServiceRun, retryInterruptedHostServiceRunForContext, } from "./serviceRuns.js";
|
|
21
|
+
import { appendLifecycleEvent, transitionExecutionRun } from "./executionRuns.js";
|
|
19
22
|
const runningProcesses = new Map();
|
|
20
|
-
const
|
|
23
|
+
const lastRunStatuses = new Map();
|
|
24
|
+
export const getLastRunStatusForProject = (projectId) => lastRunStatuses.get(projectId) ?? null;
|
|
25
|
+
export class RunAdmissionError extends Error {
|
|
26
|
+
code;
|
|
27
|
+
status = 409;
|
|
28
|
+
constructor(code, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.name = "RunAdmissionError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
21
34
|
const getRunConfig = (project) => getProjectRunConfig(project);
|
|
22
35
|
export const resolveProjectRunContextKey = (projectId, target = {}) => {
|
|
23
36
|
if (target.kind === "bundle" && target.bundleId) {
|
|
@@ -58,7 +71,73 @@ const resolveRunContextKey = (ticketOrKey) => {
|
|
|
58
71
|
export const hasRunConfig = (project = null) => hasRunnableCommands(getRunConfig(project));
|
|
59
72
|
export const isRunning = (ticketOrKey) => {
|
|
60
73
|
const contextKey = resolveRunContextKey(ticketOrKey);
|
|
61
|
-
|
|
74
|
+
const state = contextKey ? runningProcesses.get(contextKey) : null;
|
|
75
|
+
if (state?.host_session && state.start_services) {
|
|
76
|
+
return state.host_session.services.some(service => ["starting", "running", "stopping"].includes(service.status));
|
|
77
|
+
}
|
|
78
|
+
return Boolean(state);
|
|
79
|
+
};
|
|
80
|
+
export const validateRunControl = (project, options) => {
|
|
81
|
+
const active = getActiveRunStatusForProject(project.id);
|
|
82
|
+
const state = active ? runningProcesses.get(active.context_key) : undefined;
|
|
83
|
+
if (!state && hasActiveProjectServiceRun(project.id)) {
|
|
84
|
+
if (options.starting) {
|
|
85
|
+
return "Durable service authority exists without a live runtime owner. Retry stopping the whole session before starting services.";
|
|
86
|
+
}
|
|
87
|
+
const recovered = getRecoverableHostServiceRun(project.id);
|
|
88
|
+
if (!recovered) {
|
|
89
|
+
return "Service cleanup requires reconciliation of the recorded execution environment.";
|
|
90
|
+
}
|
|
91
|
+
if (options.contextKey && options.contextKey !== recovered.context_key) {
|
|
92
|
+
return "Another service session is already active for this project";
|
|
93
|
+
}
|
|
94
|
+
if (options.sessionId !== undefined && options.sessionId !== recovered.sessionId) {
|
|
95
|
+
return "The service session has changed. Refresh before retrying.";
|
|
96
|
+
}
|
|
97
|
+
return options.serviceId !== undefined ? "Restarted host sessions require whole-session cleanup" : null;
|
|
98
|
+
}
|
|
99
|
+
if (state?.admission_pending && options.starting) {
|
|
100
|
+
return "Service admission is still pending. Retry after admission settles.";
|
|
101
|
+
}
|
|
102
|
+
if (state?.cancelled && options.starting) {
|
|
103
|
+
return "The service session is stopping. Stop the whole session before retrying.";
|
|
104
|
+
}
|
|
105
|
+
if (options.contextKey && active && options.contextKey !== active.context_key) {
|
|
106
|
+
return "Another service session is already active for this project";
|
|
107
|
+
}
|
|
108
|
+
if (state && options.starting && options.executionProvider !== undefined) {
|
|
109
|
+
const requestedProvider = options.executionProvider === "host" ? "host" : project.build_execution_provider ?? "host";
|
|
110
|
+
if (requestedProvider !== state.execution_provider) {
|
|
111
|
+
return "The requested execution provider is incompatible with the active service session";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (state?.host_session) {
|
|
115
|
+
if (options.starting && state.host_session.services.some(service => (options.serviceId === undefined || service.service_id === options.serviceId) && service.status === "stopping")) {
|
|
116
|
+
return "Service process cleanup is pending. Retry stopping the service before restarting it.";
|
|
117
|
+
}
|
|
118
|
+
if (state.host_session.setup_error && options.starting) {
|
|
119
|
+
return "Shared setup failed. Stop the session before retrying.";
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
assertHostSessionRequest(state.host_session, options.serviceId, options.sessionId);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return getErrorMessage(error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (options.sessionId !== undefined) {
|
|
130
|
+
return "The service session is no longer active";
|
|
131
|
+
}
|
|
132
|
+
if (options.serviceId !== undefined) {
|
|
133
|
+
if (state || (options.executionProvider !== "host" && project.build_execution_provider === "docker_sandbox")) {
|
|
134
|
+
return "Individual service controls are not supported by Docker Sandbox";
|
|
135
|
+
}
|
|
136
|
+
if (!(getRunConfig(project)?.services ?? []).some((service, index) => serviceIdentity(service, index) === options.serviceId)) {
|
|
137
|
+
return "Unknown service ID";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
62
141
|
};
|
|
63
142
|
export const getActiveRunStatus = () => {
|
|
64
143
|
const iterator = runningProcesses.values().next();
|
|
@@ -67,6 +146,28 @@ export const getActiveRunStatus = () => {
|
|
|
67
146
|
export const getActiveRunStatusForContextKey = (contextKey) => {
|
|
68
147
|
return runningProcesses.get(contextKey)?.active_run ?? null;
|
|
69
148
|
};
|
|
149
|
+
export const startActiveRunServices = (contextKey, serviceId, sessionId) => {
|
|
150
|
+
const state = runningProcesses.get(contextKey);
|
|
151
|
+
if (!state?.host_session || state.cancelled || state.admission_pending) {
|
|
152
|
+
throw new RunAdmissionError("service_session_unavailable", "The host service session is no longer active");
|
|
153
|
+
}
|
|
154
|
+
assertHostSessionRequest(state.host_session, serviceId, sessionId);
|
|
155
|
+
if (state.host_session.services.some(service => (serviceId === undefined || service.service_id === serviceId) && service.status === "stopping")) {
|
|
156
|
+
throw new RunAdmissionError("service_cleanup_pending", "Service process cleanup is pending. Retry stopping the service before restarting it.");
|
|
157
|
+
}
|
|
158
|
+
if (state.start_services) {
|
|
159
|
+
return state.start_services(serviceId).then(() => {
|
|
160
|
+
if (runningProcesses.get(contextKey) === state && !state.cancelled) {
|
|
161
|
+
state.on_state_changed?.();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
queueHostServices(state.host_session, serviceId);
|
|
167
|
+
state.on_state_changed?.();
|
|
168
|
+
}
|
|
169
|
+
return Promise.resolve();
|
|
170
|
+
};
|
|
70
171
|
export const getActiveRunStatusForProject = (projectId) => {
|
|
71
172
|
if (!projectId) {
|
|
72
173
|
return null;
|
|
@@ -99,38 +200,6 @@ export const getPreviewStatusForProject = (projectId) => {
|
|
|
99
200
|
const isActiveRunState = (contextKey, state) => {
|
|
100
201
|
return runningProcesses.get(contextKey) === state && !state.cancelled;
|
|
101
202
|
};
|
|
102
|
-
const isProcessAlive = (proc) => proc.exitCode === null && proc.signalCode === null;
|
|
103
|
-
const untrackProcess = (state, proc) => {
|
|
104
|
-
const index = state.processes.indexOf(proc);
|
|
105
|
-
if (index >= 0) {
|
|
106
|
-
state.processes.splice(index, 1);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
const trackProcess = (state, proc) => {
|
|
110
|
-
const cleanup = () => {
|
|
111
|
-
untrackProcess(state, proc);
|
|
112
|
-
};
|
|
113
|
-
state.processes.push(proc);
|
|
114
|
-
proc.on("close", cleanup);
|
|
115
|
-
proc.on("error", cleanup);
|
|
116
|
-
return proc;
|
|
117
|
-
};
|
|
118
|
-
const trackExecutionHandle = (state, handle) => {
|
|
119
|
-
state.execution_handles?.push(handle);
|
|
120
|
-
return trackProcess(state, handle.child);
|
|
121
|
-
};
|
|
122
|
-
const spawnManagedProcess = (cmd, cwd, stdio, state) => {
|
|
123
|
-
const env = createWorkspaceProcessEnvironment(cwd);
|
|
124
|
-
delete env.PORT;
|
|
125
|
-
const shell = resolveShell();
|
|
126
|
-
const proc = spawn(shell, ["-i", "-c", cmd], {
|
|
127
|
-
cwd,
|
|
128
|
-
stdio,
|
|
129
|
-
detached: process.platform !== "win32",
|
|
130
|
-
env,
|
|
131
|
-
});
|
|
132
|
-
return trackProcess(state, proc);
|
|
133
|
-
};
|
|
134
203
|
export const openIde = (cwd, ideCommand) => {
|
|
135
204
|
spawn(resolveShell(), ["-i", "-c", `${ideCommand} .`], {
|
|
136
205
|
cwd,
|
|
@@ -152,552 +221,494 @@ export const openTicketIdeInWorkspace = (ticket, project = null, workspaceCwd =
|
|
|
152
221
|
openIde(cwd, runConfig.ide);
|
|
153
222
|
return true;
|
|
154
223
|
};
|
|
155
|
-
const
|
|
156
|
-
if (!isProcessAlive(proc)) {
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (proc.pid && process.platform !== "win32") {
|
|
160
|
-
try {
|
|
161
|
-
process.kill(-proc.pid, signal);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
if (error.code === "ESRCH") {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
try {
|
|
171
|
-
proc.kill(signal);
|
|
172
|
-
}
|
|
173
|
-
catch { }
|
|
174
|
-
};
|
|
175
|
-
const waitForProcessesToExit = async (processes, timeoutMs) => {
|
|
176
|
-
const deadline = Date.now() + timeoutMs;
|
|
177
|
-
await new Promise((resolve) => {
|
|
178
|
-
const check = () => {
|
|
179
|
-
const allDone = processes.every((proc) => !isProcessAlive(proc));
|
|
180
|
-
if (allDone || Date.now() >= deadline) {
|
|
181
|
-
resolve();
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
setTimeout(check, 100);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
setTimeout(check, 100);
|
|
188
|
-
});
|
|
189
|
-
};
|
|
190
|
-
const runCommand = (cmd, cwd, state) => new Promise((resolve, reject) => {
|
|
191
|
-
const proc = spawnManagedProcess(cmd, cwd, ["ignore", "pipe", "pipe"], state);
|
|
192
|
-
let output = "";
|
|
193
|
-
const appendOutput = (chunk) => {
|
|
194
|
-
output = `${output}${chunk.toString()}`;
|
|
195
|
-
if (output.length > 8000) {
|
|
196
|
-
output = output.slice(-8000);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
proc.stdout?.on("data", appendOutput);
|
|
200
|
-
proc.stderr?.on("data", appendOutput);
|
|
201
|
-
proc.on("close", (code) => {
|
|
202
|
-
if (code === 0) {
|
|
203
|
-
resolve();
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
const suffix = output.trim() ? `\n${output.trim()}` : "";
|
|
207
|
-
reject(new Error(`Setup command failed (exit ${code}): ${cmd}${suffix}`));
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
proc.on("error", (err) => {
|
|
211
|
-
reject(err);
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
export const runSetupCommandsInWorkspace = async (project, cwd, execution) => {
|
|
215
|
-
const runConfig = getRunConfig(project);
|
|
216
|
-
const setupCommands = runConfig?.setup ?? [];
|
|
217
|
-
if (setupCommands.length === 0) {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const state = { processes: [] };
|
|
221
|
-
for (const cmd of setupCommands) {
|
|
222
|
-
if (execution?.signal?.aborted) {
|
|
223
|
-
throw new Error("Build was stopped.");
|
|
224
|
-
}
|
|
225
|
-
if (!execution) {
|
|
226
|
-
await runCommand(cmd, cwd, state);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
const shell = getExecutionShellCommand({
|
|
230
|
-
provider: execution.provider.kind,
|
|
231
|
-
command: cmd,
|
|
232
|
-
loadHostStartupFiles: execution.runtimeEnvironment.kind !== "host"
|
|
233
|
-
|| execution.runtimeEnvironment.inheritance !== "governed",
|
|
234
|
-
});
|
|
235
|
-
const handle = execution.provider.execute({
|
|
236
|
-
phase: "setup",
|
|
237
|
-
target: execution.target,
|
|
238
|
-
command: shell.command,
|
|
239
|
-
args: shell.args,
|
|
240
|
-
cwd,
|
|
241
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
242
|
-
launcherEnvironment: execution.launcherEnvironment,
|
|
243
|
-
runtimeEnvironment: execution.runtimeEnvironment,
|
|
244
|
-
detached: process.platform !== "win32",
|
|
245
|
-
});
|
|
246
|
-
execution.onHandle?.(handle);
|
|
247
|
-
try {
|
|
248
|
-
let output = "";
|
|
249
|
-
const appendOutput = (chunk) => {
|
|
250
|
-
output = `${output}${chunk.toString()}`;
|
|
251
|
-
if (output.length > 8000) {
|
|
252
|
-
output = output.slice(-8000);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
handle.child.stdout?.on("data", appendOutput);
|
|
256
|
-
handle.child.stderr?.on("data", appendOutput);
|
|
257
|
-
await new Promise((resolve, reject) => {
|
|
258
|
-
handle.child.once("error", reject);
|
|
259
|
-
handle.child.once("close", code => {
|
|
260
|
-
if (code === 0) {
|
|
261
|
-
resolve();
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const diagnostic = redactRepositorySecrets(output).content.trim();
|
|
265
|
-
const suffix = diagnostic ? `\n${diagnostic}` : "";
|
|
266
|
-
reject(new Error(`Setup command failed (exit ${code ?? "unknown"}).${suffix}`));
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
const placementFailure = await handle.placementFailure;
|
|
270
|
-
if (placementFailure) {
|
|
271
|
-
throw placementFailure;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
finally {
|
|
275
|
-
execution.onHandle?.(null);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
};
|
|
224
|
+
export const runSetupCommandsInWorkspace = runWorkspaceSetup;
|
|
279
225
|
const clearRunState = (contextKey, state, onStopped) => {
|
|
280
226
|
if (runningProcesses.get(contextKey) !== state) {
|
|
281
227
|
return;
|
|
282
228
|
}
|
|
229
|
+
if (state.release_execution_environment || state.finalization_failed) {
|
|
230
|
+
state.cancelled = true;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
283
233
|
deactivatePreviewProxyForProject(state.active_run.project_id);
|
|
284
234
|
state.preview_log_buffer = "";
|
|
235
|
+
if (state.active_run.project_id && state.active_run.config_snapshot) {
|
|
236
|
+
lastRunStatuses.set(state.active_run.project_id, {
|
|
237
|
+
...structuredClone(state.active_run),
|
|
238
|
+
config_snapshot: state.active_run.config_snapshot,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
285
241
|
runningProcesses.delete(contextKey);
|
|
286
242
|
onStopped?.();
|
|
287
243
|
};
|
|
288
244
|
const canCreateDurableServiceRun = (project, targetDescriptor) => Boolean(targetDescriptor
|
|
289
245
|
&& project.id
|
|
290
246
|
&& project.active_execution_profile_revision_id);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const relativePath = relative(workspacePath, serviceCwd);
|
|
300
|
-
if (relativePath === ".." || relativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
|
|
301
|
-
throw new Error("Service working directories must remain inside the canonical target");
|
|
302
|
-
}
|
|
303
|
-
const realWorkspacePath = realpathSync(workspacePath);
|
|
304
|
-
const realServiceCwd = realpathSync(serviceCwd);
|
|
305
|
-
const realRelativePath = relative(realWorkspacePath, realServiceCwd);
|
|
306
|
-
if (realRelativePath === ".." || realRelativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
|
|
307
|
-
throw new Error("Service working directories must not escape the canonical target through symbolic links");
|
|
308
|
-
}
|
|
309
|
-
return serviceCwd;
|
|
310
|
-
};
|
|
311
|
-
const finalizeDurableRun = async (state, status, eventType, message) => {
|
|
312
|
-
const runId = state.execution_run_id;
|
|
313
|
-
if (!runId) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
const release = state.release_execution_environment;
|
|
317
|
-
if (release) {
|
|
318
|
-
try {
|
|
319
|
-
await release();
|
|
320
|
-
state.release_execution_environment = null;
|
|
247
|
+
const startRun = ({ activeRun, cwd, executionAgent, executionModel, logLabelPrefix, onStopped, project, runConfig, skipSetup = false, targetDescriptor, profileSelection, serviceId, sessionId, }) => {
|
|
248
|
+
const contextKey = activeRun.context_key;
|
|
249
|
+
const admittedProvider = profileSelection?.reason === "explicit_service_host_override"
|
|
250
|
+
? "host" : project?.build_execution_provider ?? "host";
|
|
251
|
+
const existing = runningProcesses.get(contextKey);
|
|
252
|
+
if (existing) {
|
|
253
|
+
if (existing.admission_pending) {
|
|
254
|
+
throw new RunAdmissionError("service_admission_pending", "Service admission is still pending. Retry after admission settles.");
|
|
321
255
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
throw error;
|
|
256
|
+
if (existing.cancelled) {
|
|
257
|
+
throw new Error("The service session is stopping. Retry the stop before starting services.");
|
|
258
|
+
}
|
|
259
|
+
if (existing.execution_provider !== admittedProvider) {
|
|
260
|
+
throw new RunAdmissionError("service_provider_conflict", "The requested execution provider is incompatible with the active service session");
|
|
261
|
+
}
|
|
262
|
+
if (existing.host_session) {
|
|
263
|
+
return startActiveRunServices(contextKey, serviceId, sessionId);
|
|
331
264
|
}
|
|
265
|
+
else if (serviceId || sessionId) {
|
|
266
|
+
throw new Error("Individual service controls are only available for host sessions");
|
|
267
|
+
}
|
|
268
|
+
return Promise.resolve();
|
|
332
269
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
executionRunId: runId,
|
|
336
|
-
status,
|
|
337
|
-
code: status === "failed" ? "service_run_failed" : null,
|
|
338
|
-
message,
|
|
339
|
-
eventType,
|
|
340
|
-
});
|
|
341
|
-
};
|
|
342
|
-
const stopRunProcesses = async (state) => {
|
|
343
|
-
for (const proc of state.processes) {
|
|
344
|
-
killManagedProcess(proc, "SIGTERM");
|
|
270
|
+
if (sessionId !== undefined) {
|
|
271
|
+
throw new Error("The service session is no longer active");
|
|
345
272
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (isProcessAlive(proc)) {
|
|
349
|
-
killManagedProcess(proc, "SIGKILL");
|
|
350
|
-
}
|
|
273
|
+
if (getActiveRunStatusForProject(activeRun.project_id)) {
|
|
274
|
+
throw new Error("Another service session is already active for this project");
|
|
351
275
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
276
|
+
if (project?.id && hasActiveProjectServiceRun(project.id)) {
|
|
277
|
+
throw new RunAdmissionError("service_authority_without_runtime", "Durable service authority exists without a live runtime owner. Reconcile it before starting services.");
|
|
278
|
+
}
|
|
279
|
+
if (!existsSync(cwd)) {
|
|
280
|
+
throw new RunAdmissionError("service_workspace_unavailable", `Service workspace does not exist: ${cwd}`);
|
|
281
|
+
}
|
|
282
|
+
runConfig = structuredClone(runConfig);
|
|
283
|
+
activeRun = { ...activeRun, execution_provider: admittedProvider, config_snapshot: createRunConfigSnapshot(runConfig, {
|
|
284
|
+
preview_service_name: project?.preview_service_name,
|
|
285
|
+
preview_path: project?.preview_path,
|
|
286
|
+
preview_capability_mode: project?.preview_capability_mode,
|
|
287
|
+
}) };
|
|
288
|
+
project = structuredClone(project);
|
|
289
|
+
profileSelection = profileSelection ? structuredClone(profileSelection) : undefined;
|
|
356
290
|
const state = {
|
|
291
|
+
admission_pending: true,
|
|
292
|
+
execution_provider: admittedProvider,
|
|
357
293
|
processes: [],
|
|
358
294
|
cancelled: false,
|
|
359
295
|
active_run: activeRun,
|
|
360
|
-
preview_status: createRunPreviewStatus(project, runConfig),
|
|
296
|
+
preview_status: createRunPreviewStatus(project, runConfig, admittedProvider),
|
|
361
297
|
preview_log_buffer: "",
|
|
362
298
|
execution_run_id: null,
|
|
363
299
|
execution_handles: [],
|
|
364
300
|
release_execution_environment: null,
|
|
365
301
|
execution_project_id: null,
|
|
302
|
+
setup_abort_controller: new AbortController(),
|
|
303
|
+
launch_operations: 0,
|
|
304
|
+
launch_promises: new Set(),
|
|
305
|
+
on_state_changed: () => {
|
|
306
|
+
if (state.execution_run_id && !state.cancelled) {
|
|
307
|
+
appendLifecycleEvent({
|
|
308
|
+
projectId: project.id,
|
|
309
|
+
runId: state.execution_run_id,
|
|
310
|
+
eventType: "service_status_changed",
|
|
311
|
+
metadata: { session_id: state.host_session?.id, services: state.host_session?.services },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
onStopped?.();
|
|
315
|
+
},
|
|
366
316
|
};
|
|
367
|
-
|
|
368
|
-
if (!
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
317
|
+
const expectsHost = admittedProvider === "host";
|
|
318
|
+
if (!expectsHost && serviceId !== undefined) {
|
|
319
|
+
throw new Error("Individual service controls are not supported by Docker Sandbox");
|
|
320
|
+
}
|
|
321
|
+
if (expectsHost) {
|
|
322
|
+
state.host_session = createHostRunSession(runConfig);
|
|
323
|
+
assertHostSessionRequest(state.host_session, serviceId, state.host_session.id);
|
|
324
|
+
state.active_run = {
|
|
325
|
+
...activeRun,
|
|
326
|
+
session_id: state.host_session.id,
|
|
327
|
+
services: state.host_session.services,
|
|
328
|
+
};
|
|
329
|
+
queueHostServices(state.host_session, serviceId);
|
|
372
330
|
}
|
|
373
|
-
|
|
374
|
-
let executionTarget = null;
|
|
331
|
+
runningProcesses.set(contextKey, state);
|
|
375
332
|
if (canCreateDurableServiceRun(project, targetDescriptor)) {
|
|
376
333
|
try {
|
|
377
|
-
const target =
|
|
378
|
-
|
|
379
|
-
descriptor: targetDescriptor,
|
|
380
|
-
expectedWorkspacePath: cwd,
|
|
381
|
-
});
|
|
382
|
-
if (!isActiveRunState(contextKey, state)) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
executionTarget = target;
|
|
386
|
-
const source = activeRun.kind === "ticket"
|
|
387
|
-
? {
|
|
388
|
-
kind: "ticket",
|
|
389
|
-
id: activeRun.ticket_id,
|
|
390
|
-
bundleId: target.identity.kind === "bundle-worktree"
|
|
391
|
-
? target.identity.bundleId
|
|
392
|
-
: null,
|
|
393
|
-
}
|
|
394
|
-
: { kind: "project", id: project.id };
|
|
334
|
+
const target = resolveExecutionTargetLocation({ project, descriptor: targetDescriptor, expectedWorkspacePath: cwd });
|
|
335
|
+
cwd = target.workspacePath;
|
|
395
336
|
const durableRun = createServiceRun({
|
|
396
|
-
projectId: project.id,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
services: runConfig.services ?? [],
|
|
401
|
-
profileSelection,
|
|
337
|
+
projectId: project.id, contextKey, sessionId: state.host_session?.id,
|
|
338
|
+
source: activeRun.kind === "ticket"
|
|
339
|
+
? { kind: "ticket", id: activeRun.ticket_id, bundleId: target.identity.kind === "bundle-worktree" ? target.identity.bundleId : null }
|
|
340
|
+
: { kind: "project", id: project.id },
|
|
341
|
+
target, services: runConfig.services ?? [], profileSelection,
|
|
402
342
|
});
|
|
403
343
|
state.execution_run_id = durableRun.id;
|
|
404
344
|
state.execution_project_id = project.id;
|
|
405
|
-
if (!isActiveRunState(contextKey, state)) {
|
|
406
|
-
await finalizeDurableRun(state, "cancelled", "service_run_cancelled");
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
execution = await resolveWorkloadExecutionEnvironment({
|
|
410
|
-
project,
|
|
411
|
-
executionRunId: durableRun.id,
|
|
412
|
-
agent: executionAgent,
|
|
413
|
-
model: executionModel,
|
|
414
|
-
target,
|
|
415
|
-
workload: "service",
|
|
416
|
-
onLeaseAcquired: release => {
|
|
417
|
-
state.release_execution_environment = release;
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
state.release_execution_environment = execution.release;
|
|
421
|
-
if (!isActiveRunState(contextKey, state)) {
|
|
422
|
-
await finalizeDurableRun(state, "cancelled", "service_run_cancelled");
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
state.execution_provider = execution.provider.kind === "docker_sandbox"
|
|
426
|
-
? "docker_sandbox"
|
|
427
|
-
: "host";
|
|
428
|
-
state.preview_status = createRunPreviewStatus(project, runConfig, state.execution_provider);
|
|
429
|
-
transitionExecutionRun({
|
|
430
|
-
projectId: project.id,
|
|
431
|
-
runId: durableRun.id,
|
|
432
|
-
status: "running",
|
|
433
|
-
domainStatus: "starting",
|
|
434
|
-
eventType: "service_run_starting",
|
|
435
|
-
eventMessage: "Service execution provider and canonical target authority were resolved.",
|
|
436
|
-
});
|
|
437
345
|
}
|
|
438
346
|
catch (error) {
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
console.error("[run] service environment release failed:", getErrorMessage(releaseError));
|
|
442
|
-
});
|
|
347
|
+
if (runningProcesses.get(contextKey) === state) {
|
|
348
|
+
runningProcesses.delete(contextKey);
|
|
443
349
|
}
|
|
444
|
-
|
|
445
|
-
clearRunState(contextKey, state, onStopped);
|
|
446
|
-
return;
|
|
350
|
+
throw new RunAdmissionError("service_admission_rejected", getErrorMessage(error));
|
|
447
351
|
}
|
|
448
352
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
353
|
+
state.admission_pending = false;
|
|
354
|
+
state.settle_session = createHostSessionSettler(state, () => runningProcesses.get(contextKey) === state, () => clearRunState(contextKey, state, onStopped));
|
|
355
|
+
const startup = async () => {
|
|
356
|
+
if (!isActiveRunState(contextKey, state)) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let execution = null;
|
|
360
|
+
let executionTarget = null;
|
|
361
|
+
if (canCreateDurableServiceRun(project, targetDescriptor)) {
|
|
362
|
+
try {
|
|
363
|
+
const target = await resolveExecutionTarget({
|
|
364
|
+
project,
|
|
365
|
+
descriptor: targetDescriptor,
|
|
366
|
+
expectedWorkspacePath: cwd,
|
|
367
|
+
});
|
|
368
|
+
if (!isActiveRunState(contextKey, state)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
executionTarget = target;
|
|
372
|
+
markServiceRunAllocationStarted(project.id, state.execution_run_id);
|
|
373
|
+
execution = await resolveWorkloadExecutionEnvironment({
|
|
374
|
+
project,
|
|
375
|
+
executionRunId: state.execution_run_id,
|
|
376
|
+
agent: executionAgent,
|
|
377
|
+
model: executionModel,
|
|
378
|
+
target,
|
|
379
|
+
workload: "service",
|
|
380
|
+
onLeaseAcquired: release => {
|
|
381
|
+
state.release_execution_environment = release;
|
|
461
382
|
},
|
|
462
383
|
});
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
384
|
+
state.release_execution_environment = execution.release;
|
|
385
|
+
if (!isActiveRunState(contextKey, state)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (execution.provider.kind !== state.execution_provider) {
|
|
389
|
+
throw new Error("The resolved execution provider differs from the admitted service provider");
|
|
390
|
+
}
|
|
391
|
+
execution = { ...execution, provider: withHostServiceRecovery(execution.provider, project.id, state.execution_run_id) };
|
|
392
|
+
if (state.execution_provider === "docker_sandbox") {
|
|
393
|
+
if (serviceId !== undefined) {
|
|
394
|
+
throw new Error("Individual service controls are not supported by Docker Sandbox");
|
|
469
395
|
}
|
|
396
|
+
state.host_session = undefined;
|
|
397
|
+
state.active_run = activeRun;
|
|
470
398
|
}
|
|
399
|
+
state.preview_status = createRunPreviewStatus(project, runConfig, state.execution_provider);
|
|
400
|
+
transitionExecutionRun({
|
|
401
|
+
projectId: project.id,
|
|
402
|
+
runId: state.execution_run_id,
|
|
403
|
+
status: "running",
|
|
404
|
+
domainStatus: "starting",
|
|
405
|
+
eventType: "service_run_starting",
|
|
406
|
+
eventMessage: "Service execution provider and canonical target authority were resolved.",
|
|
407
|
+
});
|
|
471
408
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (state.execution_run_id) {
|
|
411
|
+
for (const service of state.host_session?.services ?? []) {
|
|
412
|
+
if (state.host_session?.pending.has(service.service_id)) {
|
|
413
|
+
service.status = "failed";
|
|
414
|
+
service.error = getErrorMessage(error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
state.host_session?.pending.clear();
|
|
418
|
+
await finalizeDurableRun(state, "failed", "service_run_start_failed", getErrorMessage(error)).catch(releaseError => {
|
|
419
|
+
console.error("[run] service environment release failed:", getErrorMessage(releaseError));
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
console.error(`[run] durable service start failed for ${logLabelPrefix}:`, getErrorMessage(error));
|
|
423
|
+
clearRunState(contextKey, state, onStopped);
|
|
475
424
|
return;
|
|
476
425
|
}
|
|
477
|
-
console.error(`[run] setup failed for ${logLabelPrefix}:`, getErrorMessage(err));
|
|
478
|
-
await finalizeDurableRun(state, "failed", "service_setup_failed", getErrorMessage(err)).catch(releaseError => console.error("[run] service environment release failed:", getErrorMessage(releaseError)));
|
|
479
|
-
clearRunState(contextKey, state, onStopped);
|
|
480
|
-
return;
|
|
481
426
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
let startupComplete = false;
|
|
488
|
-
const cleanupIfAllDead = () => {
|
|
489
|
-
if (!startupComplete || runningProcesses.get(contextKey) !== state || state.cancelled) {
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
const anyAlive = state.processes.some(isProcessAlive);
|
|
493
|
-
if (!anyAlive) {
|
|
494
|
-
void finalizeDurableRun(state, serviceFailureMessage ? "failed" : "completed", serviceFailureMessage ? "service_run_failed" : "service_run_completed", serviceFailureMessage ?? undefined)
|
|
495
|
-
.catch(error => console.error("[run] service environment release failed:", getErrorMessage(error)))
|
|
496
|
-
.finally(() => clearRunState(contextKey, state, onStopped));
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
let configuredServices;
|
|
500
|
-
try {
|
|
501
|
-
configuredServices = (runConfig.services ?? []).map(service => ({
|
|
502
|
-
service,
|
|
503
|
-
cwd: resolveServiceCwd(cwd, service.cwd),
|
|
504
|
-
}));
|
|
505
|
-
}
|
|
506
|
-
catch (error) {
|
|
507
|
-
await finalizeDurableRun(state, "failed", "service_configuration_invalid", getErrorMessage(error)).catch(releaseError => console.error("[run] service environment release failed:", getErrorMessage(releaseError)));
|
|
508
|
-
clearRunState(contextKey, state, onStopped);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
try {
|
|
512
|
-
for (const { service, cwd: serviceCwd } of configuredServices) {
|
|
513
|
-
const label = service.name ?? service.cmd;
|
|
514
|
-
let handle = null;
|
|
515
|
-
const proc = execution
|
|
516
|
-
? (() => {
|
|
517
|
-
const shell = getExecutionShellCommand({
|
|
518
|
-
provider: execution.provider.kind,
|
|
519
|
-
command: service.cmd,
|
|
520
|
-
loadHostStartupFiles: execution.runtimeEnvironment.kind !== "host"
|
|
521
|
-
|| execution.runtimeEnvironment.inheritance !== "governed",
|
|
522
|
-
});
|
|
523
|
-
handle = execution.provider.execute({
|
|
524
|
-
phase: "service",
|
|
427
|
+
if (!skipSetup) {
|
|
428
|
+
try {
|
|
429
|
+
if (execution) {
|
|
430
|
+
await runSetupCommandsInWorkspace(project, cwd, {
|
|
431
|
+
provider: execution.provider,
|
|
525
432
|
target: executionTarget,
|
|
526
|
-
command: shell.command,
|
|
527
|
-
args: shell.args,
|
|
528
|
-
cwd: serviceCwd,
|
|
529
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
530
433
|
launcherEnvironment: execution.launcherEnvironment,
|
|
531
434
|
runtimeEnvironment: execution.runtimeEnvironment,
|
|
532
|
-
|
|
435
|
+
signal: state.setup_abort_controller?.signal,
|
|
436
|
+
onHandle: handle => {
|
|
437
|
+
if (handle) {
|
|
438
|
+
trackExecutionHandle(state, handle);
|
|
439
|
+
if (state.cancelled) {
|
|
440
|
+
killManagedProcess(handle.child, "SIGTERM");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
533
444
|
});
|
|
534
|
-
return trackExecutionHandle(state, handle);
|
|
535
|
-
})()
|
|
536
|
-
: spawnManagedProcess(service.cmd, serviceCwd, ["ignore", "pipe", "pipe"], state);
|
|
537
|
-
const handleLogChunk = (data) => {
|
|
538
|
-
if (!isPreviewService(service, state.preview_status)) {
|
|
539
|
-
return;
|
|
540
445
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
: inferPreviewTargetsFromLogChunk(state.preview_log_buffer, project, service, state.preview_status.capability);
|
|
546
|
-
if (previewTargets.length === 0) {
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
void (async () => {
|
|
550
|
-
for (const detectedTarget of previewTargets) {
|
|
551
|
-
const reachableTarget = await resolveReachablePreviewTarget(detectedTarget, project.preview_path);
|
|
552
|
-
if (!reachableTarget || !isActiveRunState(contextKey, state)) {
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
if (state.preview_status.detected_url === reachableTarget.url &&
|
|
556
|
-
state.preview_status.proxy_url) {
|
|
446
|
+
else {
|
|
447
|
+
for (const cmd of runConfig.setup ?? []) {
|
|
448
|
+
await runCommand(cmd, cwd, state);
|
|
449
|
+
if (!isActiveRunState(contextKey, state)) {
|
|
557
450
|
return;
|
|
558
451
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
if (state.cancelled) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
console.error(`[run] setup failed for ${logLabelPrefix}:`, getErrorMessage(err));
|
|
460
|
+
await finalizeDurableRun(state, "failed", "service_setup_failed", getErrorMessage(err)).catch(releaseError => console.error("[run] service environment release failed:", getErrorMessage(releaseError)));
|
|
461
|
+
if (state.host_session) {
|
|
462
|
+
state.host_session.setup_error = getErrorMessage(err);
|
|
463
|
+
for (const service of state.host_session.services) {
|
|
464
|
+
if (state.host_session.pending.has(service.service_id)) {
|
|
465
|
+
service.status = "failed";
|
|
466
|
+
service.error = getErrorMessage(err);
|
|
573
467
|
}
|
|
574
|
-
return;
|
|
575
468
|
}
|
|
576
|
-
|
|
577
|
-
};
|
|
578
|
-
proc.stdout?.on("data", (data) => {
|
|
579
|
-
console.log(`[run:${logLabelPrefix}:${label}] ${data.toString().trimEnd()}`);
|
|
580
|
-
handleLogChunk(data);
|
|
581
|
-
});
|
|
582
|
-
proc.stderr?.on("data", (data) => {
|
|
583
|
-
console.error(`[run:${logLabelPrefix}:${label}] ${data.toString().trimEnd()}`);
|
|
584
|
-
handleLogChunk(data);
|
|
585
|
-
});
|
|
586
|
-
proc.on("close", (code) => {
|
|
587
|
-
console.log(`[run:${logLabelPrefix}:${label}] exited with code ${code}`);
|
|
588
|
-
if (code !== 0 && !state.cancelled) {
|
|
589
|
-
serviceFailureMessage = `Service ${label} exited with code ${code ?? "unknown"}.`;
|
|
469
|
+
state.host_session.pending.clear();
|
|
590
470
|
}
|
|
471
|
+
clearRunState(contextKey, state, onStopped);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (!isActiveRunState(contextKey, state)) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
let serviceFailureMessage = null;
|
|
479
|
+
let startupComplete = false;
|
|
480
|
+
const cleanupIfAllDead = () => {
|
|
481
|
+
if (state.host_session) {
|
|
482
|
+
void state.settle_session?.().catch(error => console.error("[run] service finalization failed:", error));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!startupComplete || runningProcesses.get(contextKey) !== state || state.cancelled) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const anyAlive = state.processes.some(isProcessAlive);
|
|
489
|
+
if (!anyAlive) {
|
|
490
|
+
state.cancelled = true;
|
|
491
|
+
void finalizeDurableRun(state, serviceFailureMessage ? "failed" : "completed", serviceFailureMessage ? "service_run_failed" : "service_run_completed", serviceFailureMessage ?? undefined)
|
|
492
|
+
.catch(error => console.error("[run] service environment release failed:", getErrorMessage(error)))
|
|
493
|
+
.finally(() => clearRunState(contextKey, state, onStopped));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const launchServices = createRunServiceLauncher({
|
|
497
|
+
state, project, runConfig, cwd, execution, executionTarget, logLabelPrefix,
|
|
498
|
+
isActive: () => isActiveRunState(contextKey, state),
|
|
499
|
+
onExit: failure => {
|
|
500
|
+
serviceFailureMessage = failure ?? serviceFailureMessage;
|
|
591
501
|
cleanupIfAllDead();
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
console.error(`[run:${logLabelPrefix}:${label}] error: ${getErrorMessage(err)}`);
|
|
595
|
-
if (!state.cancelled) {
|
|
596
|
-
serviceFailureMessage = `Service ${label} failed: ${getErrorMessage(err)}`;
|
|
502
|
+
if (state.host_session && isActiveRunState(contextKey, state)) {
|
|
503
|
+
state.on_state_changed?.();
|
|
597
504
|
}
|
|
598
|
-
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
try {
|
|
508
|
+
state.start_services = requestedId => {
|
|
509
|
+
state.launch_operations = (state.launch_operations ?? 0) + 1;
|
|
510
|
+
const launching = (async () => {
|
|
511
|
+
try {
|
|
512
|
+
await launchServices(requestedId);
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
state.launch_operations -= 1;
|
|
516
|
+
await state.settle_session?.();
|
|
517
|
+
}
|
|
518
|
+
})();
|
|
519
|
+
state.launch_promises.add(launching);
|
|
520
|
+
const remove = () => { state.launch_promises.delete(launching); };
|
|
521
|
+
void launching.then(remove, remove);
|
|
522
|
+
return launching;
|
|
523
|
+
};
|
|
524
|
+
if (state.host_session) {
|
|
525
|
+
const pending = [...state.host_session.pending];
|
|
526
|
+
await Promise.all(pending.map(id => state.start_services(id)));
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
await launchServices();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
await stopRunProcesses(state);
|
|
534
|
+
await finalizeDurableRun(state, "failed", "service_placement_failed", getErrorMessage(error)).catch(releaseError => console.error("[run] service environment release failed:", getErrorMessage(releaseError)));
|
|
535
|
+
clearRunState(contextKey, state, onStopped);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (state.execution_run_id && execution && (runConfig.services ?? []).length === 0) {
|
|
539
|
+
recordServiceRunPlacement({
|
|
540
|
+
projectId: project.id,
|
|
541
|
+
executionRunId: state.execution_run_id,
|
|
542
|
+
provider: state.execution_provider,
|
|
543
|
+
environmentId: execution.environment?.id ?? null,
|
|
544
|
+
environmentRevision: execution.environment?.environment_revision ?? null,
|
|
599
545
|
});
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
546
|
+
}
|
|
547
|
+
if (!isActiveRunState(contextKey, state)) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (state.execution_run_id) {
|
|
551
|
+
transitionExecutionRun({
|
|
552
|
+
projectId: project.id,
|
|
553
|
+
runId: state.execution_run_id,
|
|
554
|
+
status: "running",
|
|
555
|
+
domainStatus: "active",
|
|
556
|
+
eventType: "service_run_active",
|
|
557
|
+
eventMessage: state.execution_provider === "docker_sandbox"
|
|
558
|
+
? "Services are active in a shared Docker Sandbox environment; interruption applies to the whole environment."
|
|
559
|
+
: "Services are active as verified host processes.",
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if ((runConfig.services ?? []).length === 0) {
|
|
563
|
+
await finalizeDurableRun(state, "completed", "service_run_completed").catch(error => console.error("[run] service environment release failed:", getErrorMessage(error)));
|
|
564
|
+
clearRunState(contextKey, state, onStopped);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
startupComplete = true;
|
|
568
|
+
cleanupIfAllDead();
|
|
569
|
+
};
|
|
570
|
+
state.startup_promise = startup().finally(async () => {
|
|
571
|
+
state.startup_complete = true;
|
|
572
|
+
if (!state.finalization_failed) {
|
|
573
|
+
await state.settle_session?.();
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
return state.startup_promise;
|
|
577
|
+
};
|
|
578
|
+
export const stopAllTickets = async () => {
|
|
579
|
+
const contextKeys = [...runningProcesses.keys()];
|
|
580
|
+
await Promise.all(contextKeys.map(contextKey => stopTicket(contextKey)));
|
|
581
|
+
};
|
|
582
|
+
export const stopTicket = async (ticketOrKey, options = {}) => {
|
|
583
|
+
let contextKey = resolveRunContextKey(ticketOrKey);
|
|
584
|
+
if (options.sessionId && ticketOrKey && typeof ticketOrKey !== "string") {
|
|
585
|
+
const frozen = [...runningProcesses.values()].find(state => state.host_session?.id === options.sessionId
|
|
586
|
+
&& state.active_run.kind === "ticket" && state.active_run.ticket_id === ticketOrKey.id);
|
|
587
|
+
contextKey = frozen?.active_run.context_key ?? contextKey;
|
|
588
|
+
}
|
|
589
|
+
if (!contextKey) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const state = runningProcesses.get(contextKey);
|
|
593
|
+
if (!state) {
|
|
594
|
+
if (retryInterruptedHostServiceRunForContext(contextKey, options)) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (options.sessionId) {
|
|
598
|
+
throw new Error("The service session is no longer active");
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (state.host_session) {
|
|
603
|
+
assertHostSessionRequest(state.host_session, options.serviceId, options.sessionId);
|
|
604
|
+
if (options.serviceId !== undefined) {
|
|
605
|
+
const session = state.host_session;
|
|
606
|
+
session.pending.delete(options.serviceId);
|
|
607
|
+
const status = session.services.find(service => service.service_id === options.serviceId);
|
|
608
|
+
const proc = session.processes.get(options.serviceId);
|
|
609
|
+
if (!proc) {
|
|
610
|
+
status.status = "stopped";
|
|
611
|
+
if (!state.start_services && session.pending.size === 0 && session.processes.size === 0) {
|
|
612
|
+
await stopTicket(contextKey, { sessionId: session.id });
|
|
603
613
|
return;
|
|
604
614
|
}
|
|
605
|
-
|
|
606
|
-
|
|
615
|
+
await state.settle_session?.();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
state.launch_operations = (state.launch_operations ?? 0) + 1;
|
|
619
|
+
try {
|
|
620
|
+
status.status = "stopping";
|
|
621
|
+
const configuredService = session.config.services.find((service, index) => serviceIdentity(service, index) === options.serviceId);
|
|
622
|
+
if (configuredService && isPreviewService(configuredService, state.preview_status)) {
|
|
623
|
+
deactivatePreviewProxyForProject(state.active_run.project_id);
|
|
624
|
+
state.preview_status = { ...state.preview_status, state: "idle", detected_url: null, detected_origin: null, detected_path: null, proxy_url: null };
|
|
625
|
+
state.preview_log_buffer = "";
|
|
607
626
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
627
|
+
killManagedProcess(proc, "SIGTERM");
|
|
628
|
+
await waitForProcessesToExit([proc], 1000);
|
|
629
|
+
killManagedProcess(proc, "SIGKILL");
|
|
630
|
+
await waitForProcessesToExit([proc], 1000);
|
|
631
|
+
if (isProcessAlive(proc)) {
|
|
632
|
+
throw new Error("The service did not exit. Retry stopping it before restarting.");
|
|
611
633
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
placement,
|
|
619
|
-
});
|
|
634
|
+
finishHostService(session, options.serviceId, proc, proc.exitCode);
|
|
635
|
+
state.on_state_changed?.();
|
|
636
|
+
}
|
|
637
|
+
finally {
|
|
638
|
+
state.launch_operations -= 1;
|
|
639
|
+
await state.settle_session?.();
|
|
620
640
|
}
|
|
641
|
+
return;
|
|
621
642
|
}
|
|
622
643
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
await finalizeDurableRun(state, "failed", "service_placement_failed", getErrorMessage(error)).catch(releaseError => console.error("[run] service environment release failed:", getErrorMessage(releaseError)));
|
|
626
|
-
clearRunState(contextKey, state, onStopped);
|
|
627
|
-
return;
|
|
644
|
+
else if (options.serviceId !== undefined || options.sessionId !== undefined) {
|
|
645
|
+
throw new Error("Individual service controls are not supported by Docker Sandbox");
|
|
628
646
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
647
|
+
const stoppingWork = !state.finalization_intent && (!state.startup_complete
|
|
648
|
+
|| Boolean(state.launch_operations) || Boolean(state.host_session?.pending.size)
|
|
649
|
+
|| state.processes.some(isProcessAlive));
|
|
650
|
+
state.cancelled = true;
|
|
651
|
+
state.setup_abort_controller?.abort();
|
|
652
|
+
state.host_session?.pending.clear();
|
|
653
|
+
if (state.host_session && stoppingWork) {
|
|
654
|
+
state.host_session.stop_requested = true;
|
|
655
|
+
}
|
|
656
|
+
for (const service of state.host_session?.services ?? []) {
|
|
657
|
+
if (["starting", "running"].includes(service.status)) {
|
|
658
|
+
service.status = "stopping";
|
|
659
|
+
}
|
|
637
660
|
}
|
|
638
|
-
|
|
661
|
+
deactivatePreviewProxyForProject(state.active_run.project_id);
|
|
662
|
+
await stopRunProcesses(state);
|
|
663
|
+
await state.startup_promise?.catch(error => {
|
|
664
|
+
console.error("[run] startup settled during stop:", error);
|
|
665
|
+
});
|
|
666
|
+
await Promise.allSettled([...(state.launch_promises ?? [])]);
|
|
667
|
+
if (runningProcesses.get(contextKey) !== state) {
|
|
639
668
|
return;
|
|
640
669
|
}
|
|
641
|
-
if (state.
|
|
642
|
-
|
|
643
|
-
projectId: project.id,
|
|
644
|
-
runId: state.execution_run_id,
|
|
645
|
-
status: "running",
|
|
646
|
-
domainStatus: "active",
|
|
647
|
-
eventType: "service_run_active",
|
|
648
|
-
eventMessage: state.execution_provider === "docker_sandbox"
|
|
649
|
-
? "Services are active in a shared Docker Sandbox environment; interruption applies to the whole environment."
|
|
650
|
-
: "Services are active as verified host processes.",
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
if ((runConfig.services ?? []).length === 0) {
|
|
654
|
-
await finalizeDurableRun(state, "completed", "service_run_completed").catch(error => console.error("[run] service environment release failed:", getErrorMessage(error)));
|
|
655
|
-
clearRunState(contextKey, state, onStopped);
|
|
670
|
+
if (state.finalization) {
|
|
671
|
+
await state.finalization;
|
|
656
672
|
return;
|
|
657
673
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
export const stopAllTickets = async () => {
|
|
662
|
-
const contextKeys = [...runningProcesses.keys()];
|
|
663
|
-
await Promise.all(contextKeys.map(stopTicket));
|
|
664
|
-
};
|
|
665
|
-
export const stopTicket = async (ticketOrKey) => {
|
|
666
|
-
const contextKey = resolveRunContextKey(ticketOrKey);
|
|
667
|
-
if (!contextKey) {
|
|
668
|
-
return;
|
|
674
|
+
await stopRunProcesses(state);
|
|
675
|
+
for (const [id, proc] of state.host_session?.processes ?? []) {
|
|
676
|
+
finishHostService(state.host_session, id, proc, proc.exitCode);
|
|
669
677
|
}
|
|
670
|
-
const state
|
|
671
|
-
|
|
678
|
+
for (const service of state.host_session?.services ?? []) {
|
|
679
|
+
if (service.status === "stopping") {
|
|
680
|
+
service.status = "stopped";
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
await state.settle_session?.();
|
|
684
|
+
if (state.finalization) {
|
|
685
|
+
await state.finalization;
|
|
672
686
|
return;
|
|
673
687
|
}
|
|
674
|
-
state.cancelled
|
|
675
|
-
await stopRunProcesses(state);
|
|
676
|
-
await finalizeDurableRun(state, "cancelled", "service_run_cancelled", state.execution_provider === "docker_sandbox"
|
|
688
|
+
state.finalization ??= finalizeDurableRun(state, "cancelled", "service_run_cancelled", state.execution_provider === "docker_sandbox"
|
|
677
689
|
? "The service run was stopped. Docker Sandbox interruption stops the whole environment and every attached service."
|
|
678
|
-
: "The service run host processes were stopped.").
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
690
|
+
: "The service run host processes were stopped.").then(() => clearRunState(contextKey, state)).catch(error => {
|
|
691
|
+
state.finalization = null;
|
|
692
|
+
throw error;
|
|
693
|
+
});
|
|
694
|
+
await state.finalization;
|
|
684
695
|
};
|
|
685
696
|
export const runTicket = async (ticket, io, project = null) => {
|
|
686
697
|
return runTicketInWorkspace(ticket, io, project, null);
|
|
687
698
|
};
|
|
688
|
-
export const runTicketInWorkspace =
|
|
699
|
+
export const runTicketInWorkspace = (ticket, io, project = null, workspaceCwd = null, profileSelection, controls = {}) => {
|
|
689
700
|
const runConfig = getRunConfig(project);
|
|
690
701
|
if (!runConfig || !hasRunnableCommands(runConfig)) {
|
|
691
|
-
return;
|
|
702
|
+
return Promise.resolve();
|
|
692
703
|
}
|
|
693
704
|
const contextKey = resolveTicketContextKey(ticket);
|
|
694
705
|
const branch = resolveTicketBranch(ticket);
|
|
695
706
|
if (!contextKey || (!branch && !workspaceCwd)) {
|
|
696
|
-
return;
|
|
707
|
+
return Promise.resolve();
|
|
697
708
|
}
|
|
698
709
|
const cwd = workspaceCwd ?? resolveWorktreePath(branch, project);
|
|
699
710
|
const executionAgent = resolveTicketTool(ticket, config.tool ?? "opencode");
|
|
700
|
-
|
|
711
|
+
return startRun({
|
|
701
712
|
activeRun: {
|
|
702
713
|
kind: "ticket",
|
|
703
714
|
context_key: contextKey,
|
|
@@ -713,7 +724,7 @@ export const runTicketInWorkspace = async (ticket, io, project = null, workspace
|
|
|
713
724
|
preferWorkflow: true,
|
|
714
725
|
}).model,
|
|
715
726
|
logLabelPrefix: `ticket:${ticket.id}`,
|
|
716
|
-
onStopped: () => emitRunStateForContext(io, ticket,
|
|
727
|
+
onStopped: () => emitRunStateForContext(io, ticket, isRunning(ticket)),
|
|
717
728
|
project: project,
|
|
718
729
|
runConfig,
|
|
719
730
|
skipSetup: isTicketRunSetupReady(ticket),
|
|
@@ -725,17 +736,18 @@ export const runTicketInWorkspace = async (ticket, io, project = null, workspace
|
|
|
725
736
|
})
|
|
726
737
|
: undefined,
|
|
727
738
|
profileSelection,
|
|
739
|
+
...controls,
|
|
728
740
|
});
|
|
729
741
|
};
|
|
730
|
-
export const runProject =
|
|
742
|
+
export const runProject = (project, options = {}) => {
|
|
731
743
|
const runConfig = getRunConfig(project);
|
|
732
744
|
if (!runConfig || !hasRunnableCommands(runConfig)) {
|
|
733
|
-
return;
|
|
745
|
+
return Promise.resolve();
|
|
734
746
|
}
|
|
735
747
|
const targetKind = options.targetKind ?? "repo_root";
|
|
736
748
|
const targetBundleId = targetKind === "bundle" ? (options.targetBundleId ?? null) : null;
|
|
737
749
|
const executionAgent = config.tool ?? "opencode";
|
|
738
|
-
|
|
750
|
+
return startRun({
|
|
739
751
|
activeRun: {
|
|
740
752
|
kind: "project",
|
|
741
753
|
context_key: resolveProjectRunContextKey(project.id, {
|
|
@@ -767,9 +779,16 @@ export const runProject = async (project, options = {}) => {
|
|
|
767
779
|
branch: options.targetBranch ?? null,
|
|
768
780
|
},
|
|
769
781
|
profileSelection: options.profileSelection,
|
|
782
|
+
serviceId: options.serviceId,
|
|
783
|
+
sessionId: options.sessionId,
|
|
770
784
|
});
|
|
771
785
|
};
|
|
772
786
|
export const stopProjectRun = async (projectId, target = {}) => {
|
|
773
|
-
|
|
787
|
+
const active = getActiveRunStatusForProject(projectId);
|
|
788
|
+
const recovered = active ? null : getRecoverableHostServiceRun(projectId);
|
|
789
|
+
if (!active && !recovered && hasActiveProjectServiceRun(projectId)) {
|
|
790
|
+
throw new Error("Service cleanup requires reconciliation of the recorded execution environment.");
|
|
791
|
+
}
|
|
792
|
+
await stopTicket(active?.context_key ?? recovered?.context_key ?? resolveProjectRunContextKey(projectId, target), target);
|
|
774
793
|
};
|
|
775
794
|
//# sourceMappingURL=run.js.map
|