agent-relay-orchestrator 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-orchestrator",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "test": "bun test"
17
17
  },
18
18
  "dependencies": {
19
- "agent-relay-sdk": "0.2.23"
19
+ "agent-relay-sdk": "0.2.24"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/bun": "latest",
package/src/control.ts CHANGED
@@ -4,6 +4,7 @@ import type { ManagedAgentReport, RelayClient, RelayCommand } from "./relay";
4
4
  import { handleSelfUpgrade } from "./self-upgrade";
5
5
  import { spawnAgent, stopSession, type SpawnOptions } from "./spawn";
6
6
  import { cleanupWorkspace, mergeWorkspace, pruneWorktrees, reconcileWorkspace, refreshWorkspaceDeps, workspacesRoot } from "./workspace-probe";
7
+ import { armWorkspacePrAutoMerge } from "./workspace-pr";
7
8
 
8
9
  interface ControlHandler {
9
10
  handleCommand(command: RelayCommand): Promise<boolean>;
@@ -123,6 +124,17 @@ export function createControlHandler(
123
124
  autoMerge: command.params.autoMerge === "on-green" || command.params.autoMerge === "on-approval" || command.params.autoMerge === "manual" ? command.params.autoMerge : undefined,
124
125
  });
125
126
  await relay.updateCommand(command.id, "succeeded", result as unknown as Record<string, unknown>);
127
+ } else if (command.type === "workspace.pr-arm-auto-merge") {
128
+ const rawPrNumber = command.params.prNumber;
129
+ const result = armWorkspacePrAutoMerge({
130
+ id: typeof command.params.workspaceId === "string" ? command.params.workspaceId : undefined,
131
+ repoRoot: typeof command.params.repoRoot === "string" ? command.params.repoRoot : undefined,
132
+ worktreePath: typeof command.params.worktreePath === "string" ? command.params.worktreePath : undefined,
133
+ branch: typeof command.params.branch === "string" ? command.params.branch : undefined,
134
+ prNumber: typeof rawPrNumber === "number" && Number.isSafeInteger(rawPrNumber) ? rawPrNumber : undefined,
135
+ prUrl: typeof command.params.prUrl === "string" ? command.params.prUrl : undefined,
136
+ });
137
+ await relay.updateCommand(command.id, result.autoMergeArmed ? "succeeded" : "failed", result as unknown as Record<string, unknown>, result.error);
126
138
  } else if (command.type === "workspace.deps-refresh") {
127
139
  const result = refreshWorkspaceDeps(
128
140
  typeof command.params.repoRoot === "string" ? command.params.repoRoot : "",
package/src/spawn.ts CHANGED
@@ -6,7 +6,7 @@ import type { OrchestratorConfig } from "./config";
6
6
  import type { ManagedAgentReport, ManagedSessionExitDiagnostics } from "./relay";
7
7
  import { resolveSpawnWorkspace, workspacesRoot } from "./workspace-probe";
8
8
  import type { WorkspaceMetadata, WorkspaceMode } from "agent-relay-sdk";
9
- import { errMessage } from "agent-relay-sdk";
9
+ import { errMessage, extractClaudeModelUnavailableMessage } from "agent-relay-sdk";
10
10
  import { isPidAlive, parseProcStateIsZombie } from "agent-relay-sdk/process-utils";
11
11
  import { shellEscape } from "agent-relay-sdk/shell-utils";
12
12
  import { tmuxCommand, tmuxHasSession } from "agent-relay-sdk/tmux-utils";
@@ -890,6 +890,10 @@ function logFileDiagnostics(logFile: string): Pick<ManagedSessionExitDiagnostics
890
890
  }
891
891
 
892
892
  function describeSessionExit(record: SessionRecord, diagnostics: Omit<ManagedSessionExitDiagnostics, "lastError">): string {
893
+ if (record.provider === "claude") {
894
+ const modelUnavailable = extractClaudeModelUnavailableMessage((diagnostics.logTail ?? []).join("\n"));
895
+ if (modelUnavailable) return modelUnavailable;
896
+ }
893
897
  const seconds = Math.max(0, Math.round(diagnostics.runtimeMs / 1000));
894
898
  const parts = [`managed ${record.provider} session ${record.name} exited after ${seconds}s`];
895
899
  if (diagnostics.systemd?.unavailable) {
@@ -1104,7 +1108,7 @@ export function captureSessionMirror(
1104
1108
  // The mirror log lives in the same directory as the provider log (both written
1105
1109
  // by the same user on this host). Derive from the record's logFile when known so
1106
1110
  // it tracks any per-session log relocation.
1107
- const logDir = record?.logFile ? dirname(record.logFile) : LOG_DIR;
1111
+ const logDir = record?.logFile ? dirname(record.logFile) : process.env.AGENT_RELAY_LOG_DIR || LOG_DIR;
1108
1112
  const mirrorPath = join(logDir, `session-mirror-${safeMirrorLogName(agentId)}.log`);
1109
1113
  let content: string;
1110
1114
  try {
@@ -0,0 +1,85 @@
1
+ import { resolve } from "node:path";
2
+
3
+ type PrReviewDecision = "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED";
4
+
5
+ interface PullRequestState {
6
+ state: "merged" | "open";
7
+ url?: string;
8
+ sha?: string;
9
+ number?: number;
10
+ reviewDecision?: PrReviewDecision;
11
+ statusCheckRollup?: unknown[];
12
+ }
13
+
14
+ function prReviewDecision(value: unknown): PrReviewDecision | undefined {
15
+ return value === "APPROVED" || value === "CHANGES_REQUESTED" || value === "REVIEW_REQUIRED" ? value : undefined;
16
+ }
17
+
18
+ export function prMergedState(cwd: string, branch: string | undefined): PullRequestState | undefined {
19
+ if (!branch) return undefined;
20
+ const proc = Bun.spawnSync(["gh", "pr", "view", branch, "--json", "state,url,mergeCommit,number,reviewDecision,statusCheckRollup"], {
21
+ cwd,
22
+ stdin: "ignore",
23
+ stdout: "pipe",
24
+ stderr: "pipe",
25
+ env: process.env,
26
+ });
27
+ if (proc.exitCode !== 0) return undefined;
28
+ try {
29
+ const data = JSON.parse(proc.stdout.toString()) as {
30
+ state?: string;
31
+ url?: string;
32
+ mergeCommit?: { oid?: string } | null;
33
+ number?: number;
34
+ reviewDecision?: unknown;
35
+ statusCheckRollup?: unknown;
36
+ };
37
+ if (!data.state) return undefined;
38
+ const sha = data.mergeCommit?.oid;
39
+ const number = typeof data.number === "number" && Number.isSafeInteger(data.number) ? data.number : undefined;
40
+ const reviewDecision = prReviewDecision(data.reviewDecision);
41
+ const statusCheckRollup = Array.isArray(data.statusCheckRollup) ? data.statusCheckRollup : undefined;
42
+ return {
43
+ state: data.state === "MERGED" ? "merged" : "open",
44
+ url: data.url,
45
+ ...(sha ? { sha } : {}),
46
+ ...(number ? { number } : {}),
47
+ ...(reviewDecision ? { reviewDecision } : {}),
48
+ ...(statusCheckRollup ? { statusCheckRollup } : {}),
49
+ };
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ export function armWorkspacePrAutoMerge(input: {
56
+ id?: string;
57
+ repoRoot?: string;
58
+ worktreePath?: string;
59
+ branch?: string;
60
+ prNumber?: number;
61
+ prUrl?: string;
62
+ }): { workspaceId?: string; status: "merge_planned"; autoMergeArmed: boolean; prNumber?: number; prUrl?: string; error?: string } {
63
+ const cwd = input.worktreePath ? resolve(input.worktreePath) : input.repoRoot ? resolve(input.repoRoot) : undefined;
64
+ if (!cwd) return { workspaceId: input.id, status: "merge_planned", autoMergeArmed: false, error: "worktreePath or repoRoot required" };
65
+ const target = input.prNumber ? String(input.prNumber) : input.prUrl ?? input.branch;
66
+ if (!target) return { workspaceId: input.id, status: "merge_planned", autoMergeArmed: false, error: "prNumber, prUrl, or branch required" };
67
+ const proc = Bun.spawnSync(["gh", "pr", "merge", target, "--auto", "--merge"], {
68
+ cwd,
69
+ stdin: "ignore",
70
+ stdout: "pipe",
71
+ stderr: "pipe",
72
+ env: process.env,
73
+ });
74
+ if (proc.exitCode !== 0) {
75
+ return {
76
+ workspaceId: input.id,
77
+ status: "merge_planned",
78
+ autoMergeArmed: false,
79
+ prNumber: input.prNumber,
80
+ prUrl: input.prUrl,
81
+ error: proc.stderr.toString().trim() || proc.stdout.toString().trim() || "gh pr merge failed",
82
+ };
83
+ }
84
+ return { workspaceId: input.id, status: "merge_planned", autoMergeArmed: true, prNumber: input.prNumber, prUrl: input.prUrl };
85
+ }
@@ -6,6 +6,7 @@ import type { WorkspaceDepsProvision, WorkspaceDepsRefreshDir, WorkspaceDepsRefr
6
6
  import { errMessage } from "agent-relay-sdk";
7
7
  import { sanitizeFsName } from "agent-relay-sdk/fs-name";
8
8
  import { git, requireGit } from "./git";
9
+ import { prMergedState } from "./workspace-pr";
9
10
 
10
11
  const MAX_DIFF_PATCH_BYTES = 200_000;
11
12
 
@@ -747,25 +748,6 @@ function ghAvailable(): boolean {
747
748
  * Returns undefined when there's no PR, no branch, or gh fails —
748
749
  * we never invent a merge.
749
750
  */
750
- function prMergedState(cwd: string, branch: string | undefined): { state: "merged" | "open"; url?: string; sha?: string } | undefined {
751
- if (!branch) return undefined;
752
- const proc = Bun.spawnSync(["gh", "pr", "view", branch, "--json", "state,url,mergeCommit"], {
753
- cwd,
754
- stdin: "ignore",
755
- stdout: "pipe",
756
- stderr: "pipe",
757
- });
758
- if (proc.exitCode !== 0) return undefined; // no PR for the branch, or gh unavailable/unauthed
759
- try {
760
- const data = JSON.parse(proc.stdout.toString()) as { state?: string; url?: string; mergeCommit?: { oid?: string } | null };
761
- if (!data.state) return undefined;
762
- const sha = data.mergeCommit?.oid;
763
- return { state: data.state === "MERGED" ? "merged" : "open", url: data.url, ...(sha ? { sha } : {}) };
764
- } catch {
765
- return undefined;
766
- }
767
- }
768
-
769
751
  /**
770
752
  * Predict whether merging the branch's commits into base would conflict, using
771
753
  * git's three-way merge-tree (no working-tree changes). Exit 0 = clean, exit 1
@@ -803,6 +785,9 @@ function applyPrMergeState(base: WorkspaceMergePreview, cwd: string, branch: str
803
785
  if (!pr) return false;
804
786
  base.prState = pr.state;
805
787
  if (pr.url) base.prUrl = pr.url;
788
+ if (pr.number) base.prNumber = pr.number;
789
+ if (pr.reviewDecision) base.reviewDecision = pr.reviewDecision;
790
+ if (pr.statusCheckRollup) base.statusCheckRollup = pr.statusCheckRollup;
806
791
  if (pr.state !== "merged") return false;
807
792
  base.prMerged = true;
808
793
  if (pr.sha) base.prMergeSha = pr.sha;