agent-relay-runner 0.11.3 → 0.11.5

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-runner",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "directory": "runner"
21
21
  },
22
22
  "dependencies": {
23
- "agent-relay-sdk": "0.2.2"
23
+ "agent-relay-sdk": "0.2.4"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "latest",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.11.3",
4
+ "version": "0.11.5",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import type { Message } from "agent-relay-sdk";
2
2
  import { claudeProviderMessageText } from "../../../src/adapters/claude-delivery";
3
- import { CLAUDE_READ_ONLY_RELAY_CONTEXT, CLAUDE_RELAY_CONTEXT } from "../../../src/relay-instructions";
3
+ import { CLAUDE_READ_ONLY_RELAY_CONTEXT, CLAUDE_RELAY_CONTEXT, workspaceDepsNoteFromEnv } from "../../../src/relay-instructions";
4
4
 
5
5
  const port = process.env.AGENT_RELAY_RUNNER_PORT;
6
6
  if (!port) process.exit(0);
@@ -16,6 +16,11 @@ ws.onmessage = (event) => {
16
16
  deliveryCount += 1;
17
17
  if (firstDelivery) {
18
18
  console.log(process.env.AGENT_RELAY_APPROVAL === "read-only" ? CLAUDE_READ_ONLY_RELAY_CONTEXT : CLAUDE_RELAY_CONTEXT);
19
+ const wsNote = workspaceDepsNoteFromEnv();
20
+ if (wsNote) {
21
+ console.log("");
22
+ console.log(wsNote);
23
+ }
19
24
  console.log("");
20
25
  firstDelivery = false;
21
26
  }
@@ -3,6 +3,13 @@ import { homedir } from "node:os";
3
3
  import { basename, join, resolve } from "node:path";
4
4
  import type { ContextState, Message } from "agent-relay-sdk";
5
5
  import { profileAllowsRelayFeature, providerMessageText, RELAY_CONTEXT, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderPermissionDecisionInput, type ProviderStatusUpdate, type RunnerSpawnConfig, type SpawnArgs, type TerminalAttachSpec } from "../adapter";
6
+ import { workspaceDepsNoteFromEnv } from "../relay-instructions";
7
+
8
+ /** Relay context prepended to a Codex agent's first turn: the standard relay
9
+ * blurb plus, when running in an isolated workspace, the deps caveat (#159). */
10
+ function codexRelayContextBlock(): string {
11
+ return [RELAY_CONTEXT, workspaceDepsNoteFromEnv()].filter(Boolean).join("\n\n");
12
+ }
6
13
  import { prepareCodexProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
7
14
  import { CodexAppClient, type ClientEvent } from "./codex-client";
8
15
 
@@ -159,7 +166,7 @@ export class CodexAdapter implements ProviderAdapter {
159
166
  text,
160
167
  ].filter(Boolean).join("\n\n");
161
168
  if (codexRelayContextEnabled(process) && !process.meta?.relayContextSent) {
162
- input = RELAY_CONTEXT + "\n\n" + input;
169
+ input = codexRelayContextBlock() + "\n\n" + input;
163
170
  process.meta = { ...(process.meta ?? {}), relayContextSent: true };
164
171
  }
165
172
  console.error(`[agent-relay] starting Codex initial prompt in thread ${threadId}`);
@@ -171,7 +178,7 @@ export class CodexAdapter implements ProviderAdapter {
171
178
  const threadId = await ensureCodexThread(process);
172
179
  let text = [codexLaunchContext(process), providerMessageText(messages)].filter(Boolean).join("\n\n");
173
180
  if (codexRelayContextEnabled(process) && !process.meta?.relayContextSent) {
174
- text = RELAY_CONTEXT + "\n\n" + text;
181
+ text = codexRelayContextBlock() + "\n\n" + text;
175
182
  process.meta = { ...(process.meta ?? {}), relayContextSent: true };
176
183
  }
177
184
  console.error(codexDeliveryNotice(messages, threadId));
@@ -23,3 +23,38 @@ export const CLAUDE_READ_ONLY_RELAY_CONTEXT = `${CLAUDE_RELAY_CONTEXT}
23
23
  This Claude session is running with restricted read-only Relay permissions. Do not invoke Agent Relay skills. If you need to reply, use this Bash command shape:
24
24
 
25
25
  agent-relay /reply <messageId> "<your reply>"`;
26
+
27
+ /**
28
+ * Provider-agnostic caveat injected into every spawned agent that runs in an
29
+ * isolated workspace, regardless of which project it is working in. Isolated
30
+ * workspaces are git worktrees whose node_modules are provisioned by the
31
+ * orchestrator (see AGENT_RELAY_WORKSPACE_DEPS): symlinked from the main
32
+ * checkout by default. The agent needs to know this so it doesn't run a clean
33
+ * install that mutates the shared node_modules. Returns "" when no note applies
34
+ * (shared workspace, or deps installed fresh / unknown).
35
+ */
36
+ export function workspaceDepsNote(input: { mode?: string | null; depsMode?: string | null }): string {
37
+ if (input.mode !== "isolated") return "";
38
+ switch (input.depsMode) {
39
+ case "symlink":
40
+ return "[agent-relay] Isolated workspace: this is a git worktree, and its node_modules are SYMLINKED from the main checkout — dependencies are already installed and ready to use. Do NOT run a clean dependency install (`bun install` / `npm install` / `pnpm install`): it writes through the symlink and mutates the main checkout's shared node_modules. Build caches written under node_modules are shared too. If you genuinely need to change dependencies in isolation, ask the host to spawn with AGENT_RELAY_WORKSPACE_DEPS=install.";
41
+ case "none":
42
+ return "[agent-relay] Isolated workspace: dependencies were not provisioned (AGENT_RELAY_WORKSPACE_DEPS=none). You may need to install node_modules before typecheck/test/build work.";
43
+ default:
44
+ return "";
45
+ }
46
+ }
47
+
48
+ /** Resolve the workspace deps caveat from the runner/monitor environment.
49
+ * AGENT_RELAY_WORKSPACE_JSON carries the resolved workspace metadata (mode +
50
+ * deps) and is the authoritative source. Best-effort: never throws. */
51
+ export function workspaceDepsNoteFromEnv(env: Record<string, string | undefined> = process.env): string {
52
+ const json = env.AGENT_RELAY_WORKSPACE_JSON;
53
+ if (!json) return "";
54
+ try {
55
+ const parsed = JSON.parse(json) as { mode?: string; deps?: { mode?: string } };
56
+ return workspaceDepsNote({ mode: parsed.mode ?? null, depsMode: parsed.deps?.mode ?? null });
57
+ } catch {
58
+ return "";
59
+ }
60
+ }
package/src/runner.ts CHANGED
@@ -361,6 +361,7 @@ export class AgentRunner {
361
361
  const deliverable: Message[] = [];
362
362
  const providerAlreadyBusy = this.claims.reasons().includes("provider-turn");
363
363
  for (const message of messages) {
364
+ let toDeliver = message;
364
365
  if (message.claimable) {
365
366
  const claimed = await this.http.claimMessageResult(message.id, this.agentId).catch(() => ({ ok: false, claimExpiresAt: undefined }));
366
367
  if (!claimed.ok) continue;
@@ -374,9 +375,12 @@ export class AgentRunner {
374
375
  agentId: this.agentId,
375
376
  metadata: { messageId: message.id, completedBy: "runner" },
376
377
  }).catch((error) => console.error(`[runner] task ${taskId} in_progress update failed: ${error}`));
378
+ // Runner owns claim + status here; drop the server's self-claim instruction
379
+ // so the agent doesn't improvise a stray claim send (see stripRunnerClaimedGuidance).
380
+ toDeliver = { ...message, body: stripRunnerClaimedGuidance(message.body) };
377
381
  }
378
382
  }
379
- deliverable.push(message);
383
+ deliverable.push(toDeliver);
380
384
  }
381
385
  if (deliverable.length === 0) {
382
386
  this.delivering = false;
@@ -1075,6 +1079,19 @@ export function taskIdFromMessage(message: Pick<Message, "payload">): number | u
1075
1079
  return Number.isSafeInteger(taskId) ? taskId as number : undefined;
1076
1080
  }
1077
1081
 
1082
+ // The server appends a self-claim/status instruction to task message bodies
1083
+ // (taskMessageBody in src/db.ts: "Claim this task before working it, then update
1084
+ // task status when finished."). For runner-managed agents the runner already owns
1085
+ // claim + status, so that line is not only redundant but actively harmful: the
1086
+ // agent tries to follow it, has no real claim command, and improvises a stray
1087
+ // `agent-relay /send-claimable claim <id>` that leaves an orphan claimable message.
1088
+ // Strip the trailing guidance from messages the runner has auto-claimed.
1089
+ const RUNNER_CLAIMED_GUIDANCE_RE = /\s*Claim this task before working it, then update task status when finished\.\s*$/;
1090
+
1091
+ export function stripRunnerClaimedGuidance(body: string): string {
1092
+ return body.replace(RUNNER_CLAIMED_GUIDANCE_RE, "");
1093
+ }
1094
+
1078
1095
  function csvTags(raw: string | undefined): string[] {
1079
1096
  return (raw || "").split(",").map((tag) => tag.trim()).filter(Boolean);
1080
1097
  }