agent-relay 3.1.18 → 3.1.20

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 (93) hide show
  1. package/README.md +13 -1
  2. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  3. package/bin/agent-relay-broker-darwin-x64 +0 -0
  4. package/bin/agent-relay-broker-linux-arm64 +0 -0
  5. package/bin/agent-relay-broker-linux-x64 +0 -0
  6. package/dist/index.cjs +446 -192
  7. package/dist/src/cli/bootstrap.js +0 -15
  8. package/dist/src/cli/bootstrap.js.map +1 -1
  9. package/dist/src/cli/commands/agent-management.d.ts +1 -0
  10. package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
  11. package/dist/src/cli/commands/agent-management.js +235 -16
  12. package/dist/src/cli/commands/agent-management.js.map +1 -1
  13. package/dist/src/cli/commands/core.js +1 -1
  14. package/dist/src/cli/commands/core.js.map +1 -1
  15. package/dist/src/cli/index.d.ts.map +1 -1
  16. package/dist/src/cli/index.js +13 -1
  17. package/dist/src/cli/index.js.map +1 -1
  18. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  19. package/dist/src/cli/lib/broker-lifecycle.js +3 -5
  20. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  21. package/dist/src/cli/lib/connect-daytona.js +2 -2
  22. package/dist/src/cli/lib/connect-daytona.js.map +1 -1
  23. package/dist/src/cli/lib/core-maintenance.d.ts.map +1 -1
  24. package/dist/src/cli/lib/core-maintenance.js +52 -0
  25. package/dist/src/cli/lib/core-maintenance.js.map +1 -1
  26. package/install.sh +32 -0
  27. package/package.json +13 -13
  28. package/packages/acp-bridge/package.json +2 -2
  29. package/packages/config/package.json +1 -1
  30. package/packages/hooks/package.json +4 -4
  31. package/packages/memory/package.json +2 -2
  32. package/packages/openclaw/dist/cli.js +79 -2
  33. package/packages/openclaw/dist/cli.js.map +1 -1
  34. package/packages/openclaw/dist/config.d.ts +28 -1
  35. package/packages/openclaw/dist/config.d.ts.map +1 -1
  36. package/packages/openclaw/dist/config.js +145 -0
  37. package/packages/openclaw/dist/config.js.map +1 -1
  38. package/packages/openclaw/dist/index.d.ts +2 -2
  39. package/packages/openclaw/dist/index.d.ts.map +1 -1
  40. package/packages/openclaw/dist/index.js +1 -1
  41. package/packages/openclaw/dist/index.js.map +1 -1
  42. package/packages/openclaw/dist/setup.d.ts.map +1 -1
  43. package/packages/openclaw/dist/setup.js +24 -1
  44. package/packages/openclaw/dist/setup.js.map +1 -1
  45. package/packages/openclaw/dist/types.d.ts +23 -0
  46. package/packages/openclaw/dist/types.d.ts.map +1 -1
  47. package/packages/openclaw/package.json +2 -2
  48. package/packages/openclaw/skill/SKILL.md +46 -0
  49. package/packages/openclaw/src/cli.ts +90 -2
  50. package/packages/openclaw/src/config.ts +165 -1
  51. package/packages/openclaw/src/index.ts +7 -1
  52. package/packages/openclaw/src/setup.ts +26 -1
  53. package/packages/openclaw/src/types.ts +25 -0
  54. package/packages/policy/package.json +2 -2
  55. package/packages/sdk/dist/__tests__/integration.test.js +35 -0
  56. package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
  57. package/packages/sdk/dist/client.d.ts +9 -0
  58. package/packages/sdk/dist/client.d.ts.map +1 -1
  59. package/packages/sdk/dist/client.js +48 -25
  60. package/packages/sdk/dist/client.js.map +1 -1
  61. package/packages/sdk/dist/protocol.d.ts +1 -0
  62. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  63. package/packages/sdk/dist/relay.d.ts +8 -0
  64. package/packages/sdk/dist/relay.d.ts.map +1 -1
  65. package/packages/sdk/dist/relay.js +50 -5
  66. package/packages/sdk/dist/relay.js.map +1 -1
  67. package/packages/sdk/dist/workflows/cli.js +2 -0
  68. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  69. package/packages/sdk/dist/workflows/runner.d.ts +11 -0
  70. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  71. package/packages/sdk/dist/workflows/runner.js +350 -167
  72. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  73. package/packages/sdk/dist/workflows/trajectory.d.ts +6 -1
  74. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  75. package/packages/sdk/dist/workflows/trajectory.js +16 -2
  76. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  77. package/packages/sdk/package.json +2 -2
  78. package/packages/sdk/src/__tests__/integration.test.ts +49 -0
  79. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +50 -1
  80. package/packages/sdk/src/client.ts +57 -24
  81. package/packages/sdk/src/protocol.ts +1 -1
  82. package/packages/sdk/src/relay.ts +70 -5
  83. package/packages/sdk/src/workflows/cli.ts +2 -0
  84. package/packages/sdk/src/workflows/runner.ts +414 -185
  85. package/packages/sdk/src/workflows/trajectory.ts +22 -2
  86. package/packages/sdk-py/pyproject.toml +1 -1
  87. package/packages/sdk-py/src/agent_relay/client.py +18 -1
  88. package/packages/sdk-py/src/agent_relay/relay.py +4 -0
  89. package/packages/sdk-py/src/agent_relay/types.py +4 -0
  90. package/packages/telemetry/package.json +1 -1
  91. package/packages/trajectory/package.json +2 -2
  92. package/packages/user-directory/package.json +2 -2
  93. package/packages/utils/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -9102,7 +9102,8 @@ var AgentRelayClient = class _AgentRelayClient {
9102
9102
  agent,
9103
9103
  ...input.task != null ? { initial_task: input.task } : {},
9104
9104
  ...input.idleThresholdSecs != null ? { idle_threshold_secs: input.idleThresholdSecs } : {},
9105
- ...input.continueFrom != null ? { continue_from: input.continueFrom } : {}
9105
+ ...input.continueFrom != null ? { continue_from: input.continueFrom } : {},
9106
+ ...input.skipRelayPrompt != null ? { skip_relay_prompt: input.skipRelayPrompt } : {}
9106
9107
  });
9107
9108
  return result;
9108
9109
  }
@@ -9117,7 +9118,8 @@ var AgentRelayClient = class _AgentRelayClient {
9117
9118
  };
9118
9119
  const result = await this.requestOk("spawn_agent", {
9119
9120
  agent,
9120
- ...input.task != null ? { initial_task: input.task } : {}
9121
+ ...input.task != null ? { initial_task: input.task } : {},
9122
+ ...input.skipRelayPrompt != null ? { skip_relay_prompt: input.skipRelayPrompt } : {}
9121
9123
  });
9122
9124
  return result;
9123
9125
  }
@@ -9132,7 +9134,8 @@ var AgentRelayClient = class _AgentRelayClient {
9132
9134
  provider: input.provider,
9133
9135
  args: input.args,
9134
9136
  channels: input.channels,
9135
- task: input.task
9137
+ task: input.task,
9138
+ skipRelayPrompt: input.skipRelayPrompt
9136
9139
  });
9137
9140
  }
9138
9141
  return this.spawnPty({
@@ -9148,7 +9151,8 @@ var AgentRelayClient = class _AgentRelayClient {
9148
9151
  shadowMode: input.shadowMode,
9149
9152
  idleThresholdSecs: input.idleThresholdSecs,
9150
9153
  restartPolicy: input.restartPolicy,
9151
- continueFrom: input.continueFrom
9154
+ continueFrom: input.continueFrom,
9155
+ skipRelayPrompt: input.skipRelayPrompt
9152
9156
  });
9153
9157
  }
