agent-relay-runner 0.18.0 → 0.19.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-runner",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
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.9"
23
+ "agent-relay-sdk": "0.2.10"
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.18.0",
4
+ "version": "0.19.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -2,6 +2,9 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { homedir, tmpdir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import type { Message } from "agent-relay-sdk";
5
+ import { shellEscape as shellQuote } from "agent-relay-sdk/shell-utils";
6
+ import { tmuxCommand, tmuxHasSession } from "agent-relay-sdk/tmux-utils";
7
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
5
8
  import { profileAllowsRelayFeature, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderStatusUpdate, type RunnerSpawnConfig, type SemanticStatus, type SpawnArgs } from "../adapter";
6
9
  import { prepareClaudeProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
7
10
  import { claudeProviderMessageText } from "./claude-delivery";
@@ -410,24 +413,16 @@ function hasSettingsArg(args: string[]): boolean {
410
413
  }
411
414
 
412
415
  export function tmuxSessionName(prefix: string, instanceId: string, label?: string): string {
413
- if (label) return `${prefix}-${label.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase()}`;
416
+ if (label) return `${prefix}-${sanitizeFsName(label, { replacement: "-", collapse: false, lowercase: true })}`;
414
417
  return `${prefix}-${instanceId.slice(0, 8)}`;
415
418
  }
416
419
 
417
420
  export function tmuxSocketName(sessionName: string): string {
418
- return `agent-relay-${sessionName}`.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
421
+ return sanitizeFsName(`agent-relay-${sessionName}`, { replacement: "-", collapse: false, lowercase: true });
419
422
  }
420
423
 
421
- function tmuxCommand(socketName: string | undefined, ...args: string[]): string[] {
422
- return socketName ? ["tmux", "-L", socketName, ...args] : ["tmux", ...args];
423
- }
424
-
425
- export function tmuxHasSession(sessionName: string, socketName?: string): boolean {
426
- const result = Bun.spawnSync(tmuxCommand(socketName, "has-session", "-t", sessionName), {
427
- stdin: "ignore", stdout: "ignore", stderr: "ignore",
428
- });
429
- return result.exitCode === 0;
430
- }
424
+ // Shared tmux helpers; tmuxHasSession re-exported for ./claude consumers + tests.
425
+ export { tmuxHasSession };
431
426
 
432
427
  function captureTmuxPane(sessionName: string, socketName?: string): string {
433
428
  const result = Bun.spawnSync(tmuxCommand(socketName, "capture-pane", "-p", "-t", sessionName, "-S", "-80"), {
@@ -520,7 +515,7 @@ const LAUNCHER_DIR = join(tmpdir(), "agent-relay-launchers");
520
515
 
521
516
  function writeLauncherScript(sessionName: string, shellCmd: string): string {
522
517
  mkdirSync(LAUNCHER_DIR, { recursive: true });
523
- const sanitized = sessionName.replace(/[^a-zA-Z0-9._-]/g, "-");
518
+ const sanitized = sanitizeFsName(sessionName, { replacement: "-", collapse: false });
524
519
  const scriptPath = join(LAUNCHER_DIR, `${sanitized}.sh`);
525
520
  writeFileSync(scriptPath, `#!/usr/bin/env bash\nexec ${shellCmd}\n`, { mode: 0o755 });
526
521
  return scriptPath;
@@ -539,11 +534,8 @@ export function tmuxEnvKeys(env: Record<string, string>, providerEnv: Record<str
539
534
  return [...keys].sort();
540
535
  }
541
536
 
542
- export function shellQuote(arg: string): string {
543
- if (arg.length === 0) return "''";
544
- if (/^[a-zA-Z0-9_./:@=+,-]+$/.test(arg)) return arg;
545
- return `'${arg.replace(/'/g, "'\\''")}'`;
546
- }
537
+ // Shared shell-quoting; re-exported so `./claude` consumers + tests resolve it.
538
+ export { shellQuote };
547
539
 
548
540
  export function findClaudeRigRC(cwd: string): string | null {
549
541
  const home = homedir();
@@ -3,6 +3,7 @@ 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 { isRecord, stringValue } from "agent-relay-sdk";
6
+ import { isPidAlive, killPid, waitForPidsExit } from "agent-relay-sdk/process-utils";
6
7
  import { profileAllowsRelayFeature, providerMessageText, RELAY_CONTEXT, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderPermissionDecisionInput, type ProviderSessionEvent, type ProviderStatusUpdate, type RunnerSpawnConfig, type SpawnArgs, type TerminalAttachSpec } from "../adapter";
7
8
  import { workspaceDepsNoteFromEnv } from "../relay-instructions";
8
9
  import { logger } from "../logger";
@@ -940,12 +941,6 @@ async function processTreePids(rootPids: number[]): Promise<number[]> {
940
941
  return processTreePidsFromTable(table, rootPids);
941
942
  }
942
943
 
943
- function killPid(pid: number, signal: "SIGTERM" | "SIGKILL"): void {
944
- try {
945
- process.kill(pid, signal);
946
- } catch {}
947
- }
948
-
949
944
  function codexRelayContextEnabled(process: ManagedProcess): boolean {
950
945
  const config = process.meta?.config as RunnerSpawnConfig | undefined;
951
946
  return config ? profileAllowsRelayFeature(config, "context") : true;
@@ -962,21 +957,3 @@ function codexLaunchContext(process: ManagedProcess): string | undefined {
962
957
  text,
963
958
  ].join("\n");
964
959
  }
965
-
966
- function isPidAlive(pid: number): boolean {
967
- try {
968
- process.kill(pid, 0);
969
- return true;
970
- } catch {
971
- return false;
972
- }
973
- }
974
-
975
- async function waitForPidsExit(pids: number[], timeoutMs: number): Promise<boolean> {
976
- const deadline = Date.now() + timeoutMs;
977
- while (Date.now() < deadline) {
978
- if (!pids.some(isPidAlive)) return true;
979
- await Bun.sleep(100);
980
- }
981
- return !pids.some(isPidAlive);
982
- }
@@ -2,7 +2,8 @@ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, write
2
2
  import { homedir } from "node:os";
3
3
  import { basename, join } from "node:path";
4
4
  import type { Artifact, Message } from "agent-relay-sdk";
5
- import { isRecord } from "agent-relay-sdk";
5
+ import { errMessage, isRecord } from "agent-relay-sdk";
6
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
6
7
 
7
8
  const DEFAULT_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
8
9
 
@@ -72,7 +73,7 @@ async function messageWithCachedAttachments(
72
73
  nextRefs.push(withCacheMetadata(ref, cached));
73
74
  changed = true;
74
75
  } catch (error) {
75
- options.onError?.(`attachment ${artifactId} cache failed: ${error instanceof Error ? error.message : String(error)}`);
76
+ options.onError?.(`attachment ${artifactId} cache failed: ${errMessage(error)}`);
76
77
  nextRefs.push(withCacheError(ref, error));
77
78
  changed = true;
78
79
  }
@@ -138,7 +139,7 @@ function withCacheError(ref: Record<string, unknown>, error: unknown): Record<st
138
139
  ...metadata,
139
140
  agentRelay: {
140
141
  ...agentRelay,
141
- cacheError: error instanceof Error ? error.message : String(error),
142
+ cacheError: errMessage(error),
142
143
  },
143
144
  },
144
145
  };
@@ -183,5 +184,5 @@ function safeFilename(value: string): string {
183
184
  }
184
185
 
185
186
  function safePathPart(value: string): string {
186
- return value.replace(/[^a-zA-Z0-9_.-]/g, "_").replace(/_+/g, "_").slice(0, 180);
187
+ return sanitizeFsName(value, { replacement: "_", collapseReplacement: true, maxLen: 180 });
187
188
  }
package/src/config.ts CHANGED
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir, hostname } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { stringValue } from "agent-relay-sdk";
5
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
5
6
  import type { ProviderConfig } from "./adapter";
6
7
 
7
8
  interface GlobalRunnerConfig {
@@ -98,7 +99,7 @@ export function providerConfigPublic(config: LoadedProviderConfig): Record<strin
98
99
 
99
100
  export function runnerId(provider: string, cwd: string, label?: string): string {
100
101
  const project = cwd.split("/").filter(Boolean).at(-1) || "workspace";
101
- const cleanLabel = (label || project).replace(/[^a-zA-Z0-9._-]+/g, "-").toLowerCase();
102
+ const cleanLabel = sanitizeFsName(label || project, { replacement: "-", lowercase: true });
102
103
  return `${hostname()}-${provider}-${cleanLabel}-${crypto.randomUUID().slice(0, 8)}`;
103
104
  }
104
105
 
@@ -1,6 +1,6 @@
1
1
  import type { Server, ServerWebSocket } from "bun";
2
2
  import type { Message, ReplyObligation } from "agent-relay-sdk";
3
- import { isRecord } from "agent-relay-sdk";
3
+ import { errMessage, isRecord } from "agent-relay-sdk";
4
4
  import type { ProviderPermissionDecisionInput, ProviderStatusEvent, SemanticStatus, TerminalAttachSpec } from "./adapter";
5
5
  import { logger, parseLogLevel, LOG_LEVELS } from "./logger";
6
6
 
@@ -70,19 +70,19 @@ export function startControlServer(options: ControlServerOptions): ControlServer
70
70
  if (!options.onTerminalAttachSpec) return Response.json({ error: "terminal attach is unavailable" }, { status: 404 });
71
71
  return options.onTerminalAttachSpec()
72
72
  .then((spec) => Response.json(spec))
73
- .catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 409 }));
73
+ .catch((error) => Response.json({ error: errMessage(error) }, { status: 409 }));
74
74
  }
