agent-relay 3.1.19 → 3.1.21

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 (90) 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 +435 -190
  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/install.sh +9 -3
  24. package/package.json +13 -13
  25. package/packages/acp-bridge/package.json +2 -2
  26. package/packages/config/package.json +1 -1
  27. package/packages/hooks/package.json +4 -4
  28. package/packages/memory/package.json +2 -2
  29. package/packages/openclaw/dist/cli.js +79 -2
  30. package/packages/openclaw/dist/cli.js.map +1 -1
  31. package/packages/openclaw/dist/config.d.ts +28 -1
  32. package/packages/openclaw/dist/config.d.ts.map +1 -1
  33. package/packages/openclaw/dist/config.js +145 -0
  34. package/packages/openclaw/dist/config.js.map +1 -1
  35. package/packages/openclaw/dist/index.d.ts +2 -2
  36. package/packages/openclaw/dist/index.d.ts.map +1 -1
  37. package/packages/openclaw/dist/index.js +1 -1
  38. package/packages/openclaw/dist/index.js.map +1 -1
  39. package/packages/openclaw/dist/setup.d.ts.map +1 -1
  40. package/packages/openclaw/dist/setup.js +24 -1
  41. package/packages/openclaw/dist/setup.js.map +1 -1
  42. package/packages/openclaw/dist/types.d.ts +23 -0
  43. package/packages/openclaw/dist/types.d.ts.map +1 -1
  44. package/packages/openclaw/package.json +2 -2
  45. package/packages/openclaw/skill/SKILL.md +46 -0
  46. package/packages/openclaw/src/cli.ts +90 -2
  47. package/packages/openclaw/src/config.ts +165 -1
  48. package/packages/openclaw/src/index.ts +7 -1
  49. package/packages/openclaw/src/setup.ts +26 -1
  50. package/packages/openclaw/src/types.ts +25 -0
  51. package/packages/policy/package.json +2 -2
  52. package/packages/sdk/dist/__tests__/integration.test.js +35 -0
  53. package/packages/sdk/dist/__tests__/integration.test.js.map +1 -1
  54. package/packages/sdk/dist/client.d.ts +9 -0
  55. package/packages/sdk/dist/client.d.ts.map +1 -1
  56. package/packages/sdk/dist/client.js +33 -22
  57. package/packages/sdk/dist/client.js.map +1 -1
  58. package/packages/sdk/dist/protocol.d.ts +1 -0
  59. package/packages/sdk/dist/protocol.d.ts.map +1 -1
  60. package/packages/sdk/dist/relay.d.ts +8 -0
  61. package/packages/sdk/dist/relay.d.ts.map +1 -1
  62. package/packages/sdk/dist/relay.js +50 -5
  63. package/packages/sdk/dist/relay.js.map +1 -1
  64. package/packages/sdk/dist/workflows/cli.js +2 -0
  65. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  66. package/packages/sdk/dist/workflows/runner.d.ts +11 -0
  67. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  68. package/packages/sdk/dist/workflows/runner.js +350 -167
  69. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  70. package/packages/sdk/dist/workflows/trajectory.d.ts +6 -1
  71. package/packages/sdk/dist/workflows/trajectory.d.ts.map +1 -1
  72. package/packages/sdk/dist/workflows/trajectory.js +16 -2
  73. package/packages/sdk/dist/workflows/trajectory.js.map +1 -1
  74. package/packages/sdk/package.json +2 -2
  75. package/packages/sdk/src/__tests__/integration.test.ts +49 -0
  76. package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +50 -1
  77. package/packages/sdk/src/client.ts +44 -21
  78. package/packages/sdk/src/protocol.ts +1 -1
  79. package/packages/sdk/src/relay.ts +70 -5
  80. package/packages/sdk/src/workflows/cli.ts +2 -0
  81. package/packages/sdk/src/workflows/runner.ts +414 -185
  82. package/packages/sdk/src/workflows/trajectory.ts +22 -2
  83. package/packages/sdk-py/pyproject.toml +1 -1
  84. package/packages/sdk-py/src/agent_relay/client.py +18 -1
  85. package/packages/sdk-py/src/agent_relay/relay.py +4 -0
  86. package/packages/sdk-py/src/agent_relay/types.py +4 -0
  87. package/packages/telemetry/package.json +1 -1
  88. package/packages/trajectory/package.json +2 -2
  89. package/packages/user-directory/package.json +2 -2
  90. 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
  }
