agent-relay-server 0.23.0 → 0.25.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.
@@ -21,6 +21,26 @@ import type { WorkspaceRecord, WorkspaceStatus } from "./types";
21
21
  // initialize primer (don't brief an agent on a dead workspace). Was duplicated.
22
22
  export const TERMINAL_WORKSPACE_STATUSES = new Set<WorkspaceStatus>(["cleaned", "merged", "abandoned"]);
23
23
 
24
+ // The "handed off, waiting to land" statuses — an agent has finished and the
25
+ // auto-merge-back is responsible for getting the branch onto base. SINGLE HOME:
26
+ // the auto-land consumer (maintenance `autoMergeCleanFastForwards`) and the
27
+ // strand-escalation set MUST both derive from this. They drifted before (#242):
28
+ // `relay_workspace_ready` sets `ready`, but the consumer only scanned
29
+ // `review_requested`, so a clean `ready` worktree was never a merge candidate and
30
+ // parked forever while this phase view kept reporting "healthy, wait." Producer
31
+ // and consumer now read the same set so a `ready` can never silently fall out of
32
+ // the land queue again. (`review_requested` is the same healthy hand-off state —
33
+ // it's also where a failed auto-merge lands for a retry, see routes.ts.)
34
+ export const READY_TO_LAND_STATUSES = new Set<WorkspaceStatus>(["ready", "review_requested"]);
35
+
36
+ // How long a workspace may sit in a ready-to-land status before the directive
37
+ // projection stops saying "healthy, just wait" and surfaces it as needs-attention
38
+ // (#242 watchdog). A clean auto-merge runs ~every 2 min, so a handful of missed
39
+ // sweeps means something is wrong (wrong status filter, no online orchestrator,
40
+ // an unpushed branch, a wedged steward) and the agent/human should be told —
41
+ // instead of the old behavior where it looked healthy for 90 minutes.
42
+ export const LAND_PENDING_STALL_MS = 15 * 60 * 1000;
43
+
24
44
  export type WorkspacePhase =
25
45
  | "working" // active — your turn: commit, then mark ready
26
46
  | "land-pending" // ready | review_requested — handed off; auto-merge will land it
@@ -66,7 +86,17 @@ const READY_ACTION: WorkspaceNextAction = {
66
86
  // Map every WorkspaceStatus to the branch agent's mental model. Statuses that
67
87
  // look scary but are healthy (review_requested, conflict) carry actionNeeded:false
68
88
  // and an explicit "not your job" hint.
69
- export function describeWorkspacePhase(workspace: Pick<WorkspaceRecord, "status" | "branch" | "stewardAgentId">): WorkspacePhaseView {
89
+ //
90
+ // `opts.now` (defaults to wall-clock) drives the #242 stall watchdog: a workspace
91
+ // pending-to-land past LAND_PENDING_STALL_MS flips from the "healthy, wait" view
92
+ // to needs-attention with a real blocker, so the status surface the agent polls
93
+ // can't keep masking a stuck land. The clock is `readyAt` (set once when the agent
94
+ // marks ready, immune to the heartbeat `updated_at` bump) — not `updatedAt`, which
95
+ // keeps ticking on every heartbeat and made the stall look fresh forever.
96
+ export function describeWorkspacePhase(
97
+ workspace: Pick<WorkspaceRecord, "status" | "branch" | "stewardAgentId" | "readyAt">,
98
+ opts: { now?: number; stallMs?: number } = {},
99
+ ): WorkspacePhaseView {
70
100
  switch (workspace.status) {
71
101
  case "active":
72
102
  return {
@@ -78,10 +108,27 @@ export function describeWorkspacePhase(workspace: Pick<WorkspaceRecord, "status"
78
108
  blockers: [],
79
109
  };
80
110
  case "ready":
81
- case "review_requested":
111
+ case "review_requested": {
82
112
  // The #235 crux: these are the SAME healthy "handed off, waiting" state.
83
113
  // `review_requested` reads like an escalation but is the normal post-ready
84
114
  // node; an absent steward is the healthy case, not a stall.
115
+ const now = opts.now ?? Date.now();
116
+ const stallMs = opts.stallMs ?? LAND_PENDING_STALL_MS;
117
+ const pendingMs = typeof workspace.readyAt === "number" ? now - workspace.readyAt : undefined;
118
+ // #242 watchdog: past the bound this is no longer "healthy, wait." Surface it
119
+ // as needs-attention with a real blocker instead of the anti-panic view, so
120
+ // the agent (and the dashboard) stop reporting a wedged land as healthy.
121
+ if (pendingMs !== undefined && pendingMs > stallMs) {
122
+ const mins = Math.round(pendingMs / 60_000);
123
+ return {
124
+ phase: "land-pending",
125
+ headline: `Stalled — handed off ${mins} min ago but still hasn't landed. A clean auto-merge runs every ~2 min, so this is past the healthy window and likely stuck (no online orchestrator, an unpushed branch, or a wedged merge/steward).`,
126
+ hint: "Do NOT merge, push, rebase, or touch the main checkout yourself. Flag this to a human or the repo steward — the auto-merge/steward path isn't progressing and needs attention.",
127
+ actionNeeded: true,
128
+ nextActions: [WAIT_ACTION],
129
+ blockers: [`pending land for ~${mins} min with no progress — auto-merge/steward isn't landing it`],
130
+ };
131
+ }
85
132
  return {
86
133
  phase: "land-pending",
87
134
  headline: "Handed off — waiting for the auto-merge to land your branch. This is the normal, healthy post-ready state (not an escalation).",
@@ -90,6 +137,7 @@ export function describeWorkspacePhase(workspace: Pick<WorkspaceRecord, "status"
90
137
  nextActions: [WAIT_ACTION],
91
138
  blockers: [],
92
139
  };
140
+ }
93
141
  case "merge_planned":
94
142
  return {
95
143
  phase: "landing",
@@ -157,7 +205,7 @@ export function worktreeMcpInstructions(workspace: Pick<WorkspaceRecord, "branch
157
205
  `You are in an isolated git worktree on branch ${branch}, based on ${base} — NOT the main checkout. ${base} moves under you as other agents land in parallel; that's expected.`,
158
206
  "Changes reach the base via: commit your work, then call `relay_workspace_ready`. Relay rebases onto the latest base, lands, and pushes for you.",
159
207
  "Do NOT push, rebase, merge, resolve conflicts, or `cd` into the main checkout — Relay (and a steward, spawned only if a clean auto-merge isn't possible) own all of that.",
160
- "After `ready` the status is `review_requested` that is the NORMAL, healthy hand-off state, not a stall. Call `relay_workspace_status` with `wait:true` to block until your branch lands; you'll then continue on a fresh rebased branch (name gains a `--N` suffix).",
208
+ "After `ready` the status is `ready` (a normal, healthy hand-off state, not a stall). Call `relay_workspace_status` with `wait:true` to block until your branch lands; you'll then continue on a fresh rebased branch (name gains a `--N` suffix).",
161
209
  "Call `relay_workspace_status` anytime to see where you are and the exact next step.",
162
210
  ].join("\n");
163
211
  }