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.
|
|
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.
|
|
23
|
+
"agent-relay-sdk": "0.2.19"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
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 {
|
package/src/adapters/claude.ts
CHANGED
|
@@ -48,7 +48,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
48
48
|
await this.shutdownTmux(tmuxSession, opts, tmuxSocket);
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
await
|
|
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
|
|
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 {
|
package/src/adapters/codex.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
}
|