agent-relay 3.2.17 → 3.2.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 (67) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +86 -43
  6. package/dist/src/cli/commands/cloud.d.ts +1 -9
  7. package/dist/src/cli/commands/cloud.d.ts.map +1 -1
  8. package/dist/src/cli/commands/cloud.js +326 -323
  9. package/dist/src/cli/commands/cloud.js.map +1 -1
  10. package/dist/src/cli/commands/connect.d.ts.map +1 -1
  11. package/dist/src/cli/commands/connect.js +6 -10
  12. package/dist/src/cli/commands/connect.js.map +1 -1
  13. package/package.json +16 -10
  14. package/packages/acp-bridge/package.json +2 -2
  15. package/packages/brand/README.md +36 -0
  16. package/packages/brand/brand.css +226 -0
  17. package/packages/brand/package.json +20 -0
  18. package/packages/cloud/dist/api-client.d.ts +33 -0
  19. package/packages/cloud/dist/api-client.d.ts.map +1 -0
  20. package/packages/cloud/dist/api-client.js +123 -0
  21. package/packages/cloud/dist/api-client.js.map +1 -0
  22. package/packages/cloud/dist/auth.d.ts +13 -0
  23. package/packages/cloud/dist/auth.d.ts.map +1 -0
  24. package/packages/cloud/dist/auth.js +248 -0
  25. package/packages/cloud/dist/auth.js.map +1 -0
  26. package/packages/cloud/dist/index.d.ts +5 -0
  27. package/packages/cloud/dist/index.d.ts.map +1 -0
  28. package/packages/cloud/dist/index.js +5 -0
  29. package/packages/cloud/dist/index.js.map +1 -0
  30. package/packages/cloud/dist/types.d.ts +73 -0
  31. package/packages/cloud/dist/types.d.ts.map +1 -0
  32. package/packages/cloud/dist/types.js +19 -0
  33. package/packages/cloud/dist/types.js.map +1 -0
  34. package/packages/cloud/dist/workflows.d.ts +34 -0
  35. package/packages/cloud/dist/workflows.d.ts.map +1 -0
  36. package/packages/cloud/dist/workflows.js +389 -0
  37. package/packages/cloud/dist/workflows.js.map +1 -0
  38. package/packages/cloud/package.json +44 -0
  39. package/packages/cloud/src/api-client.ts +169 -0
  40. package/packages/cloud/src/auth.ts +314 -0
  41. package/packages/cloud/src/index.ts +41 -0
  42. package/packages/cloud/src/types.ts +97 -0
  43. package/packages/cloud/src/workflows.ts +539 -0
  44. package/packages/cloud/tsconfig.json +21 -0
  45. package/packages/config/package.json +1 -1
  46. package/packages/hooks/package.json +4 -4
  47. package/packages/memory/package.json +2 -2
  48. package/packages/openclaw/package.json +2 -2
  49. package/packages/policy/package.json +2 -2
  50. package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.d.ts +2 -0
  51. package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.d.ts.map +1 -0
  52. package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.js +62 -0
  53. package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.js.map +1 -0
  54. package/packages/sdk/dist/workflows/runner.d.ts +4 -0
  55. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  56. package/packages/sdk/dist/workflows/runner.js +76 -39
  57. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  58. package/packages/sdk/package.json +2 -2
  59. package/packages/sdk/src/__tests__/workflow-runner.test.ts +73 -2
  60. package/packages/sdk/src/workflows/__tests__/e2big-and-verify.test.ts +117 -0
  61. package/packages/sdk/src/workflows/runner.ts +105 -38
  62. package/packages/sdk-py/pyproject.toml +1 -1
  63. package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserver.swift +2 -0
  64. package/packages/telemetry/package.json +1 -1
  65. package/packages/trajectory/package.json +2 -2
  66. package/packages/user-directory/package.json +2 -2
  67. package/packages/utils/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/sdk",
3
- "version": "3.2.17",
3
+ "version": "3.2.21",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -112,7 +112,7 @@
112
112
  "typescript": "^5.7.3"
113
113
  },
