agent-relay 3.2.21 → 3.2.22

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 (34) 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 +147 -12
  6. package/package.json +9 -9
  7. package/packages/acp-bridge/package.json +2 -2
  8. package/packages/brand/package.json +1 -1
  9. package/packages/cloud/package.json +2 -2
  10. package/packages/config/package.json +1 -1
  11. package/packages/hooks/package.json +4 -4
  12. package/packages/memory/package.json +2 -2
  13. package/packages/openclaw/package.json +2 -2
  14. package/packages/policy/package.json +2 -2
  15. package/packages/sdk/dist/workflows/cli.js +46 -2
  16. package/packages/sdk/dist/workflows/cli.js.map +1 -1
  17. package/packages/sdk/dist/workflows/file-db.d.ts +2 -0
  18. package/packages/sdk/dist/workflows/file-db.d.ts.map +1 -1
  19. package/packages/sdk/dist/workflows/file-db.js +20 -3
  20. package/packages/sdk/dist/workflows/file-db.js.map +1 -1
  21. package/packages/sdk/dist/workflows/runner.d.ts +6 -1
  22. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  23. package/packages/sdk/dist/workflows/runner.js +157 -11
  24. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  25. package/packages/sdk/package.json +2 -2
  26. package/packages/sdk/src/__tests__/resume-fallback.test.ts +415 -0
  27. package/packages/sdk/src/workflows/cli.ts +53 -2
  28. package/packages/sdk/src/workflows/file-db.ts +22 -3
  29. package/packages/sdk/src/workflows/runner.ts +178 -11
  30. package/packages/sdk-py/pyproject.toml +1 -1
  31. package/packages/telemetry/package.json +1 -1
  32. package/packages/trajectory/package.json +2 -2
  33. package/packages/user-directory/package.json +2 -2
  34. package/packages/utils/package.json +2 -2
@@ -1952,14 +1952,25 @@ export class WorkflowRunner {
1952
1952
  }
1953
1953
 
1954
1954
  /** Resume a previously paused or partially completed run. */
