agent-relay-runner 0.29.0 → 0.30.1

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.29.0",
3
+ "version": "0.30.1",
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.18"
23
+ "agent-relay-sdk": "0.2.19"
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.29.0",
4
+ "version": "0.30.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/adapter.ts CHANGED
@@ -174,7 +174,7 @@ export function profileAllowsRelayFeature(config: RunnerSpawnConfig, feature: ke
174
174
 
175
175
  export const RELAY_CONTEXT = `[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id — relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present you can stand up long-living child agents and shut down your own — find them later with relay_find_agents spawnedBy:me. CLI fallback: agent-relay /reply <messageId> --stdin < response.md; if a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide`;
176
176
 
177
- const PROVIDER_MESSAGE_BODY_PREVIEW_CHARS = 4000;
177
+ export const PROVIDER_MESSAGE_BODY_PREVIEW_CHARS = 4000;
178
178
 
179
179
  function attachmentRefs(message: Message): Record<string, unknown>[] {
180
180
  const payloadRefs = message.payload?.attachments;
@@ -1,8 +1,7 @@
1
1
  import type { Message } from "agent-relay-sdk";
2
2
  import { isRecord } from "agent-relay-sdk";
3
- import { isNotificationMessage, NOTIFICATION_NUDGE, providerAttachmentText } from "../adapter";
3
+ import { isNotificationMessage, NOTIFICATION_NUDGE, PROVIDER_MESSAGE_BODY_PREVIEW_CHARS, providerAttachmentText } from "../adapter";
4
4
 
5
- const PROVIDER_MESSAGE_BODY_PREVIEW_CHARS = 4000;
6
5
  const REMINDER_EVERY_DELIVERIES = 5;
7
6
 
8
7
  interface ClaudeDeliveryTextOptions {
@@ -48,7 +48,7 @@ export class ClaudeAdapter implements ProviderAdapter {
48
48
  await this.shutdownTmux(tmuxSession, opts, tmuxSocket);
49
49
  return;
50
50
  }
51
- await terminateProcess(process, opts);
51
+ await terminateSingleProcess(process, opts);
52
52
  }
53
53
 
54
54
  async compact(process: ManagedProcess): Promise<Record<string, unknown>> {
@@ -586,7 +586,7 @@ export function findClaudeRigRC(cwd: string): string | null {
586
586
  }
587
587
  }
588
588
 
589
- async function terminateProcess(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
589
+ async function terminateSingleProcess(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
590
590
  const proc = process.process;
591
591
  if (!proc) return;
592
592
  try {
@@ -3,7 +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
+ import { isPidAlive, killPid, processTreePids, processTreePidsFromTable, waitForPidsExit } from "agent-relay-sdk/process-utils";
7
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";
8
8
  import { workspaceDepsNoteFromEnv } from "../relay-instructions";
9
9
  import { relayMcpCodexConfigArgs, tomlString } from "../relay-mcp";
@@ -18,6 +18,7 @@ function codexRelayContextBlock(): string {
18
18
  import { prepareCodexProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
19
19
  import { CodexAppClient, type ClientEvent } from "./codex-client";
20
20
 
21
+ export { processTreePidsFromTable };
21
22
  export const DEFAULT_CODEX_TOOL_OUTPUT_TOKEN_LIMIT = 12_000;
22
23
 
23
24
  type PendingCodexApproval = {
@@ -28,12 +29,22 @@ type PendingCodexApproval = {
28
29
  view: Record<string, unknown>;
29
30
  };
30
31
 
32
+ type PendingCodexCompact = {
33
+ threadId: string;
34
+ timeout: ReturnType<typeof setTimeout>;
35
+ resolve: () => void;
36
+ reject: (error: Error) => void;
37
+ };
38
+
39
+ const CODEX_COMPACT_TIMEOUT_MS = 10 * 60 * 1000;
40
+
31
41
  export class CodexAdapter implements ProviderAdapter {
32
42
  readonly provider = "codex";
33
43
  private statusCb: (status: ProviderStatusUpdate) => void = () => {};
34
44
  private sessionEventCb: (event: ProviderSessionEvent) => void = () => {};
35
45
  private readonly subagentThreads = new Map<string, { label?: string; role?: string; parentId?: string }>();
36
46
  private readonly pendingApprovals = new Map<string, PendingCodexApproval>();
47
+ private pendingCompact?: PendingCodexCompact;
37
48
  // Active turn id for the main thread, captured from turn/started so an interrupt
38
49
  // can target the in-flight turn. Cleared on turn/completed.
39
50
  private activeTurnId?: string;
@@ -65,6 +76,7 @@ export class CodexAdapter implements ProviderAdapter {
65
76
  }
66
77
 
67
78
  private resetThreadState(): void {
79
+ this.cancelPendingCompact(new Error("Codex compact canceled by thread reset"));
68
80
  this.subagentThreads.clear();
69
81
  this.pendingApprovals.clear();
70
82
  this.activeTurnId = undefined;
@@ -179,7 +191,7 @@ export class CodexAdapter implements ProviderAdapter {
179
191
  async shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
180
192
  const client = process.meta?.client as CodexAppClient | undefined;
181
193
  client?.close();
182
- await terminateProcess(process, opts);
194
+ await terminateProcessTree(process, opts);
183
195
  }
184
196
 
185
197
  async compact(process: ManagedProcess): Promise<Record<string, unknown>> {
@@ -195,9 +207,12 @@ export class CodexAdapter implements ProviderAdapter {
195
207
  context: { ...currentContext, lifecycleState: "compacting", lastUpdatedAt: Date.now() },
196
208
  };
197
209
  }
210
+ const completion = this.waitForPendingCompact(threadId);
198
211
  try {
199
212
  await client.threadCompactStart(threadId);
213
+ await completion;
200
214
  } catch (error) {
215
+ this.clearPendingCompact(threadId);
201
216
  this.statusCb({ status: "idle", reason: "provider-turn" });
202
217
  throw error;
203
218
  }
@@ -377,6 +392,12 @@ export class CodexAdapter implements ProviderAdapter {
377
392
  updatedAt: Date.now(),
378
393
  },
379
394
  });
395
+ } else {
396
+ const client = process.meta?.client as CodexAppClient | undefined;
397
+ logger.warn("codex", `rejecting unknown Codex server-request method: ${event.message.method}`);
398
+ client?.rejectServerRequest(event.message.id, -32601, `Method not found: ${event.message.method}`, {
399
+ method: event.message.method,
400
+ });
380
401
  }
381
402
  return;
382
403
  }
@@ -426,8 +447,13 @@ export class CodexAdapter implements ProviderAdapter {
426
447
  this.finishMainTurn();
427
448
  }
428
449
  }
450
+ if ((method.includes("thread/compacted") || method.includes("thread.compacted")) && threadId) {
451
+ this.completePendingCompact(threadId);
452
+ }
429
453
  if ((method.includes("item/completed") || method.includes("item.completed")) && !isSubagent) {
430
- this.handleCodexItem(isRecord(params?.item) ? params.item : undefined);
454
+ const item = isRecord(params?.item) ? params.item : undefined;
455
+ this.handleCodexItem(item);
456
+ if (codexItemIsCompactionComplete(item) && threadId) this.completePendingCompact(threadId);
431
457
  }
432
458
  if (!isSubagent) this.handleCodexItemDelta(method, params);
433
459
  if (method.includes("thread/status") || method.includes("thread.status")) {
@@ -553,6 +579,47 @@ export class CodexAdapter implements ProviderAdapter {
553
579
  this.statusCb({ status: "idle", reason: "provider-turn", id: turnId });
554
580
  }
555
581
 
582
+ private waitForPendingCompact(threadId: string): Promise<void> {
583
+ if (this.pendingCompact) throw new Error("Codex compact is already in progress");
584
+ return new Promise<void>((resolve, reject) => {
585
+ const pending: PendingCodexCompact = {
586
+ threadId,
587
+ resolve,
588
+ reject,
589
+ timeout: setTimeout(() => {
590
+ if (this.pendingCompact !== pending) return;
591
+ this.pendingCompact = undefined;
592
+ reject(new Error(`Codex compact timed out after ${Math.round(CODEX_COMPACT_TIMEOUT_MS / 1000)}s`));
593
+ }, CODEX_COMPACT_TIMEOUT_MS),
594
+ };
595
+ this.pendingCompact = pending;
596
+ });
597
+ }
598
+
599
+ private completePendingCompact(threadId: string): boolean {
600
+ const pending = this.pendingCompact;
601
+ if (!pending || pending.threadId !== threadId) return false;
602
+ clearTimeout(pending.timeout);
603
+ this.pendingCompact = undefined;
604
+ pending.resolve();
605
+ return true;
606
+ }
607
+
608
+ private clearPendingCompact(threadId?: string): void {
609
+ const pending = this.pendingCompact;
610
+ if (!pending || (threadId && pending.threadId !== threadId)) return;
611
+ clearTimeout(pending.timeout);
612
+ this.pendingCompact = undefined;
613
+ }
614
+
615
+ private cancelPendingCompact(error: Error): void {
616
+ const pending = this.pendingCompact;
617
+ if (!pending) return;
618
+ clearTimeout(pending.timeout);
619
+ this.pendingCompact = undefined;
620
+ pending.reject(error);
621
+ }
622
+
556
623
  private providerStateFromThreadStatus(status: unknown, params?: Record<string, unknown>): Record<string, unknown> | undefined {
557
624
  const state = codexProviderStateFromThreadStatus(status, params);
558
625
  if (state?.state !== "blocked" || state.reason !== "waitingOnApproval" || state.pendingApproval) return state;
@@ -565,6 +632,11 @@ function codexItemId(item: Record<string, unknown> | undefined): string | undefi
565
632
  return stringValue(item?.itemId) ?? stringValue(item?.id);
566
633
  }
567
634
 
635
+ function codexItemIsCompactionComplete(item: Record<string, unknown> | undefined): boolean {
636
+ const type = stringValue(item?.type);
637
+ return type === "context_compaction" || type === "contextCompaction" || type === "compaction";
638
+ }
639
+
568
640
  function codexDeltaText(params: Record<string, unknown> | undefined): string {
569
641
  const delta = params?.delta;
570
642
  if (typeof delta === "string") return delta;
@@ -1101,7 +1173,7 @@ async function connectWithRetry(client: CodexAppClient, attempts = 40): Promise<
1101
1173
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
1102
1174
  }
1103
1175
 
1104
- async function terminateProcess(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
1176
+ async function terminateProcessTree(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
1105
1177
  const processes = [
1106
1178
  process.meta?.tui as Bun.Subprocess | undefined,
1107
1179
  process.meta?.appServer as Bun.Subprocess | undefined,
@@ -1130,37 +1202,6 @@ async function terminateProcess(process: ManagedProcess, opts: { graceful: boole
1130
1202
  }
1131
1203
  }
1132
1204
 
1133
- export function processTreePidsFromTable(table: string, rootPids: number[]): number[] {
1134
- const childrenByParent = new Map<number, number[]>();
1135
- for (const line of table.split("\n")) {
1136
- const match = line.trim().match(/^(\d+)\s+(\d+)$/);
1137
- if (!match) continue;
1138
- const pid = Number(match[1]);
1139
- const ppid = Number(match[2]);
1140
- if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;
1141
- const children = childrenByParent.get(ppid) ?? [];
1142
- children.push(pid);
1143
- childrenByParent.set(ppid, children);
1144
- }
1145
-
1146
- const seen = new Set<number>();
1147
- const visit = (pid: number) => {
1148
- if (seen.has(pid)) return;
1149
- seen.add(pid);
1150
- for (const child of childrenByParent.get(pid) ?? []) visit(child);
1151
- };
1152
- for (const pid of rootPids) visit(pid);
1153
- return [...seen].sort((a, b) => b - a);
1154
- }
1155
-
1156
- async function processTreePids(rootPids: number[]): Promise<number[]> {
1157
- if (rootPids.length === 0) return [];
1158
- const proc = Bun.spawn(["ps", "-e", "-o", "pid=,ppid="], { stdout: "pipe", stderr: "ignore" });
1159
- const table = await new Response(proc.stdout).text();
1160
- await proc.exited;
1161
- return processTreePidsFromTable(table, rootPids);
1162
- }
1163
-
1164
1205
  function codexRelayContextEnabled(process: ManagedProcess): boolean {
1165
1206
  const config = process.meta?.config as RunnerSpawnConfig | undefined;
1166
1207
  return config ? profileAllowsRelayFeature(config, "context") : true;
package/src/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  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
- import { stringValue } from "agent-relay-sdk";
4
+ import { DEFAULT_RELAY_URL, stringValue } from "agent-relay-sdk";
5
5
  import { sanitizeFsName } from "agent-relay-sdk/fs-name";
6
6
  import type { ProviderConfig } from "./adapter";
7
7
 
@@ -15,8 +15,6 @@ interface LoadedProviderConfig extends ProviderConfig {
15
15
  path: string;
16
16
  }
17
17
 
18
- const DEFAULT_RELAY_URL = "http://127.0.0.1:4850";
19
-
20
18
  function agentRelayHome(): string {
21
19
  return process.env.AGENT_RELAY_HOME || join(homedir(), ".agent-relay");
22
20
  }