75
75
  if (url.pathname === "/reply-obligations" && req.method === "GET") {
76
76
  if (!options.onReplyObligations) return Response.json({ pending: false, count: 0 });
77
77
  return options.onReplyObligations()
78
78
  .then((obligations) => Response.json(replyObligationSummary(obligations)))
79
- .catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 503 }));
79
+ .catch((error) => Response.json({ error: errMessage(error) }, { status: 503 }));
80
80
  }
81
81
  if (url.pathname === "/reply-obligations/claude-stop" && req.method === "GET") {
82
82
  if (!options.onReplyObligations) return Response.json({});
83
83
  return options.onReplyObligations()
84
84
  .then((obligations) => Response.json(replyObligationStopDecision(obligations)))
85
- .catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 503 }));
85
+ .catch((error) => Response.json({ error: errMessage(error) }, { status: 503 }));
86
86
  }
87
87
  if (url.pathname === "/permissions/request" && req.method === "POST") {
88
88
  return handlePermissionRequest(req, options, pendingPermissionRequests);
@@ -360,7 +360,7 @@ async function handleSessionTurn(req: Request, options: ControlServerOptions): P
360
360
  await options.onSessionTurn({ transcriptPath, lastAssistantMessage });
361
361
  return Response.json({ ok: true });
362
362
  } catch (error) {
363
- return Response.json({ ok: false, reason: error instanceof Error ? error.message : String(error) }, { status: 500 });
363
+ return Response.json({ ok: false, reason: errMessage(error) }, { status: 500 });
364
364
  }