9154
9158
  async spawnClaude(input) {
@@ -9214,25 +9218,35 @@ var AgentRelayClient = class _AgentRelayClient {
9214
9218
  if (!this.child) {
9215
9219
  return;
9216
9220
  }
9217
- try {
9218
- await this.requestOk("shutdown", {});
9219
- } catch {
9220
- }
9221
+ void this.requestOk("shutdown", {}).catch(() => {
9222
+ });
9221
9223
  const child = this.child;
9222
9224
  const wait = this.exitPromise ?? Promise.resolve();
9223
- const timeout = setTimeout(() => {
9224
- if (!child.killed) {
9225
- child.kill("SIGTERM");
9226
- }
9227
- }, this.options.shutdownTimeoutMs);
9228
- try {
9229
- await wait;
9230
- } finally {
9231
- clearTimeout(timeout);
9232
- if (this.child) {
9233
- this.child.kill("SIGKILL");
9234
- }
9225
+ const waitForExit = async (timeoutMs) => {
9226
+ let timer;
9227
+ const result = await Promise.race([
9228
+ wait.then(() => true),
9229
+ new Promise((resolve3) => {
9230
+ timer = setTimeout(() => resolve3(false), timeoutMs);
9231
+ })
9232
+ ]);
9233
+ if (timer !== void 0)
9234
+ clearTimeout(timer);
9235
+ return result;
9236
+ };
9237
+ if (await waitForExit(this.options.shutdownTimeoutMs)) {
9238
+ return;
9239
+ }
9240
+ if (child.exitCode === null && child.signalCode === null) {
9241
+ child.kill("SIGTERM");
9235
9242
  }
9243
+ if (await waitForExit(1e3)) {
9244
+ return;
9245
+ }
9246
+ if (child.exitCode === null && child.signalCode === null) {
9247
+ child.kill("SIGKILL");
9248
+ }
9249
+ await waitForExit(1e3);
9236
9250
  }
9237
9251
  async waitForExit() {
9238
9252
  if (!this.child) {
@@ -9250,8 +9264,7 @@ var AgentRelayClient = class _AgentRelayClient {
9250
9264
  "init",
9251
9265
  "--name",
9252
9266
  this.options.brokerName,
9253
- "--channels",
9254
- this.options.channels.join(","),
9267
+ ...this.options.channels.length > 0 ? ["--channels", this.options.channels.join(",")] : [],
9255
9268
  ...this.options.binaryArgs
9256
9269
  ];
9257
9270
  const env = { ...this.options.env };
@@ -9262,7 +9275,7 @@ var AgentRelayClient = class _AgentRelayClient {
9262
9275
  env.PATH = `${binDir}${import_node_path.default.delimiter}${currentPath}`;
9263
9276
  }
9264
9277
  }
9265
- console.log(`[broker] Starting: ${resolvedBinary} ${args.join(" ")}`);
9278
+ console.error(`[broker] Starting: ${resolvedBinary} ${args.join(" ")}`);
9266
9279
  const child = (0, import_node_child_process.spawn)(resolvedBinary, args, {
9267
9280
  cwd: this.options.cwd,
9268
9281
  env,
@@ -9298,7 +9311,7 @@ var AgentRelayClient = class _AgentRelayClient {
9298
9311
  });
9299
9312
  });
9300
9313
  const helloAck = await this.requestHello();
9301
- console.log("[broker] Broker ready (hello handshake complete)");
9314
+ console.error("[broker] Broker ready (hello handshake complete)");
9302
9315
  if (helloAck.workspace_key) {
9303
9316
  this.workspaceKey = helloAck.workspace_key;
9304
9317
  }
@@ -9471,8 +9484,10 @@ function getLatestVersionSync() {
9471
9484
  timeout: 15e3,
9472
9485
  stdio: ["pipe", "pipe", "pipe"]
9473
9486
  }).toString();
9474
- const match = result.match(/"tag_name"\s*:\s*"v?([^"]+)"/);
9475
- return match?.[1] ?? null;
9487
+ const match = result.match(/"tag_name"\s*:\s*"([^"]+)"/);
9488
+ if (!match?.[1])
9489
+ return null;
9490
+ return match[1].replace(/^openclaw-/, "").replace(/^v/, "");
9476
9491
  } catch {
9477
9492
  return null;
9478
9493
  }
@@ -9502,6 +9517,13 @@ function installBrokerBinary() {
9502
9517
  });
9503
9518
  import_node_fs.default.chmodSync(targetPath, 493);