114
114
  "dependencies": {
115
- "@agent-relay/config": "3.2.17",
115
+ "@agent-relay/config": "3.2.21",
116
116
  "@relaycast/sdk": "^1.1.0",
117
117
  "@sinclair/typebox": "^0.34.48",
118
118
  "chalk": "^4.1.2",
@@ -642,8 +642,7 @@ agents:
642
642
  );
643
643
 
644
644
  expect(ownerAssignments).toContainEqual({ owner: 'relay-worker', specialist: 'relay-worker' });
645
- expect(run.status).toBe('failed');
646
- expect(run.error).toContain('verification failed');
645
+ expect(run.status, run.error).toBe('completed');
647
646
 
648
647
  const spawnCalls = (mockRelayInstance.spawnPty as any).mock.calls;
649
648
  expect(spawnCalls).toHaveLength(1);
@@ -652,6 +651,78 @@ agents:
652
651
  expect(spawnCalls[0][0].name).not.toContain('-review-');
653
652
  });
654
653
 
654
+ it('should spill oversized interactive tasks to a temp file before PTY spawn', async () => {
655
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'relay-pty-task-'));
656
+ const oversizedBytes = WorkflowRunner.PTY_TASK_ARG_SIZE_LIMIT + 1024;
657
+ let spawnedTask = '';
658
+ let taskFilePath = '';
659
+ let taskFileContents = '';
660
+ runner = new WorkflowRunner({ db, workspaceId: 'ws-test', cwd: tmpDir });
661
+
662
+ mockRelayInstance.spawnPty.mockImplementation(
663
+ async ({ name, task }: { name: string; task?: string }) => {
664
+ spawnedTask = task ?? '';
665
+ const match = spawnedTask.match(/TASK_FILE:(.+)\n/);
666
+ if (match) {
667
+ taskFilePath = match[1].trim();
668
+ taskFileContents = readFileSync(taskFilePath, 'utf-8');
669
+ }
670
+
671
+ const output = mockSpawnOutputs.shift() ?? 'LEAD_DONE\n';
672
+ queueMicrotask(() => {
673
+ if (typeof mockRelayInstance.onWorkerOutput === 'function') {
674
+ mockRelayInstance.onWorkerOutput({ name, chunk: output });
675
+ }
676
+ });
677
+
678
+ return { ...mockAgent, name };
679
+ }
680
+ );
681
+
682
+ try {
683
+ mockSpawnOutputs = ['LEAD_DONE\n'];
684
+
685
+ const run = await runner.execute(
686
+ makeConfig({
687
+ agents: [{ name: 'team-lead', cli: 'claude', role: 'Lead coordinator' }],
688
+ workflows: [
689
+ {
690
+ name: 'default',
691
+ steps: [
692
+ {
693
+ name: 'prepare',
694
+ type: 'deterministic',
695
+ command: `node -e "process.stdout.write('A'.repeat(${oversizedBytes}))"`,
696
+ },
697
+ {
698
+ name: 'lead-step',
699
+ agent: 'team-lead',
700
+ dependsOn: ['prepare'],
701
+ task: 'Review the injected context below and then print LEAD_DONE:\n{{steps.prepare.output}}\n/exit',
702
+ verification: { type: 'exit_code', value: 0 },
703
+ },
704
+ ],
705
+ },
706
+ ],
707
+ }),
708
+ 'default'
709
+ );
710
+
711
+ expect(run.status, run.error).toBe('completed');
712
+ expect(spawnedTask).toContain('TASK_FILE:');
713
+ expect(spawnedTask).not.toContain('{{steps.prepare.output}}');
714
+ expect(Buffer.byteLength(spawnedTask, 'utf8')).toBeLessThan(2048);
715
+ expect(taskFilePath).toBeTruthy();
716
+ expect(Buffer.byteLength(taskFileContents, 'utf8')).toBeGreaterThan(
717
+ WorkflowRunner.PTY_TASK_ARG_SIZE_LIMIT
718
+ );
719
+ expect(taskFileContents).toContain('Review the injected context below');
720
+ expect(existsSync(taskFilePath)).toBe(false);
721
+ } finally {
722
+ rmSync(tmpDir, { recursive: true, force: true });
723
+ }
724
+ });
725
+
655
726
  it('should pass canonical bypass args to interactive codex PTY spawns', async () => {
656
727
  mockSpawnOutputs = [
657
728
  'LEAD_DONE\n',
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('@relaycast/sdk', () => ({
4
+ RelayCast: vi.fn(),
5
+ RelayError: class RelayError extends Error {},
6
+ }));
7
+
8
+ vi.mock('../../relay.js', () => ({
9
+ AgentRelay: vi.fn(),
10
+ }));
11
+
12
+ const { WorkflowRunner } = await import('../runner.js');
13
+
14
+ describe('runVerification output_contains (token double-count fix)', () => {
15
+ function createRunner(): InstanceType<typeof WorkflowRunner> {
16
+ return new WorkflowRunner({ cwd: '/tmp/test' });
17
+ }
18
+
19
+ function runVerification(
20
+ runner: InstanceType<typeof WorkflowRunner>,
21
+ check: { type: 'output_contains'; value: string },
22
+ output: string,
23
+ stepName: string,
24
+ injectedTaskText?: string
25
+ ) {
26
+ return (runner as any).runVerification(check, output, stepName, injectedTaskText, {
27
+ allowFailure: true,
28
+ });
29
+ }
30
+
31
+ it('passes when token is in output and not in task injection', () => {
32
+ const runner = createRunner();
33
+ const result = runVerification(
34
+ runner,
35
+ { type: 'output_contains', value: 'DONE' },
36
+ 'Task completed. DONE',
37
+ 'step1'
38
+ );
39
+ expect(result.passed).toBe(true);
40
+ });
41
+
42
+ it('fails when token is missing from output entirely', () => {
43
+ const runner = createRunner();
44
+ const result = runVerification(
45
+ runner,
46
+ { type: 'output_contains', value: 'DONE' },
47
+ 'Task completed without the marker',
48
+ 'step1'
49
+ );
50
+ expect(result.passed).toBe(false);
51
+ expect(result.error).toContain('does not contain "DONE"');
52
+ });
53
+
54
+ it('passes when token is in both task injection and agent output', () => {
55
+ const runner = createRunner();
56
+ const result = runVerification(
57
+ runner,
58
+ { type: 'output_contains', value: 'REFLECTION_COMPLETE' },
59
+ 'Your task: output REFLECTION_COMPLETE when done\n\nI have finished. REFLECTION_COMPLETE',
60
+ 'step1',
61
+ 'Your task: output REFLECTION_COMPLETE when done'
62
+ );
63
+ expect(result.passed).toBe(true);
64
+ });
65
+
66
+ it('fails when token appears only in task injection (not produced by agent)', () => {
67
+ const runner = createRunner();
68
+ const result = runVerification(
69
+ runner,
70
+ { type: 'output_contains', value: 'REFLECTION_COMPLETE' },
71
+ 'Your task: output REFLECTION_COMPLETE when done\n\nI worked on it but forgot the marker.',
72
+ 'step1',
73
+ 'Your task: output REFLECTION_COMPLETE when done'
74
+ );
75
+ expect(result.passed).toBe(false);
76
+ expect(result.error).toContain('does not contain "REFLECTION_COMPLETE"');
77
+ });
78
+
79
+ it('handles token appearing multiple times in task injection', () => {
80
+ const runner = createRunner();
81
+ const taskText = 'Output DONE when done. Remember: DONE is required.';
82
+ const output = taskText + '\n\nAll work complete. DONE';
83
+ const result = runVerification(
84
+ runner,
85
+ { type: 'output_contains', value: 'DONE' },
86
+ output,
87
+ 'step1',
88
+ taskText
89
+ );
90
+ expect(result.passed).toBe(true);
91
+ });
92
+
93
+ it('fails when token appears same number of times as in task injection', () => {
94
+ const runner = createRunner();
95
+ const taskText = 'Output DONE when done. Remember: DONE is required.';
96
+ const output = taskText + '\n\nAll work complete but no marker here.';
97
+ const result = runVerification(
98
+ runner,
99
+ { type: 'output_contains', value: 'DONE' },
100
+ output,
101
+ 'step1',
102
+ taskText
103
+ );
104
+ expect(result.passed).toBe(false);
105
+ });
106
+
107
+ it('handles empty token gracefully', () => {
108
+ const runner = createRunner();
109
+ const result = runVerification(
110
+ runner,
111
+ { type: 'output_contains', value: '' },
112
+ 'some output',
113
+ 'step1'
114
+ );
115
+ expect(result.passed).toBe(false);
116
+ });
117
+ });
@@ -9,6 +9,7 @@ import { randomBytes } from 'node:crypto';
9
9
  import {
10
10
  createWriteStream,
11
11
  existsSync,
12
+ mkdtempSync,
12
13
  mkdirSync,
13
14
  readFileSync,
14
15
  readdirSync,
@@ -17,7 +18,8 @@ import {
17
18
  writeFileSync,
18
19
  } from 'node:fs';
19
20
  import type { Dirent, WriteStream } from 'node:fs';
20
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
21
+ import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
22
+ import { tmpdir } from 'node:os';
21
23
  import path from 'node:path';
22
24
  import chalk from 'chalk';
23
25
 
@@ -97,6 +99,7 @@ interface SpawnResult {
97
99
  output: string;
98
100
  exitCode?: number;
99
101
  exitSignal?: string;
102
+ promptTaskText?: string;
100
103
  }
101
104
 
102
105
  /** Error carrying exit code/signal from a failed subprocess spawn. */
@@ -364,6 +367,7 @@ export class WorkflowRunner {
364
367
  private readonly activeReviewers = new Map<string, number>();
365
368
  /** Structured CLI session reports captured during the current run, keyed by step name. */
366
369
  private readonly agentReports = new Map<string, CliSessionReport>();
370
+ private static readonly PTY_TASK_ARG_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB
367
371
 
368
372
  constructor(options: WorkflowRunnerOptions = {}) {
369
373
  this.db = options.db ?? new InMemoryWorkflowDb();
@@ -3539,6 +3543,7 @@ export class WorkflowRunner {
3539
3543
  let ownerOutput: string;
3540
3544
  let ownerElapsed: number;
3541
3545
  let completionReason: WorkflowStepCompletionReason | undefined;
3546
+ let promptTaskText: string | undefined;
3542
3547
 
3543
3548
  if (usesDedicatedOwner) {
3544
3549
  const result = await this.executeSupervisedAgentStep(
@@ -3592,6 +3597,12 @@ export class WorkflowRunner {
3592
3597
  : undefined,
3593
3598
  });
3594
3599
  const output = typeof spawnResult === 'string' ? spawnResult : spawnResult.output;
3600
+ promptTaskText =
3601
+ typeof spawnResult === 'string'
3602
+ ? effectiveOwner.interactive === false
3603
+ ? undefined
3604
+ : ownerTask
3605
+ : spawnResult.promptTaskText ?? ownerTask;
3595
3606
  lastExitCode = typeof spawnResult === 'string' ? undefined : spawnResult.exitCode;
3596
3607
  lastExitSignal = typeof spawnResult === 'string' ? undefined : spawnResult.exitSignal;
3597
3608
  ownerElapsed = Date.now() - ownerStartTime;
@@ -3602,8 +3613,8 @@ export class WorkflowRunner {
3602
3613
  step,
3603
3614
  output,
3604
3615
  output,
3605
- ownerTask,
3606
- resolvedTask
3616
+ promptTaskText ?? ownerTask,
3617
+ promptTaskText ?? ownerTask
3607
3618
  );
3608
3619
  completionReason = completionDecision.completionReason;
3609
3620
  } catch (error) {
@@ -3654,7 +3665,7 @@ export class WorkflowRunner {
3654
3665
  step.verification,
3655
3666
  specialistOutput,
3656
3667
  step.name,
3657
- effectiveOwner.interactive === false ? undefined : resolvedTask
3668
+ promptTaskText
3658
3669
  );
3659
3670
  completionReason = verificationResult.completionReason;
3660
3671
  }
@@ -4028,7 +4039,14 @@ export class WorkflowRunner {
4028
4039
  detail: `Worker ${workerRuntimeName} exited`,
4029
4040
  raw: { worker: workerRuntimeName, exitCode: result.exitCode, exitSignal: result.exitSignal },
4030
4041
  });
4031
- if (step.verification?.type === 'output_contains' && result.output.includes(step.verification.value)) {
4042
+ if (
4043
+ step.verification?.type === 'output_contains' &&
4044
+ this.outputContainsVerificationToken(
4045
+ result.output,
4046
+ step.verification.value,
4047
+ result.promptTaskText
4048
+ )
4049
+ ) {
4032
4050
  this.log(
4033
4051
  `[${step.name}] Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`
4034
4052
  );
@@ -4079,13 +4097,14 @@ export class WorkflowRunner {
4079
4097
  const ownerElapsed = Date.now() - ownerStartTime;
4080
4098
  const ownerOutput = ownerResultObj.output;
4081
4099
  this.log(`[${step.name}] Owner "${supervised.owner.name}" exited`);
4082
- const specialistOutput = (await workerPromise).output;
4100
+ const workerResultObj = await workerPromise;
4101
+ const specialistOutput = workerResultObj.output;
4083
4102
  const completionDecision = this.resolveOwnerCompletionDecision(
4084
4103
  step,
4085
4104
  ownerOutput,
4086
4105
  specialistOutput,
4087
- supervisorTask,
4088
- resolvedTask
4106
+ ownerResultObj.promptTaskText ?? supervisorTask,
4107
+ workerResultObj.promptTaskText ?? specialistTask
4089
4108
  );
4090
4109
  return {
4091
4110
  specialistOutput,
@@ -4359,6 +4378,10 @@ export class WorkflowRunner {
4359
4378
  injectedTaskText: string
4360
4379
  ): boolean {
4361
4380
  const marker = `STEP_COMPLETE:${step.name}`;
4381
+ const strippedOutput = this.stripInjectedTaskEcho(output, injectedTaskText);
4382
+ if (strippedOutput.includes(marker)) {
4383
+ return true;
4384
+ }
4362
4385
  const taskHasMarker = injectedTaskText.includes(marker);
4363
4386
  const first = output.indexOf(marker);
4364
4387
  if (first === -1) {
@@ -4448,6 +4471,65 @@ export class WorkflowRunner {
4448
4471
  .join('\n');
4449
4472
  }
4450
4473
 
4474
+ private stripInjectedTaskEcho(output: string, injectedTaskText?: string): string {
4475
+ if (!injectedTaskText) {
4476
+ return output;
4477
+ }
4478
+
4479
+ const candidates = [
4480
+ injectedTaskText,
4481
+ injectedTaskText.replace(/\r\n/g, '\n'),
4482
+ injectedTaskText.replace(/\n/g, '\r\n'),
4483
+ ].filter((candidate, index, all) => candidate.length > 0 && all.indexOf(candidate) === index);
4484
+
4485
+ for (const candidate of candidates) {
4486
+ const start = output.indexOf(candidate);
4487
+ if (start !== -1) {
4488
+ return output.slice(0, start) + output.slice(start + candidate.length);
4489
+ }
4490
+ }
4491
+
4492
+ return output;
4493
+ }
4494
+
4495
+ private outputContainsVerificationToken(
4496
+ output: string,
4497
+ token: string,
4498
+ injectedTaskText?: string
4499
+ ): boolean {
4500
+ if (!token) {
4501
+ return false;
4502
+ }
4503
+ return this.stripInjectedTaskEcho(output, injectedTaskText).includes(token);
4504
+ }
4505
+
4506
+ private prepareInteractiveSpawnTask(
4507
+ agentName: string,
4508
+ taskText: string
4509
+ ): { spawnTaskText: string; promptTaskText: string; taskTmpFile?: string } {
4510
+ if (Buffer.byteLength(taskText, 'utf8') <= WorkflowRunner.PTY_TASK_ARG_SIZE_LIMIT) {
4511
+ return {
4512
+ spawnTaskText: taskText,
4513
+ promptTaskText: taskText,
4514
+ };
4515
+ }
4516
+
4517
+ const taskTmpDir = mkdtempSync(path.join(tmpdir(), 'relay-pty-task-'));
4518
+ const taskTmpFile = path.join(taskTmpDir, `${agentName}-${Date.now()}.txt`);
4519
+ writeFileSync(taskTmpFile, taskText, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
4520
+ const promptTaskText =
4521
+ `TASK_FILE:${taskTmpFile}\n` +
4522
+ 'Read that file completely before taking any action.\n' +
4523
+ 'Treat the file contents as the full workflow task and follow them exactly.\n' +
4524
+ 'Do not ask for the task again.';
4525
+
4526
+ return {
4527
+ spawnTaskText: promptTaskText,
4528
+ promptTaskText,
4529
+ taskTmpFile,
4530
+ };
4531
+ }
4532
+
4451
4533
  private firstMeaningfulLine(output: string): string | undefined {
4452
4534
  return output
4453
4535
  .split('\n')
@@ -5218,6 +5300,7 @@ export class WorkflowRunner {
5218
5300
  '(b) outputting the exact text "/exit" on its own line as a fallback. ' +
5219
5301
  'Do not wait for further input — terminate immediately after finishing. ' +
5220
5302
  'Do NOT spawn sub-agents unless the task explicitly requires it.';
5303
+ const preparedTask = this.prepareInteractiveSpawnTask(agentName, taskWithExit);
5221
5304
 
5222
5305
  // Register PTY output listener before spawning so we capture everything
5223
5306
  this.ptyOutputBuffers.set(agentName, []);
@@ -5257,7 +5340,7 @@ export class WorkflowRunner {
5257
5340
  model: agentDef.constraints?.model,
5258
5341
  args: interactiveSpawnPolicy.args,
5259
5342
  channels: agentChannels,
5260
- task: taskWithExit,
5343
+ task: preparedTask.spawnTaskText,
5261
5344
  idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
5262
5345
  cwd: agentCwd,
5263
5346
  });
@@ -5368,6 +5451,7 @@ export class WorkflowRunner {
5368
5451
  agentDef,
5369
5452
  step,
5370
5453
  timeoutMs,
5454
+ preparedTask.promptTaskText,
5371
5455
  options.preserveOnIdle ?? this.shouldPreserveIdleSupervisor(agentDef, step, options.evidenceRole)
5372
5456
  );
5373
5457
 
@@ -5385,7 +5469,7 @@ export class WorkflowRunner {
5385
5469
  step.verification,
5386
5470
  ptyOutput,
5387
5471
  step.name,
5388
- undefined,
5472
+ preparedTask.promptTaskText,
5389
5473
  { allowFailure: true }
5390
5474
  );
5391
5475
  if (verificationResult.passed) {
@@ -5446,6 +5530,9 @@ export class WorkflowRunner {
5446
5530
  this.unregisterWorker(agentName);
5447
5531
  this.supervisedRuntimeAgents.delete(agentName);
5448
5532
  this.runtimeStepAgents.delete(agentName);
5533
+ if (preparedTask.taskTmpFile) {
5534
+ await unlink(preparedTask.taskTmpFile).catch(() => undefined);
5535
+ }
5449
5536
  }
5450
5537
 
5451
5538
  let output: string;
@@ -5480,6 +5567,7 @@ export class WorkflowRunner {
5480
5567
  output,
5481
5568
  exitCode: agent?.exitCode,
5482
5569
  exitSignal: agent?.exitSignal,
5570
+ promptTaskText: preparedTask.promptTaskText,
5483
5571
  };
5484
5572
  }
5485
5573
 
@@ -5547,6 +5635,7 @@ export class WorkflowRunner {
5547
5635
  agentDef: AgentDefinition,
5548
5636
  step: WorkflowStep,
5549
5637
  timeoutMs?: number,
5638
+ promptTaskText?: string,
5550
5639
  preserveIdleSupervisor = false
5551
5640
  ): Promise<'exited' | 'timeout' | 'released' | 'force-released'> {
5552
5641
  const nudgeConfig = this.currentConfig?.swarm.idleNudge;
@@ -5572,21 +5661,14 @@ export class WorkflowRunner {
5572
5661
  ]);
5573
5662
  if (result.kind === 'idle' && result.result === 'idle') {
5574
5663
  // Check verification before treating idle as complete.
5575
- // Mirror runVerification's double-occurrence guard: if the task text
5576
- // contains the token (from the prompt instruction), require a second
5577
- // occurrence from the agent's actual output to avoid false positives.
5578
5664
  if (step.verification && step.verification.type === 'output_contains') {
5579
5665
  const token = step.verification.value;
5580
5666
  const ptyOutput = (this.ptyOutputBuffers.get(agent.name) ?? []).join('');
5581
- const taskText = step.task ?? '';
5582
- const taskHasToken = taskText.includes(token);
5583
- let verificationPassed = true;
5584
- if (taskHasToken) {
5585
- const first = ptyOutput.indexOf(token);
5586
- verificationPassed = first !== -1 && ptyOutput.includes(token, first + token.length);
5587
- } else {
5588
- verificationPassed = ptyOutput.includes(token);
5589
- }
5667
+ const verificationPassed = this.outputContainsVerificationToken(
5668
+ ptyOutput,
5669
+ token,
5670
+ promptTaskText
5671
+ );
5590
5672
  if (!verificationPassed) {
5591
5673
  // The broker fires agent_idle only once per idle transition.
5592
5674
  // If the agent is still working (will produce output then idle again),
@@ -5798,23 +5880,8 @@ export class WorkflowRunner {
5798
5880
 
5799
5881
  switch (check.type) {
5800
5882
  case 'output_contains': {
5801
- // Guard against false positives: the PTY captures the injected task text
5802
- // verbatim, so if the verification token appears in the task itself the
5803
- // check would pass immediately without the agent doing any real work.
5804
- // When the task contains the token, require a SECOND occurrence — one
5805
- // from the task injection and one from the agent's actual response.
5806
5883
  const token = check.value;
5807
- const taskHasToken = injectedTaskText ? injectedTaskText.includes(token) : false;
5808
- if (taskHasToken) {
5809
- const first = output.indexOf(token);
5810
- const hasSecond = first !== -1 && output.includes(token, first + token.length);
5811
- if (!hasSecond) {
5812
- return fail(
5813
- `Verification failed for "${stepName}": output does not contain "${token}" ` +
5814
- `(token found only in task injection — agent must output it explicitly)`
5815
- );
5816
- }
5817
- } else if (!output.includes(token)) {
5884
+ if (!this.outputContainsVerificationToken(output, token, injectedTaskText)) {
5818
5885
  return fail(`Verification failed for "${stepName}": output does not contain "${token}"`);
5819
5886
  }
5820
5887
  break;
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-relay-sdk"
7
- version = "3.2.17"
7
+ version = "3.2.21"
8
8
  description = "Python SDK for Agent Relay workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -218,6 +218,8 @@ public final class RelayObserver: NSObject, URLSessionWebSocketDelegate, @unchec
218
218
  }
219
219
 
220
220
  private func _handleSocketError(_ error: Error) {
221
+ isConnectionReady = false
222
+
221
223
  guard reconnectAttempts < maxReconnectAttempts else {
222
224
  _connectionState = .disconnected
223
225
  let delegate = self.delegate
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/telemetry",
3
- "version": "3.2.17",
3
+ "version": "3.2.21",
4
4
  "description": "Anonymous telemetry for Agent Relay usage analytics",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/trajectory",
3
- "version": "3.2.17",
3
+ "version": "3.2.21",
4
4
  "description": "Trajectory integration utilities (trail/PDERO) for Relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/config": "3.2.17"
25
+ "@agent-relay/config": "3.2.21"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/user-directory",
3
- "version": "3.2.17",
3
+ "version": "3.2.21",
4
4
  "description": "User directory service for agent-relay (per-user credential storage)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test:watch": "vitest"
23
23
  },
24
24
  "dependencies": {
25
- "@agent-relay/utils": "3.2.17"
25
+ "@agent-relay/utils": "3.2.21"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.19.3",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/utils",
3
- "version": "3.2.17",
3
+ "version": "3.2.21",
4
4
  "description": "Shared utilities for agent-relay: logging, name generation, command resolution, update checking",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -112,7 +112,7 @@
112
112
  "vitest": "^3.2.4"
113
113
  },
114
114
  "dependencies": {
115
- "@agent-relay/config": "3.2.17",
115
+ "@agent-relay/config": "3.2.21",
116
116
  "compare-versions": "^6.1.1"
117
117
  },
118
118
  "publishConfig": {