@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.
Files changed (159) hide show
  1. package/dist/client/assets/ActivityView-CRkM5TeN.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-KFUiR00H.js +1 -0
  3. package/dist/client/assets/VerificationDialog-BIDttgiW.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BAIGV9Mj.js → architectureDiagram-3BPJPVTR-u0PSZlNP.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-B-5v5jSZ.js → blockDiagram-GPEHLZMM-DFXLHRIk.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-C6wU415o.js → c4Diagram-AAUBKEIU-Dvic8tCt.js} +1 -1
  7. package/dist/client/assets/channel-Dhv_uSKx.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-COmvX2tP.js → chunk-2J33WTMH-t5CxhRXJ.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-BlANptdY.js → chunk-3OPIFGDE-YHT1dP8Q.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-ETyA2_GU.js → chunk-5ZQYHXKU-DcxuI7lb.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-D5-DudwF.js → chunk-727SXJPM-D1F6DmrT.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-k1AJ4sA3.js → chunk-AQP2D5EJ-BIx6Ohbk.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-jfksXwY9.js → chunk-CSCIHK7Q-B355h6HA.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CHj0FDj9.js → chunk-KSCS5N6A-CLHGYOLQ.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-1ODGxXhG.js → chunk-L5ZTLDWV-DFLYcMx8.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-Bgh9X2W0.js → chunk-LZXEDZCA-DtljFujX.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-DDkb3sp4.js → chunk-ND2GUHAM-CIuUQ86K.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-B6B8PZ_-.js → chunk-NZK2D7GU-CIwqlxek.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DhiUIFgj.js → chunk-O5CBEL6O-W42BEPu4.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-Dzd0fbP3.js → chunk-WU5MYG2G-C_zuaNxV.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-9XKvSSLq.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Dbr5_49Y.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DoNDMCXI.js → dagre-BM42HDAG-UKfIUJTP.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-ETDXSa5q.js → diagram-2AECGRRQ-Xn6yojqq.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CgTUep7e.js → diagram-5GNKFQAL-BzpY1ZEl.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-BKTx2iRe.js → diagram-KO2AKTUF-Dyrvzvsg.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-9iEiScHT.js → diagram-LMA3HP47-AqImKwAr.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-UIZRh7Qa.js → diagram-OG6HWLK6-CaQQ46mi.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-BpNJmJva.js → erDiagram-TEJ5UH35-Dkw4Aa5u.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-C-rK0XZ1.js → flowDiagram-I6XJVG4X-CJTuGX9h.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DWyfPvv_.js → ganttDiagram-6RSMTGT7-CidgOdS-.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-x71bzdc_.js → gitGraphDiagram-PVQCEYII-Bj02mIQj.js} +1 -1
  33. package/dist/client/assets/index-ClRB9YWJ.css +2 -0
  34. package/dist/client/assets/index-DBMdsTvy.js +276 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CbRus0Xu.js → infoDiagram-5YYISTIA-GwJWfo_r.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DvaMHrgN.js → ishikawaDiagram-YF4QCWOH-rbrlWbUn.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BQBu4YGn.js → journeyDiagram-JHISSGLW-BLi_uzQM.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-8rU-U7L1.js → kanban-definition-UN3LZRKU-ynlywm-_.js} +1 -1
  39. package/dist/client/assets/{line-CSZnwjLA.js → line-BNyTZBB4.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-CTOl2slE.js → mermaid-parser.core-CzUOsZAD.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-B9gr0Vqf.js → mermaid.core-K0QR6SzN.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-But1_PWQ.js → mindmap-definition-RKZ34NQL-BKcgqXm0.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-CnlVmfSG.js → pieDiagram-4H26LBE5-Bjx0lvDI.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Dww1ZGuR.js → quadrantDiagram-W4KKPZXB-CmUlgyUv.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CNf_loe8.js → requirementDiagram-4Y6WPE33-Dj2mzg1y.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBhi37Jj.js → sankeyDiagram-5OEKKPKP-DC--H6qf.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B64hb4E4.js → sequenceDiagram-3UESZ5HK-BkNrt6Ra.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DcS6oB-S.js → stateDiagram-AJRCARHV-BuxxRu1Y.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Cyo85GPN.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-Dq-4tXfh.js → timeline-definition-PNZ67QCA-Db3pCVSp.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CeDklSE_.js → vennDiagram-CIIHVFJN-BgBSGThQ.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-D2cJISMm.js → wardleyDiagram-YWT4CUSO-BJhPdjYS.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CBF-tTpF.js → xychartDiagram-2RQKCTM6-BY4Ra0kX.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +341 -4
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +172 -249
  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/executionRetry.js +13 -1
  78. package/dist/server/lib/executionRetry.js.map +1 -1
  79. package/dist/server/lib/executionRunAuthority.js +2 -2
  80. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  81. package/dist/server/lib/executionTargets.js +7 -4
  82. package/dist/server/lib/executionTargets.js.map +1 -1
  83. package/dist/server/lib/executionTimeline.js +8 -13
  84. package/dist/server/lib/executionTimeline.js.map +1 -1
  85. package/dist/server/lib/executionTimelineHierarchy.js +83 -0
  86. package/dist/server/lib/executionTimelineHierarchy.js.map +1 -0
  87. package/dist/server/lib/failedBuildSequenceRecovery.js +133 -0
  88. package/dist/server/lib/failedBuildSequenceRecovery.js.map +1 -0
  89. package/dist/server/lib/hostRunSession.js +62 -0
  90. package/dist/server/lib/hostRunSession.js.map +1 -0
  91. package/dist/server/lib/hostServiceRecovery.js +159 -0
  92. package/dist/server/lib/hostServiceRecovery.js.map +1 -0
  93. package/dist/server/lib/hostServiceRunProvider.js +32 -0
  94. package/dist/server/lib/hostServiceRunProvider.js.map +1 -0
  95. package/dist/server/lib/mobileAccess.js +6 -2
  96. package/dist/server/lib/mobileAccess.js.map +1 -1
  97. package/dist/server/lib/projectAutoConfig.js +5 -2
  98. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  99. package/dist/server/lib/projectTargets.js +8 -2
  100. package/dist/server/lib/projectTargets.js.map +1 -1
  101. package/dist/server/lib/projects.js +25 -1
  102. package/dist/server/lib/projects.js.map +1 -1
  103. package/dist/server/lib/reviewFindingReconciliation.js +9 -2
  104. package/dist/server/lib/reviewFindingReconciliation.js.map +1 -1
  105. package/dist/server/lib/reviewFindings.js +82 -21
  106. package/dist/server/lib/reviewFindings.js.map +1 -1
  107. package/dist/server/lib/reviewRepairAssessmentRecords.js +41 -0
  108. package/dist/server/lib/reviewRepairAssessmentRecords.js.map +1 -0
  109. package/dist/server/lib/reviewRepairCompletion.js +78 -18
  110. package/dist/server/lib/reviewRepairCompletion.js.map +1 -1
  111. package/dist/server/lib/reviewRepairIterationRecords.js +4 -2
  112. package/dist/server/lib/reviewRepairIterationRecords.js.map +1 -1
  113. package/dist/server/lib/reviewRepairIterations.js +8 -2
  114. package/dist/server/lib/reviewRepairIterations.js.map +1 -1
  115. package/dist/server/lib/reviewRepairProgress.js +177 -0
  116. package/dist/server/lib/reviewRepairProgress.js.map +1 -0
  117. package/dist/server/lib/reviewRepairReconciliation.js +119 -140
  118. package/dist/server/lib/reviewRepairReconciliation.js.map +1 -1
  119. package/dist/server/lib/reviewRepairResumeOverride.js +55 -0
  120. package/dist/server/lib/reviewRepairResumeOverride.js.map +1 -0
  121. package/dist/server/lib/run.js +523 -504
  122. package/dist/server/lib/run.js.map +1 -1
  123. package/dist/server/lib/runProcessIdentity.js +71 -0
  124. package/dist/server/lib/runProcessIdentity.js.map +1 -0
  125. package/dist/server/lib/runProcesses.js +261 -0
  126. package/dist/server/lib/runProcesses.js.map +1 -0
  127. package/dist/server/lib/runServices.js +221 -0
  128. package/dist/server/lib/runServices.js.map +1 -0
  129. package/dist/server/lib/runSessionLifecycle.js +68 -0
  130. package/dist/server/lib/runSessionLifecycle.js.map +1 -0
  131. package/dist/server/lib/runStatus.js +7 -0
  132. package/dist/server/lib/runStatus.js.map +1 -0
  133. package/dist/server/lib/serviceRuns.js +273 -16
  134. package/dist/server/lib/serviceRuns.js.map +1 -1
  135. package/dist/server/lib/ticketAutomation.js +4 -27
  136. package/dist/server/lib/ticketAutomation.js.map +1 -1
  137. package/dist/server/lib/ticketRunOperations.js +103 -34
  138. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  139. package/dist/server/lib/tickets.js +0 -13
  140. package/dist/server/lib/tickets.js.map +1 -1
  141. package/dist/server/lib/types.js.map +1 -1
  142. package/dist/server/workers/build.js +44 -69
  143. package/dist/server/workers/build.js.map +1 -1
  144. package/dist/server/workers/plan.js +0 -7
  145. package/dist/server/workers/plan.js.map +1 -1
  146. package/dist/server/workers/review.js +95 -146
  147. package/dist/server/workers/review.js.map +1 -1
  148. package/package.json +1 -1
  149. package/dist/client/assets/ActivityView-CKvbDXeA.js +0 -1
  150. package/dist/client/assets/GitHubActionsDialog-Dzo0vtak.js +0 -1
  151. package/dist/client/assets/VerificationDialog-BJnOitrJ.js +0 -4
  152. package/dist/client/assets/channel-5mIy7nm1.js +0 -1
  153. package/dist/client/assets/classDiagram-4FO5ZUOK-BiQ-X2Sg.js +0 -1
  154. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CF-wCYW-.js +0 -1
  155. package/dist/client/assets/index-CIAO5aE_.css +0 -2
  156. package/dist/client/assets/index-nCGyalrn.js +0 -276
  157. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-PWeeL0tR.js +0 -1
  158. package/dist/server/lib/automationDeliveryDecision.js +0 -34
  159. package/dist/server/lib/automationDeliveryDecision.js.map +0 -1
@@ -1,23 +1,36 @@
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";
19
22
  const runningProcesses = new Map();
20
- const MAX_PREVIEW_LOG_BUFFER_CHARS = 16_000;
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
- return contextKey ? runningProcesses.has(contextKey) : false;
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 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
- };
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 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;
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
- 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;
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
- 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");
270
+ if (sessionId !== undefined) {
271
+ throw new Error("The service session is no longer active");
345
272
  }