9504
9519
  if (process.platform === "darwin") {
9520
+ try {
9521
+ (0, import_node_child_process.execSync)(`xattr -d com.apple.quarantine "${targetPath}" 2>/dev/null || true`, {
9522
+ timeout: 1e4,
9523
+ stdio: ["pipe", "pipe", "pipe"]
9524
+ });
9525
+ } catch {
9526
+ }
9505
9527
  try {
9506
9528
  (0, import_node_child_process.execSync)(`codesign --force --sign - "${targetPath}"`, {
9507
9529
  timeout: 1e4,
@@ -45187,7 +45209,7 @@ var AgentRelay = class {
45187
45209
  this.clientOptions = {
45188
45210
  binaryPath: options.binaryPath,
45189
45211
  binaryArgs: options.binaryArgs,
45190
- brokerName: options.brokerName,
45212
+ brokerName: options.brokerName ?? options.workspaceName,
45191
45213
  channels: this.defaultChannels,
45192
45214
  cwd: options.cwd,
45193
45215
  env: options.env,
@@ -45246,7 +45268,8 @@ var AgentRelay = class {
45246
45268
  shadowOf: input.shadowOf,
45247
45269
  shadowMode: input.shadowMode,
45248
45270
  idleThresholdSecs: input.idleThresholdSecs,
45249
- restartPolicy: input.restartPolicy
45271
+ restartPolicy: input.restartPolicy,
45272
+ skipRelayPrompt: input.skipRelayPrompt
45250
45273
  });
45251
45274
  } catch (error95) {
45252
45275
  await this.invokeLifecycleHook(input.onError, {
@@ -45279,6 +45302,7 @@ var AgentRelay = class {
45279
45302
  shadowMode: options?.shadowMode,
45280
45303
  idleThresholdSecs: options?.idleThresholdSecs,
45281
45304
  restartPolicy: options?.restartPolicy,
45305
+ skipRelayPrompt: options?.skipRelayPrompt,
45282
45306
  onStart: options?.onStart,
45283
45307
  onSuccess: options?.onSuccess,
45284
45308
  onError: options?.onError
@@ -45588,10 +45612,21 @@ var AgentRelay = class {
45588
45612
  this.unsubEvent();
45589
45613
  this.unsubEvent = void 0;
45590
45614
  }
45591
- if (this.client) {
45592
- await this.client.shutdown();
45593
- this.client = void 0;
45615
+ let client = this.client;
45616
+ if (!client && this.startPromise) {
45617
+ try {
45618
+ client = await this.startPromise;
45619
+ } catch {
45620
+ client = void 0;
45621
+ }
45622
+ }
45623
+ if (client) {
45624
+ await client.shutdown();
45625
+ if (this.client === client) {
45626
+ this.client = void 0;
45627
+ }
45594
45628
  }
45629
+ this.startPromise = void 0;
45595
45630
  this.knownAgents.clear();
45596
45631
  this.readyAgents.clear();
45597
45632
  this.messageReadyAgents.clear();
@@ -45663,8 +45698,10 @@ var AgentRelay = class {
45663
45698
  * 4. Auto-create a fresh workspace via the Relaycast REST API
45664
45699
  */
45665
45700
  async ensureRelaycastApiKey() {
45666
- if (this.relayApiKey)
45701
+ if (this.relayApiKey) {
45702
+ this.wireRelaycastBaseUrl();
45667
45703
  return;
45704
+ }
45668
45705
  const envKey = this.clientOptions.env?.RELAY_API_KEY ?? process.env.RELAY_API_KEY;
45669
45706
  if (envKey) {
45670
45707
  this.relayApiKey = envKey;
@@ -45673,11 +45710,19 @@ var AgentRelay = class {
45673
45710
  } else if (!this.clientOptions.env.RELAY_API_KEY) {
45674
45711
  this.clientOptions.env.RELAY_API_KEY = envKey;
45675
45712
  }
45713
+ this.wireRelaycastBaseUrl();
45676
45714
  return;
45677
45715
  }
45678
45716
  if (!this.clientOptions.env) {
45679
45717
  this.clientOptions.env = { ...process.env };
45680
45718
  }
45719
+ this.wireRelaycastBaseUrl();
45720
+ }
45721
+ /** Inject relaycastBaseUrl into broker env. Explicit option wins over inherited env. */
45722
+ wireRelaycastBaseUrl() {
45723
+ if (this.relaycastBaseUrl && this.clientOptions.env) {
45724
+ this.clientOptions.env.RELAYCAST_BASE_URL = this.relaycastBaseUrl;
45725
+ }
45681
45726
  }
45682
45727
  async ensureStarted() {
45683
45728
  if (this.client)
@@ -45840,12 +45885,31 @@ var AgentRelay = class {
45840
45885
  name,
45841
45886
  reason: releaseOptions.reason
45842
45887
  };
45888
+ if (!relay.knownAgents.has(name)) {
45889
+ await relay.invokeLifecycleHook(releaseOptions.onStart, releaseContext, `release("${name}") onStart`);
45890
+ await relay.invokeLifecycleHook(releaseOptions.onSuccess, releaseContext, `release("${name}") onSuccess`);
45891
+ return;
45892
+ }
45843
45893
  const client = await relay.ensureStarted();
45844
45894
  await relay.invokeLifecycleHook(releaseOptions.onStart, releaseContext, `release("${name}") onStart`);
45845
45895
  try {
45846
45896
  await client.release(name, releaseOptions.reason);
45847
45897
  await relay.invokeLifecycleHook(releaseOptions.onSuccess, releaseContext, `release("${name}") onSuccess`);
45848
45898
  } catch (error95) {
45899
+ if (error95 instanceof AgentRelayProtocolError && error95.code === "agent_not_found") {
45900
+ relay.exitedAgents.add(name);
45901
+ relay.readyAgents.delete(name);
45902
+ relay.messageReadyAgents.delete(name);
45903
+ relay.idleAgents.delete(name);
45904
+ relay.knownAgents.delete(name);
45905
+ relay.outputListeners.delete(name);
45906
+ relay.exitResolvers.get(name)?.resolve("released");
45907
+ relay.exitResolvers.delete(name);
45908
+ relay.idleResolvers.get(name)?.resolve("exited");
45909
+ relay.idleResolvers.delete(name);
45910
+ await relay.invokeLifecycleHook(releaseOptions.onSuccess, releaseContext, `release("${name}") onSuccess`);
45911
+ return;
45912
+ }
45849
45913
  await relay.invokeLifecycleHook(releaseOptions.onError, {
45850
45914
  ...releaseContext,
45851
45915
  error: error95
@@ -45987,6 +46051,7 @@ var AgentRelay = class {
45987
46051
  task,
45988
46052
  model: options?.model,
45989
46053
  cwd: options?.cwd,
46054
+ skipRelayPrompt: options?.skipRelayPrompt,
45990
46055
  onStart: options?.onStart,
45991
46056
  onSuccess: options?.onSuccess,
45992
46057
  onError: options?.onError
@@ -46008,7 +46073,8 @@ var AgentRelay = class {
46008
46073
  transport: "headless",
46009
46074
  args,
46010
46075
  channels,
46011
- task
46076
+ task,
46077
+ skipRelayPrompt: options?.skipRelayPrompt
46012
46078
  });
46013
46079
  } catch (error95) {
46014
46080
  await this.invokeLifecycleHook(options?.onError, {
@@ -47233,7 +47299,7 @@ var WorkflowTrajectory = class {
47233
47299
  id,
47234
47300
  version: 1,
47235
47301
  task: {
47236
- title: `${workflowName} run #${this.runId.slice(0, 8)}`,
47302
+ title: workflowName,
47237
47303
  source: { system: "workflow-runner", id: this.runId }
47238
47304
  },
47239
47305
  status: "active",
@@ -47287,6 +47353,8 @@ var WorkflowTrajectory = class {
47287
47353
  if (participants?.reviewer) {
47288
47354
  await this.registerAgent(participants.reviewer, "reviewer");
47289
47355
  }
47356
+ this.closeCurrentChapter();
47357
+ this.openChapter(`Execution: ${step.name}`, agent);
47290
47358
  const intent = step.task ? step.task.trim().split(/\n|\.(?=\s)/)[0].trim().slice(0, 120) : `${step.type ?? "deterministic"} step`;
47291
47359
  this.addEvent("note", `"${step.name}": ${intent}`, void 0, { agent });
47292
47360
  await this.flush();
@@ -47440,12 +47508,24 @@ var WorkflowTrajectory = class {
47440
47508
  await this.moveToCompleted();
47441
47509
  }
47442
47510
  /** Abandon the trajectory. */
47443
- async abandon(reason) {
47511
+ async abandon(reason, meta5) {
47444
47512
  if (!this.enabled || !this.trajectory)
47445
47513
  return;
47514
+ const elapsed = Date.now() - this.startTime;
47515
+ const elapsedStr = elapsed > 6e4 ? `${Math.round(elapsed / 6e4)} minutes` : `${Math.round(elapsed / 1e3)} seconds`;
47516
+ const summary = meta5?.summary ?? `Workflow abandoned: ${reason}`;
47517
+ this.openRetrospective();
47518
+ this.addEvent("reflection", `${summary} (abandoned after ${elapsedStr})`, "high");
47446
47519
  this.addEvent("error", `Workflow abandoned: ${reason}`, "high");
47447
47520
  this.trajectory.status = "abandoned";
47448
47521
  this.trajectory.completedAt = (/* @__PURE__ */ new Date()).toISOString();
47522
+ this.trajectory.retrospective = {
47523
+ summary,
47524
+ approach: `${this.swarmPattern} workflow (${this.trajectory.agents.filter((a) => a.role !== "workflow-runner").length} agents)`,
47525
+ confidence: meta5?.confidence ?? 0,
47526
+ learnings: meta5?.learnings,
47527
+ challenges: meta5?.challenges
47528
+ };
47449
47529
  this.closeCurrentChapter();
47450
47530
  await this.flush();
47451
47531
  await this.moveToCompleted();
@@ -47604,6 +47684,16 @@ var WorkflowTrajectory = class {
47604
47684
  };
47605
47685
 
47606
47686
  // packages/sdk/dist/workflows/runner.js
47687
+ var SpawnExitError = class extends Error {
47688
+ exitCode;
47689
+ exitSignal;
47690
+ constructor(message, exitCode, exitSignal) {
47691
+ super(message);
47692
+ this.name = "SpawnExitError";
47693
+ this.exitCode = exitCode;
47694
+ this.exitSignal = exitSignal ?? void 0;
47695
+ }
47696
+ };
47607
47697
  var _resolvedCursorCli;
47608
47698
  function resolveCursorCli() {
47609
47699
  if (_resolvedCursorCli !== void 0)
@@ -47647,6 +47737,8 @@ var WorkflowRunner = class _WorkflowRunner {
47647
47737
  activeAgentHandles = /* @__PURE__ */ new Map();
47648
47738
  // PTY-based output capture: accumulate terminal output per-agent
47649
47739
  ptyOutputBuffers = /* @__PURE__ */ new Map();
47740
+ /** Snapshot of PTY output from the most recent failed attempt, keyed by step name. */
47741
+ lastFailedStepOutput = /* @__PURE__ */ new Map();
47650
47742
  ptyListeners = /* @__PURE__ */ new Map();
47651
47743
  ptyLogStreams = /* @__PURE__ */ new Map();
47652
47744
  /** Path to workers.json so `agents:kill` can find workflow-spawned agents */
@@ -48374,7 +48466,10 @@ ${err.suggestion}`);
48374
48466
  // ── Execution ───────────────────────────────────────────────────────────
48375
48467
  /** Execute a named workflow from a validated config. */
48376
48468
  async execute(config3, workflowName, vars) {
48469
+ this.abortController = new AbortController();
48470
+ this.paused = false;
48377
48471
  const resolved = vars ? this.resolveVariables(config3, vars) : config3;
48472
+ this.validateConfig(resolved);
48378
48473
  const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
48379
48474
  if (pathResult.errors.length > 0) {
48380
48475
  throw new Error(`Path validation failed:
@@ -48437,6 +48532,8 @@ ${err.suggestion}`);
48437
48532
  }
48438
48533
  /** Resume a previously paused or partially completed run. */
48439
48534
  async resume(runId, vars) {
48535
+ this.abortController = new AbortController();
48536
+ this.paused = false;
48440
48537
  const run = await this.db.getRun(runId);
48441
48538
  if (!run) {
48442
48539
  throw new Error(`Run "${runId}" not found`);
@@ -48483,8 +48580,6 @@ ${err.suggestion}`);
48483
48580
  async runWorkflowCore(input) {
48484
48581
  const { run, workflow: workflow2, config: config3, stepStates, isResume } = input;
48485
48582
  const runId = run.id;
48486
- this.abortController = new AbortController();
48487
- this.paused = false;
48488
48583
  this.currentConfig = config3;
48489
48584
  this.currentRunId = runId;
48490
48585
  this.runStartTime = Date.now();
@@ -48508,14 +48603,18 @@ ${err.suggestion}`);
48508
48603
  config3.swarm.channel = channel;
48509
48604
  await this.db.updateRun(runId, { config: config3 });
48510
48605
  }
48511
- if (!this.executor) {
48512
- this.log("Resolving Relaycast API key...");
48513
- await this.ensureRelaycastApiKey(channel);
48514
- this.log("API key resolved");
48515
- if (this.relayApiKeyAutoCreated && this.relayApiKey) {
48516
- this.log(`Workspace created \u2014 follow this run in Relaycast:`);
48517
- this.log(` Observer: https://agentrelay.dev/observer?key=${this.relayApiKey}`);
48518
- this.log(` Channel: ${channel}`);
48606
+ const relaycastDisabled = this.relayOptions.env?.AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST === "1";
48607
+ const requiresBroker = !this.executor && workflow2.steps.some((step) => step.type !== "deterministic" && step.type !== "worktree");
48608
+ if (requiresBroker) {
48609
+ if (!relaycastDisabled) {
48610
+ this.log("Resolving Relaycast API key...");
48611
+ await this.ensureRelaycastApiKey(channel);
48612
+ this.log("API key resolved");
48613
+ if (this.relayApiKeyAutoCreated && this.relayApiKey) {
48614
+ this.log(`Workspace created \u2014 follow this run in Relaycast:`);
48615
+ this.log(` Observer: https://agentrelay.dev/observer?key=${this.relayApiKey}`);
48616
+ this.log(` Channel: ${channel}`);
48617
+ }
48519
48618
  }
48520
48619
  this.log("Starting broker...");
48521
48620
  const brokerBaseName = import_node_path8.default.basename(this.cwd) || "workflow";
@@ -48523,7 +48622,7 @@ ${err.suggestion}`);
48523
48622
  this.relay = new AgentRelay({
48524
48623
  ...this.relayOptions,
48525
48624
  brokerName,
48526
- channels: [channel],
48625
+ channels: relaycastDisabled ? [] : [channel],
48527
48626
  env: this.getRelayEnv(),
48528
48627
  // Workflows spawn agents across multiple waves; each spawn requires a PTY +
48529
48628
  // Relaycast registration. 60s is too tight when the broker is saturated with
@@ -48571,6 +48670,18 @@ ${err.suggestion}`);
48571
48670
  }
48572
48671
  };
48573
48672
  this.relay.onMessageReceived = (msg) => {
48673
+ this.emit({
48674
+ type: "broker:event",
48675
+ runId,
48676
+ event: {
48677
+ kind: "relay_inbound",
48678
+ event_id: msg.eventId,
48679
+ from: msg.from,
48680
+ target: msg.to,
48681
+ body: msg.text,
48682
+ thread_id: msg.threadId
48683
+ }
48684
+ });
48574
48685
  const body = msg.text.length > 120 ? msg.text.slice(0, 117) + "..." : msg.text;
48575
48686
  const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
48576
48687
  const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
@@ -48581,18 +48692,59 @@ ${err.suggestion}`);
48581
48692
  }
48582
48693
  };
48583
48694
  this.relay.onAgentSpawned = (agent) => {
48695
+ this.emit({
48696
+ type: "broker:event",
48697
+ runId,
48698
+ event: {
48699
+ kind: "agent_spawned",
48700
+ name: agent.name,
48701
+ runtime: agent.runtime
48702
+ }
48703
+ });
48584
48704
  if (!this.activeAgentHandles.has(agent.name)) {
48585
48705
  this.log(`[spawned] ${agent.name} (${agent.runtime})`);
48586
48706
  }
48587
48707
  };
48708
+ this.relay.onAgentReleased = (agent) => {
48709
+ this.emit({
48710
+ type: "broker:event",
48711
+ runId,
48712
+ event: {
48713
+ kind: "agent_released",
48714
+ name: agent.name
48715
+ }
48716
+ });
48717
+ };
48588
48718
  this.relay.onAgentExited = (agent) => {
48719
+ this.emit({
48720
+ type: "broker:event",
48721
+ runId,
48722
+ event: {
48723
+ kind: "agent_exited",
48724
+ name: agent.name,
48725
+ code: agent.exitCode,
48726
+ signal: agent.exitSignal
48727
+ }
48728
+ });
48589
48729
  this.lastActivity.delete(agent.name);
48590
48730
  this.lastIdleLog.delete(agent.name);
48591
48731
  if (!this.activeAgentHandles.has(agent.name)) {
48592
48732
  this.log(`[exited] ${agent.name} (code: ${agent.exitCode ?? "?"})`);
48593
48733
  }
48594
48734
  };
48735
+ this.relay.onDeliveryUpdate = (event) => {
48736
+ this.emit({ type: "broker:event", runId, event });
48737
+ };
48595
48738
  this.relay.onAgentIdle = ({ name, idleSecs }) => {
48739
+ this.emit({
48740
+ type: "broker:event",
48741
+ runId,
48742
+ event: {
48743
+ kind: "agent_idle",
48744
+ name,
48745
+ idle_secs: idleSecs
48746
+ }
48747
+ });
48596
48748
  const bucket = Math.floor(idleSecs / 30) * 30;
48597
48749
  if (bucket >= 30 && this.lastIdleLog.get(name) !== bucket) {
48598
48750
  this.lastIdleLog.set(name, bucket);
@@ -48605,17 +48757,19 @@ ${err.suggestion}`);
48605
48757
  this.unsubBrokerStderr = this.relay.onBrokerStderr((line) => {
48606
48758
  console.log(`[broker] ${line}`);
48607
48759
  });
48608
- this.log(`Creating channel: ${channel}...`);
48609
- if (isResume) {
48610
- await this.createAndJoinRelaycastChannel(channel);
48611
- } else {
48612
- await this.createAndJoinRelaycastChannel(channel, workflow2.description);
48613
- }
48614
- this.log("Channel ready");
48615
- if (isResume) {
48616
- this.postToChannel(`Workflow **${workflow2.name}** resumed \u2014 ${pendingCount} pending steps`);
48617
- } else {
48618
- this.postToChannel(`Workflow **${workflow2.name}** started \u2014 ${workflow2.steps.length} steps, pattern: ${config3.swarm.pattern}`);
48760
+ if (!relaycastDisabled) {
48761
+ this.log(`Creating channel: ${channel}...`);
48762
+ if (isResume) {
48763
+ await this.createAndJoinRelaycastChannel(channel);
48764
+ } else {
48765
+ await this.createAndJoinRelaycastChannel(channel, workflow2.description);
48766
+ }
48767
+ this.log("Channel ready");
48768
+ if (isResume) {
48769
+ this.postToChannel(`Workflow **${workflow2.name}** resumed \u2014 ${pendingCount} pending steps`);
48770
+ } else {
48771
+ this.postToChannel(`Workflow **${workflow2.name}** started \u2014 ${workflow2.steps.length} steps, pattern: ${config3.swarm.pattern}`);
48772
+ }
48619
48773
  }
48620
48774
  }
48621
48775
  const agentMap = /* @__PURE__ */ new Map();
@@ -48627,7 +48781,9 @@ ${err.suggestion}`);
48627
48781
  }
48628
48782
  this.log(`Executing ${workflow2.steps.length} steps (pattern: ${config3.swarm.pattern})`);
48629
48783
  await this.executeSteps(workflow2, stepStates, agentMap, config3.errorHandling, runId);
48630
- const allCompleted = [...stepStates.values()].every((s) => s.row.status === "completed" || s.row.status === "skipped");
48784
+ const errorStrategy = config3.errorHandling?.strategy ?? workflow2.onError ?? "fail-fast";
48785
+ const continueOnError = errorStrategy === "continue" || errorStrategy === "skip";
48786
+ const allCompleted = [...stepStates.values()].every((s) => s.row.status === "completed" || s.row.status === "skipped" || continueOnError && s.row.status === "failed");
48631
48787
  if (allCompleted) {
48632
48788
  this.log("Workflow completed successfully");
48633
48789
  await this.updateRunStatus(runId, "completed");
@@ -48647,24 +48803,52 @@ ${err.suggestion}`);
48647
48803
  await this.updateRunStatus(runId, "failed", errorMsg);
48648
48804
  this.emit({ type: "run:failed", runId, error: errorMsg });
48649
48805
  const outcomes = this.collectOutcomes(stepStates, workflow2.steps);
48806
+ const summary = this.trajectory.buildRunSummary(outcomes);
48807
+ const confidence = this.trajectory.computeConfidence(outcomes);
48808
+ const learnings = this.trajectory.extractLearnings(outcomes);
48809
+ const challenges = this.trajectory.extractChallenges(outcomes);
48650
48810
  this.postFailureReport(workflow2.name, outcomes, errorMsg);
48651
48811
  this.logRunSummary(workflow2.name, outcomes, runId);
48652
- await this.trajectory.abandon(errorMsg);
48812
+ await this.trajectory.abandon(errorMsg, {
48813
+ summary,
48814
+ confidence,
48815
+ learnings,
48816
+ challenges
48817
+ });
48653
48818
  }
48654
48819
  } catch (err) {
48655
48820
  const errorMsg = err instanceof Error ? err.message : String(err);
48656
48821
  const status = !isResume && this.abortController?.signal.aborted ? "cancelled" : "failed";
48657
48822
  await this.updateRunStatus(runId, status, errorMsg);
48658
48823
  if (status === "cancelled") {
48824
+ for (const [stepName, state] of stepStates) {
48825
+ if (state.row.status === "pending" || state.row.status === "running") {
48826
+ state.row.status = "failed";
48827
+ state.row.error = "Cancelled";
48828
+ await this.db.updateStep(state.row.id, {
48829
+ status: "failed",
48830
+ error: "Cancelled",
48831
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
48832
+ });
48833
+ this.emit({ type: "step:failed", runId, stepName, error: "Cancelled" });
48834
+ }
48835
+ }
48659
48836
  this.emit({ type: "run:cancelled", runId });
48660
48837
  this.postToChannel(`Workflow **${workflow2.name}** cancelled`);
48661
48838
  await this.trajectory.abandon("Cancelled by user");
48662
48839
  } else {
48663
48840
  this.emit({ type: "run:failed", runId, error: errorMsg });
48664
48841
  this.postToChannel(`Workflow failed: ${errorMsg}`);
48665
- await this.trajectory.abandon(errorMsg);
48842
+ const outcomes = this.collectOutcomes(stepStates, workflow2.steps);
48843
+ await this.trajectory.abandon(errorMsg, {
48844
+ summary: this.trajectory.buildRunSummary(outcomes),
48845
+ confidence: this.trajectory.computeConfidence(outcomes),
48846
+ learnings: this.trajectory.extractLearnings(outcomes),
48847
+ challenges: this.trajectory.extractChallenges(outcomes)
48848
+ });
48666
48849
  }
48667
48850
  } finally {
48851
+ this.lastFailedStepOutput.clear();
48668
48852
  for (const stream of this.ptyLogStreams.values())
48669
48853
  stream.end();
48670
48854
  this.ptyLogStreams.clear();
@@ -48675,9 +48859,11 @@ ${err.suggestion}`);
48675
48859
  if (this.relay) {
48676
48860
  this.relay.onMessageReceived = null;
48677
48861
  this.relay.onAgentSpawned = null;
48862
+ this.relay.onAgentReleased = null;
48678
48863
  this.relay.onAgentExited = null;
48679
48864
  this.relay.onAgentIdle = null;
48680
48865
  this.relay.onWorkerOutput = null;
48866
+ this.relay.onDeliveryUpdate = null;
48681
48867
  }
48682
48868
  this.lastIdleLog.clear();
48683
48869
  this.lastActivity.clear();
@@ -48898,7 +49084,7 @@ ${trimmedOutput.slice(0, 200)}`);
48898
49084
  }
48899
49085
  async executeStep(step, stepStates, agentMap, errorHandling, runId) {
48900
49086
  if (this.isDeterministicStep(step)) {
48901
- return this.executeDeterministicStep(step, stepStates, runId);
49087
+ return this.executeDeterministicStep(step, stepStates, runId, errorHandling);
48902
49088
  }
48903
49089
  if (this.isWorktreeStep(step)) {
48904
49090
  return this.executeWorktreeStep(step, stepStates, runId);
@@ -48909,131 +49095,154 @@ ${trimmedOutput.slice(0, 200)}`);
48909
49095
  * Execute a deterministic step (shell command).
48910
49096
  * Fast, reliable, $0 LLM cost.
48911
49097
  */
48912
- async executeDeterministicStep(step, stepStates, runId) {
49098
+ async executeDeterministicStep(step, stepStates, runId, errorHandling) {
48913
49099
  const state = stepStates.get(step.name);
48914
49100
  if (!state)
48915
49101
  throw new Error(`Step state not found: ${step.name}`);
48916
- this.checkAborted();
48917
- state.row.status = "running";
48918
- state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
48919
- await this.db.updateStep(state.row.id, {
48920
- status: "running",
48921
- startedAt: state.row.startedAt,
48922
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
48923
- });
48924
- this.emit({ type: "step:started", runId, stepName: step.name });
48925
- this.postToChannel(`**[${step.name}]** Started (deterministic)`);
48926
- const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
48927
- let resolvedCommand = this.interpolateStepTask(step.command ?? "", stepOutputContext);
48928
- resolvedCommand = resolvedCommand.replace(/\{\{([\w][\w.\-]*)\}\}/g, (_match, key) => {
48929
- if (key.startsWith("steps."))
48930
- return _match;
48931
- const value = this.resolveDotPath(key, stepOutputContext);
48932
- return value !== void 0 ? String(value) : _match;
48933
- });
48934
- const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
48935
- try {
48936
- if (this.executor?.executeDeterministicStep) {
48937
- const result = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
48938
- const failOnError = step.failOnError !== false;
48939
- if (failOnError && result.exitCode !== 0) {
48940
- throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
48941
- }
48942
- const output2 = step.captureOutput !== false ? result.output : `Command completed (exit code ${result.exitCode})`;
48943
- state.row.status = "completed";
48944
- state.row.output = output2;
48945
- state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
49102
+ const maxRetries = step.retries ?? errorHandling?.maxRetries ?? 0;
49103
+ const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
49104
+ let lastError;
49105
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
49106
+ this.checkAborted();
49107
+ if (attempt > 0) {
49108
+ this.emit({ type: "step:retrying", runId, stepName: step.name, attempt });
49109
+ this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
49110
+ state.row.retryCount = attempt;
48946
49111
  await this.db.updateStep(state.row.id, {
48947
- status: "completed",
48948
- output: output2,
48949
- completedAt: state.row.completedAt,
49112
+ retryCount: attempt,
48950
49113
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
48951
49114
  });
48952
- await this.persistStepOutput(runId, step.name, output2);
48953
- this.emit({ type: "step:completed", runId, stepName: step.name, output: output2 });
48954
- return;
49115
+ await this.delay(retryDelay);
48955
49116
  }
48956
- const output = await new Promise((resolve3, reject) => {
48957
- const child = (0, import_node_child_process3.spawn)("sh", ["-c", resolvedCommand], {
48958
- stdio: "pipe",
48959
- cwd: stepCwd,
48960
- env: { ...process.env }
48961
- });
48962
- const stdoutChunks = [];
48963
- const stderrChunks = [];
48964
- const abortSignal = this.abortController?.signal;
48965
- let abortHandler;
48966
- if (abortSignal && !abortSignal.aborted) {
48967
- abortHandler = () => {
48968
- child.kill("SIGTERM");
48969
- setTimeout(() => child.kill("SIGKILL"), 5e3);
48970
- };
48971
- abortSignal.addEventListener("abort", abortHandler, { once: true });
48972
- }
48973
- let timedOut = false;
48974
- let timer;
48975
- if (step.timeoutMs) {
48976
- timer = setTimeout(() => {
48977
- timedOut = true;
48978
- child.kill("SIGTERM");
48979
- setTimeout(() => child.kill("SIGKILL"), 5e3);
48980
- }, step.timeoutMs);
48981
- }
48982
- child.stdout?.on("data", (chunk) => {
48983
- stdoutChunks.push(chunk.toString());
48984
- });
48985
- child.stderr?.on("data", (chunk) => {
48986
- stderrChunks.push(chunk.toString());
48987
- });
48988
- child.on("close", (code) => {
48989
- if (timer)
48990
- clearTimeout(timer);
48991
- if (abortHandler && abortSignal) {
48992
- abortSignal.removeEventListener("abort", abortHandler);
49117
+ state.row.status = "running";
49118
+ state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
49119
+ await this.db.updateStep(state.row.id, {
49120
+ status: "running",
49121
+ startedAt: state.row.startedAt,
49122
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49123
+ });
49124
+ this.emit({ type: "step:started", runId, stepName: step.name });
49125
+ this.postToChannel(`**[${step.name}]** Started (deterministic)`);
49126
+ const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
49127
+ let resolvedCommand = this.interpolateStepTask(step.command ?? "", stepOutputContext);
49128
+ resolvedCommand = resolvedCommand.replace(/\{\{([\w][\w.\-]*)\}\}/g, (_match, key) => {
49129
+ if (key.startsWith("steps."))
49130
+ return _match;
49131
+ const value = this.resolveDotPath(key, stepOutputContext);
49132
+ return value !== void 0 ? String(value) : _match;
49133
+ });
49134
+ const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
49135
+ try {
49136
+ if (this.executor?.executeDeterministicStep) {
49137
+ const result = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
49138
+ const failOnError = step.failOnError !== false;
49139
+ if (failOnError && result.exitCode !== 0) {
49140
+ throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
48993
49141
  }
48994
- if (abortSignal?.aborted) {
48995
- reject(new Error(`Step "${step.name}" aborted`));
48996
- return;
49142
+ const output2 = step.captureOutput !== false ? result.output : `Command completed (exit code ${result.exitCode})`;
49143
+ if (step.verification) {
49144
+ this.runVerification(step.verification, output2, step.name);
48997
49145
  }
48998
- if (timedOut) {
48999
- reject(new Error(`Step "${step.name}" timed out (no step timeout set, check global swarm.timeoutMs)`));
49000
- return;
49146
+ state.row.status = "completed";
49147
+ state.row.output = output2;
49148
+ state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
49149
+ await this.db.updateStep(state.row.id, {
49150
+ status: "completed",
49151
+ output: output2,
49152
+ completedAt: state.row.completedAt,
49153
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49154
+ });
49155
+ await this.persistStepOutput(runId, step.name, output2);
49156
+ this.emit({ type: "step:completed", runId, stepName: step.name, output: output2 });
49157
+ return;
49158
+ }
49159
+ const output = await new Promise((resolve3, reject) => {
49160
+ const child = (0, import_node_child_process3.spawn)("sh", ["-c", resolvedCommand], {
49161
+ stdio: "pipe",
49162
+ cwd: stepCwd,
49163
+ env: { ...process.env }
49164
+ });
49165
+ const stdoutChunks = [];
49166
+ const stderrChunks = [];
49167
+ const abortSignal = this.abortController?.signal;
49168
+ let abortHandler;
49169
+ if (abortSignal && !abortSignal.aborted) {
49170
+ abortHandler = () => {
49171
+ child.kill("SIGTERM");
49172
+ setTimeout(() => child.kill("SIGKILL"), 5e3);
49173
+ };
49174
+ abortSignal.addEventListener("abort", abortHandler, { once: true });
49001
49175
  }
49002
- const stdout = stdoutChunks.join("");
49003
- const stderr = stderrChunks.join("");
49004
- const failOnError = step.failOnError !== false;
49005
- if (failOnError && code !== 0 && code !== null) {
49006
- reject(new Error(`Command failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
49007
- return;
49176
+ let timedOut = false;
49177
+ let timer;
49178
+ if (step.timeoutMs) {
49179
+ timer = setTimeout(() => {
49180
+ timedOut = true;
49181
+ child.kill("SIGTERM");
49182
+ setTimeout(() => child.kill("SIGKILL"), 5e3);
49183
+ }, step.timeoutMs);
49008
49184
  }
49009
- resolve3(step.captureOutput !== false ? stdout : `Command completed (exit code ${code ?? 0})`);
49185
+ child.stdout?.on("data", (chunk) => {
49186
+ stdoutChunks.push(chunk.toString());
49187
+ });
49188
+ child.stderr?.on("data", (chunk) => {
49189
+ stderrChunks.push(chunk.toString());
49190
+ });
49191
+ child.on("close", (code) => {
49192
+ if (timer)
49193
+ clearTimeout(timer);
49194
+ if (abortHandler && abortSignal) {
49195
+ abortSignal.removeEventListener("abort", abortHandler);
49196
+ }
49197
+ if (abortSignal?.aborted) {
49198
+ reject(new Error(`Step "${step.name}" aborted`));
49199
+ return;
49200
+ }
49201
+ if (timedOut) {
49202
+ reject(new Error(`Step "${step.name}" timed out (no step timeout set, check global swarm.timeoutMs)`));
49203
+ return;
49204
+ }
49205
+ const stdout = stdoutChunks.join("");
49206
+ const stderr = stderrChunks.join("");
49207
+ const failOnError = step.failOnError !== false;
49208
+ if (failOnError && code !== 0 && code !== null) {
49209
+ reject(new Error(`Command failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
49210
+ return;
49211
+ }
49212
+ resolve3(step.captureOutput !== false ? stdout : `Command completed (exit code ${code ?? 0})`);
49213
+ });
49214
+ child.on("error", (err) => {
49215
+ if (timer)
49216
+ clearTimeout(timer);
49217
+ if (abortHandler && abortSignal) {
49218
+ abortSignal.removeEventListener("abort", abortHandler);
49219
+ }
49220
+ reject(new Error(`Failed to execute command: ${err.message}`));
49221
+ });
49010
49222
  });
49011
- child.on("error", (err) => {
49012
- if (timer)
49013
- clearTimeout(timer);
49014
- if (abortHandler && abortSignal) {
49015
- abortSignal.removeEventListener("abort", abortHandler);
49016
- }
49017
- reject(new Error(`Failed to execute command: ${err.message}`));
49223
+ if (step.verification) {
49224
+ this.runVerification(step.verification, output, step.name);
49225
+ }
49226
+ state.row.status = "completed";
49227
+ state.row.output = output;
49228
+ state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
49229
+ await this.db.updateStep(state.row.id, {
49230
+ status: "completed",
49231
+ output,
49232
+ completedAt: state.row.completedAt,
49233
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49018
49234
  });
49019
- });
49020
- state.row.status = "completed";
49021
- state.row.output = output;
49022
- state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
49023
- await this.db.updateStep(state.row.id, {
49024
- status: "completed",
49025
- output,
49026
- completedAt: state.row.completedAt,
49027
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49028
- });
49029
- await this.persistStepOutput(runId, step.name, output);
49030
- this.emit({ type: "step:completed", runId, stepName: step.name, output });
49031
- } catch (err) {
49032
- const errorMsg = err instanceof Error ? err.message : String(err);
49033
- this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
49034
- await this.markStepFailed(state, errorMsg, runId);
49035
- throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
49235
+ await this.persistStepOutput(runId, step.name, output);
49236
+ this.emit({ type: "step:completed", runId, stepName: step.name, output });
49237
+ return;
49238
+ } catch (err) {
49239
+ lastError = err instanceof Error ? err.message : String(err);
49240
+ }
49036
49241
  }
49242
+ const errorMsg = lastError ?? "Unknown error";
49243
+ this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
49244
+ await this.markStepFailed(state, errorMsg, runId);
49245
+ throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
49037
49246
  }
49038
49247
  /**
49039
49248
  * Execute a worktree step (git worktree setup).
@@ -49202,8 +49411,12 @@ ${trimmedOutput.slice(0, 200)}`);
49202
49411
  const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
49203
49412
  const timeoutMs = step.timeoutMs ?? ownerDef.constraints?.timeoutMs ?? specialistDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
49204
49413
  let lastError;
49414
+ let lastExitCode;
49415
+ let lastExitSignal;
49205
49416
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
49206
49417
  this.checkAborted();
49418
+ lastExitCode = void 0;
49419
+ lastExitSignal = void 0;
49207
49420
  if (attempt > 0) {
49208
49421
  this.emit({ type: "step:retrying", runId, stepName: step.name, attempt });
49209
49422
  this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
@@ -49243,6 +49456,15 @@ ${trimmedOutput.slice(0, 200)}`);
49243
49456
  });
49244
49457
  const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
49245
49458
  let resolvedTask = this.interpolateStepTask(step.task ?? "", stepOutputContext);
49459
+ if (attempt > 0 && lastError) {
49460
+ const priorOutput = (this.lastFailedStepOutput.get(step.name) ?? "").slice(-2e3);
49461
+ resolvedTask = `[RETRY \u2014 Attempt ${attempt + 1}/${maxRetries + 1}]
49462
+ Previous attempt failed: ${lastError}
49463
+ ` + (priorOutput ? `Previous output (last 2000 chars):
49464
+ ${priorOutput}
49465
+ ` : "") + `---
49466
+ ${resolvedTask}`;
49467
+ }
49246
49468
  if (specialistDef.interactive !== false || ownerDef.interactive !== false) {
49247
49469
  const nonInteractiveInfo = this.buildNonInteractiveAwareness(agentMap, stepStates);
49248
49470
  if (nonInteractiveInfo) {
@@ -49273,7 +49495,10 @@ ${trimmedOutput.slice(0, 200)}`);
49273
49495
  this.log(`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ""}`);
49274
49496
  const resolvedStep = { ...step, task: ownerTask };
49275
49497
  const ownerStartTime = Date.now();
49276
- const output = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs);
49498
+ const spawnResult = this.executor ? await this.executor.executeAgentStep(resolvedStep, effectiveOwner, ownerTask, timeoutMs) : await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs);
49499
+ const output = typeof spawnResult === "string" ? spawnResult : spawnResult.output;
49500
+ lastExitCode = typeof spawnResult === "string" ? void 0 : spawnResult.exitCode;
49501
+ lastExitSignal = typeof spawnResult === "string" ? void 0 : spawnResult.exitSignal;
49277
49502
  ownerElapsed = Date.now() - ownerStartTime;
49278
49503
  this.log(`[${step.name}] Owner "${effectiveOwner.name}" exited`);
49279
49504
  if (usesOwnerFlow) {
@@ -49283,7 +49508,7 @@ ${trimmedOutput.slice(0, 200)}`);
49283
49508
  ownerOutput = output;
49284
49509
  }
49285
49510
  if (step.verification) {
49286
- this.runVerification(step.verification, specialistOutput, step.name, resolvedTask);
49511
+ this.runVerification(step.verification, specialistOutput, step.name, effectiveOwner.interactive === false ? void 0 : resolvedTask);
49287
49512
  }
49288
49513
  let combinedOutput = specialistOutput;
49289
49514
  if (usesOwnerFlow && reviewDef) {
@@ -49301,11 +49526,15 @@ ${trimmedOutput.slice(0, 200)}`);
49301
49526
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49302
49527
  });
49303
49528
  await this.persistStepOutput(runId, step.name, combinedOutput);
49304
- this.emit({ type: "step:completed", runId, stepName: step.name, output: combinedOutput });
49529
+ this.emit({ type: "step:completed", runId, stepName: step.name, output: combinedOutput, exitCode: lastExitCode, exitSignal: lastExitSignal });
49305
49530
  await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
49306
49531
  return;
49307
49532
  } catch (err) {
49308
49533
  lastError = err instanceof Error ? err.message : String(err);
49534
+ if (err instanceof SpawnExitError) {
49535
+ lastExitCode = err.exitCode;
49536
+ lastExitSignal = err.exitSignal;
49537
+ }
49309
49538
  const ownerTimedOut = usesDedicatedOwner ? /\bowner timed out\b/i.test(lastError) : /\btimed out\b/i.test(lastError) && !lastError.includes(`${step.name}-review`);
49310
49539
  if (ownerTimedOut) {
49311
49540
  this.emit({ type: "step:owner-timeout", runId, stepName: step.name, ownerName: ownerDef.name });
@@ -49320,7 +49549,10 @@ ${trimmedOutput.slice(0, 200)}`);
49320
49549
  verificationValue
49321
49550
  });
49322
49551
  this.postToChannel(`**[${step.name}]** Failed: ${lastError ?? "Unknown error"}`);
49323
- await this.markStepFailed(state, lastError ?? "Unknown error", runId);
49552
+ await this.markStepFailed(state, lastError ?? "Unknown error", runId, {
49553
+ exitCode: lastExitCode,
49554
+ exitSignal: lastExitSignal
49555
+ });
49324
49556
  throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? "Unknown error"}`);
49325
49557
  }
49326
49558
  injectStepOwnerContract(step, resolvedTask, ownerDef, specialistDef) {
@@ -49438,10 +49670,10 @@ Output exactly: STEP_COMPLETE:${step.name}`;
49438
49670
  throw error95;
49439
49671
  });
49440
49672
  const workerSettled = workerPromise.catch(() => void 0);
49441
- workerPromise.then((output) => {
49673
+ workerPromise.then((result) => {
49442
49674
  workerReleased = true;
49443
49675
  this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited`);
49444
- if (step.verification?.type === "output_contains" && output.includes(step.verification.value)) {
49676
+ if (step.verification?.type === "output_contains" && result.output.includes(step.verification.value)) {
49445
49677
  this.postToChannel(`**[${step.name}]** Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`);
49446
49678
  }
49447
49679
  }).catch((error95) => {
@@ -49459,7 +49691,7 @@ Output exactly: STEP_COMPLETE:${step.name}`;
49459
49691
  this.log(`[${step.name}] Spawning owner "${supervised.owner.name}" (cli: ${supervised.owner.cli})`);
49460
49692
  const ownerStartTime = Date.now();
49461
49693
  try {
49462
- const ownerOutput = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
49694
+ const ownerResultObj = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
49463
49695
  agentNameSuffix: "owner",
49464
49696
  onSpawned: ({ actualName }) => {
49465
49697
  this.supervisedRuntimeAgents.set(actualName, {
@@ -49473,9 +49705,10 @@ Output exactly: STEP_COMPLETE:${step.name}`;
49473
49705
  }
49474
49706
  });
49475
49707
  const ownerElapsed = Date.now() - ownerStartTime;
49708
+ const ownerOutput = ownerResultObj.output;
49476
49709
  this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
49477
49710
  this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
49478
- const specialistOutput = await workerPromise;
49711
+ const specialistOutput = (await workerPromise).output;
49479
49712
  return { specialistOutput, ownerOutput, ownerElapsed };
49480
49713
  } catch (error95) {
49481
49714
  const message = error95 instanceof Error ? error95.message : String(error95);
@@ -49673,7 +49906,7 @@ Then output /exit.`;
49673
49906
  })();
49674
49907
  };
49675
49908
  try {
49676
- reviewOutput = await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
49909
+ await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
49677
49910
  onSpawned: ({ agent }) => {
49678
49911
  reviewerHandle = agent;
49679
49912
  },
@@ -49853,7 +50086,7 @@ DO NOT:
49853
50086
  const stdoutChunks = [];
49854
50087
  const stderrChunks = [];
49855
50088
  try {
49856
- const output = await new Promise((resolve3, reject) => {
50089
+ const { stdout: output, exitCode, exitSignal } = await new Promise((resolve3, reject) => {
49857
50090
  const child = (0, import_node_child_process3.spawn)(cmd, args, {
49858
50091
  stdio: ["ignore", "pipe", "pipe"],
49859
50092
  cwd: this.resolveAgentCwd(agentDef),
@@ -49899,7 +50132,7 @@ DO NOT:
49899
50132
  setTimeout(() => child.kill("SIGKILL"), 5e3);
49900
50133
  }, timeoutMs);
49901
50134
  }
49902
- child.on("close", (code) => {
50135
+ child.on("close", (code, signal) => {
49903
50136
  clearInterval(heartbeat);
49904
50137
  if (timer)
49905
50138
  clearTimeout(timer);
@@ -49917,10 +50150,14 @@ DO NOT:
49917
50150
  }
49918
50151
  if (code !== 0 && code !== null) {
49919
50152
  const stderr = stderrChunks.join("");
49920
- reject(new Error(`Step "${step.name}" exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
50153
+ reject(new SpawnExitError(`Step "${step.name}" exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`, code, signal));
49921
50154
  return;
49922
50155
  }
49923
- resolve3(stdout);
50156
+ resolve3({
50157
+ stdout,
50158
+ exitCode: code ?? void 0,
50159
+ exitSignal: signal ?? void 0
50160
+ });
49924
50161
  });
49925
50162
  child.on("error", (err) => {
49926
50163
  clearInterval(heartbeat);
@@ -49932,8 +50169,10 @@ DO NOT:
49932
50169
  reject(new Error(`Failed to spawn ${cmd}: ${err.message}`));
49933
50170
  });
49934
50171
  });
49935
- return output;
50172
+ return { output, exitCode, exitSignal };
49936
50173
  } finally {
50174
+ const combinedOutput = stdoutChunks.join("") + stderrChunks.join("");
50175
+ this.lastFailedStepOutput.set(step.name, combinedOutput);
49937
50176
  stopHeartbeat?.();
49938
50177
  logStream.end();
49939
50178
  this.unregisterWorker(agentName);
@@ -50053,8 +50292,12 @@ DO NOT:
50053
50292
  throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? "unknown"}ms`);
50054
50293
  }
50055
50294
  }
50295
+ if (exitResult === "force-released") {
50296
+ throw new Error(`Step "${step.name}" failed \u2014 agent was force-released after exhausting idle nudges without completing`);
50297
+ }
50056
50298
  } finally {
50057
50299
  ptyChunks = this.ptyOutputBuffers.get(agentName) ?? [];
50300
+ this.lastFailedStepOutput.set(step.name, ptyChunks.join(""));
50058
50301
  stopHeartbeat?.();
50059
50302
  this.activeAgentHandles.delete(agentName);
50060
50303
  this.ptyOutputBuffers.delete(agentName);
@@ -50072,9 +50315,13 @@ DO NOT:
50072
50315
  output = ptyChunks.join("");
50073
50316
  } else {
50074
50317
  const summaryPath = import_node_path8.default.join(this.summaryDir, `${step.name}.md`);
50075
- output = (0, import_node_fs4.existsSync)(summaryPath) ? await (0, import_promises3.readFile)(summaryPath, "utf-8") : exitResult === "timeout" ? "Agent completed (released after idle timeout)" : exitResult === "released" ? "Agent completed (force-released after idle nudging)" : `Agent exited (${exitResult})`;
50318
+ output = (0, import_node_fs4.existsSync)(summaryPath) ? await (0, import_promises3.readFile)(summaryPath, "utf-8") : exitResult === "timeout" ? "Agent completed (released after idle timeout)" : exitResult === "released" ? "Agent completed (idle \u2014 treated as done)" : `Agent exited (${exitResult})`;
50076
50319
  }
50077
- return output;
50320
+ return {
50321
+ output,
50322
+ exitCode: agent?.exitCode,
50323
+ exitSignal: agent?.exitSignal
50324
+ };
50078
50325
  }
50079
50326
  // ── Idle nudging ────────────────────────────────────────────────────────
50080
50327
  /** Patterns where a hub agent coordinates spoke agents. */
@@ -50133,7 +50380,7 @@ DO NOT:
50133
50380
  if (exitResult !== "timeout") {
50134
50381
  return exitResult;
50135
50382
  }
50136
- if (remaining !== void 0 && Date.now() - startTime >= remaining) {
50383
+ if (timeoutMs !== void 0 && Date.now() - startTime >= timeoutMs) {
50137
50384
  return "timeout";
50138
50385
  }
50139
50386
  if (nudgeCount < maxNudges) {
@@ -50146,7 +50393,7 @@ DO NOT:
50146
50393
  this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` still idle after ${nudgeCount} nudge(s) \u2014 force-releasing`);
50147
50394
  this.emit({ type: "step:force-released", runId: this.currentRunId ?? "", stepName: step.name });
50148
50395
  await agent.release();
50149
- return "released";
50396
+ return "force-released";
50150
50397
  }
50151
50398
  }
50152
50399
  /**
@@ -50241,7 +50488,7 @@ DO NOT:
50241
50488
  }
50242
50489
  await this.db.updateRun(runId, patch);
50243
50490
  }
50244
- async markStepFailed(state, error95, runId) {
50491
+ async markStepFailed(state, error95, runId, exitInfo) {
50245
50492
  state.row.status = "failed";
50246
50493
  state.row.error = error95;
50247
50494
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -50251,7 +50498,14 @@ DO NOT:
50251
50498
  completedAt: state.row.completedAt,
50252
50499
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
50253
50500
  });
50254
- this.emit({ type: "step:failed", runId, stepName: state.row.stepName, error: error95 });
50501
+ this.emit({
50502
+ type: "step:failed",
50503
+ runId,
50504
+ stepName: state.row.stepName,
50505
+ error: error95,
50506
+ exitCode: exitInfo?.exitCode,
50507
+ exitSignal: exitInfo?.exitSignal
50508
+ });
50255
50509
  }
50256
50510
  async markDownstreamSkipped(failedStepName, allSteps, stepStates, runId) {
50257
50511
  const queue = [failedStepName];