@straiffi/archon 1.10.9 → 1.10.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/client/assets/ActivityView-BFn_Nhj4.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-iZD86Yju.js +1 -0
  3. package/dist/client/assets/VerificationDialog-CqKE8t1d.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BAIGV9Mj.js → architectureDiagram-3BPJPVTR-C8KefY67.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-B-5v5jSZ.js → blockDiagram-GPEHLZMM-BO2Q_Osr.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-C6wU415o.js → c4Diagram-AAUBKEIU-DGPkhbrK.js} +1 -1
  7. package/dist/client/assets/channel-DXKn3TOv.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-COmvX2tP.js → chunk-2J33WTMH-Dtb7Xu8C.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-BlANptdY.js → chunk-3OPIFGDE-Bnf4GbOF.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-ETyA2_GU.js → chunk-5ZQYHXKU-BWGid3g7.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-D5-DudwF.js → chunk-727SXJPM-ScA8IRGP.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-k1AJ4sA3.js → chunk-AQP2D5EJ-Bygg04Ip.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-jfksXwY9.js → chunk-CSCIHK7Q-CxdDDtfN.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CHj0FDj9.js → chunk-KSCS5N6A-BgpZFsB5.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-1ODGxXhG.js → chunk-L5ZTLDWV-CKSew0OL.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-Bgh9X2W0.js → chunk-LZXEDZCA-DVG9Rx0K.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-DDkb3sp4.js → chunk-ND2GUHAM-CHBqNmuF.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-B6B8PZ_-.js → chunk-NZK2D7GU-DvZYWn7V.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DhiUIFgj.js → chunk-O5CBEL6O-BL7lRTSe.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-Dzd0fbP3.js → chunk-WU5MYG2G-C-il2wHZ.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-U0fU20hT.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C6bQNSxL.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DoNDMCXI.js → dagre-BM42HDAG-B00l5tLi.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-ETDXSa5q.js → diagram-2AECGRRQ-Tum6g1_5.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CgTUep7e.js → diagram-5GNKFQAL-BK1GX5jc.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-BKTx2iRe.js → diagram-KO2AKTUF-BrzmD5NB.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-9iEiScHT.js → diagram-LMA3HP47-BhrlgXiJ.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-UIZRh7Qa.js → diagram-OG6HWLK6-K-fWsJ3a.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-BpNJmJva.js → erDiagram-TEJ5UH35-Der6PjHE.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-C-rK0XZ1.js → flowDiagram-I6XJVG4X-BVWgGhXe.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DWyfPvv_.js → ganttDiagram-6RSMTGT7-DcoZ5ggj.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-x71bzdc_.js → gitGraphDiagram-PVQCEYII-BhhbMTBu.js} +1 -1
  33. package/dist/client/assets/index-B6BltHSr.css +2 -0
  34. package/dist/client/assets/index-CFOYm3j9.js +276 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CbRus0Xu.js → infoDiagram-5YYISTIA-Lwc-agyc.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DvaMHrgN.js → ishikawaDiagram-YF4QCWOH-B95U1Xcl.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BQBu4YGn.js → journeyDiagram-JHISSGLW-xFpigy5D.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-8rU-U7L1.js → kanban-definition-UN3LZRKU-D3mudQnR.js} +1 -1
  39. package/dist/client/assets/{line-CSZnwjLA.js → line-DtTL3ZI2.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-CTOl2slE.js → mermaid-parser.core-Dau7K6zV.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-B9gr0Vqf.js → mermaid.core-B8qtvATu.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-But1_PWQ.js → mindmap-definition-RKZ34NQL-Cus04Yq3.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-CnlVmfSG.js → pieDiagram-4H26LBE5-BCMGpBnn.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Dww1ZGuR.js → quadrantDiagram-W4KKPZXB-CAmOr-PJ.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CNf_loe8.js → requirementDiagram-4Y6WPE33-kisD3jc4.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBhi37Jj.js → sankeyDiagram-5OEKKPKP-CsCedmM0.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B64hb4E4.js → sequenceDiagram-3UESZ5HK-B-dAi954.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DcS6oB-S.js → stateDiagram-AJRCARHV-X0BnedIW.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-6B8YH8Y2.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-Dq-4tXfh.js → timeline-definition-PNZ67QCA-DfFoSjhr.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CeDklSE_.js → vennDiagram-CIIHVFJN-CKSCBwhG.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-D2cJISMm.js → wardleyDiagram-YWT4CUSO-DNIBQmHO.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CBF-tTpF.js → xychartDiagram-2RQKCTM6-FvB55TQN.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +368 -5
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +194 -250
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agentRuns.js +14 -1
  60. package/dist/server/lib/agentRuns.js.map +1 -1
  61. package/dist/server/lib/automationLoopProjection.js +55 -12
  62. package/dist/server/lib/automationLoopProjection.js.map +1 -1
  63. package/dist/server/lib/automationProgression.js +2 -5
  64. package/dist/server/lib/automationProgression.js.map +1 -1
  65. package/dist/server/lib/automationResumeDecision.js +19 -1
  66. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  67. package/dist/server/lib/buildControlEligibility.js +4 -0
  68. package/dist/server/lib/buildControlEligibility.js.map +1 -0
  69. package/dist/server/lib/buildExecutionAdmission.js +76 -0
  70. package/dist/server/lib/buildExecutionAdmission.js.map +1 -0
  71. package/dist/server/lib/buildSequenceResume.js +79 -0
  72. package/dist/server/lib/buildSequenceResume.js.map +1 -0
  73. package/dist/server/lib/buildWorkload.js +11 -0
  74. package/dist/server/lib/buildWorkload.js.map +1 -0
  75. package/dist/server/lib/checkAutoFixDetails.js +36 -1
  76. package/dist/server/lib/checkAutoFixDetails.js.map +1 -1
  77. package/dist/server/lib/executionEnvironmentReconciliation.js +15 -2
  78. package/dist/server/lib/executionEnvironmentReconciliation.js.map +1 -1
  79. package/dist/server/lib/executionEnvironments.js +15 -0
  80. package/dist/server/lib/executionEnvironments.js.map +1 -1
  81. package/dist/server/lib/executionRetry.js +13 -1
  82. package/dist/server/lib/executionRetry.js.map +1 -1
  83. package/dist/server/lib/executionRunAuthority.js +2 -2
  84. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  85. package/dist/server/lib/executionTargets.js +7 -4
  86. package/dist/server/lib/executionTargets.js.map +1 -1
  87. package/dist/server/lib/executionTimeline.js +8 -13
  88. package/dist/server/lib/executionTimeline.js.map +1 -1
  89. package/dist/server/lib/executionTimelineHierarchy.js +83 -0
  90. package/dist/server/lib/executionTimelineHierarchy.js.map +1 -0
  91. package/dist/server/lib/failedBuildSequenceRecovery.js +133 -0
  92. package/dist/server/lib/failedBuildSequenceRecovery.js.map +1 -0
  93. package/dist/server/lib/hostRunSession.js +62 -0
  94. package/dist/server/lib/hostRunSession.js.map +1 -0
  95. package/dist/server/lib/hostServiceRecovery.js +159 -0
  96. package/dist/server/lib/hostServiceRecovery.js.map +1 -0
  97. package/dist/server/lib/hostServiceRunProvider.js +32 -0
  98. package/dist/server/lib/hostServiceRunProvider.js.map +1 -0
  99. package/dist/server/lib/mobileAccess.js +6 -2
  100. package/dist/server/lib/mobileAccess.js.map +1 -1
  101. package/dist/server/lib/projectAutoConfig.js +5 -2
  102. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  103. package/dist/server/lib/projectTargets.js +16 -2
  104. package/dist/server/lib/projectTargets.js.map +1 -1
  105. package/dist/server/lib/projects.js +25 -1
  106. package/dist/server/lib/projects.js.map +1 -1
  107. package/dist/server/lib/reviewFindingReconciliation.js +9 -2
  108. package/dist/server/lib/reviewFindingReconciliation.js.map +1 -1
  109. package/dist/server/lib/reviewFindings.js +82 -21
  110. package/dist/server/lib/reviewFindings.js.map +1 -1
  111. package/dist/server/lib/reviewRepairAssessmentRecords.js +41 -0
  112. package/dist/server/lib/reviewRepairAssessmentRecords.js.map +1 -0
  113. package/dist/server/lib/reviewRepairCompletion.js +78 -18
  114. package/dist/server/lib/reviewRepairCompletion.js.map +1 -1
  115. package/dist/server/lib/reviewRepairIterationRecords.js +4 -2
  116. package/dist/server/lib/reviewRepairIterationRecords.js.map +1 -1
  117. package/dist/server/lib/reviewRepairIterations.js +8 -2
  118. package/dist/server/lib/reviewRepairIterations.js.map +1 -1
  119. package/dist/server/lib/reviewRepairProgress.js +177 -0
  120. package/dist/server/lib/reviewRepairProgress.js.map +1 -0
  121. package/dist/server/lib/reviewRepairReconciliation.js +119 -140
  122. package/dist/server/lib/reviewRepairReconciliation.js.map +1 -1
  123. package/dist/server/lib/reviewRepairResumeOverride.js +55 -0
  124. package/dist/server/lib/reviewRepairResumeOverride.js.map +1 -0
  125. package/dist/server/lib/run.js +541 -504
  126. package/dist/server/lib/run.js.map +1 -1
  127. package/dist/server/lib/runProcessIdentity.js +71 -0
  128. package/dist/server/lib/runProcessIdentity.js.map +1 -0
  129. package/dist/server/lib/runProcesses.js +273 -0
  130. package/dist/server/lib/runProcesses.js.map +1 -0
  131. package/dist/server/lib/runServices.js +223 -0
  132. package/dist/server/lib/runServices.js.map +1 -0
  133. package/dist/server/lib/runSessionLifecycle.js +77 -0
  134. package/dist/server/lib/runSessionLifecycle.js.map +1 -0
  135. package/dist/server/lib/runStatus.js +8 -0
  136. package/dist/server/lib/runStatus.js.map +1 -0
  137. package/dist/server/lib/serviceRunCleanup.js +70 -0
  138. package/dist/server/lib/serviceRunCleanup.js.map +1 -0
  139. package/dist/server/lib/serviceRuns.js +458 -19
  140. package/dist/server/lib/serviceRuns.js.map +1 -1
  141. package/dist/server/lib/ticketAutomation.js +4 -27
  142. package/dist/server/lib/ticketAutomation.js.map +1 -1
  143. package/dist/server/lib/ticketRunOperations.js +103 -34
  144. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  145. package/dist/server/lib/tickets.js +0 -13
  146. package/dist/server/lib/tickets.js.map +1 -1
  147. package/dist/server/lib/types.js.map +1 -1
  148. package/dist/server/workers/build.js +44 -69
  149. package/dist/server/workers/build.js.map +1 -1
  150. package/dist/server/workers/plan.js +0 -7
  151. package/dist/server/workers/plan.js.map +1 -1
  152. package/dist/server/workers/review.js +95 -146
  153. package/dist/server/workers/review.js.map +1 -1
  154. package/package.json +1 -1
  155. package/dist/client/assets/ActivityView-CKvbDXeA.js +0 -1
  156. package/dist/client/assets/GitHubActionsDialog-Dzo0vtak.js +0 -1
  157. package/dist/client/assets/VerificationDialog-BJnOitrJ.js +0 -4
  158. package/dist/client/assets/channel-5mIy7nm1.js +0 -1
  159. package/dist/client/assets/classDiagram-4FO5ZUOK-BiQ-X2Sg.js +0 -1
  160. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CF-wCYW-.js +0 -1
  161. package/dist/client/assets/index-CIAO5aE_.css +0 -2
  162. package/dist/client/assets/index-nCGyalrn.js +0 -276
  163. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-PWeeL0tR.js +0 -1
  164. package/dist/server/lib/automationDeliveryDecision.js +0 -34
  165. package/dist/server/lib/automationDeliveryDecision.js.map +0 -1
@@ -1,23 +1,49 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, realpathSync } from "fs";
3
- import { isAbsolute, relative, resolve } from "path";
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 { applyDetectedPreviewTarget, buildPreviewProxyUrl, createIdlePreviewStatus, createRunPreviewStatus, detectPreviewTargets, inferPreviewTargetsFromLogChunk, isPreviewService, normalizePreviewLogText, resolveReachablePreviewTarget, } from "./preview.js";
9
- import { deactivatePreviewProxyForProject, ensurePreviewProxyForProject, } from "./previewProxy.js";
10
- import { getExecutionShellCommand, resolveShell } from "./shell.js";
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 { completeServiceRun, createServiceRun, recordServiceRunReleaseFailure, recordServiceRunPlacement, } from "./serviceRuns.js";
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";
22
+ import { forceReleaseProjectServiceCleanup, retryProjectServiceCleanup } from "./serviceRunCleanup.js";
19
23
  const runningProcesses = new Map();