@@ -45196,7 +45209,7 @@ var AgentRelay = class {
45196
45209
  this.clientOptions = {
45197
45210
  binaryPath: options.binaryPath,
45198
45211
  binaryArgs: options.binaryArgs,
45199
- brokerName: options.brokerName,
45212
+ brokerName: options.brokerName ?? options.workspaceName,
45200
45213
  channels: this.defaultChannels,
45201
45214
  cwd: options.cwd,
45202
45215
  env: options.env,
@@ -45255,7 +45268,8 @@ var AgentRelay = class {
45255
45268
  shadowOf: input.shadowOf,
45256
45269
  shadowMode: input.shadowMode,
45257
45270
  idleThresholdSecs: input.idleThresholdSecs,
45258
- restartPolicy: input.restartPolicy
45271
+ restartPolicy: input.restartPolicy,
45272
+ skipRelayPrompt: input.skipRelayPrompt
45259
45273
  });
45260
45274
  } catch (error95) {
45261
45275
  await this.invokeLifecycleHook(input.onError, {
@@ -45288,6 +45302,7 @@ var AgentRelay = class {
45288
45302
  shadowMode: options?.shadowMode,
45289
45303
  idleThresholdSecs: options?.idleThresholdSecs,
45290
45304
  restartPolicy: options?.restartPolicy,
45305
+ skipRelayPrompt: options?.skipRelayPrompt,
45291
45306
  onStart: options?.onStart,
45292
45307
  onSuccess: options?.onSuccess,
45293
45308
  onError: options?.onError
@@ -45597,10 +45612,21 @@ var AgentRelay = class {
45597
45612
  this.unsubEvent();
45598
45613
  this.unsubEvent = void 0;
45599
45614
  }
45600
- if (this.client) {
45601
- await this.client.shutdown();
45602
- 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
+ }
45603
45628
  }
45629
+ this.startPromise = void 0;
45604
45630
  this.knownAgents.clear();
45605
45631
  this.readyAgents.clear();
45606
45632
  this.messageReadyAgents.clear();
@@ -45672,8 +45698,10 @@ var AgentRelay = class {
45672
45698
  * 4. Auto-create a fresh workspace via the Relaycast REST API
45673
45699
  */
45674
45700
  async ensureRelaycastApiKey() {
45675
- if (this.relayApiKey)
45701
+ if (this.relayApiKey) {
45702
+ this.wireRelaycastBaseUrl();
45676
45703
  return;
45704
+ }
45677
45705
  const envKey = this.clientOptions.env?.RELAY_API_KEY ?? process.env.RELAY_API_KEY;
45678
45706
  if (envKey) {
45679
45707
  this.relayApiKey = envKey;
@@ -45682,11 +45710,19 @@ var AgentRelay = class {
45682
45710
  } else if (!this.clientOptions.env.RELAY_API_KEY) {
45683
45711
  this.clientOptions.env.RELAY_API_KEY = envKey;
45684
45712
  }
45713
+ this.wireRelaycastBaseUrl();
45685
45714
  return;
45686
45715
  }
45687
45716
  if (!this.clientOptions.env) {
45688
45717
  this.clientOptions.env = { ...process.env };
45689
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
+ }
45690
45726
  }
45691
45727
  async ensureStarted() {
45692
45728
  if (this.client)
@@ -45849,12 +45885,31 @@ var AgentRelay = class {
45849
45885
  name,
45850
45886
  reason: releaseOptions.reason
45851
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
+ }
45852
45893
  const client = await relay.ensureStarted();
45853
45894
  await relay.invokeLifecycleHook(releaseOptions.onStart, releaseContext, `release("${name}") onStart`);
45854
45895
  try {
45855
45896
  await client.release(name, releaseOptions.reason);
45856
45897
  await relay.invokeLifecycleHook(releaseOptions.onSuccess, releaseContext, `release("${name}") onSuccess`);
45857
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
+ }
45858
45913
  await relay.invokeLifecycleHook(releaseOptions.onError, {
45859
45914
  ...releaseContext,
45860
45915
  error: error95
@@ -45996,6 +46051,7 @@ var AgentRelay = class {
45996
46051
  task,
45997
46052
  model: options?.model,
45998
46053
  cwd: options?.cwd,
46054
+ skipRelayPrompt: options?.skipRelayPrompt,
45999
46055
  onStart: options?.onStart,
46000
46056
  onSuccess: options?.onSuccess,
46001
46057
  onError: options?.onError
@@ -46017,7 +46073,8 @@ var AgentRelay = class {
46017
46073
  transport: "headless",
46018
46074
  args,
46019
46075
  channels,
46020
- task
46076
+ task,
46077
+ skipRelayPrompt: options?.skipRelayPrompt
46021
46078
  });
46022
46079
  } catch (error95) {
46023
46080
  await this.invokeLifecycleHook(options?.onError, {
@@ -47242,7 +47299,7 @@ var WorkflowTrajectory = class {
47242
47299
  id,
47243
47300
  version: 1,
47244
47301
  task: {
47245
- title: `${workflowName} run #${this.runId.slice(0, 8)}`,
47302
+ title: workflowName,
47246
47303
  source: { system: "workflow-runner", id: this.runId }
47247
47304
  },
47248
47305
  status: "active",
@@ -47296,6 +47353,8 @@ var WorkflowTrajectory = class {
47296
47353
  if (participants?.reviewer) {
47297
47354
  await this.registerAgent(participants.reviewer, "reviewer");
47298
47355
  }
47356
+ this.closeCurrentChapter();
47357
+ this.openChapter(`Execution: ${step.name}`, agent);
47299
47358
  const intent = step.task ? step.task.trim().split(/\n|\.(?=\s)/)[0].trim().slice(0, 120) : `${step.type ?? "deterministic"} step`;
47300
47359
  this.addEvent("note", `"${step.name}": ${intent}`, void 0, { agent });
47301
47360
  await this.flush();
@@ -47449,12 +47508,24 @@ var WorkflowTrajectory = class {
47449
47508
  await this.moveToCompleted();
47450
47509
  }
47451
47510
  /** Abandon the trajectory. */
47452
- async abandon(reason) {
47511
+ async abandon(reason, meta5) {
47453
47512
  if (!this.enabled || !this.trajectory)
47454
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");
47455
47519
  this.addEvent("error", `Workflow abandoned: ${reason}`, "high");
47456
47520
  this.trajectory.status = "abandoned";
47457
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
+ };
47458
47529
  this.closeCurrentChapter();
47459
47530
  await this.flush();
47460
47531
  await this.moveToCompleted();
@@ -47613,6 +47684,16 @@ var WorkflowTrajectory = class {
47613
47684
  };
47614
47685
 
47615
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
+ };
47616
47697
  var _resolvedCursorCli;
47617
47698
  function resolveCursorCli() {
47618
47699
  if (_resolvedCursorCli !== void 0)
@@ -47656,6 +47737,8 @@ var WorkflowRunner = class _WorkflowRunner {
47656
47737
  activeAgentHandles = /* @__PURE__ */ new Map();
47657
47738
  // PTY-based output capture: accumulate terminal output per-agent
47658
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();
47659
47742
  ptyListeners = /* @__PURE__ */ new Map();
47660
47743
  ptyLogStreams = /* @__PURE__ */ new Map();
47661
47744
  /** Path to workers.json so `agents:kill` can find workflow-spawned agents */
@@ -48383,7 +48466,10 @@ ${err.suggestion}`);
48383
48466
  // ── Execution ───────────────────────────────────────────────────────────
48384
48467
  /** Execute a named workflow from a validated config. */
48385
48468
  async execute(config3, workflowName, vars) {
48469
+ this.abortController = new AbortController();
48470
+ this.paused = false;
48386
48471
  const resolved = vars ? this.resolveVariables(config3, vars) : config3;
48472
+ this.validateConfig(resolved);
48387
48473
  const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
48388
48474
  if (pathResult.errors.length > 0) {
48389
48475
  throw new Error(`Path validation failed:
@@ -48446,6 +48532,8 @@ ${err.suggestion}`);
48446
48532
  }
48447
48533
  /** Resume a previously paused or partially completed run. */
48448
48534
  async resume(runId, vars) {
48535
+ this.abortController = new AbortController();
48536
+ this.paused = false;
48449
48537
  const run = await this.db.getRun(runId);
48450
48538
  if (!run) {
48451
48539
  throw new Error(`Run "${runId}" not found`);
@@ -48492,8 +48580,6 @@ ${err.suggestion}`);
48492
48580
  async runWorkflowCore(input) {
48493
48581
  const { run, workflow: workflow2, config: config3, stepStates, isResume } = input;
48494
48582
  const runId = run.id;
48495
- this.abortController = new AbortController();
48496
- this.paused = false;
48497
48583
  this.currentConfig = config3;
48498
48584
  this.currentRunId = runId;
48499
48585
  this.runStartTime = Date.now();
@@ -48517,14 +48603,18 @@ ${err.suggestion}`);
48517
48603
  config3.swarm.channel = channel;
48518
48604
  await this.db.updateRun(runId, { config: config3 });
48519
48605
  }
48520
- if (!this.executor) {
48521
- this.log("Resolving Relaycast API key...");
48522
- await this.ensureRelaycastApiKey(channel);
48523
- this.log("API key resolved");
48524
- if (this.relayApiKeyAutoCreated && this.relayApiKey) {
48525
- this.log(`Workspace created \u2014 follow this run in Relaycast:`);
48526
- this.log(` Observer: https://agentrelay.dev/observer?key=${this.relayApiKey}`);
48527
- 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
+ }
48528
48618
  }
48529
48619
  this.log("Starting broker...");
48530
48620
  const brokerBaseName = import_node_path8.default.basename(this.cwd) || "workflow";
@@ -48532,7 +48622,7 @@ ${err.suggestion}`);
48532
48622
  this.relay = new AgentRelay({
48533
48623
  ...this.relayOptions,
48534
48624
  brokerName,
48535
- channels: [channel],
48625
+ channels: relaycastDisabled ? [] : [channel],
48536
48626
  env: this.getRelayEnv(),
48537
48627
  // Workflows spawn agents across multiple waves; each spawn requires a PTY +
48538
48628
  // Relaycast registration. 60s is too tight when the broker is saturated with
@@ -48580,6 +48670,18 @@ ${err.suggestion}`);
48580
48670
  }
48581
48671
  };
48582
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
+ });
48583
48685
  const body = msg.text.length > 120 ? msg.text.slice(0, 117) + "..." : msg.text;
48584
48686
  const fromShort = msg.from.replace(/-[a-f0-9]{6,}$/, "");
48585
48687
  const toShort = msg.to.replace(/-[a-f0-9]{6,}$/, "");
@@ -48590,18 +48692,59 @@ ${err.suggestion}`);
48590
48692
  }
48591
48693
  };
48592
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
+ });
48593
48704
  if (!this.activeAgentHandles.has(agent.name)) {
48594
48705
  this.log(`[spawned] ${agent.name} (${agent.runtime})`);
48595
48706
  }
48596
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
+ };
48597
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
+ });
48598
48729
  this.lastActivity.delete(agent.name);
48599
48730
  this.lastIdleLog.delete(agent.name);
48600
48731
  if (!this.activeAgentHandles.has(agent.name)) {
48601
48732
  this.log(`[exited] ${agent.name} (code: ${agent.exitCode ?? "?"})`);
48602
48733
  }
48603
48734
  };
48735
+ this.relay.onDeliveryUpdate = (event) => {
48736
+ this.emit({ type: "broker:event", runId, event });
48737
+ };
48604
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
+ });
48605
48748
  const bucket = Math.floor(idleSecs / 30) * 30;
48606
48749
  if (bucket >= 30 && this.lastIdleLog.get(name) !== bucket) {
48607
48750
  this.lastIdleLog.set(name, bucket);
@@ -48614,17 +48757,19 @@ ${err.suggestion}`);
48614
48757
  this.unsubBrokerStderr = this.relay.onBrokerStderr((line) => {
48615
48758
  console.log(`[broker] ${line}`);
48616
48759
  });
48617
- this.log(`Creating channel: ${channel}...`);
48618
- if (isResume) {
48619
- await this.createAndJoinRelaycastChannel(channel);
48620
- } else {
48621
- await this.createAndJoinRelaycastChannel(channel, workflow2.description);
48622
- }
48623
- this.log("Channel ready");
48624
- if (isResume) {
48625
- this.postToChannel(`Workflow **${workflow2.name}** resumed \u2014 ${pendingCount} pending steps`);
48626
- } else {
48627
- 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
+ }
48628
48773
  }
48629
48774
  }
48630
48775
  const agentMap = /* @__PURE__ */ new Map();
@@ -48636,7 +48781,9 @@ ${err.suggestion}`);
48636
48781
  }
48637
48782
  this.log(`Executing ${workflow2.steps.length} steps (pattern: ${config3.swarm.pattern})`);
48638
48783
  await this.executeSteps(workflow2, stepStates, agentMap, config3.errorHandling, runId);
48639
- 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");
48640
48787
  if (allCompleted) {
48641
48788
  this.log("Workflow completed successfully");
48642
48789
  await this.updateRunStatus(runId, "completed");
@@ -48656,24 +48803,52 @@ ${err.suggestion}`);
48656
48803
  await this.updateRunStatus(runId, "failed", errorMsg);
48657
48804
  this.emit({ type: "run:failed", runId, error: errorMsg });
48658
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);
48659
48810
  this.postFailureReport(workflow2.name, outcomes, errorMsg);
48660
48811
  this.logRunSummary(workflow2.name, outcomes, runId);
48661
- await this.trajectory.abandon(errorMsg);
48812
+ await this.trajectory.abandon(errorMsg, {
48813
+ summary,
48814
+ confidence,
48815
+ learnings,
48816
+ challenges
48817
+ });
48662
48818
  }
