agent-relay-orchestrator 0.38.0 → 0.40.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 +2 -2
- package/src/control.ts +23 -0
- package/src/spawn.ts +6 -2
- package/src/workspace-pr.ts +125 -0
- package/src/workspace-probe.ts +4 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-orchestrator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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.
|
|
19
|
+
"agent-relay-sdk": "0.2.25"
|
|
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, mergeWorkspacePr } from "./workspace-pr";
|
|
7
8
|
|
|
8
9
|
interface ControlHandler {
|
|
9
10
|
handleCommand(command: RelayCommand): Promise<boolean>;
|
|
@@ -123,6 +124,28 @@ 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);
|
|
138
|
+
} else if (command.type === "workspace.pr-merge") {
|
|
139
|
+
const rawPrNumber = command.params.prNumber;
|
|
140
|
+
const result = mergeWorkspacePr({
|
|
141
|
+
id: typeof command.params.workspaceId === "string" ? command.params.workspaceId : undefined,
|
|
142
|
+
repoRoot: typeof command.params.repoRoot === "string" ? command.params.repoRoot : undefined,
|
|
143
|
+
worktreePath: typeof command.params.worktreePath === "string" ? command.params.worktreePath : undefined,
|
|
144
|
+
branch: typeof command.params.branch === "string" ? command.params.branch : undefined,
|
|
145
|
+
prNumber: typeof rawPrNumber === "number" && Number.isSafeInteger(rawPrNumber) ? rawPrNumber : undefined,
|
|
146
|
+
prUrl: typeof command.params.prUrl === "string" ? command.params.prUrl : undefined,
|
|
147
|
+
});
|
|
148
|
+
await relay.updateCommand(command.id, result.relayMerged ? "succeeded" : "failed", result as unknown as Record<string, unknown>, result.error);
|
|
126
149
|
} else if (command.type === "workspace.deps-refresh") {
|
|
127
150
|
const result = refreshWorkspaceDeps(
|
|
128
151
|
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,125 @@
|
|
|
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
|
+
mergeable?: string;
|
|
13
|
+
mergeStateStatus?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function prReviewDecision(value: unknown): PrReviewDecision | undefined {
|
|
17
|
+
return value === "APPROVED" || value === "CHANGES_REQUESTED" || value === "REVIEW_REQUIRED" ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function prMergedState(cwd: string, branch: string | undefined): PullRequestState | undefined {
|
|
21
|
+
if (!branch) return undefined;
|
|
22
|
+
const proc = Bun.spawnSync(["gh", "pr", "view", branch, "--json", "state,url,mergeCommit,number,reviewDecision,statusCheckRollup,mergeable,mergeStateStatus"], {
|
|
23
|
+
cwd,
|
|
24
|
+
stdin: "ignore",
|
|
25
|
+
stdout: "pipe",
|
|
26
|
+
stderr: "pipe",
|
|
27
|
+
env: process.env,
|
|
28
|
+
});
|
|
29
|
+
if (proc.exitCode !== 0) return undefined;
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.parse(proc.stdout.toString()) as {
|
|
32
|
+
state?: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
mergeCommit?: { oid?: string } | null;
|
|
35
|
+
number?: number;
|
|
36
|
+
reviewDecision?: unknown;
|
|
37
|
+
statusCheckRollup?: unknown;
|
|
38
|
+
mergeable?: unknown;
|
|
39
|
+
mergeStateStatus?: unknown;
|
|
40
|
+
};
|
|
41
|
+
if (!data.state) return undefined;
|
|
42
|
+
const sha = data.mergeCommit?.oid;
|
|
43
|
+
const number = typeof data.number === "number" && Number.isSafeInteger(data.number) ? data.number : undefined;
|
|
44
|
+
const reviewDecision = prReviewDecision(data.reviewDecision);
|
|
45
|
+
const statusCheckRollup = Array.isArray(data.statusCheckRollup) ? data.statusCheckRollup : undefined;
|
|
46
|
+
const mergeable = typeof data.mergeable === "string" ? data.mergeable : undefined;
|
|
47
|
+
const mergeStateStatus = typeof data.mergeStateStatus === "string" ? data.mergeStateStatus : undefined;
|
|
48
|
+
return {
|
|
49
|
+
state: data.state === "MERGED" ? "merged" : "open",
|
|
50
|
+
url: data.url,
|
|
51
|
+
...(sha ? { sha } : {}),
|
|
52
|
+
...(number ? { number } : {}),
|
|
53
|
+
...(reviewDecision ? { reviewDecision } : {}),
|
|
54
|
+
...(statusCheckRollup ? { statusCheckRollup } : {}),
|
|
55
|
+
...(mergeable ? { mergeable } : {}),
|
|
56
|
+
...(mergeStateStatus ? { mergeStateStatus } : {}),
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function armWorkspacePrAutoMerge(input: {
|
|
64
|
+
id?: string;
|
|
65
|
+
repoRoot?: string;
|
|
66
|
+
worktreePath?: string;
|
|
67
|
+
branch?: string;
|
|
68
|
+
prNumber?: number;
|
|
69
|
+
prUrl?: string;
|
|
70
|
+
}): { workspaceId?: string; status: "merge_planned"; autoMergeArmed: boolean; prNumber?: number; prUrl?: string; error?: string } {
|
|
71
|
+
const cwd = input.worktreePath ? resolve(input.worktreePath) : input.repoRoot ? resolve(input.repoRoot) : undefined;
|
|
72
|
+
if (!cwd) return { workspaceId: input.id, status: "merge_planned", autoMergeArmed: false, error: "worktreePath or repoRoot required" };
|
|
73
|
+
const target = input.prNumber ? String(input.prNumber) : input.prUrl ?? input.branch;
|
|
74
|
+
if (!target) return { workspaceId: input.id, status: "merge_planned", autoMergeArmed: false, error: "prNumber, prUrl, or branch required" };
|
|
75
|
+
const proc = Bun.spawnSync(["gh", "pr", "merge", target, "--auto", "--merge"], {
|
|
76
|
+
cwd,
|
|
77
|
+
stdin: "ignore",
|
|
78
|
+
stdout: "pipe",
|
|
79
|
+
stderr: "pipe",
|
|
80
|
+
env: process.env,
|
|
81
|
+
});
|
|
82
|
+
if (proc.exitCode !== 0) {
|
|
83
|
+
return {
|
|
84
|
+
workspaceId: input.id,
|
|
85
|
+
status: "merge_planned",
|
|
86
|
+
autoMergeArmed: false,
|
|
87
|
+
prNumber: input.prNumber,
|
|
88
|
+
prUrl: input.prUrl,
|
|
89
|
+
error: proc.stderr.toString().trim() || proc.stdout.toString().trim() || "gh pr merge failed",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { workspaceId: input.id, status: "merge_planned", autoMergeArmed: true, prNumber: input.prNumber, prUrl: input.prUrl };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function mergeWorkspacePr(input: {
|
|
96
|
+
id?: string;
|
|
97
|
+
repoRoot?: string;
|
|
98
|
+
worktreePath?: string;
|
|
99
|
+
branch?: string;
|
|
100
|
+
prNumber?: number;
|
|
101
|
+
prUrl?: string;
|
|
102
|
+
}): { workspaceId?: string; status: "merge_planned"; relayMerged: boolean; prNumber?: number; prUrl?: string; error?: string } {
|
|
103
|
+
const cwd = input.worktreePath ? resolve(input.worktreePath) : input.repoRoot ? resolve(input.repoRoot) : undefined;
|
|
104
|
+
if (!cwd) return { workspaceId: input.id, status: "merge_planned", relayMerged: false, error: "worktreePath or repoRoot required" };
|
|
105
|
+
const target = input.prNumber ? String(input.prNumber) : input.prUrl ?? input.branch;
|
|
106
|
+
if (!target) return { workspaceId: input.id, status: "merge_planned", relayMerged: false, error: "prNumber, prUrl, or branch required" };
|
|
107
|
+
const proc = Bun.spawnSync(["gh", "pr", "merge", target, "--merge"], {
|
|
108
|
+
cwd,
|
|
109
|
+
stdin: "ignore",
|
|
110
|
+
stdout: "pipe",
|
|
111
|
+
stderr: "pipe",
|
|
112
|
+
env: process.env,
|
|
113
|
+
});
|
|
114
|
+
if (proc.exitCode !== 0) {
|
|
115
|
+
return {
|
|
116
|
+
workspaceId: input.id,
|
|
117
|
+
status: "merge_planned",
|
|
118
|
+
relayMerged: false,
|
|
119
|
+
prNumber: input.prNumber,
|
|
120
|
+
prUrl: input.prUrl,
|
|
121
|
+
error: proc.stderr.toString().trim() || proc.stdout.toString().trim() || "gh pr merge failed",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { workspaceId: input.id, status: "merge_planned", relayMerged: true, prNumber: input.prNumber, prUrl: input.prUrl };
|
|
125
|
+
}
|
package/src/workspace-probe.ts
CHANGED
|
@@ -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; if (pr.mergeable) base.mergeable = pr.mergeable; if (pr.mergeStateStatus) base.mergeStateStatus = pr.mergeStateStatus;
|
|
806
791
|
if (pr.state !== "merged") return false;
|
|
807
792
|
base.prMerged = true;
|
|
808
793
|
if (pr.sha) base.prMergeSha = pr.sha;
|