20
- const MAX_PREVIEW_LOG_BUFFER_CHARS = 16_000;
24
+ const lastRunStatuses = new Map();
25
+ const STOP_SETTLEMENT_TIMEOUT_MS = 5_000;
26
+ const settleBeforeStopDeadline = (promise) => new Promise(resolve => {
27
+ const timer = setTimeout(resolve, STOP_SETTLEMENT_TIMEOUT_MS);
28
+ timer.unref();
29
+ void promise.then(() => {
30
+ clearTimeout(timer);
31
+ resolve();
32
+ }, () => {
33
+ clearTimeout(timer);
34
+ resolve();
35
+ });
36
+ });
37
+ export const getLastRunStatusForProject = (projectId) => lastRunStatuses.get(projectId) ?? null;
38
+ export class RunAdmissionError extends Error {
39
+ code;
40
+ status = 409;
41
+ constructor(code, message) {
42
+ super(message);
43
+ this.code = code;
44
+ this.name = "RunAdmissionError";
45
+ }
46
+ }
21
47
  const getRunConfig = (project) => getProjectRunConfig(project);
22
48
  export const resolveProjectRunContextKey = (projectId, target = {}) => {
23
49
  if (target.kind === "bundle" && target.bundleId) {
@@ -58,7 +84,70 @@ const resolveRunContextKey = (ticketOrKey) => {
58
84
  export const hasRunConfig = (project = null) => hasRunnableCommands(getRunConfig(project));
59
85
  export const isRunning = (ticketOrKey) => {
60
86
  const contextKey = resolveRunContextKey(ticketOrKey);
61
- return contextKey ? runningProcesses.has(contextKey) : false;
87
+ const state = contextKey ? runningProcesses.get(contextKey) : null;
88
+ if (state?.host_session && state.start_services) {
89
+ return state.host_session.services.some(service => ["starting", "running", "stopping"].includes(service.status));
90
+ }
91
+ return Boolean(state);
92
+ };
93
+ export const validateRunControl = (project, options) => {
94
+ const active = getActiveRunStatusForProject(project.id);
95
+ const state = active ? runningProcesses.get(active.context_key) : undefined;
96
+ if (!state && hasActiveProjectServiceRun(project.id)) {
97
+ if (options.starting) {
98
+ return "Durable service authority exists without a live runtime owner. Retry stopping the whole session before starting services.";
99
+ }
100
+ const recovered = getRecoverableHostServiceRun(project.id);
101
+ if (options.contextKey && recovered && options.contextKey !== recovered.context_key) {
102
+ return "Another service session is already active for this project";
103
+ }
104
+ if (options.sessionId !== undefined && recovered && options.sessionId !== recovered.sessionId) {
105
+ return "The service session has changed. Refresh before retrying.";
106
+ }
107
+ return options.serviceId !== undefined ? "Restarted sessions require whole-session cleanup" : null;
108
+ }
109
+ if (state?.admission_pending && options.starting) {
110
+ return "Service admission is still pending. Retry after admission settles.";
111
+ }
112
+ if (state?.cancelled && options.starting) {
113
+ return "The service session is stopping. Stop the whole session before retrying.";
114
+ }
115
+ if (options.contextKey && active && options.contextKey !== active.context_key) {
116
+ return "Another service session is already active for this project";
117
+ }
118
+ if (state && options.starting && options.executionProvider !== undefined) {
119
+ const requestedProvider = options.executionProvider === "host" ? "host" : project.build_execution_provider ?? "host";
120
+ if (requestedProvider !== state.execution_provider) {
121
+ return "The requested execution provider is incompatible with the active service session";
122
+ }
123
+ }
124
+ if (state?.host_session) {
125
+ if (options.starting && state.host_session.services.some(service => (options.serviceId === undefined || service.service_id === options.serviceId) && service.status === "stopping")) {
126
+ return "Service process cleanup is pending. Retry stopping the service before restarting it.";
127
+ }
128
+ if (state.host_session.setup_error && options.starting) {
129
+ return "Shared setup failed. Stop the session before retrying.";
130
+ }
131
+ try {
132
+ assertHostSessionRequest(state.host_session, options.serviceId, options.sessionId);
133
+ return null;
134
+ }
135
+ catch (error) {
136
+ return getErrorMessage(error);
137
+ }
138
+ }
139
+ if (options.sessionId !== undefined) {
140
+ return "The service session is no longer active";
141
+ }
142
+ if (options.serviceId !== undefined) {
143
+ if (state || (options.executionProvider !== "host" && project.build_execution_provider === "docker_sandbox")) {
144
+ return "Individual service controls are not supported by Docker Sandbox";
145
+ }
146
+ if (!(getRunConfig(project)?.services ?? []).some((service, index) => serviceIdentity(service, index) === options.serviceId)) {
147
+ return "Unknown service ID";
148
+ }
149
+ }
150
+ return null;
62
151
  };
63
152
  export const getActiveRunStatus = () => {
64
153
  const iterator = runningProcesses.values().next();
@@ -67,6 +156,28 @@ export const getActiveRunStatus = () => {
67
156
  export const getActiveRunStatusForContextKey = (contextKey) => {
68
157
  return runningProcesses.get(contextKey)?.active_run ?? null;
69
158
  };
159
+ export const startActiveRunServices = (contextKey, serviceId, sessionId) => {
160
+ const state = runningProcesses.get(contextKey);
161
+ if (!state?.host_session || state.cancelled || state.admission_pending) {
162
+ throw new RunAdmissionError("service_session_unavailable", "The host service session is no longer active");
163
+ }
164
+ assertHostSessionRequest(state.host_session, serviceId, sessionId);
165
+ if (state.host_session.services.some(service => (serviceId === undefined || service.service_id === serviceId) && service.status === "stopping")) {
166
+ throw new RunAdmissionError("service_cleanup_pending", "Service process cleanup is pending. Retry stopping the service before restarting it.");
167
+ }
168
+ if (state.start_services) {
169
+ return state.start_services(serviceId).then(() => {
170
+ if (runningProcesses.get(contextKey) === state && !state.cancelled) {
171
+ state.on_state_changed?.();
172
+ }
173
+ });
174
+ }
175
+ else {
176
+ queueHostServices(state.host_session, serviceId);
177
+ state.on_state_changed?.();
178
+ }
179
+ return Promise.resolve();
180
+ };
70
181
  export const getActiveRunStatusForProject = (projectId) => {
71
182
  if (!projectId) {
72
183
  return null;
@@ -99,38 +210,6 @@ export const getPreviewStatusForProject = (projectId) => {
99
210
  const isActiveRunState = (contextKey, state) => {
100
211
  return runningProcesses.get(contextKey) === state && !state.cancelled;
101
212
  };
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
213
  export const openIde = (cwd, ideCommand) => {
135
214
  spawn(resolveShell(), ["-i", "-c", `${ideCommand} .`], {
136
215
  cwd,
@@ -152,552 +231,495 @@ export const openTicketIdeInWorkspace = (ticket, project = null, workspaceCwd =
152
231
  openIde(cwd, runConfig.ide);
153
232
  return true;
154
233
  };
155
- const killManagedProcess = (proc, signal) => {
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
- };
234
+ export const runSetupCommandsInWorkspace = runWorkspaceSetup;
279
235
  const clearRunState = (contextKey, state, onStopped) => {
280
236
  if (runningProcesses.get(contextKey) !== state) {
281
237
  return;
282
238
  }
239
+ if (state.release_execution_environment || state.finalization_failed) {
240
+ state.cancelled = true;
241
+ return;
242
+ }
283
243
  deactivatePreviewProxyForProject(state.active_run.project_id);
284
244
  state.preview_log_buffer = "";
245
+ if (state.active_run.project_id && state.active_run.config_snapshot) {
246
+ lastRunStatuses.set(state.active_run.project_id, {
247
+ ...structuredClone(state.active_run),
248
+ config_snapshot: state.active_run.config_snapshot,
249
+ });
250
+ }
285
251
  runningProcesses.delete(contextKey);
286
252
  onStopped?.();
287
253
  };
288
254
  const canCreateDurableServiceRun = (project, targetDescriptor) => Boolean(targetDescriptor
289
255
  && project.id
290
256
  && project.active_execution_profile_revision_id);
291
- const resolveServiceCwd = (workspacePath, configuredCwd) => {
292
- if (!configuredCwd) {
293
- return workspacePath;
294
- }
295
- if (isAbsolute(configuredCwd)) {
296
- throw new Error("Service working directories must be relative to the canonical target");
297
- }
298
- const serviceCwd = resolve(workspacePath, configuredCwd);
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;
257
+ const startRun = ({ activeRun, cwd, executionAgent, executionModel, logLabelPrefix, onStopped, project, runConfig, skipSetup = false, targetDescriptor, profileSelection, serviceId, sessionId, }) => {
258
+ const contextKey = activeRun.context_key;
259
+ const admittedProvider = profileSelection?.reason === "explicit_service_host_override"
260
+ ? "host" : project?.build_execution_provider ?? "host";
261
+ const existing = runningProcesses.get(contextKey);
262
+ if (existing) {
263
+ if (existing.admission_pending) {
264
+ throw new RunAdmissionError("service_admission_pending", "Service admission is still pending. Retry after admission settles.");
321
265
  }
322
- catch (error) {
323
- recordServiceRunReleaseFailure({
324
- projectId: state.execution_project_id,
325
- executionRunId: runId,
326
- message: getErrorMessage(error),
327
- intendedStatus: status,
328
- intendedEventType: eventType,
329
- });
330
- throw error;
266
+ if (existing.cancelled) {
267
+ throw new Error("The service session is stopping. Retry the stop before starting services.");
268
+ }
269
+ if (existing.execution_provider !== admittedProvider) {
270
+ throw new RunAdmissionError("service_provider_conflict", "The requested execution provider is incompatible with the active service session");
331
271
  }
272
+ if (existing.host_session) {
273
+ return startActiveRunServices(contextKey, serviceId, sessionId);
274
+ }
275
+ else if (serviceId || sessionId) {
276
+ throw new Error("Individual service controls are only available for host sessions");
277
+ }
278
+ return Promise.resolve();
332
279
  }
333
- completeServiceRun({
334
- projectId: state.execution_project_id,
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");
280
+ if (sessionId !== undefined) {
281
+ throw new Error("The service session is no longer active");
345
282
  }
346
- await waitForProcessesToExit(state.processes, 1000);
347
- for (const proc of state.processes) {
348
- if (isProcessAlive(proc)) {
349
- killManagedProcess(proc, "SIGKILL");
350
- }
283
+ if (getActiveRunStatusForProject(activeRun.project_id)) {
284
+ throw new Error("Another service session is already active for this project");
351
285
  }
352
- await waitForProcessesToExit(state.processes, 1000);
353
- };
354
- const startRun = async ({ activeRun, cwd, executionAgent, executionModel, logLabelPrefix, onStopped, project, runConfig, skipSetup = false, targetDescriptor, profileSelection, }) => {
355
- const contextKey = activeRun.context_key;
286
+ if (project?.id && hasActiveProjectServiceRun(project.id)) {
287
+ throw new RunAdmissionError("service_authority_without_runtime", "Durable service authority exists without a live runtime owner. Reconcile it before starting services.");
288
+ }
289
+ if (!existsSync(cwd)) {
290
+ throw new RunAdmissionError("service_workspace_unavailable", `Service workspace does not exist: ${cwd}`);
291
+ }
292
+ runConfig = structuredClone(runConfig);
293
+ activeRun = { ...activeRun, execution_provider: admittedProvider, config_snapshot: createRunConfigSnapshot(runConfig, {
294
+ preview_service_name: project?.preview_service_name,
295
+ preview_path: project?.preview_path,
296
+ preview_capability_mode: project?.preview_capability_mode,
297
+ }) };
298
+ project = structuredClone(project);
299
+ profileSelection = profileSelection ? structuredClone(profileSelection) : undefined;
356
300
  const state = {
301
+ admission_pending: true,
302
+ execution_provider: admittedProvider,
357
303
  processes: [],
358
304
  cancelled: false,
359
305
  active_run: activeRun,
360
- preview_status: createRunPreviewStatus(project, runConfig),
306
+ preview_status: createRunPreviewStatus(project, runConfig, admittedProvider),
361
307
  preview_log_buffer: "",
362
308
  execution_run_id: null,
363
309
  execution_handles: [],
364
310
  release_execution_environment: null,
365
311
  execution_project_id: null,
312
+ setup_abort_controller: new AbortController(),
313
+ launch_operations: 0,
314
+ launch_promises: new Set(),
315
+ on_state_changed: () => {
316
+ if (state.execution_run_id && !state.cancelled) {
317
+ appendLifecycleEvent({
318
+ projectId: project.id,
319
+ runId: state.execution_run_id,
320
+ eventType: "service_status_changed",
321
+ metadata: { session_id: state.host_session?.id, services: state.host_session?.services },
322
+ });
323
+ }
324
+ onStopped?.();
325
+ },
366
326
  };
367
- runningProcesses.set(contextKey, state);
368
- if (!existsSync(cwd)) {
369
- console.error(`[run] path does not exist: ${cwd}`);
370
- clearRunState(contextKey, state, onStopped);
371
- return;
327
+ const expectsHost = admittedProvider === "host";
328
+ if (!expectsHost && serviceId !== undefined) {
329
+ throw new Error("Individual service controls are not supported by Docker Sandbox");
330
+ }
331
+ if (expectsHost) {
332
+ state.host_session = createHostRunSession(runConfig);
333
+ assertHostSessionRequest(state.host_session, serviceId, state.host_session.id);
334
+ state.active_run = {
335
+ ...activeRun,
336
+ session_id: state.host_session.id,
337
+ services: state.host_session.services,
338
+ };
339
+ queueHostServices(state.host_session, serviceId);
372
340
  }
373
- let execution = null;
374
- let executionTarget = null;
341
+ runningProcesses.set(contextKey, state);
375
342
  if (canCreateDurableServiceRun(project, targetDescriptor)) {
376
343
  try {
377
- const target = await resolveExecutionTarget({
378
- project,
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 };
344
+ const target = resolveExecutionTargetLocation({ project, descriptor: targetDescriptor, expectedWorkspacePath: cwd });
345
+ cwd = target.workspacePath;
395
346
  const durableRun = createServiceRun({
396
- projectId: project.id,
397
- contextKey,
398
- source,
399
- target,
400
- services: runConfig.services ?? [],
401
- profileSelection,
347
+ projectId: project.id, contextKey, sessionId: state.host_session?.id,
348
+ source: activeRun.kind === "ticket"
349
+ ? { kind: "ticket", id: activeRun.ticket_id, bundleId: target.identity.kind === "bundle-worktree" ? target.identity.bundleId : null }
350
+ : { kind: "project", id: project.id },
351
+ target, services: runConfig.services ?? [], profileSelection,
402
352
  });
403
353
  state.execution_run_id = durableRun.id;
404
354
  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
355
  }
438
356
  catch (error) {
439
- if (state.execution_run_id) {
440
- await finalizeDurableRun(state, "failed", "service_run_start_failed", getErrorMessage(error)).catch(releaseError => {
441
- console.error("[run] service environment release failed:", getErrorMessage(releaseError));
442
- });
357
+ if (runningProcesses.get(contextKey) === state) {
358
+ runningProcesses.delete(contextKey);
443
359
  }
444
- console.error(`[run] durable service start failed for ${logLabelPrefix}:`, getErrorMessage(error));
445
- clearRunState(contextKey, state, onStopped);
446
- return;
360
+ throw new RunAdmissionError("service_admission_rejected", getErrorMessage(error));
447
361
  }
448
362
  }
449
- if (!skipSetup) {
450
- try {
451
- if (execution) {
452
- await runSetupCommandsInWorkspace(project, cwd, {
453
- provider: execution.provider,
454
- target: executionTarget,
455
- launcherEnvironment: execution.launcherEnvironment,
456
- runtimeEnvironment: execution.runtimeEnvironment,
457
- onHandle: handle => {
458
- if (handle) {
459
- trackExecutionHandle(state, handle);
460
- }
363
+ state.admission_pending = false;
364
+ state.settle_session = createHostSessionSettler(state, () => runningProcesses.get(contextKey) === state, () => clearRunState(contextKey, state, onStopped));
365
+ const startup = async () => {
366
+ if (!isActiveRunState(contextKey, state)) {
367
+ return;
368
+ }
369
+ let execution = null;
370
+ let executionTarget = null;
371
+ if (canCreateDurableServiceRun(project, targetDescriptor)) {
372
+ try {
373
+ const target = await resolveExecutionTarget({
374
+ project,
375
+ descriptor: targetDescriptor,
376
+ expectedWorkspacePath: cwd,
377
+ });
378
+ if (!isActiveRunState(contextKey, state)) {
379
+ return;
380
+ }
381
+ executionTarget = target;
382
+ markServiceRunAllocationStarted(project.id, state.execution_run_id);
383
+ execution = await resolveWorkloadExecutionEnvironment({
384
+ project,
385
+ executionRunId: state.execution_run_id,
386
+ agent: executionAgent,
387
+ model: executionModel,
388
+ target,
389
+ workload: "service",
390
+ signal: state.setup_abort_controller?.signal,
391
+ onLeaseAcquired: release => {
392
+ state.release_execution_environment = release;
461
393
  },
462
394
  });
463
- }
464
- else {
465
- for (const cmd of runConfig.setup ?? []) {
466
- await runCommand(cmd, cwd, state);
467
- if (!isActiveRunState(contextKey, state)) {
468
- return;
395
+ state.release_execution_environment = execution.release;
396
+ if (!isActiveRunState(contextKey, state)) {
397
+ return;
398
+ }
399
+ if (execution.provider.kind !== state.execution_provider) {
400
+ throw new Error("The resolved execution provider differs from the admitted service provider");
401
+ }
402
+ execution = { ...execution, provider: withHostServiceRecovery(execution.provider, project.id, state.execution_run_id) };
403
+ if (state.execution_provider === "docker_sandbox") {
404
+ if (serviceId !== undefined) {
405
+ throw new Error("Individual service controls are not supported by Docker Sandbox");
469
406
  }
407
+ state.host_session = undefined;
408
+ state.active_run = activeRun;
470
409
  }
410
+ state.preview_status = createRunPreviewStatus(project, runConfig, state.execution_provider);
411
+ transitionExecutionRun({
412
+ projectId: project.id,
413
+ runId: state.execution_run_id,
414
+ status: "running",
415
+ domainStatus: "starting",
416
+ eventType: "service_run_starting",
417
+ eventMessage: "Service execution provider and canonical target authority were resolved.",
418
+ });
471
419
  }
472
- }
473
- catch (err) {
474
- if (state.cancelled) {
420
+ catch (error) {
421
+ if (state.execution_run_id) {
422
+ for (const service of state.host_session?.services ?? []) {
423
+ if (state.host_session?.pending.has(service.service_id)) {
424
+ service.status = "failed";
425
+ service.error = getErrorMessage(error);
426
+ }
427
+ }
428
+ state.host_session?.pending.clear();
429
+ await finalizeDurableRun(state, "failed", "service_run_start_failed", getErrorMessage(error)).catch(releaseError => {
430
+ console.error("[run] service environment release failed:", getErrorMessage(releaseError));
431
+ });
432
+ }
433
+ console.error(`[run] durable service start failed for ${logLabelPrefix}:`, getErrorMessage(error));
434
+ clearRunState(contextKey, state, onStopped);
475
435
  return;
476
436
  }
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
- }
482
- }
483
- if (!isActiveRunState(contextKey, state)) {
484
- return;
485
- }
486
- let serviceFailureMessage = null;
487
- let startupComplete = false;
488
- const cleanupIfAllDead = () => {
489
- if (!startupComplete || runningProcesses.get(contextKey) !== state || state.cancelled) {
490
- return;
491
437
  }
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",
438
+ if (!skipSetup) {
439
+ try {
440
+ if (execution) {
441
+ await runSetupCommandsInWorkspace(project, cwd, {
442
+ provider: execution.provider,
525
443
  target: executionTarget,
526
- command: shell.command,
527
- args: shell.args,
528
- cwd: serviceCwd,
529
- stdio: ["ignore", "pipe", "pipe"],
530
444
  launcherEnvironment: execution.launcherEnvironment,
531
445
  runtimeEnvironment: execution.runtimeEnvironment,
532
- detached: process.platform !== "win32",
446
+ signal: state.setup_abort_controller?.signal,
447
+ onHandle: handle => {
448
+ if (handle) {
449
+ trackExecutionHandle(state, handle);
450
+ if (state.cancelled) {
451
+ killManagedProcess(handle.child, "SIGTERM");
452
+ }
453
+ }
454
+ },
533
455
  });
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
456
  }
541
- state.preview_log_buffer = normalizePreviewLogText(`${state.preview_log_buffer}${data.toString()}`).slice(-MAX_PREVIEW_LOG_BUFFER_CHARS);
542
- const detectedTargets = detectPreviewTargets(state.preview_log_buffer, state.preview_status.capability);
543
- const previewTargets = detectedTargets.length > 0
544
- ? detectedTargets
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) {
457
+ else {
458
+ for (const cmd of runConfig.setup ?? []) {
459
+ await runCommand(cmd, cwd, state);
460
+ if (!isActiveRunState(contextKey, state)) {
557
461
  return;
558
462
  }
559
- try {
560
- const proxyOrigin = await ensurePreviewProxyForProject(project.id, reachableTarget.url);
561
- state.preview_status = {
562
- ...applyDetectedPreviewTarget(state.preview_status, reachableTarget),
563
- proxy_url: buildPreviewProxyUrl(proxyOrigin, reachableTarget.url),
564
- };
565
- }
566
- catch (error) {
567
- state.preview_status = {
568
- ...state.preview_status,
569
- state: "error",
570
- proxy_url: null,
571
- message: getErrorMessage(error) || "Preview proxy failed",
572
- };
463
+ }
464
+ }
465
+ }
466
+ catch (err) {
467
+ if (state.cancelled) {
468
+ return;
469
+ }
470
+ console.error(`[run] setup failed for ${logLabelPrefix}:`, getErrorMessage(err));
471
+ await finalizeDurableRun(state, "failed", "service_setup_failed", getErrorMessage(err)).catch(releaseError => console.error("[run] service environment release failed:", getErrorMessage(releaseError)));
472
+ if (state.host_session) {
473
+ state.host_session.setup_error = getErrorMessage(err);
474
+ for (const service of state.host_session.services) {
475
+ if (state.host_session.pending.has(service.service_id)) {
476
+ service.status = "failed";
477
+ service.error = getErrorMessage(err);
573
478
  }
574
- return;
575
479
  }
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"}.`;
480
+ state.host_session.pending.clear();
590
481
  }
482
+ clearRunState(contextKey, state, onStopped);
483
+ return;
484
+ }
485
+ }
486
+ if (!isActiveRunState(contextKey, state)) {
487
+ return;
488
+ }
489
+ let serviceFailureMessage = null;
490
+ let startupComplete = false;
491
+ const cleanupIfAllDead = () => {
492
+ if (state.host_session) {
493
+ void state.settle_session?.().catch(error => console.error("[run] service finalization failed:", error));
494
+ return;
495
+ }
496
+ if (!startupComplete || runningProcesses.get(contextKey) !== state || state.cancelled) {
497
+ return;
498
+ }
499
+ const anyAlive = state.processes.some(isProcessAlive);
500
+ if (!anyAlive) {
501
+ state.cancelled = true;
502
+ void finalizeDurableRun(state, serviceFailureMessage ? "failed" : "completed", serviceFailureMessage ? "service_run_failed" : "service_run_completed", serviceFailureMessage ?? undefined)
503
+ .catch(error => console.error("[run] service environment release failed:", getErrorMessage(error)))
504
+ .finally(() => clearRunState(contextKey, state, onStopped));
505
+ }
506
+ };
507
+ const launchServices = createRunServiceLauncher({
508
+ state, project, runConfig, cwd, execution, executionTarget, logLabelPrefix,
509
+ isActive: () => isActiveRunState(contextKey, state),
510
+ onExit: failure => {
511
+ serviceFailureMessage = failure ?? serviceFailureMessage;
591
512
  cleanupIfAllDead();
592
- });
593
- proc.on("error", (err) => {
594
- console.error(`[run:${logLabelPrefix}:${label}] error: ${getErrorMessage(err)}`);
595
- if (!state.cancelled) {
596
- serviceFailureMessage = `Service ${label} failed: ${getErrorMessage(err)}`;
513
+ if (state.host_session && isActiveRunState(contextKey, state)) {
514
+ state.on_state_changed?.();
597
515
  }
598
- cleanupIfAllDead();
516
+ },
517
+ });
518
+ try {
519
+ state.start_services = requestedId => {
520
+ state.launch_operations = (state.launch_operations ?? 0) + 1;
521
+ const launching = (async () => {
522
+ try {
523
+ await launchServices(requestedId);
524
+ }
525
+ finally {
526
+ state.launch_operations -= 1;
527
+ await state.settle_session?.();
528
+ }
529
+ })();
530
+ state.launch_promises.add(launching);
531
+ const remove = () => { state.launch_promises.delete(launching); };
532
+ void launching.then(remove, remove);
533
+ return launching;
534
+ };
535
+ if (state.host_session) {
536
+ const pending = [...state.host_session.pending];
537
+ await Promise.all(pending.map(id => state.start_services(id)));
538
+ }
539
+ else {
540
+ await launchServices();
541
+ }
542
+ }
543
+ catch (error) {
544
+ await stopRunProcesses(state);
545
+ await finalizeDurableRun(state, "failed", "service_placement_failed", getErrorMessage(error)).catch(releaseError => console.error("[run] service environment release failed:", getErrorMessage(releaseError)));
546
+ clearRunState(contextKey, state, onStopped);
547
+ return;
548
+ }
549
+ if (state.execution_run_id && execution && (runConfig.services ?? []).length === 0) {
550
+ recordServiceRunPlacement({
551
+ projectId: project.id,
552
+ executionRunId: state.execution_run_id,
553
+ provider: state.execution_provider,
554
+ environmentId: execution.environment?.id ?? null,
555
+ environmentRevision: execution.environment?.environment_revision ?? null,
599
556
  });
600
- if (handle) {
601
- const placementFailure = await handle.placementFailure;
602
- if (!isActiveRunState(contextKey, state)) {
557
+ }
558
+ if (!isActiveRunState(contextKey, state)) {
559
+ return;
560
+ }
561
+ if (state.execution_run_id) {
562
+ transitionExecutionRun({
563
+ projectId: project.id,
564
+ runId: state.execution_run_id,
565
+ status: "running",
566
+ domainStatus: "active",
567
+ eventType: "service_run_active",
568
+ eventMessage: state.execution_provider === "docker_sandbox"
569
+ ? "Services are active in a shared Docker Sandbox environment; interruption applies to the whole environment."
570
+ : "Services are active as verified host processes.",
571
+ });
572
+ }
573
+ if ((runConfig.services ?? []).length === 0) {
574
+ await finalizeDurableRun(state, "completed", "service_run_completed").catch(error => console.error("[run] service environment release failed:", getErrorMessage(error)));
575
+ clearRunState(contextKey, state, onStopped);
576
+ return;
577
+ }
578
+ startupComplete = true;
579
+ cleanupIfAllDead();
580
+ };
581
+ state.startup_promise = startup().finally(async () => {
582
+ state.startup_complete = true;
583
+ if (!state.finalization_failed) {
584
+ await state.settle_session?.();
585
+ }
586
+ });
587
+ return state.startup_promise;
588
+ };
589
+ export const stopAllTickets = async () => {
590
+ const contextKeys = [...runningProcesses.keys()];
591
+ await Promise.all(contextKeys.map(contextKey => stopTicket(contextKey)));
592
+ };
593
+ export const stopTicket = async (ticketOrKey, options = {}) => {
594
+ let contextKey = resolveRunContextKey(ticketOrKey);
595
+ if (options.sessionId && ticketOrKey && typeof ticketOrKey !== "string") {
596
+ const frozen = [...runningProcesses.values()].find(state => state.host_session?.id === options.sessionId
597
+ && state.active_run.kind === "ticket" && state.active_run.ticket_id === ticketOrKey.id);
598
+ contextKey = frozen?.active_run.context_key ?? contextKey;
599
+ }
600
+ if (!contextKey) {
601
+ return;
602
+ }
603
+ const state = runningProcesses.get(contextKey);
604
+ if (!state) {
605
+ if (retryInterruptedHostServiceRunForContext(contextKey, options)) {
606
+ return;
607
+ }
608
+ if (options.sessionId) {
609
+ throw new Error("The service session is no longer active");
610
+ }
611
+ return;
612
+ }
613
+ if (state.host_session) {
614
+ assertHostSessionRequest(state.host_session, options.serviceId, options.sessionId);
615
+ if (options.serviceId !== undefined) {
616
+ const session = state.host_session;
617
+ session.pending.delete(options.serviceId);
618
+ const status = session.services.find(service => service.service_id === options.serviceId);
619
+ const proc = session.processes.get(options.serviceId);
620
+ if (!proc) {
621
+ status.status = "stopped";
622
+ if (!state.start_services && session.pending.size === 0 && session.processes.size === 0) {
623
+ await stopTicket(contextKey, { sessionId: session.id });
603
624
  return;
604
625
  }
605
- if (placementFailure) {
606
- throw placementFailure;
626
+ await state.settle_session?.();
627
+ return;
628
+ }
629
+ state.launch_operations = (state.launch_operations ?? 0) + 1;
630
+ try {
631
+ status.status = "stopping";
632
+ const configuredService = session.config.services.find((service, index) => serviceIdentity(service, index) === options.serviceId);
633
+ if (configuredService && isPreviewService(configuredService, state.preview_status)) {
634
+ deactivatePreviewProxyForProject(state.active_run.project_id);
635
+ state.preview_status = { ...state.preview_status, state: "idle", detected_url: null, detected_origin: null, detected_path: null, proxy_url: null };
636
+ state.preview_log_buffer = "";
607
637
  }
608
- const placement = await handle.placementIdentity;
609
- if (!isActiveRunState(contextKey, state)) {
610
- return;
638
+ killManagedProcess(proc, "SIGTERM");
639
+ await waitForProcessesToExit([proc], 1000);
640
+ killManagedProcess(proc, "SIGKILL");
641
+ await waitForProcessesToExit([proc], 1000);
642
+ if (isProcessAlive(proc)) {
643
+ throw new Error("The service did not exit. Retry stopping it before restarting.");
611
644
  }
612
- recordServiceRunPlacement({
613
- projectId: project.id,
614
- executionRunId: state.execution_run_id,
615
- provider: state.execution_provider,
616
- environmentId: execution?.environment?.id ?? null,
617
- environmentRevision: execution?.environment?.environment_revision ?? null,
618
- placement,
619
- });
645
+ finishHostService(session, options.serviceId, proc, proc.exitCode);
646
+ state.on_state_changed?.();
620
647
  }
648
+ finally {
649
+ state.launch_operations -= 1;
650
+ await state.settle_session?.();
651
+ }
652
+ return;
621
653
  }
622
654
  }
623
- catch (error) {
624
- await stopRunProcesses(state);
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;
628
- }
629
- if (state.execution_run_id && execution && (runConfig.services ?? []).length === 0) {
630
- recordServiceRunPlacement({
631
- projectId: project.id,
632
- executionRunId: state.execution_run_id,
633
- provider: state.execution_provider,
634
- environmentId: execution.environment?.id ?? null,
635
- environmentRevision: execution.environment?.environment_revision ?? null,
636
- });
655
+ else if (options.serviceId !== undefined || options.sessionId !== undefined) {
656
+ throw new Error("Individual service controls are not supported by Docker Sandbox");
637
657
  }
638
- if (!isActiveRunState(contextKey, state)) {
639
- return;
658
+ const stoppingWork = !state.finalization_intent && (!state.startup_complete
659
+ || Boolean(state.launch_operations) || Boolean(state.host_session?.pending.size)
660
+ || state.processes.some(isProcessAlive));
661
+ state.cancelled = true;
662
+ state.setup_abort_controller?.abort();
663
+ state.host_session?.pending.clear();
664
+ if (state.host_session && stoppingWork) {
665
+ state.host_session.stop_requested = true;
666
+ }
667
+ for (const service of state.host_session?.services ?? []) {
668
+ if (["starting", "running"].includes(service.status)) {
669
+ service.status = "stopping";
670
+ }
640
671
  }
641
- if (state.execution_run_id) {
642
- transitionExecutionRun({
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
- });
672
+ deactivatePreviewProxyForProject(state.active_run.project_id);
673
+ await stopRunProcesses(state);
674
+ if (state.startup_promise) {
675
+ await settleBeforeStopDeadline(state.startup_promise);
652
676
  }
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);
677
+ await settleBeforeStopDeadline(Promise.allSettled([...(state.launch_promises ?? [])]));
678
+ if (runningProcesses.get(contextKey) !== state) {
656
679
  return;
657
680
  }
658
- startupComplete = true;
659
- cleanupIfAllDead();
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) {
681
+ if (state.finalization) {
682
+ await state.finalization;
668
683
  return;
669
684
  }
670
- const state = runningProcesses.get(contextKey);
671
- if (!state) {
685
+ await stopRunProcesses(state);
686
+ for (const [id, proc] of state.host_session?.processes ?? []) {
687
+ finishHostService(state.host_session, id, proc, proc.exitCode);
688
+ }
689
+ for (const service of state.host_session?.services ?? []) {
690
+ if (service.status === "stopping") {
691
+ service.status = "stopped";
692
+ }
693
+ }
694
+ await state.settle_session?.();
695
+ if (state.finalization) {
696
+ await state.finalization;
672
697
  return;
673
698
  }
674
- state.cancelled = true;
675
- await stopRunProcesses(state);
676
- await finalizeDurableRun(state, "cancelled", "service_run_cancelled", state.execution_provider === "docker_sandbox"
699
+ state.finalization ??= finalizeDurableRun(state, "cancelled", "service_run_cancelled", state.execution_provider === "docker_sandbox"
677
700
  ? "The service run was stopped. Docker Sandbox interruption stops the whole environment and every attached service."
678
- : "The service run host processes were stopped.").catch(error => console.error("[run] service environment release failed:", getErrorMessage(error)));
679
- if (runningProcesses.get(contextKey) === state) {
680
- deactivatePreviewProxyForProject(state.active_run.project_id);
681
- state.preview_log_buffer = "";
682
- runningProcesses.delete(contextKey);
683
- }
701
+ : "The service run host processes were stopped.").then(() => clearRunState(contextKey, state)).catch(error => {
702
+ state.finalization = null;
703
+ throw error;
704
+ });
705
+ await state.finalization;
684
706
  };
685
707
  export const runTicket = async (ticket, io, project = null) => {
686
708
  return runTicketInWorkspace(ticket, io, project, null);
687
709
  };
688
- export const runTicketInWorkspace = async (ticket, io, project = null, workspaceCwd = null, profileSelection) => {
710
+ export const runTicketInWorkspace = (ticket, io, project = null, workspaceCwd = null, profileSelection, controls = {}) => {
689
711
  const runConfig = getRunConfig(project);
690
712
  if (!runConfig || !hasRunnableCommands(runConfig)) {
691
- return;
713
+ return Promise.resolve();
692
714
  }
693
715
  const contextKey = resolveTicketContextKey(ticket);
694
716
  const branch = resolveTicketBranch(ticket);
695
717
  if (!contextKey || (!branch && !workspaceCwd)) {
696
- return;
718
+ return Promise.resolve();
697
719
  }
698
720
  const cwd = workspaceCwd ?? resolveWorktreePath(branch, project);
699
721
  const executionAgent = resolveTicketTool(ticket, config.tool ?? "opencode");
700
- await startRun({
722
+ return startRun({
701
723
  activeRun: {
702
724
  kind: "ticket",
703
725
  context_key: contextKey,
@@ -713,7 +735,7 @@ export const runTicketInWorkspace = async (ticket, io, project = null, workspace
713
735
  preferWorkflow: true,
714
736
  }).model,
715
737
  logLabelPrefix: `ticket:${ticket.id}`,
716
- onStopped: () => emitRunStateForContext(io, ticket, false),
738
+ onStopped: () => emitRunStateForContext(io, ticket, isRunning(ticket)),
717
739
  project: project,
718
740
  runConfig,
719
741
  skipSetup: isTicketRunSetupReady(ticket),
@@ -725,17 +747,18 @@ export const runTicketInWorkspace = async (ticket, io, project = null, workspace
725
747
  })
726
748
  : undefined,
727
749
  profileSelection,
750
+ ...controls,
728
751
  });
729
752
  };
730
- export const runProject = async (project, options = {}) => {
753
+ export const runProject = (project, options = {}) => {
731
754
  const runConfig = getRunConfig(project);
732
755
  if (!runConfig || !hasRunnableCommands(runConfig)) {
733
- return;
756
+ return Promise.resolve();
734
757
  }
735
758
  const targetKind = options.targetKind ?? "repo_root";
736
759
  const targetBundleId = targetKind === "bundle" ? (options.targetBundleId ?? null) : null;
737
760
  const executionAgent = config.tool ?? "opencode";
738
- await startRun({
761
+ return startRun({
739
762
  activeRun: {
740
763
  kind: "project",
741
764
  context_key: resolveProjectRunContextKey(project.id, {
@@ -767,9 +790,23 @@ export const runProject = async (project, options = {}) => {
767
790
  branch: options.targetBranch ?? null,
768
791
  },
769
792
  profileSelection: options.profileSelection,
793
+ serviceId: options.serviceId,
794
+ sessionId: options.sessionId,
770
795
  });
771
796
  };
772
797
  export const stopProjectRun = async (projectId, target = {}) => {
773
- await stopTicket(resolveProjectRunContextKey(projectId, target));
798
+ const active = getActiveRunStatusForProject(projectId);
799
+ const recovered = active ? null : getRecoverableHostServiceRun(projectId);
800
+ if (!active && hasActiveProjectServiceRun(projectId)) {
801
+ await retryProjectServiceCleanup(projectId);
802
+ return;
803
+ }
804
+ await stopTicket(active?.context_key ?? recovered?.context_key ?? resolveProjectRunContextKey(projectId, target), target);
805
+ };
806
+ export const forceReleaseProjectRunCleanup = (projectId) => {
807
+ if (getActiveRunStatusForProject(projectId)) {
808
+ throw new Error("A live service session cannot be force released. Stop it normally first.");
809
+ }
810
+ return forceReleaseProjectServiceCleanup(projectId);
774
811
  };
775
812
  //# sourceMappingURL=run.js.map