48663
48819
  } catch (err) {
48664
48820
  const errorMsg = err instanceof Error ? err.message : String(err);
48665
48821
  const status = !isResume && this.abortController?.signal.aborted ? "cancelled" : "failed";
48666
48822
  await this.updateRunStatus(runId, status, errorMsg);
48667
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
+ }
48668
48836
  this.emit({ type: "run:cancelled", runId });
48669
48837
  this.postToChannel(`Workflow **${workflow2.name}** cancelled`);
48670
48838
  await this.trajectory.abandon("Cancelled by user");
48671
48839
  } else {
48672
48840
  this.emit({ type: "run:failed", runId, error: errorMsg });
48673
48841
  this.postToChannel(`Workflow failed: ${errorMsg}`);
48674
- 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
+ });
48675
48849
  }
48676
48850
  } finally {
48851
+ this.lastFailedStepOutput.clear();
48677
48852
  for (const stream of this.ptyLogStreams.values())
48678
48853
  stream.end();
48679
48854
  this.ptyLogStreams.clear();
@@ -48684,9 +48859,11 @@ ${err.suggestion}`);
48684
48859
  if (this.relay) {
48685
48860
  this.relay.onMessageReceived = null;
48686
48861
  this.relay.onAgentSpawned = null;
48862
+ this.relay.onAgentReleased = null;
48687
48863
  this.relay.onAgentExited = null;
48688
48864
  this.relay.onAgentIdle = null;
48689
48865
  this.relay.onWorkerOutput = null;
48866
+ this.relay.onDeliveryUpdate = null;
48690
48867
  }
48691
48868
  this.lastIdleLog.clear();
48692
48869
  this.lastActivity.clear();
@@ -48907,7 +49084,7 @@ ${trimmedOutput.slice(0, 200)}`);
48907
49084
  }