1955
- async resume(runId: string, vars?: VariableContext): Promise<WorkflowRunRow> {
1955
+ async resume(runId: string, vars?: VariableContext, config?: RelayYamlConfig): Promise<WorkflowRunRow> {
1956
1956
  // Set up abort controller early so callers can abort() even during setup
1957
1957
  this.abortController = new AbortController();
1958
1958
  this.paused = false;
1959
1959
 
1960
- const run = await this.db.getRun(runId);
1960
+ let run = await this.db.getRun(runId);
1961
+ let stepStates = new Map<string, StepState>();
1961
1962
  if (!run) {
1962
- throw new Error(`Run "${runId}" not found`);
1963
+ const reconstructed = this.reconstructRunFromCache(runId, config);
1964
+ if (!reconstructed) {
1965
+ throw new Error(`Run "${runId}" not found (no database entry or cached step outputs)`);
1966
+ }
1967
+ this.log('[resume] Reconstructing run from cached step outputs (workflow-runs.jsonl missing)');
1968
+ run = reconstructed.run;
1969
+ stepStates = reconstructed.stepStates;
1970
+ await this.db.insertRun(run);
1971
+ for (const [, state] of stepStates) {
1972
+ await this.db.insertStep(state.row);
1973
+ }
1963
1974
  }
1964
1975
  this.persistRunIdHint(runId);
1965
1976
 
@@ -1967,25 +1978,26 @@ export class WorkflowRunner {
1967
1978
  throw new Error(`Run "${runId}" is in status "${run.status}" and cannot be resumed`);
1968
1979
  }
1969
1980
 
1970
- const config = vars ? this.resolveVariables(run.config, vars) : run.config;
1981
+ const resolvedConfig = vars ? this.resolveVariables(run.config, vars) : run.config;
1971
1982
 
1972
1983
  // Resolve path definitions (same as execute()) so workdir lookups work on resume
1973
- const pathResult = this.resolvePathDefinitions(config.paths, this.cwd);
1984
+ const pathResult = this.resolvePathDefinitions(resolvedConfig.paths, this.cwd);
1974
1985
  if (pathResult.errors.length > 0) {
1975
1986
  throw new Error(`Path validation failed:\n ${pathResult.errors.join('\n ')}`);
1976
1987
  }
1977
1988
  this.resolvedPaths = pathResult.resolved;
1978
1989
 
1979
- const workflows = config.workflows ?? [];
1990
+ const workflows = resolvedConfig.workflows ?? [];
1980
1991
  const workflow = workflows.find((w) => w.name === run.workflowName);
1981
1992
  if (!workflow) {
1982
1993
  throw new Error(`Workflow "${run.workflowName}" not found in stored config`);
1983
1994
  }
1984
1995
 
1985
- const existingSteps = await this.db.getStepsByRunId(runId);
1986
- const stepStates = new Map<string, StepState>();
1987
- for (const stepRow of existingSteps) {
1988
- stepStates.set(stepRow.stepName, { row: stepRow });
1996
+ if (stepStates.size === 0) {
1997
+ const existingSteps = await this.db.getStepsByRunId(runId);
1998
+ for (const stepRow of existingSteps) {
1999
+ stepStates.set(stepRow.stepName, { row: stepRow });
2000
+ }
1989
2001
  }
1990
2002
 
1991
2003
  // Reset failed steps to pending for retry
@@ -2006,7 +2018,7 @@ export class WorkflowRunner {
2006
2018
  return this.runWorkflowCore({
2007
2019
  run,
2008
2020
  workflow,
2009
- config,
2021
+ config: resolvedConfig,
2010
2022
  stepStates,
2011
2023
  isResume: true,
2012
2024
  });
@@ -6547,8 +6559,16 @@ export class WorkflowRunner {
6547
6559
  .slice(0, 32);
6548
6560
  }
6549
6561
 
6562
+ /** Validate that a runId is safe for use in file paths (no traversal). */
6563
+ private validateRunId(runId: string): void {
6564
+ if (/[/\\]|^\.\.?$/.test(runId) || runId.includes('..')) {
6565
+ throw new Error(`Invalid runId: "${runId}" contains path traversal characters`);
6566
+ }
6567
+ }
6568
+
6550
6569
  /** Directory for persisted step outputs: .agent-relay/step-outputs/{runId}/ */
6551
6570
  private getStepOutputDir(runId: string): string {
6571
+ this.validateRunId(runId);
6552
6572
  return path.join(this.cwd, '.agent-relay', 'step-outputs', runId);
6553
6573
  }
6554
6574
 
@@ -6638,6 +6658,153 @@ export class WorkflowRunner {
6638
6658
  }
6639
6659
  }
6640
6660
 
6661
+ /** Match the best workflow from config given a set of cached step names. */
6662
+ private matchWorkflowFromCache(
6663
+ workflows: WorkflowDefinition[],
6664
+ cachedStepNames: Set<string>
6665
+ ): WorkflowDefinition | null {
6666
+ if (workflows.length === 1) return workflows[0];
6667
+
6668
+ if (cachedStepNames.size === 0) {
6669
+ // No cached steps to disambiguate — ambiguous when multiple workflows exist
6670
+ this.log('[resume] Multiple workflows in config with empty cache — cannot disambiguate');
6671
+ return null;
6672
+ }
6673
+
6674
+ // Score each workflow by how many cached steps match, excluding those with unknown steps
6675
+ const scored = workflows
6676
+ .map((candidate) => ({
6677
+ workflow: candidate,
6678
+ matchedSteps: candidate.steps.filter((step) => cachedStepNames.has(step.name)).length,
6679
+ unknownSteps: [...cachedStepNames].filter(
6680
+ (name) => !candidate.steps.some((step) => step.name === name)
6681
+ ).length,
6682
+ }))
6683
+ .filter((candidate) => candidate.unknownSteps === 0)
6684
+ .sort((a, b) => b.matchedSteps - a.matchedSteps);
6685
+
6686
+ return scored[0]?.workflow ?? null;
6687
+ }
6688
+
6689
+ private reconstructRunFromCache(
6690
+ runId: string,
6691
+ config?: RelayYamlConfig
6692
+ ): { run: WorkflowRunRow; stepStates: Map<string, StepState> } | null {
6693
+ const stepOutputDir = this.getStepOutputDir(runId);
6694
+ if (!existsSync(stepOutputDir)) return null;
6695
+
6696
+ let resumeConfig = config ?? this.currentConfig;
6697
+ if (!resumeConfig) {
6698
+ // Attempt to load config from relay.yaml on disk (resume() may call before runWorkflowCore sets currentConfig)
6699
+ const yamlPath = path.join(this.cwd, 'relay.yaml');
6700
+ if (existsSync(yamlPath)) {
6701
+ try {
6702
+ const raw = readFileSync(yamlPath, 'utf-8');
6703
+ resumeConfig = this.parseYamlString(raw, yamlPath);
6704
+ } catch {
6705
+ return null;
6706
+ }
6707
+ } else {
6708
+ return null;
6709
+ }
6710
+ }
6711
+
6712
+ let entries: Dirent[];
6713
+ try {
6714
+ entries = readdirSync(stepOutputDir, { withFileTypes: true });
6715
+ } catch {
6716
+ return null;
6717
+ }
6718
+
6719
+ const cachedStepNames = new Set(
6720
+ entries
6721
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
6722
+ .map((entry) => entry.name.slice(0, -3))
6723
+ .filter(Boolean)
6724
+ );
6725
+ const workflows = resumeConfig.workflows ?? [];
6726
+ if (workflows.length === 0) return null;
6727
+
6728
+ // Empty cache directory is valid — all steps will be re-run
6729
+ const workflow = this.matchWorkflowFromCache(workflows, cachedStepNames);
6730
+ if (!workflow) return null;
6731
+
6732
+ // Use actual file modification times from cached outputs instead of synthetic timestamps
6733
+ const stepMtimes = new Map<string, string>();
6734
+ let earliestMtime = Date.now();
6735
+ for (const stepName of cachedStepNames) {
6736
+ try {
6737
+ const mdPath = path.join(stepOutputDir, `${stepName}.md`);
6738
+ const reportPath = path.join(stepOutputDir, `${stepName}.report.json`);
6739
+ const mdStat = existsSync(mdPath) ? statSync(mdPath) : null;
6740
+ const reportStat = existsSync(reportPath) ? statSync(reportPath) : null;
6741
+ // Use the latest mtime between .md and .report.json
6742
+ const mtime = Math.max(mdStat?.mtimeMs ?? 0, reportStat?.mtimeMs ?? 0);
6743
+ if (mtime > 0) {
6744
+ stepMtimes.set(stepName, new Date(mtime).toISOString());
6745
+ if (mtime < earliestMtime) earliestMtime = mtime;
6746
+ }
6747
+ } catch {
6748
+ // Fall back to current time if stat fails
6749
+ }
6750
+ }
6751
+ const fallbackTime = new Date().toISOString();
6752
+
6753
+ const completedSteps = new Set(workflow.steps.filter((step) => cachedStepNames.has(step.name)).map((step) => step.name));
6754
+ // Heuristic: mark the first eligible non-completed step as failed (the likely failure point)
6755
+ const failedStepName = workflow.steps.find(
6756
+ (step) => !completedSteps.has(step.name) && (step.dependsOn ?? []).every((dep) => completedSteps.has(dep))
6757
+ )?.name;
6758
+
6759
+ const runStartedAt = new Date(earliestMtime).toISOString();
6760
+ const run: WorkflowRunRow = {
6761
+ id: runId,
6762
+ workspaceId: this.workspaceId,
6763
+ workflowName: workflow.name,
6764
+ pattern: resumeConfig.swarm.pattern,
6765
+ status: 'failed',
6766
+ config: resumeConfig,
6767
+ startedAt: runStartedAt,
6768
+ createdAt: runStartedAt,
6769
+ updatedAt: fallbackTime,
6770
+ };
6771
+
6772
+ const stepStates = new Map<string, StepState>();
6773
+ for (const step of workflow.steps) {
6774
+ const isNonAgent = step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration';
6775
+ const cachedOutput = completedSteps.has(step.name) ? this.loadStepOutput(runId, step.name) : undefined;
6776
+ const status: WorkflowStepStatus =
6777
+ completedSteps.has(step.name) ? 'completed' : step.name === failedStepName ? 'failed' : 'pending';
6778
+
6779
+ const stepRow: WorkflowStepRow = {
6780
+ id: this.generateId(),
6781
+ runId,
6782
+ stepName: step.name,
6783
+ agentName: isNonAgent ? null : (step.agent ?? null),
6784
+ stepType: isNonAgent ? (step.type as 'deterministic' | 'worktree' | 'integration') : 'agent',
6785
+ status,
6786
+ task:
6787
+ step.type === 'deterministic'
6788
+ ? (step.command ?? '')
6789
+ : step.type === 'worktree'
6790
+ ? (step.branch ?? '')
6791
+ : step.type === 'integration'
6792
+ ? (`${step.integration}.${step.action}`)
6793
+ : (step.task ?? ''),
6794
+ dependsOn: step.dependsOn ?? [],
6795
+ output: cachedOutput,
6796
+ error: status === 'failed' ? 'Recovered from cached step outputs' : undefined,
6797
+ completedAt: status === 'completed' ? (stepMtimes.get(step.name) ?? fallbackTime) : undefined,
6798
+ retryCount: 0,
6799
+ createdAt: stepMtimes.get(step.name) ?? fallbackTime,
6800
+ updatedAt: stepMtimes.get(step.name) ?? fallbackTime,
6801
+ };
6802
+ stepStates.set(step.name, { row: stepRow });
6803
+ }
6804
+
6805
+ return { run, stepStates };
6806
+ }
6807
+
6641
6808
  /** Get or create the worker logs directory (.agent-relay/team/worker-logs) */
6642
6809
  private getWorkerLogsDir(): string {
6643
6810
  const logsDir = path.join(this.cwd, '.agent-relay', 'team', 'worker-logs');
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agent-relay-sdk"
7
- version = "3.2.21"
7
+ version = "3.2.22"
8
8
  description = "Python SDK for Agent Relay workflows"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-relay/telemetry",
3
- "version": "3.2.21",
3
+ "version": "3.2.22",
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.21",
3
+ "version": "3.2.22",
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.21"
25
+ "@agent-relay/config": "3.2.22"
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.21",
3
+ "version": "3.2.22",
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.21"
25
+ "@agent-relay/utils": "3.2.22"
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.21",
3
+ "version": "3.2.22",
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.21",
115
+ "@agent-relay/config": "3.2.22",
116
116
  "compare-versions": "^6.1.1"
117
117
  },
118
118
  "publishConfig": {