346
- await waitForProcessesToExit(state.processes, 1000);
347
- for (const proc of state.processes) {
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
- 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;
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
- runningProcesses.set(contextKey, state);
368
- if (!existsSync(cwd)) {
369
- console.error(`[run] path does not exist: ${cwd}`);
370
- clearRunState(contextKey, state, onStopped);
371
- return;
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
- let execution = null;
374
- let executionTarget = null;
331
+ runningProcesses.set(contextKey, state);
375
332
  if (canCreateDurableServiceRun(project, targetDescriptor)) {
376
333
  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 };
334
+ const target = resolveExecutionTargetLocation({ project, descriptor: targetDescriptor, expectedWorkspacePath: cwd });
335
+ cwd = target.workspacePath;
395
336
  const durableRun = createServiceRun({
396
- projectId: project.id,
397
- contextKey,
398
- source,
399
- target,
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 (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
- });
347
+ if (runningProcesses.get(contextKey) === state) {
348
+ runningProcesses.delete(contextKey);
443
349
  }
444
- console.error(`[run] durable service start failed for ${logLabelPrefix}:`, getErrorMessage(error));
445
- clearRunState(contextKey, state, onStopped);
446
- return;
350
+ throw new RunAdmissionError("service_admission_rejected", getErrorMessage(error));
447
351
  }
448
352
  }
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
- }
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
- else {
465
- for (const cmd of runConfig.setup ?? []) {
466
- await runCommand(cmd, cwd, state);
467
- if (!isActiveRunState(contextKey, state)) {
468
- return;
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
- catch (err) {
474
- if (state.cancelled) {
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
- 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
- }
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
- detached: process.platform !== "win32",
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
- 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) {
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
- 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
- };
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
- proc.on("error", (err) => {
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
- cleanupIfAllDead();
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
- if (handle) {
601
- const placementFailure = await handle.placementFailure;
602
- if (!isActiveRunState(contextKey, state)) {
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
- if (placementFailure) {
606
- throw placementFailure;
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
- const placement = await handle.placementIdentity;
609
- if (!isActiveRunState(contextKey, state)) {
610
- return;
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
- 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
- });
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
- 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;
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
- 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
- });
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
- if (!isActiveRunState(contextKey, state)) {
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.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
- });
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
- 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) {
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 = runningProcesses.get(contextKey);
671
- if (!state) {
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 = true;
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.").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
- }
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 = async (ticket, io, project = null, workspaceCwd = null, profileSelection) => {
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
- await startRun({
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, false),
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 = async (project, options = {}) => {
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
- await startRun({
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
- await stopTicket(resolveProjectRunContextKey(projectId, target));
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