agent-relay-orchestrator 0.35.2 → 0.35.3

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.35.2",
3
+ "version": "0.35.3",
4
4
  "description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
5
5
  "type": "module",
6
6
  "bin": {
package/src/control.ts CHANGED
@@ -216,6 +216,7 @@ function spawnOptionsFromRecord(source: Record<string, any>, config: Orchestrato
216
216
  spawnRequestId: typeof source.spawnRequestId === "string" ? source.spawnRequestId : undefined,
217
217
  automationId: typeof source.automationId === "string" ? source.automationId : undefined,
218
218
  automationRunId: typeof source.automationRunId === "string" ? source.automationRunId : undefined,
219
+ requestedVia: typeof source.requestedVia === "string" ? source.requestedVia : undefined,
219
220
  };
220
221
  }
221
222
 
package/src/spawn.ts CHANGED
@@ -37,6 +37,9 @@ export interface SpawnOptions {
37
37
  spawnRequestId?: string;
38
38
  automationId?: string;
39
39
  automationRunId?: string;
40
+ /** How the spawn was requested (`mcp` = an agent via the MCP surface, else dashboard/CLI). Drives
41
+ * the origin tag so an MCP-spawned worker isn't mislabeled `dashboard-spawned` (#330). */
42
+ requestedVia?: string;
40
43
  }
41
44
 