48908
49085
  async executeStep(step, stepStates, agentMap, errorHandling, runId) {
48909
49086
  if (this.isDeterministicStep(step)) {
48910
- return this.executeDeterministicStep(step, stepStates, runId);
49087
+ return this.executeDeterministicStep(step, stepStates, runId, errorHandling);
48911
49088
  }
48912
49089
  if (this.isWorktreeStep(step)) {
48913
49090
  return this.executeWorktreeStep(step, stepStates, runId);
@@ -48918,131 +49095,154 @@ ${trimmedOutput.slice(0, 200)}`);
48918
49095
  * Execute a deterministic step (shell command).
48919
49096
  * Fast, reliable, $0 LLM cost.
48920
49097
  */
48921
- async executeDeterministicStep(step, stepStates, runId) {
49098
+ async executeDeterministicStep(step, stepStates, runId, errorHandling) {
48922
49099
  const state = stepStates.get(step.name);
48923
49100
  if (!state)
48924
49101
  throw new Error(`Step state not found: ${step.name}`);
48925
- this.checkAborted();
48926
- state.row.status = "running";
48927
- state.row.startedAt = (/* @__PURE__ */ new Date()).toISOString();
48928
- await this.db.updateStep(state.row.id, {
48929
- status: "running",
48930
- startedAt: state.row.startedAt,
48931
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
48932
- });
48933
- this.emit({ type: "step:started", runId, stepName: step.name });
48934
- this.postToChannel(`**[${step.name}]** Started (deterministic)`);
48935
- const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
48936
- let resolvedCommand = this.interpolateStepTask(step.command ?? "", stepOutputContext);
48937
- resolvedCommand = resolvedCommand.replace(/\{\{([\w][\w.\-]*)\}\}/g, (_match, key) => {
48938
- if (key.startsWith("steps."))
48939
- return _match;
48940
- const value = this.resolveDotPath(key, stepOutputContext);
48941
- return value !== void 0 ? String(value) : _match;
48942
- });
48943
- const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
48944
- try {
48945
- if (this.executor?.executeDeterministicStep) {
48946
- const result = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
48947
- const failOnError = step.failOnError !== false;
48948
- if (failOnError && result.exitCode !== 0) {
48949
- throw new Error(`Command failed with exit code ${result.exitCode}: ${result.output.slice(0, 500)}`);
48950
- }
48951
- const output2 = step.captureOutput !== false ? result.output : `Command completed (exit code ${result.exitCode})`;
48952
- state.row.status = "completed";
48953
- state.row.output = output2;
48954
- 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;
48955
49111
  await this.db.updateStep(state.row.id, {
48956
- status: "completed",
48957
- output: output2,
48958
- completedAt: state.row.completedAt,
49112
+ retryCount: attempt,
48959
49113
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
48960
49114
  });
48961
- await this.persistStepOutput(runId, step.name, output2);
48962
- this.emit({ type: "step:completed", runId, stepName: step.name, output: output2 });
48963
- return;
49115
+ await this.delay(retryDelay);
48964
49116
  }
48965
- const output = await new Promise((resolve3, reject) => {
48966
- const child = (0, import_node_child_process3.spawn)("sh", ["-c", resolvedCommand], {
48967
- stdio: "pipe",
48968
- cwd: stepCwd,
48969
- env: { ...process.env }
48970
- });
48971
- const stdoutChunks = [];
48972
- const stderrChunks = [];
48973
- const abortSignal = this.abortController?.signal;
48974
- let abortHandler;
48975
- if (abortSignal && !abortSignal.aborted) {
48976
- abortHandler = () => {
48977
- child.kill("SIGTERM");
48978
- setTimeout(() => child.kill("SIGKILL"), 5e3);
48979
- };
48980
- abortSignal.addEventListener("abort", abortHandler, { once: true });
48981
- }
48982
- let timedOut = false;
48983
- let timer;
48984
- if (step.timeoutMs) {
48985
- timer = setTimeout(() => {
48986
- timedOut = true;
48987
- child.kill("SIGTERM");
48988
- setTimeout(() => child.kill("SIGKILL"), 5e3);
48989
- }, step.timeoutMs);
48990
- }
48991
- child.stdout?.on("data", (chunk) => {
48992
- stdoutChunks.push(chunk.toString());
48993
- });
48994
- child.stderr?.on("data", (chunk) => {
48995
- stderrChunks.push(chunk.toString());
48996
- });
48997
- child.on("close", (code) => {
48998
- if (timer)
48999
- clearTimeout(timer);
49000
- if (abortHandler && abortSignal) {
49001
- 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)}`);
49002
49141
  }