365
365
  }
366
366
 
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ import { AgentRunner } from "./runner";
8
8
  import { loadGlobalConfig, loadProviderConfig, resolveCwd, runnerId } from "./config";
9
9
  import { VERSION } from "./version";
10
10
  import type { AgentProfile, WorkspaceMetadata } from "agent-relay-sdk";
11
- import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
11
+ import { errMessage, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
12
12
 
13
13
  interface CliOptions {
14
14
  provider: "claude" | "codex";
@@ -157,7 +157,7 @@ export async function resolveRunnerToken(input: {
157
157
  const detail = body?.error ? `: ${body.error}` : "";
158
158
  console.warn(`[agent-relay] interactive scoped-token exchange failed (${res.status}${detail}); continuing with existing token. Root-token runtime fallback is deprecated.`);
159
159
  } catch (error) {
160
- console.warn(`[agent-relay] interactive scoped-token exchange failed (${error instanceof Error ? error.message : String(error)}); continuing with existing token. Root-token runtime fallback is deprecated.`);
160
+ console.warn(`[agent-relay] interactive scoped-token exchange failed (${errMessage(error)}); continuing with existing token. Root-token runtime fallback is deprecated.`);
161
161
  }
162
162
  return { token: input.token, rootFallback: true };
163
163
  }
package/src/logger.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
3
4
 
4
5
  // Phase 1 observability (#198): one leveled, runtime-togglable logger for the
5
6
  // Runner and the provider adapters below it. Replaces the ad-hoc scatter of
@@ -26,10 +27,10 @@ export function parseLogLevel(value: string | undefined | null): LogLevel | unde
26
27
  return (LOG_LEVELS as string[]).includes(v) ? (v as LogLevel) : undefined;
27
28
  }
28
29
 
29
- // Matches the runner's safeLogName and the orchestrator's safeMirrorLogName so all
30
- // three resolve the identical filename for a given agent id.
30
+ // Shared with the orchestrator (session-mirror reader) + runner outbox via the
31
+ // SDK helper, so all sites resolve the identical filename for a given agent id.
31
32
  function safeLogName(value: string): string {
32
- return value.replace(/[^a-zA-Z0-9_.-]+/g, "_").slice(0, 180);
33
+ return sanitizeFsName(value, { replacement: "_", maxLen: 180 });
33
34
  }
34
35
 
35
36
  export interface LoggerConfig {
package/src/outbox.ts CHANGED
@@ -2,6 +2,8 @@ import { Database } from "bun:sqlite";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
6
+ import { errMessage } from "agent-relay-sdk";
5
7
  import { logger } from "./logger";
6
8
 
7
9
  // Phase 2 (#196) — the "nothing is ever lost" half. Runner→server events that used to be
@@ -230,7 +232,7 @@ export class Outbox {
230
232
  this.db.query("DELETE FROM outbox WHERE seq = ?").run(row.seq);
231
233
  } catch (error) {
232
234
  const attempts = row.attempts + 1;
233
- const reason = error instanceof Error ? error.message : String(error);
235
+ const reason = errMessage(error);
234
236
  if (attempts >= this.maxAttempts) {
235
237
  this.db.query("UPDATE outbox SET attempts = ?, poisoned = 1 WHERE seq = ?").run(attempts, row.seq);
236
238
  logger.fatal("outbox", `event seq=${row.seq} kind=${row.kind} poisoned after ${attempts} attempts: ${reason}`);
@@ -284,7 +286,7 @@ export class Outbox {
284
286
  }
285
287
 
286
288
  function safeName(value: string): string {
287
- return value.replace(/[^a-zA-Z0-9_.-]+/g, "_").slice(0, 180) || "agent";
289
+ return sanitizeFsName(value, { replacement: "_", maxLen: 180, fallback: "agent" });
288
290
  }
289
291
 
290
292
  function safeParse(json: string): unknown {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
4
5
  import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
5
6
  import { CLAUDE_RELAY_MANUAL } from "./relay-instructions";
6
7
 
@@ -35,34 +36,41 @@ function profileRequiresIsolatedHome(config: RunnerSpawnConfig): boolean {
35
36
  // (claude-rig / host-base launches dodge all of this via onboarded host config +
36
37
  // --dangerously-skip-permissions; only isolated homes need the bootstrap.)
37
38
 
38
- export function prepareCodexProfileHome(config: RunnerSpawnConfig): ProviderHome | undefined {
39
+ // Host auth items symlinked into a fresh isolated home so the provider is
40
+ // launch-ready without a re-login. One list per provider — single source.
41
+ const CODEX_AUTH_ITEMS = ["auth.json", "installation_id"];
42
+ const CLAUDE_AUTH_ITEMS = [".credentials.json", "statsig"];
43
+
44
+ // Shared skeleton for both providers: gate on isolated-profile, make the
45
+ // instance-keyed home, run the provider-specific first-run bootstrap. The
46
+ // bootstrap step is the only genuinely provider-specific part.
47
+ export function prepareProviderHome(provider: "claude" | "codex", config: RunnerSpawnConfig): ProviderHome | undefined {
39
48
  if (!profileRequiresIsolatedHome(config)) return undefined;
40
- const target = providerHomePath("codex", config);
49
+ const target = providerHomePath(provider, config);
41
50
  mkdirSync(target, { recursive: true });
42
- return { path: target, authLinked: bootstrapCodexFirstRun(target, config) };
51
+ const authLinked = provider === "codex"
52
+ ? bootstrapCodexFirstRun(target, config)
53
+ : bootstrapClaudeFirstRun(target, config);
54
+ return { path: target, authLinked };
43
55
  }
44
56
 
57
+ export const prepareCodexProfileHome = (config: RunnerSpawnConfig) => prepareProviderHome("codex", config);
58
+ export const prepareClaudeProfileHome = (config: RunnerSpawnConfig) => prepareProviderHome("claude", config);
59
+
45
60
  function bootstrapCodexFirstRun(codexHome: string, config: RunnerSpawnConfig): string[] {
46
61
  trustWorkspaceForCodex(codexHome, config);
47
62
  const sourceHome = process.env.CODEX_HOME || join(homedir(), ".codex");
48
- return linkExistingAuthItems(sourceHome, codexHome, ["auth.json", "installation_id"]);
63
+ return linkExistingAuthItems(sourceHome, codexHome, CODEX_AUTH_ITEMS);
49
64
  }
50
65
 
51
- export function prepareClaudeProfileHome(config: RunnerSpawnConfig): ProviderHome | undefined {
52
- if (!profileRequiresIsolatedHome(config)) return undefined;
53
- const target = providerHomePath("claude", config);
54
- mkdirSync(target, { recursive: true });
66
+ function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig): string[] {
55
67
  // Only inject the Relay usage manual when the profile actually wants a Relay
56
68
  // surface. An isolated-research profile (relay.context disabled) must not get
57
69
  // agent-relay communication instructions written into its config home.
58
- if (profileAllowsRelayFeature(config, "context")) writeClaudeRelayManual(target);
59
- return { path: target, authLinked: bootstrapClaudeFirstRun(target, config) };
60
- }
61
-
62
- function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig): string[] {
70
+ if (profileAllowsRelayFeature(config, "context")) writeClaudeRelayManual(claudeHome);
63
71
  seedClaudeConfigIfMissing(claudeHome, config);
64
72
  const sourceHome = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
65
- return linkExistingAuthItems(sourceHome, claudeHome, [".credentials.json", "statsig"]);
73
+ return linkExistingAuthItems(sourceHome, claudeHome, CLAUDE_AUTH_ITEMS);
66
74
  }
67
75
 
68
76
  function seedClaudeConfigIfMissing(claudeHome: string, config: RunnerSpawnConfig): void {
@@ -104,7 +112,7 @@ function providerHomePath(provider: "claude" | "codex", config: RunnerSpawnConfi
104
112
  }
105
113
 
106
114
  function sanitizePathPart(value: string): string {
107
- return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 120) || "profile";
115
+ return sanitizeFsName(value, { replacement: "-", collapse: false, maxLen: 120, fallback: "profile" });
108
116
  }
109
117
 
110
118
  function linkExistingAuthItems(sourceHome: string, targetHome: string, items: string[]): string[] {
@@ -37,7 +37,7 @@ export function workspaceDepsNote(input: { mode?: string | null; depsMode?: stri
37
37
  if (input.mode !== "isolated") return "";
38
38
  switch (input.depsMode) {
39
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.";
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 typecheck/build fails on a missing module (a dependency added to the base AFTER this worktree was created), run `agent-relay workspace deps` — it re-provisions only the stale dirs with a real isolated install, safely, without touching the shared node_modules. If you genuinely need to change dependencies in isolation, ask the host to spawn with AGENT_RELAY_WORKSPACE_DEPS=install.";
41
41
  case "none":
42
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
43
  default:
@@ -58,7 +58,7 @@ export function workspaceLifecycleNote(input: { mode?: string | null; branch?: s
58
58
  const base = input.baseRef ? `\`${input.baseRef}\`` : "the base branch";
59
59
  return [
60
60
  `[agent-relay] Isolated workspace: you are in a git worktree on branch ${branch}, based on ${base} — NOT the main checkout. Other agents may work in parallel and land to ${base}, so ${base} will move under you. That is expected; don't fight it.`,
61
- `Do NOT manually rebase, merge, or \`git push\` your branch. Just commit your work here. When the task is done, run \`agent-relay workspace ready\` — Relay rebases onto the latest ${base}, lands your work, and pushes for you. (\`agent-relay workspace status\` shows the current state.)`,
61
+ `Do NOT push this branch yourself — not with \`git push\`, not with \`tl push\` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, run \`agent-relay workspace ready\` — Relay rebases onto the latest ${base}, lands your work, and pushes for you. (\`agent-relay workspace status\` shows the current state.)`,
62
62
  ].join("\n");
63
63
  }
64
64
 
@@ -1,4 +1,5 @@
1
1
  import type { ReplyObligation } from "agent-relay-sdk";
2
+ import { errMessage } from "agent-relay-sdk";
2
3
  import { logger } from "./logger";
3
4
 
4
5
  // Phase 2 (#196) — the crux. The Claude Stop hook used to ask the server, synchronously
@@ -103,7 +104,7 @@ export class ReplyObligationCache {
103
104
  } catch (error) {
104
105
  // Server-down is a non-event: keep serving the last snapshot. Debug, not error —
105
106
  // this is expected during outages and must not spam the log.
106
- logger.debug("obligation-cache", `refresh failed, serving cached snapshot (${this.snapshot.length}): ${error instanceof Error ? error.message : String(error)}`);
107
+ logger.debug("obligation-cache", `refresh failed, serving cached snapshot (${this.snapshot.length}): ${errMessage(error)}`);
107
108
  }
108
109
  }
109
110
  }
package/src/runner.ts CHANGED
@@ -3,7 +3,7 @@ import { closeSync, mkdirSync, openSync, readSync, statSync, writeFileSync } fro
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import type { AgentProfile, ContextState, Message, MessageSessionMeta, ProviderCapabilities, SendMessageInput, TaskStatusInput, WorkspaceMetadata } from "agent-relay-sdk";
6
- import { RelayBusClient, RelayHttpClient } from "agent-relay-sdk";
6
+ import { errMessage, RelayBusClient, RelayHttpClient } from "agent-relay-sdk";
7
7
  import { contextStateFromProbeMetrics, readContextProbeState } from "agent-relay-sdk/context-probe";
8
8
  import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderStatusUpdate, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
9
9
  import { messagesWithCachedAttachments } from "./attachment-cache";
@@ -574,7 +574,7 @@ export class AgentRunner {
574
574
  ...(providerResult ? { providerResult } : {}),
575
575
  });
576
576
  } catch (error) {
577
- await this.updateCommand(commandId, "failed", undefined, error instanceof Error ? error.message : String(error)).catch(() => {});
577
+ await this.updateCommand(commandId, "failed", undefined, errMessage(error)).catch(() => {});
578
578
  } finally {
579
579
  this.claims.finishClaim("command", commandId);
580
580
  if (exitAfterCommand) {
@@ -1078,7 +1078,7 @@ export class AgentRunner {
1078
1078
  },
1079
1079
  });
1080
1080
  } catch (error) {
1081
- logger.error("outbox", `failed to queue hook-fatal report: ${error instanceof Error ? error.message : String(error)}`);
1081
+ logger.error("outbox", `failed to queue hook-fatal report: ${errMessage(error)}`);
1082
1082
  }
1083
1083
  }
1084
1084
 
@@ -1474,7 +1474,7 @@ export class AgentRunner {
1474
1474
  fallbackBaseDir: process.env.AGENT_RELAY_ORCHESTRATOR_BASE_DIR,
1475
1475
  });
1476
1476
  } catch (error) {
1477
- this.logRunnerDiagnostic(`session scratch setup failed: ${error instanceof Error ? error.message : String(error)}`);
1477
+ this.logRunnerDiagnostic(`session scratch setup failed: ${errMessage(error)}`);
1478
1478
  }
1479
1479
  }
1480
1480
 
@@ -1873,7 +1873,7 @@ function commandTimeoutMs(params: Record<string, unknown>, fallback = 10_000): n
1873
1873
  }
1874
1874
 
1875
1875
  export function shouldLogDeliveryFailure(error: unknown): boolean {
1876
- const message = error instanceof Error ? error.message : String(error);
1876
+ const message = errMessage(error);
1877
1877
  return message !== "no Claude monitor connected";
1878
1878
  }
1879
1879