42
45
  interface SessionInfo {
@@ -222,7 +225,11 @@ export function buildEnv(opts: SpawnOptions & { label: string; agentId: string }
222
225
  AGENT_RELAY_APPROVAL: opts.approvalMode || "guarded",
223
226
  ...(opts.profile ? { AGENT_RELAY_AGENT_PROFILE: opts.profile } : {}),
224
227
  ...(opts.agentProfile ? { AGENT_RELAY_AGENT_PROFILE_JSON: JSON.stringify(opts.agentProfile) } : {}),
225
- AGENT_RELAY_TAGS: [...new Set(["headless", "dashboard-spawned", config.hostname, ...(opts.tags ?? [])])].join(","),
228
+ // #330 tag by TRUE origin. An MCP spawn (an agent spawning a helper) is `agent-spawned`, not
229
+ // `dashboard-spawned`; the old blanket `dashboard-spawned` mislabeled every headless spawn as
230
+ // dashboard-originated. Dashboard/CLI/automation spawns (no `requestedVia: "mcp"`) keep the
231
+ // `dashboard-spawned` tag the smoke test and UI filter on.
232
+ AGENT_RELAY_TAGS: [...new Set(["headless", opts.requestedVia === "mcp" ? "agent-spawned" : "dashboard-spawned", config.hostname, ...(opts.tags ?? [])])].join(","),
226
233
  AGENT_RELAY_CAPS: [...new Set(opts.capabilities ?? [])].join(","),
227
234
  AGENT_RELAY_CAPABILITIES: [...new Set(opts.capabilities ?? [])].join(","),
228
235
  AGENT_RELAY_HEADLESS: "1",
@@ -90,6 +90,14 @@ export async function resolveSpawnWorkspace(input: WorkspaceResolutionInput): Pr
90
90
  const requestedMode = input.workspaceMode ?? "inherit";
91
91
  const sourceCwd = resolve(input.cwd);
92
92
  const probe = await probeWorkspace(sourceCwd);
93
+ // #328 backstop — an EXPLICIT `isolated` request needs a git repo to branch off. Without one,
94
+ // the old code silently downgraded to `shared` (no branch, no worktree, no signal), so a worker
95
+ // asked for an isolated branch instead edited a live tree. Fail loud and actionable instead of
96
+ // fabricating a different mode than the caller asked for. (`inherit`→isolated, from a policy or
97
+ // automation in a non-repo dir, keeps its legitimate shared fallback below.)
98
+ if (requestedMode === "isolated" && !probe.isGitRepo) {
99
+ throw new Error(`workspaceMode "isolated" requires a git repo, but ${sourceCwd} is not one. Pass an explicit cwd under a repo, or omit cwd to inherit the caller's.`);
100
+ }
93
101
  const inheritedMode = input.policyName || input.automationRunId ? "isolated" : "shared";
94
102
  const mode: WorkspaceMode = requestedMode === "inherit" ? inheritedMode : requestedMode;
95
103
  if (mode !== "isolated" || !probe.isGitRepo || !probe.repoRoot) {
@@ -915,7 +923,7 @@ export function mergeWorkspace(input: WorkspaceMergeInput): WorkspaceMergeResult
915
923
  // Nothing to land (ahead=0, clean): the branch tree is already in base. Resolve it
916
924
  // to a terminal state so it leaves the steward queue instead of looping forever in
917
925
  // review_requested (#230). Reclaim the spent worktree/branch when the owner is gone.
918
- if (preview.noop) return resolveNoopMerge(input, worktreePath, repoRoot, branch, head);
926
+ if (preview.noop) return resolveNoopMerge(input, worktreePath, repoRoot, branch, preview, head);
919
927
  if (preview.reason) return head({ status: "review_requested", error: preview.reason });
920
928
  if (preview.conflict) return head({ conflict: true, status: "conflict", error: "merge would conflict with base" });
921
929
 
@@ -924,22 +932,45 @@ export function mergeWorkspace(input: WorkspaceMergeInput): WorkspaceMergeResult
924
932
  }
925
933
 
926
934
  /**
927
- * Resolve a no-op land (#230): ahead=0 with a clean worktree, so the branch's tree
928
- * is already contained in base (never diverged, or already landed via
929
- * squash/cherry-pick/PR). There's nothing to merge mark the workspace terminal
930
- * (`merged`, `noop: true`) so it leaves the steward queue. When the owner is gone
931
- * (`deleteBranch !== false`) the spent worktree/branch are reclaimed too, mirroring a
932
- * terminal land; a live owner keeps its worktree (cleanup reclaims it later). Safe:
933
- * there is no unmerged work to lose.
935
+ * Resolve a no-op land (#230): ahead=0 with a clean worktree, so the branch's tree is
936
+ * already contained in base. Nothing to merge; no unmerged work to lose either way.
937
+ * Liveness splits the outcome, mirroring the real-land path in {@link mergeRebaseFf}:
938
+ * - Live owner (`deleteBranch === false`, from owner liveness #204): recycle onto a
939
+ * fresh branch cut from base and return to `active` (land-and-continue, #206). A noop
940
+ * land must NOT brick a still-connected session terminal `merged` here strands the
941
+ * agent with no checkout once cleanup reclaims the worktree (#327).
942
+ * - Gone owner: reclaim the spent worktree/branch and go terminal `merged`.
934
943
  */
935
944
  function resolveNoopMerge(
936
945
  input: WorkspaceMergeInput,
937
946
  worktreePath: string,
938
947
  repoRoot: string,
939
948
  branch: string | undefined,
949
+ preview: WorkspaceMergePreview,
940
950
  head: (field: Partial<WorkspaceMergeResult>) => WorkspaceMergeResult,
941
951
  ): WorkspaceMergeResult {
942
- if (input.deleteBranch !== false && branch) {
952
+ // Live owner (#327): recycle-to-continue instead of bricking the session.
953
+ if (input.deleteBranch === false) {
954
+ const base = preview.baseRef;
955
+ if (base && branch) {
956
+ const fresh = nextBranchName(repoRoot, branch);
957
+ if (git(["checkout", "-B", fresh, base], worktreePath).ok) {
958
+ // Old branch's tree is already in base (that's what noop means) — safe to drop.
959
+ const oldDeleted = git(["branch", "-D", branch], repoRoot).ok;
960
+ const baseSha = git(["rev-parse", base], worktreePath).stdout || undefined;
961
+ // Recycled onto base, which may declare deps the symlinked node_modules lacks (#51).
962
+ const depsRefresh = refreshWorkspaceDeps(repoRoot, worktreePath);
963
+ const reportDeps = depsRefresh.refreshed || depsRefresh.stale || depsRefresh.error;
964
+ // merged:false → no `branch.landed` notice (nothing landed); newBranch makes the
965
+ // relay repoint the row and return it to `active` rather than terminal `merged`.
966
+ return head({ merged: false, noop: true, status: "active", baseSha, worktreeRemoved: false, branch: fresh, newBranch: fresh, branchDeleted: oldDeleted, ...(reportDeps ? { depsRefresh } : {}), error: undefined });
967
+ }
968
+ }
969
+ // No base or checkout failed — stay live on the current branch, don't strand at `merged`.
970
+ return head({ merged: false, noop: true, status: "active", worktreeRemoved: false, branchDeleted: false, error: undefined });
971
+ }
972
+ // Owner is gone — reclaim the spent worktree/branch and go terminal.
973
+ if (branch) {
943
974
  const removed = git(["worktree", "remove", "--force", worktreePath], repoRoot);
944
975
  const worktreeRemoved = removed.ok;
945
976
  const branchDeleted = worktreeRemoved ? git(["branch", "-D", branch], repoRoot).ok : false;