49003
- if (abortSignal?.aborted) {
49004
- reject(new Error(`Step "${step.name}" aborted`));
49005
- 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);
49006
49145
  }
49007
- if (timedOut) {
49008
- reject(new Error(`Step "${step.name}" timed out (no step timeout set, check global swarm.timeoutMs)`));
49009
- 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 });
49010
49175
  }
49011
- const stdout = stdoutChunks.join("");
49012
- const stderr = stderrChunks.join("");
49013
- const failOnError = step.failOnError !== false;
49014
- if (failOnError && code !== 0 && code !== null) {
49015
- reject(new Error(`Command failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
49016
- 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);
49017
49184
  }
49018
- 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
+ });
49019
49222
  });
49020
- child.on("error", (err) => {
49021
- if (timer)
49022
- clearTimeout(timer);
49023
- if (abortHandler && abortSignal) {
49024
- abortSignal.removeEventListener("abort", abortHandler);
49025
- }
49026
- 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()
49027
49234
  });
49028
- });
49029
- state.row.status = "completed";
49030
- state.row.output = output;
49031
- state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
49032
- await this.db.updateStep(state.row.id, {
49033
- status: "completed",
49034
- output,
49035
- completedAt: state.row.completedAt,
49036
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49037
- });
49038
- await this.persistStepOutput(runId, step.name, output);
49039
- this.emit({ type: "step:completed", runId, stepName: step.name, output });
49040
- } catch (err) {
49041
- const errorMsg = err instanceof Error ? err.message : String(err);
49042
- this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
49043
- await this.markStepFailed(state, errorMsg, runId);
49044
- 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
+ }
49045
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}`);
49046
49246
  }
49047
49247
  /**
49048
49248
  * Execute a worktree step (git worktree setup).
@@ -49211,8 +49411,12 @@ ${trimmedOutput.slice(0, 200)}`);
49211
49411
  const retryDelay = errorHandling?.retryDelayMs ?? 1e3;
49212
49412
  const timeoutMs = step.timeoutMs ?? ownerDef.constraints?.timeoutMs ?? specialistDef.constraints?.timeoutMs ?? this.currentConfig?.swarm?.timeoutMs;
49213
49413
  let lastError;
49414
+ let lastExitCode;
49415
+ let lastExitSignal;
49214
49416
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
49215
49417
  this.checkAborted();
49418
+ lastExitCode = void 0;
49419
+ lastExitSignal = void 0;
49216
49420
  if (attempt > 0) {
49217
49421
  this.emit({ type: "step:retrying", runId, stepName: step.name, attempt });
49218
49422
  this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${maxRetries + 1})`);
@@ -49252,6 +49456,15 @@ ${trimmedOutput.slice(0, 200)}`);
49252
49456
  });
49253
49457
  const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
49254
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
+ }
49255
49468
  if (specialistDef.interactive !== false || ownerDef.interactive !== false) {
49256
49469
  const nonInteractiveInfo = this.buildNonInteractiveAwareness(agentMap, stepStates);
49257
49470
  if (nonInteractiveInfo) {
@@ -49282,7 +49495,10 @@ ${trimmedOutput.slice(0, 200)}`);
49282
49495
  this.log(`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ""}`);
49283
49496
  const resolvedStep = { ...step, task: ownerTask };
49284
49497
  const ownerStartTime = Date.now();
49285
- 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;
49286
49502
  ownerElapsed = Date.now() - ownerStartTime;
49287
49503
  this.log(`[${step.name}] Owner "${effectiveOwner.name}" exited`);
49288
49504
  if (usesOwnerFlow) {
@@ -49292,7 +49508,7 @@ ${trimmedOutput.slice(0, 200)}`);
49292
49508
  ownerOutput = output;
49293
49509
  }
49294
49510
  if (step.verification) {
49295
- this.runVerification(step.verification, specialistOutput, step.name, resolvedTask);
49511
+ this.runVerification(step.verification, specialistOutput, step.name, effectiveOwner.interactive === false ? void 0 : resolvedTask);
49296
49512
  }
49297
49513
  let combinedOutput = specialistOutput;
49298
49514
  if (usesOwnerFlow && reviewDef) {
@@ -49310,11 +49526,15 @@ ${trimmedOutput.slice(0, 200)}`);
49310
49526
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
49311
49527
  });
49312
49528
  await this.persistStepOutput(runId, step.name, combinedOutput);
49313
- 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 });
49314
49530
  await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
49315
49531
  return;
49316
49532
  } catch (err) {
49317
49533
  lastError = err instanceof Error ? err.message : String(err);
49534
+ if (err instanceof SpawnExitError) {
49535
+ lastExitCode = err.exitCode;
49536
+ lastExitSignal = err.exitSignal;
49537
+ }
49318
49538
  const ownerTimedOut = usesDedicatedOwner ? /\bowner timed out\b/i.test(lastError) : /\btimed out\b/i.test(lastError) && !lastError.includes(`${step.name}-review`);
49319
49539
  if (ownerTimedOut) {
49320
49540
  this.emit({ type: "step:owner-timeout", runId, stepName: step.name, ownerName: ownerDef.name });
@@ -49329,7 +49549,10 @@ ${trimmedOutput.slice(0, 200)}`);
49329
49549
  verificationValue
49330
49550
  });
49331
49551
  this.postToChannel(`**[${step.name}]** Failed: ${lastError ?? "Unknown error"}`);
49332
- await this.markStepFailed(state, lastError ?? "Unknown error", runId);
49552
+ await this.markStepFailed(state, lastError ?? "Unknown error", runId, {
49553
+ exitCode: lastExitCode,
49554
+ exitSignal: lastExitSignal
49555
+ });
49333
49556
  throw new Error(`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? "Unknown error"}`);
49334
49557
  }
49335
49558
  injectStepOwnerContract(step, resolvedTask, ownerDef, specialistDef) {
@@ -49447,10 +49670,10 @@ Output exactly: STEP_COMPLETE:${step.name}`;
49447
49670
  throw error95;
49448
49671
  });
49449
49672
  const workerSettled = workerPromise.catch(() => void 0);
49450
- workerPromise.then((output) => {
49673
+ workerPromise.then((result) => {
49451
49674
  workerReleased = true;
49452
49675
  this.postToChannel(`**[${step.name}]** Worker \`${workerRuntimeName}\` exited`);
49453
- if (step.verification?.type === "output_contains" && output.includes(step.verification.value)) {
49676
+ if (step.verification?.type === "output_contains" && result.output.includes(step.verification.value)) {
49454
49677
  this.postToChannel(`**[${step.name}]** Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`);
49455
49678
  }
49456
49679
  }).catch((error95) => {
@@ -49468,7 +49691,7 @@ Output exactly: STEP_COMPLETE:${step.name}`;
49468
49691
  this.log(`[${step.name}] Spawning owner "${supervised.owner.name}" (cli: ${supervised.owner.cli})`);
49469
49692
  const ownerStartTime = Date.now();
49470
49693
  try {
49471
- const ownerOutput = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
49694
+ const ownerResultObj = await this.spawnAndWait(supervised.owner, ownerStep, timeoutMs, {
49472
49695
  agentNameSuffix: "owner",
49473
49696
  onSpawned: ({ actualName }) => {
49474
49697
  this.supervisedRuntimeAgents.set(actualName, {
@@ -49482,9 +49705,10 @@ Output exactly: STEP_COMPLETE:${step.name}`;
49482
49705
  }
49483
49706
  });
49484
49707
  const ownerElapsed = Date.now() - ownerStartTime;
49708
+ const ownerOutput = ownerResultObj.output;
49485
49709
  this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
49486
49710
  this.assertOwnerCompletionMarker(step, ownerOutput, supervisorTask);
49487
- const specialistOutput = await workerPromise;
49711
+ const specialistOutput = (await workerPromise).output;
49488
49712
  return { specialistOutput, ownerOutput, ownerElapsed };
49489
49713
  } catch (error95) {
49490
49714
  const message = error95 instanceof Error ? error95.message : String(error95);
@@ -49682,7 +49906,7 @@ Then output /exit.`;
49682
49906
  })();
49683
49907
  };
49684
49908
  try {
49685
- reviewOutput = await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
49909
+ await this.spawnAndWait(reviewerDef, reviewStep, safetyTimeoutMs, {
49686
49910
  onSpawned: ({ agent }) => {
49687
49911
  reviewerHandle = agent;
49688
49912
  },
@@ -49862,7 +50086,7 @@ DO NOT:
49862
50086
  const stdoutChunks = [];
49863
50087
  const stderrChunks = [];
49864
50088
  try {
49865
- const output = await new Promise((resolve3, reject) => {
50089
+ const { stdout: output, exitCode, exitSignal } = await new Promise((resolve3, reject) => {
49866
50090
  const child = (0, import_node_child_process3.spawn)(cmd, args, {
49867
50091
  stdio: ["ignore", "pipe", "pipe"],
49868
50092
  cwd: this.resolveAgentCwd(agentDef),
@@ -49908,7 +50132,7 @@ DO NOT:
49908
50132
  setTimeout(() => child.kill("SIGKILL"), 5e3);
49909
50133
  }, timeoutMs);
49910
50134
  }
49911
- child.on("close", (code) => {
50135
+ child.on("close", (code, signal) => {
49912
50136
  clearInterval(heartbeat);
49913
50137
  if (timer)
49914
50138
  clearTimeout(timer);
@@ -49926,10 +50150,14 @@ DO NOT:
49926
50150
  }
49927
50151
  if (code !== 0 && code !== null) {
49928
50152
  const stderr = stderrChunks.join("");
49929
- 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));
49930
50154
  return;
49931
50155
  }
49932
- resolve3(stdout);
50156
+ resolve3({
50157
+ stdout,
50158
+ exitCode: code ?? void 0,
50159
+ exitSignal: signal ?? void 0
50160
+ });
49933
50161
  });
49934
50162
  child.on("error", (err) => {
49935
50163
  clearInterval(heartbeat);
@@ -49941,8 +50169,10 @@ DO NOT:
49941
50169
  reject(new Error(`Failed to spawn ${cmd}: ${err.message}`));
49942
50170
  });
49943
50171
  });
49944
- return output;
50172
+ return { output, exitCode, exitSignal };
49945
50173
  } finally {
50174
+ const combinedOutput = stdoutChunks.join("") + stderrChunks.join("");
50175
+ this.lastFailedStepOutput.set(step.name, combinedOutput);
49946
50176
  stopHeartbeat?.();
49947
50177
  logStream.end();
49948
50178
  this.unregisterWorker(agentName);
@@ -50062,8 +50292,12 @@ DO NOT:
50062
50292
  throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? "unknown"}ms`);
50063
50293
  }
50064
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
+ }
50065
50298
  } finally {
50066
50299
  ptyChunks = this.ptyOutputBuffers.get(agentName) ?? [];
50300
+ this.lastFailedStepOutput.set(step.name, ptyChunks.join(""));
50067
50301
  stopHeartbeat?.();
50068
50302
  this.activeAgentHandles.delete(agentName);
50069
50303
  this.ptyOutputBuffers.delete(agentName);
@@ -50081,9 +50315,13 @@ DO NOT:
50081
50315
  output = ptyChunks.join("");
50082
50316
  } else {
50083
50317
  const summaryPath = import_node_path8.default.join(this.summaryDir, `${step.name}.md`);
50084
- 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})`;
50085
50319
  }
50086
- return output;
50320
+ return {
50321
+ output,
50322
+ exitCode: agent?.exitCode,
50323
+ exitSignal: agent?.exitSignal
50324
+ };
50087
50325
  }
50088
50326
  // ── Idle nudging ────────────────────────────────────────────────────────
50089
50327
  /** Patterns where a hub agent coordinates spoke agents. */
@@ -50142,7 +50380,7 @@ DO NOT:
50142
50380
  if (exitResult !== "timeout") {
50143
50381
  return exitResult;
50144
50382
  }
50145
- if (remaining !== void 0 && Date.now() - startTime >= remaining) {
50383
+ if (timeoutMs !== void 0 && Date.now() - startTime >= timeoutMs) {
50146
50384
  return "timeout";
50147
50385
  }
50148
50386
  if (nudgeCount < maxNudges) {
@@ -50155,7 +50393,7 @@ DO NOT:
50155
50393
  this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` still idle after ${nudgeCount} nudge(s) \u2014 force-releasing`);
50156
50394
  this.emit({ type: "step:force-released", runId: this.currentRunId ?? "", stepName: step.name });
50157
50395
  await agent.release();
50158
- return "released";
50396
+ return "force-released";
50159
50397
  }
50160
50398
  }
50161
50399
  /**
@@ -50250,7 +50488,7 @@ DO NOT:
50250
50488
  }
50251
50489
  await this.db.updateRun(runId, patch);
50252
50490
  }
50253
- async markStepFailed(state, error95, runId) {
50491
+ async markStepFailed(state, error95, runId, exitInfo) {
50254
50492
  state.row.status = "failed";
50255
50493
  state.row.error = error95;
50256
50494
  state.row.completedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -50260,7 +50498,14 @@ DO NOT:
50260
50498
  completedAt: state.row.completedAt,
50261
50499
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
50262
50500
  });
50263
- 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
+ });
50264
50509
  }
50265
50510
  async markDownstreamSkipped(failedStepName, allSteps, stepStates, runId) {
50266
50511
  const queue = [failedStepName];