@rallycry/conveyor-agent 10.7.3 → 10.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  DEFAULT_LIFECYCLE_CONFIG,
6
6
  DEFAULT_SONNET_MODEL,
7
7
  Lifecycle,
8
+ PortDiscovery,
8
9
  PtyHarness,
9
10
  SessionRunner,
10
11
  TUI_KINDS,
@@ -25,8 +26,9 @@ import {
25
26
  resolveSessionStart,
26
27
  runSetupCommand,
27
28
  runStartCommand,
28
- sampleKeyUsage
29
- } from "./chunk-UOHROQ6A.js";
29
+ sampleKeyUsage,
30
+ terminateProcessGroup
31
+ } from "./chunk-CWTQXS34.js";
30
32
  import "./chunk-7TQO4ZF4.js";
31
33
 
32
34
  // src/cli.ts
@@ -45,14 +47,18 @@ var DEFAULT_POLL_INTERVAL_MS = 1e3;
45
47
  var DEFAULT_PROBE_TIMEOUT_MS = 2e3;
46
48
  var POSTGRES_DEFAULT_PORT = 5432;
47
49
  var FIREBASE_DEFAULT_PORT = 9099;
48
- async function startLazySidecars(env, onLog) {
50
+ async function startLazySidecars(env, onLog, signal) {
51
+ throwIfAborted(signal);
49
52
  const markerPath = env.CONVEYOR_SIDECAR_START_FILE;
50
53
  if (!markerPath) return;
51
54
  try {
52
55
  await mkdir(dirname(markerPath), { recursive: true });
56
+ throwIfAborted(signal);
53
57
  await writeFile(markerPath, "start\n", "utf8");
58
+ throwIfAborted(signal);
54
59
  onLog("Started lazy sidecars");
55
60
  } catch (err) {
61
+ if (signal?.aborted) throw abortError();
56
62
  const message = err instanceof Error ? err.message : String(err);
57
63
  onLog(`WARNING: failed to start lazy sidecars: ${message}`);
58
64
  }
@@ -96,7 +102,16 @@ function resolveSidecarTargets(env = process.env) {
96
102
  }
97
103
  return targets;
98
104
  }
99
- function defaultProbe(target) {
105
+ function abortError() {
106
+ const error = new Error("Operation aborted");
107
+ error.name = "AbortError";
108
+ return error;
109
+ }
110
+ function throwIfAborted(signal) {
111
+ if (signal?.aborted) throw abortError();
112
+ }
113
+ function defaultProbe(target, signal) {
114
+ throwIfAborted(signal);
100
115
  return new Promise((resolve) => {
101
116
  let settled = false;
102
117
  const socket = net.createConnection({ host: target.host, port: target.port });
@@ -104,23 +119,56 @@ function defaultProbe(target) {
104
119
  if (settled) return;
105
120
  settled = true;
106
121
  socket.destroy();
122
+ signal?.removeEventListener("abort", onAbort);
107
123
  resolve(ok);
108
124
  };
125
+ const onAbort = () => done(false);
109
126
  socket.once("connect", () => done(true));
110
127
  socket.once("error", () => done(false));
111
128
  socket.setTimeout(DEFAULT_PROBE_TIMEOUT_MS, () => done(false));
129
+ signal?.addEventListener("abort", onAbort, { once: true });
130
+ });
131
+ }
132
+ var delay = (ms, signal) => {
133
+ throwIfAborted(signal);
134
+ return new Promise((resolve, reject) => {
135
+ const timer = setTimeout(() => {
136
+ signal?.removeEventListener("abort", onAbort);
137
+ resolve();
138
+ }, ms);
139
+ const onAbort = () => {
140
+ clearTimeout(timer);
141
+ reject(abortError());
142
+ };
143
+ signal?.addEventListener("abort", onAbort, { once: true });
144
+ });
145
+ };
146
+ function raceWithAbort(promise, signal) {
147
+ if (!signal) return promise;
148
+ throwIfAborted(signal);
149
+ return new Promise((resolve, reject) => {
150
+ const onAbort = () => reject(abortError());
151
+ signal.addEventListener("abort", onAbort, { once: true });
152
+ void promise.then(
153
+ (value) => {
154
+ signal.removeEventListener("abort", onAbort);
155
+ resolve(value);
156
+ },
157
+ (error) => {
158
+ signal.removeEventListener("abort", onAbort);
159
+ reject(error);
160
+ }
161
+ );
112
162
  });
113
163
  }
114
- var delay = (ms) => new Promise((resolve) => {
115
- setTimeout(resolve, ms);
116
- });
117
164
  async function waitForTarget(target, opts) {
118
- const { onLog, pollIntervalMs, probe } = opts;
165
+ const { onLog, pollIntervalMs, probe, signal } = opts;
119
166
  const timeoutMs = opts.timeoutMs ?? target.timeoutMs ?? FALLBACK_TIMEOUT_MS;
120
167
  const deadline = Date.now() + timeoutMs;
121
168
  onLog(`Waiting for ${target.name} on ${target.host}:${target.port}...`);
122
169
  while (true) {
123
- if (await probe(target)) {
170
+ throwIfAborted(signal);
171
+ if (await raceWithAbort(probe(target, signal), signal)) {
124
172
  onLog(`${target.name} is ready`);
125
173
  return;
126
174
  }
@@ -130,7 +178,7 @@ async function waitForTarget(target, opts) {
130
178
  );
131
179
  return;
132
180
  }
133
- await delay(pollIntervalMs);
181
+ await delay(pollIntervalMs, signal);
134
182
  }
135
183
  }
136
184
  async function waitForSidecars(opts = {}) {
@@ -141,18 +189,282 @@ async function waitForSidecars(opts = {}) {
141
189
  timeoutMs,
142
190
  pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
143
191
  probe = defaultProbe,
144
- startLazy = true
192
+ startLazy = true,
193
+ signal
145
194
  } = opts;
146
195
  if (startLazy) {
147
- await startLazySidecars(env, onLog);
196
+ await startLazySidecars(env, onLog, signal);
148
197
  }
198
+ throwIfAborted(signal);
149
199
  const targets = resolveSidecarTargets(env);
150
200
  if (targets.length === 0) return;
151
201
  await Promise.all(
152
- targets.map((target) => waitForTarget(target, { onLog, timeoutMs, pollIntervalMs, probe }))
202
+ targets.map(
203
+ (target) => waitForTarget(target, { onLog, timeoutMs, pollIntervalMs, probe, signal })
204
+ )
153
205
  );
154
206
  }
155
207
 
208
+ // src/setup/workspace-command-supervisor.ts
209
+ var defaultWriteOutput = (stream, data) => {
210
+ (stream === "stderr" ? process.stderr : process.stdout).write(data);
211
+ };
212
+ function stopWorkspaceCommands(supervisor) {
213
+ return supervisor?.stop() ?? Promise.resolve();
214
+ }
215
+ async function startWorkspaceCommandsAfterConnect(options) {
216
+ const connected = await options.connect();
217
+ if (!connected || options.isShuttingDown()) return null;
218
+ const supervisor = options.create();
219
+ supervisor.start();
220
+ return supervisor;
221
+ }
222
+ var WorkspaceCommandSupervisor = class {
223
+ abortController = new AbortController();
224
+ expectedStartCommandStops = /* @__PURE__ */ new WeakSet();
225
+ config;
226
+ workspaceDir;
227
+ connection;
228
+ env;
229
+ awaitGitReadyFn;
230
+ startLazySidecarsFn;
231
+ waitForSidecarsFn;
232
+ runSetupCommandFn;
233
+ runStartCommandFn;
234
+ loadForwardPortsFn;
235
+ writeOutput;
236
+ terminateStartCommand;
237
+ startCommandChild = null;
238
+ liveStartCommandChildren = /* @__PURE__ */ new Set();
239
+ startCommandTerminations = /* @__PURE__ */ new WeakMap();
240
+ startCommandEndPromises = /* @__PURE__ */ new WeakMap();
241
+ resolveStartCommandEnd = /* @__PURE__ */ new WeakMap();
242
+ backgroundTasks = /* @__PURE__ */ new Set();
243
+ startCommandQueue = Promise.resolve();
244
+ shutdownPromise = null;
245
+ startCommandLaunchRequested = false;
246
+ started = false;
247
+ stopped = false;
248
+ constructor(options) {
249
+ this.config = options.config;
250
+ this.workspaceDir = options.workspaceDir;
251
+ this.connection = options.connection;
252
+ this.env = options.env ?? process.env;
253
+ this.awaitGitReadyFn = options.awaitGitReady ?? ((opts) => awaitGitReady({
254
+ onLog: opts.onLog,
255
+ signal: opts.signal
256
+ }));
257
+ this.startLazySidecarsFn = options.startLazySidecars ?? ((env, onLog, signal) => startLazySidecars(env, onLog, signal));
258
+ this.waitForSidecarsFn = options.waitForSidecars ?? ((opts) => waitForSidecars({ onLog: opts.onLog, startLazy: opts.startLazy, signal: opts.signal }));
259
+ this.runSetupCommandFn = options.runSetupCommand ?? ((command, cwd, onOutput, signal) => runSetupCommand(command, cwd, onOutput, signal));
260
+ this.runStartCommandFn = options.runStartCommand ?? runStartCommand;
261
+ this.loadForwardPortsFn = options.loadForwardPorts ?? loadForwardPorts;
262
+ this.writeOutput = options.writeOutput ?? defaultWriteOutput;
263
+ this.terminateStartCommand = options.terminateStartCommand ?? terminateProcessGroup;
264
+ }
265
+ start() {
266
+ if (this.started || this.stopped) return;
267
+ this.started = true;
268
+ this.connection.onRunStartCommand(() => this.restartStartCommand());
269
+ this.trackBackgroundTask(
270
+ this.startLazySidecarsFn(
271
+ this.env,
272
+ (message) => this.forwardSetupOutput("stdout", `[sidecars] ${message}
273
+ `),
274
+ this.abortController.signal
275
+ ).catch((error) => this.reportUnexpectedError(error))
276
+ );
277
+ if (this.config) {
278
+ this.trackBackgroundTask(
279
+ this.runSetupAndStart().catch((error) => this.reportUnexpectedError(error))
280
+ );
281
+ }
282
+ }
283
+ stop() {
284
+ if (this.shutdownPromise) return this.shutdownPromise;
285
+ this.stopped = true;
286
+ this.abortController.abort();
287
+ const backgroundTasks = [...this.backgroundTasks];
288
+ const termination = this.terminateAllStartCommands();
289
+ this.shutdownPromise = (async () => {
290
+ await Promise.allSettled([termination, this.startCommandQueue, ...backgroundTasks]);
291
+ await this.terminateAllStartCommands();
292
+ })();
293
+ return this.shutdownPromise;
294
+ }
295
+ async runSetupAndStart() {
296
+ const gitState = await this.awaitGitReadyFn({
297
+ onLog: (message) => this.forwardSetupOutput("stdout", `[git] ${message}
298
+ `),
299
+ signal: this.abortController.signal
300
+ });
301
+ if (this.stopped) return;
302
+ if (gitState === "failed" || gitState === "timeout") {
303
+ this.connection.sendEvent({
304
+ type: "setup_error",
305
+ message: "Workspace not ready \u2014 skipping setup/start"
306
+ });
307
+ return;
308
+ }
309
+ await this.waitForSidecarsFn({
310
+ onLog: (message) => this.forwardSetupOutput("stdout", `[sidecars] ${message}
311
+ `),
312
+ startLazy: false,
313
+ signal: this.abortController.signal
314
+ });
315
+ if (this.stopped) return;
316
+ await this.runConfiguredSetup();
317
+ if (this.stopped) return;
318
+ const startCommandRunning = this.config?.startCommand ? await this.ensureStartCommandLaunched(this.config.startCommand) : false;
319
+ if (this.stopped) return;
320
+ const forwardPorts = await this.loadForwardPortsFn(this.workspaceDir);
321
+ if (this.stopped) return;
322
+ const previewPorts = buildSessionPreviewPorts(forwardPorts);
323
+ this.connection.sendEvent({
324
+ type: "setup_complete",
325
+ startCommandRunning,
326
+ ...previewPorts.length > 0 ? { previewPorts } : {}
327
+ });
328
+ }
329
+ async runConfiguredSetup() {
330
+ const command = this.config?.setupCommand;
331
+ if (!command) return;
332
+ try {
333
+ await this.runSetupCommandFn(
334
+ command,
335
+ this.workspaceDir,
336
+ (stream, data) => this.forwardSetupOutput(stream, data),
337
+ this.abortController.signal
338
+ );
339
+ } catch (error) {
340
+ if (this.stopped) return;
341
+ this.connection.sendEvent({
342
+ type: "setup_error",
343
+ message: error instanceof Error ? error.message : "Setup command failed"
344
+ });
345
+ }
346
+ }
347
+ restartStartCommand() {
348
+ if (this.stopped || !this.config?.startCommand) return;
349
+ this.connection.sendEvent({
350
+ type: "start_command_output",
351
+ stream: "stdout",
352
+ data: "[conveyor-agent] Restarting start command...\n"
353
+ });
354
+ void this.enqueueStartCommandReplacement(this.config.startCommand);
355
+ }
356
+ async ensureStartCommandLaunched(command) {
357
+ if (!this.startCommandLaunchRequested) {
358
+ return this.enqueueStartCommandReplacement(command);
359
+ }
360
+ await this.startCommandQueue;
361
+ return !this.stopped && this.liveStartCommandChildren.size > 0;
362
+ }
363
+ enqueueStartCommandReplacement(command) {
364
+ this.startCommandLaunchRequested = true;
365
+ const operation = this.startCommandQueue.then(async () => {
366
+ if (this.stopped) return false;
367
+ await this.terminateAllStartCommands();
368
+ if (this.stopped) return false;
369
+ return this.launchStartCommand(command);
370
+ });
371
+ this.startCommandQueue = operation.then(
372
+ () => void 0,
373
+ () => void 0
374
+ );
375
+ return operation;
376
+ }
377
+ launchStartCommand(command) {
378
+ if (this.stopped) return false;
379
+ this.connection.sendEvent({ type: "start_command_started" });
380
+ try {
381
+ const child = this.runStartCommandFn(command, this.workspaceDir, (stream, data) => {
382
+ if (this.stopped) return;
383
+ this.connection.sendEvent({ type: "start_command_output", stream, data });
384
+ this.writeOutput(stream, data);
385
+ });
386
+ this.liveStartCommandChildren.add(child);
387
+ const ended = new Promise((resolve) => {
388
+ this.resolveStartCommandEnd.set(child, resolve);
389
+ });
390
+ this.startCommandEndPromises.set(child, ended);
391
+ if (this.stopped) {
392
+ void this.terminateStartCommandChild(child);
393
+ return false;
394
+ }
395
+ this.startCommandChild = child;
396
+ child.on("exit", (code, signal) => {
397
+ if (this.startCommandChild === child) this.startCommandChild = null;
398
+ this.liveStartCommandChildren.delete(child);
399
+ this.settleStartCommandEnd(child);
400
+ if (this.stopped || this.expectedStartCommandStops.has(child)) return;
401
+ const message = `start command exited${code === null ? "" : ` with code ${code}`}${signal ? ` (signal: ${signal})` : ""}`;
402
+ this.connection.sendEvent({ type: "start_command_exited", code, signal, message });
403
+ if (code !== null && code !== 0) {
404
+ this.connection.sendEvent({
405
+ type: "start_command_error",
406
+ message: `start command exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`
407
+ });
408
+ }
409
+ });
410
+ child.on("error", (error) => {
411
+ if (child.pid === void 0) {
412
+ if (this.startCommandChild === child) this.startCommandChild = null;
413
+ this.liveStartCommandChildren.delete(child);
414
+ this.settleStartCommandEnd(child);
415
+ this.startCommandTerminations.delete(child);
416
+ }
417
+ if (this.stopped) return;
418
+ this.connection.sendEvent({ type: "start_command_error", message: error.message });
419
+ });
420
+ return true;
421
+ } catch (error) {
422
+ if (!this.stopped) {
423
+ this.connection.sendEvent({
424
+ type: "start_command_error",
425
+ message: error instanceof Error ? error.message : String(error)
426
+ });
427
+ }
428
+ return false;
429
+ }
430
+ }
431
+ terminateStartCommandChild(child) {
432
+ const existing = this.startCommandTerminations.get(child);
433
+ if (existing) return existing;
434
+ this.expectedStartCommandStops.add(child);
435
+ const processTermination = Promise.resolve(this.terminateStartCommand(child));
436
+ const ended = this.startCommandEndPromises.get(child);
437
+ const termination = (ended ? Promise.race([processTermination, ended]) : processTermination).then(() => void 0).finally(() => this.startCommandTerminations.delete(child));
438
+ this.startCommandTerminations.set(child, termination);
439
+ return termination;
440
+ }
441
+ async terminateAllStartCommands() {
442
+ const live = [...this.liveStartCommandChildren].filter((child) => child.exitCode === null);
443
+ await Promise.all(live.map((child) => this.terminateStartCommandChild(child)));
444
+ }
445
+ settleStartCommandEnd(child) {
446
+ this.resolveStartCommandEnd.get(child)?.();
447
+ this.resolveStartCommandEnd.delete(child);
448
+ this.startCommandEndPromises.delete(child);
449
+ }
450
+ trackBackgroundTask(task) {
451
+ this.backgroundTasks.add(task);
452
+ void task.finally(() => this.backgroundTasks.delete(task));
453
+ }
454
+ forwardSetupOutput(stream, data) {
455
+ if (this.stopped) return;
456
+ this.connection.sendEvent({ type: "setup_output", stream, data });
457
+ this.writeOutput(stream, data);
458
+ }
459
+ reportUnexpectedError(error) {
460
+ if (this.stopped) return;
461
+ this.connection.sendEvent({
462
+ type: "setup_error",
463
+ message: error instanceof Error ? error.message : String(error)
464
+ });
465
+ }
466
+ };
467
+
156
468
  // src/utils/session-identity.ts
157
469
  async function checkSessionTaskIdentity(params) {
158
470
  const { sessionId, taskId, fetchSessionTaskId, logger: logger6 } = params;
@@ -553,6 +865,8 @@ var AdhocSessionRunner = class {
553
865
  connection;
554
866
  lifecycle;
555
867
  harness;
868
+ portDiscovery;
869
+ commandSupervisor;
556
870
  config;
557
871
  callbacks;
558
872
  abortController = new AbortController();
@@ -567,6 +881,14 @@ var AdhocSessionRunner = class {
567
881
  buildAdhocPtyBridge(this.connection),
568
882
  resolveTuiAdapter(resolveAdhocTui(process.env))
569
883
  );
884
+ this.portDiscovery = deps?.portDiscovery ?? new PortDiscovery({
885
+ report: (ports) => this.connection.reportDiscoveredPorts(ports)
886
+ });
887
+ this.commandSupervisor = deps?.commandSupervisor ?? new WorkspaceCommandSupervisor({
888
+ config: loadConveyorConfig(),
889
+ workspaceDir: config.workspaceDir,
890
+ connection: this.connection
891
+ });
570
892
  this.lifecycle = new Lifecycle(
571
893
  // No git flush: the WIP snapshot machinery is branch/task-shaped; the human
572
894
  // commits/pushes explicitly from the interactive shell.
@@ -605,6 +927,13 @@ var AdhocSessionRunner = class {
605
927
  await this.connection.call("connectAgent", {
606
928
  sessionId: this.config.connection.sessionId
607
929
  });
930
+ await this.portDiscovery.start().catch(() => {
931
+ });
932
+ if (this.stopped) {
933
+ this._finalState = "finished";
934
+ return;
935
+ }
936
+ this.commandSupervisor.start();
608
937
  await this.connection.emitStatus("running");
609
938
  this.callbacks.onEvent?.({ type: "adhoc_runner_started", projectId: this.config.projectId });
610
939
  this.lifecycle.startIdleTimer();
@@ -617,7 +946,7 @@ var AdhocSessionRunner = class {
617
946
  this.connection.sendEvent({ type: "error", message });
618
947
  this._finalState = "error";
619
948
  } finally {
620
- this.shutdown();
949
+ await this.shutdown();
621
950
  }
622
951
  }
623
952
  /**
@@ -651,6 +980,8 @@ var AdhocSessionRunner = class {
651
980
  requestStop() {
652
981
  if (this.stopped) return;
653
982
  this.stopped = true;
983
+ this.portDiscovery.stop();
984
+ void this.commandSupervisor.stop();
654
985
  this.abortController.abort();
655
986
  if (this.stopResolver) {
656
987
  const resolve = this.stopResolver;
@@ -670,8 +1001,10 @@ var AdhocSessionRunner = class {
670
1001
  });
671
1002
  }
672
1003
  }
673
- shutdown() {
1004
+ async shutdown() {
674
1005
  this.stopped = true;
1006
+ this.portDiscovery.stop();
1007
+ await this.commandSupervisor.stop();
675
1008
  this.lifecycle.destroy();
676
1009
  this.connection.sendEvent({ type: "shutdown", reason: this._finalState ?? "finished" });
677
1010
  this.connection.disconnect();
@@ -1381,10 +1714,13 @@ var runner = new SessionRunner(
1381
1714
  var reviewChildren = new ReviewChildSupervisor(runner.connection, CONVEYOR_WORKSPACE);
1382
1715
  var sessionChildren = new SessionChildSupervisor(runner.connection, CONVEYOR_WORKSPACE);
1383
1716
  var shutdownSignal;
1717
+ var workspaceCommandSupervisor = null;
1718
+ var shutdownCompletion = null;
1384
1719
  var shutdownAgent = (signal) => {
1385
1720
  logger5.info(`Received ${signal}, flushing git and stopping agent`);
1721
+ const commandShutdown = stopWorkspaceCommands(workspaceCommandSupervisor);
1386
1722
  shutdownSignal = signal;
1387
- void (async () => {
1723
+ shutdownCompletion ??= (async () => {
1388
1724
  try {
1389
1725
  await Promise.all([reviewChildren.stopAll(), sessionChildren.stopAll()]);
1390
1726
  } catch {
@@ -1392,9 +1728,11 @@ var shutdownAgent = (signal) => {
1392
1728
  try {
1393
1729
  await runner.flushGitOnShutdown();
1394
1730
  } finally {
1731
+ await commandShutdown;
1395
1732
  runner.stop();
1396
1733
  }
1397
1734
  })();
1735
+ void shutdownCompletion;
1398
1736
  setTimeout(() => {
1399
1737
  logger5.warn(`Forcing exit after ${signal} timeout`);
1400
1738
  logger5.warn(
@@ -1413,63 +1751,25 @@ var shutdownAgent = (signal) => {
1413
1751
  };
1414
1752
  process.on("SIGTERM", () => shutdownAgent("SIGTERM"));
1415
1753
  process.on("SIGINT", () => shutdownAgent("SIGINT"));
1416
- await runner.connect();
1417
- var logSetupOutput = (stream, data) => {
1418
- runner.connection.sendEvent({ type: "setup_output", stream, data });
1419
- (stream === "stderr" ? process.stderr : process.stdout).write(data);
1420
- };
1421
- var startCommandChild = null;
1422
- var expectedStartCommandStops = /* @__PURE__ */ new WeakSet();
1423
- var stopStartCommandChild = () => {
1424
- const child = startCommandChild;
1425
- if (!child || child.exitCode !== null || child.killed) return;
1426
- expectedStartCommandStops.add(child);
1427
- try {
1428
- if (child.pid) process.kill(-child.pid, "SIGTERM");
1429
- else child.kill("SIGTERM");
1430
- } catch {
1431
- try {
1432
- child.kill("SIGTERM");
1433
- } catch {
1434
- }
1435
- }
1436
- };
1437
- var launchStartCommand = (command) => {
1438
- logger5.info("Running start command", { command });
1439
- runner.connection.sendEvent({ type: "start_command_started" });
1440
- const child = runStartCommand(command, CONVEYOR_WORKSPACE, (stream, data) => {
1441
- runner.connection.sendEvent({ type: "start_command_output", stream, data });
1442
- (stream === "stderr" ? process.stderr : process.stdout).write(data);
1443
- });
1444
- startCommandChild = child;
1445
- child.on("exit", (code, signal) => {
1446
- if (startCommandChild === child) startCommandChild = null;
1447
- if (expectedStartCommandStops.has(child)) return;
1448
- logger5.info("Start command exited", { code, signal });
1449
- runner.connection.sendEvent({
1450
- type: "start_command_exited",
1451
- code,
1452
- signal,
1453
- message: `start command exited${code === null ? "" : ` with code ${code}`}${signal ? ` (signal: ${signal})` : ""}`
1454
- });
1455
- if (code !== null && code !== 0) {
1456
- runner.connection.sendEvent({
1457
- type: "start_command_error",
1458
- message: `start command exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`
1459
- });
1460
- }
1461
- });
1462
- child.on("error", (err) => {
1463
- if (startCommandChild === child) startCommandChild = null;
1464
- logger5.error("Start command error", { error: err.message });
1465
- runner.connection.sendEvent({ type: "start_command_error", message: err.message });
1754
+ workspaceCommandSupervisor = await startWorkspaceCommandsAfterConnect({
1755
+ connect: () => runner.connect(),
1756
+ isShuttingDown: () => shutdownSignal !== void 0,
1757
+ create: () => new WorkspaceCommandSupervisor({
1758
+ config: loadConveyorConfig(),
1759
+ workspaceDir: CONVEYOR_WORKSPACE,
1760
+ connection: runner.connection
1761
+ })
1762
+ });
1763
+ if (!workspaceCommandSupervisor) {
1764
+ logger5.info("Agent stopped before workspace commands were started");
1765
+ if (shutdownCompletion) await shutdownCompletion;
1766
+ logAgentExit(shutdownSignal ? "signal" : "clean", {
1767
+ exitCode: 0,
1768
+ signal: shutdownSignal,
1769
+ finalState: runner.finalState ?? void 0
1466
1770
  });
1467
- };
1468
- void startLazySidecars(
1469
- process.env,
1470
- (message) => logSetupOutput("stdout", `[sidecars] ${message}
1471
- `)
1472
- );
1771
+ process.exit(0);
1772
+ }
1473
1773
  void checkSessionTaskIdentity({
1474
1774
  sessionId: process.env.CONVEYOR_SESSION_ID,
1475
1775
  taskId: CONVEYOR_TASK_ID,
@@ -1505,63 +1805,8 @@ if (CONVEYOR_MODE === "task" || CONVEYOR_MODE === "pack") {
1505
1805
  });
1506
1806
  });
1507
1807
  }
1508
- var conveyorConfig = loadConveyorConfig();
1509
- runner.connection.onRunStartCommand(() => {
1510
- if (!conveyorConfig?.startCommand) return;
1511
- runner.connection.sendEvent({
1512
- type: "start_command_output",
1513
- stream: "stdout",
1514
- data: "[conveyor-agent] Restarting start command...\n"
1515
- });
1516
- stopStartCommandChild();
1517
- launchStartCommand(conveyorConfig.startCommand);
1518
- });
1519
- if (conveyorConfig) {
1520
- void (async () => {
1521
- const gitState = await awaitGitReady({
1522
- onLog: (m) => logSetupOutput("stdout", `[git] ${m}
1523
- `)
1524
- });
1525
- if (gitState === "failed" || gitState === "timeout") {
1526
- runner.connection.sendEvent({
1527
- type: "setup_error",
1528
- message: "Workspace not ready \u2014 skipping setup/start"
1529
- });
1530
- return;
1531
- }
1532
- await waitForSidecars({
1533
- onLog: (message) => logSetupOutput("stdout", `[sidecars] ${message}
1534
- `),
1535
- startLazy: false
1536
- });
1537
- if (conveyorConfig.setupCommand) {
1538
- logger5.info("Running setup command (background)", {
1539
- command: conveyorConfig.setupCommand
1540
- });
1541
- try {
1542
- await runSetupCommand(conveyorConfig.setupCommand, CONVEYOR_WORKSPACE, logSetupOutput);
1543
- logger5.info("Setup command completed");
1544
- } catch (error) {
1545
- const msg = error instanceof Error ? error.message : "Setup command failed";
1546
- logger5.error("Setup command failed", { error: msg });
1547
- runner.connection.sendEvent({ type: "setup_error", message: msg });
1548
- }
1549
- }
1550
- let startCommandRunning = false;
1551
- if (conveyorConfig.startCommand) {
1552
- launchStartCommand(conveyorConfig.startCommand);
1553
- startCommandRunning = true;
1554
- }
1555
- const forwardPorts = await loadForwardPorts(CONVEYOR_WORKSPACE);
1556
- const previewPorts = buildSessionPreviewPorts(forwardPorts);
1557
- runner.connection.sendEvent({
1558
- type: "setup_complete",
1559
- startCommandRunning,
1560
- ...previewPorts.length > 0 ? { previewPorts } : {}
1561
- });
1562
- })();
1563
- }
1564
- runner.run().then(() => {
1808
+ runner.run().then(async () => {
1809
+ await stopWorkspaceCommands(workspaceCommandSupervisor);
1565
1810
  const errored = runner.finalState === "error";
1566
1811
  const reason = shutdownSignal ? "signal" : errored ? "error" : "clean";
1567
1812
  logAgentExit(reason, {
@@ -1570,7 +1815,8 @@ runner.run().then(() => {
1570
1815
  finalState: runner.finalState ?? void 0
1571
1816
  });
1572
1817
  process.exit(errored ? 1 : 0);
1573
- }).catch((error) => {
1818
+ }).catch(async (error) => {
1819
+ await stopWorkspaceCommands(workspaceCommandSupervisor);
1574
1820
  const msg = error instanceof Error ? error.message : String(error);
1575
1821
  logger5.error("Agent runner failed", { error: msg });
1576
1822
  logAgentExit("error", { exitCode: 1, finalState: runner.finalState ?? void 0 });