agent-relay-codex 0.4.35 → 0.6.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/bin/agent-relay-codex.ts +1 -0
- package/hooks/session-start.ts +6 -64
- package/live-sidecar.ts +156 -16
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/relay.ts +6 -1
package/bin/agent-relay-codex.ts
CHANGED
|
@@ -952,6 +952,7 @@ async function start(args: string[]): Promise<void> {
|
|
|
952
952
|
AGENT_RELAY_CODEX_PACKAGE_ROOT: activePackageRoot(),
|
|
953
953
|
AGENT_RELAY_CODEX_RUN_ID: runId,
|
|
954
954
|
AGENT_RELAY_CODEX_RUNTIME_DIR: runDir,
|
|
955
|
+
AGENT_RELAY_CONTEXT_PATH: join(runDir, "agent-context.json"),
|
|
955
956
|
CODEX_APP_SERVER_URL: listenUrl,
|
|
956
957
|
CODEX_THREAD_MODE: threadMode,
|
|
957
958
|
CODEX_THREAD_ID: threadId || undefined,
|
package/hooks/session-start.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { parseApprovalMode } from "../approval.ts";
|
|
6
6
|
import { loadAgentRelayProfile } from "../profile.ts";
|
|
7
|
-
import { buildAgentIdentity } from "../relay.ts";
|
|
8
7
|
import { pickThreadId, type HookInput } from "./session-start-lib.ts";
|
|
9
8
|
|
|
10
9
|
type HookHandshake = {
|
|
@@ -84,8 +83,7 @@ if (process.env.AGENT_RELAY_DISABLED === "1") {
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
if (process.env.AGENT_RELAY_CODEX_MANAGED === "1") {
|
|
87
|
-
|
|
88
|
-
process.exit(0);
|
|
86
|
+
outputContinue();
|
|
89
87
|
}
|
|
90
88
|
|
|
91
89
|
if (!appServerUrl || !runId) {
|
|
@@ -99,6 +97,7 @@ const sessionKey = sanitize(threadId || "auto");
|
|
|
99
97
|
const sessionDir = join(runtimeDir, sessionKey);
|
|
100
98
|
const pidPath = join(sessionDir, "sidecar.pid");
|
|
101
99
|
const statePath = join(sessionDir, "live-state.json");
|
|
100
|
+
const contextPath = join(sessionDir, "agent-context.json");
|
|
102
101
|
const logPath = join(sessionDir, "sidecar.log");
|
|
103
102
|
mkdirSync(sessionDir, { recursive: true });
|
|
104
103
|
mkdirSync(runtimeDir, { recursive: true });
|
|
@@ -114,24 +113,7 @@ if (activePid !== null) {
|
|
|
114
113
|
threadId: threadId || undefined,
|
|
115
114
|
timestamp: new Date().toISOString(),
|
|
116
115
|
});
|
|
117
|
-
|
|
118
|
-
const identity = buildAgentIdentity({
|
|
119
|
-
relayUrl,
|
|
120
|
-
cwd,
|
|
121
|
-
rig,
|
|
122
|
-
label: profile.label,
|
|
123
|
-
capabilities: profile.capabilities,
|
|
124
|
-
tags: profile.tags,
|
|
125
|
-
channels: profile.channels,
|
|
126
|
-
profileName: profile.profileName,
|
|
127
|
-
profileMeta: profile.meta,
|
|
128
|
-
threadId,
|
|
129
|
-
model: input.model,
|
|
130
|
-
appServerUrl,
|
|
131
|
-
});
|
|
132
|
-
outputContext(buildStartupContext(identity.id, relayUrl, approvalMode));
|
|
133
|
-
}
|
|
134
|
-
outputContext(`Agent Relay sidecar already running (pid ${activePid}). Relay URL: ${relayUrl}. Approval mode: ${approvalMode} (${describeApprovalMode(approvalMode)}).`);
|
|
116
|
+
outputContinue();
|
|
135
117
|
}
|
|
136
118
|
|
|
137
119
|
const spawnEnv: Record<string, string | undefined> = {
|
|
@@ -141,6 +123,7 @@ const spawnEnv: Record<string, string | undefined> = {
|
|
|
141
123
|
CODEX_THREAD_MODE: threadId ? "resume" : process.env.CODEX_THREAD_MODE || "start",
|
|
142
124
|
AGENT_RELAY_CODEX_CWD: cwd,
|
|
143
125
|
AGENT_RELAY_CODEX_STATE_PATH: statePath,
|
|
126
|
+
AGENT_RELAY_CONTEXT_PATH: process.env.AGENT_RELAY_CONTEXT_PATH || contextPath,
|
|
144
127
|
CODEX_MODEL: input.model || process.env.CODEX_MODEL || "",
|
|
145
128
|
AGENT_RELAY_APPROVAL: process.env.AGENT_RELAY_APPROVAL || profile.approval || "",
|
|
146
129
|
AGENT_RELAY_CODEX_PARENT_PID: String(process.ppid),
|
|
@@ -183,45 +166,4 @@ try {
|
|
|
183
166
|
throw error;
|
|
184
167
|
}
|
|
185
168
|
|
|
186
|
-
|
|
187
|
-
outputContext(`Agent Relay sidecar started in auto-thread mode. Relay URL: ${relayUrl}. Incoming messages will arrive as live user turns once the sidecar resolves the active thread.`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const identity = buildAgentIdentity({
|
|
191
|
-
relayUrl,
|
|
192
|
-
cwd,
|
|
193
|
-
rig,
|
|
194
|
-
label: profile.label,
|
|
195
|
-
capabilities: profile.capabilities,
|
|
196
|
-
tags: profile.tags,
|
|
197
|
-
channels: profile.channels,
|
|
198
|
-
profileName: profile.profileName,
|
|
199
|
-
profileMeta: profile.meta,
|
|
200
|
-
threadId,
|
|
201
|
-
model: input.model,
|
|
202
|
-
appServerUrl,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
outputContext(buildStartupContext(identity.id, relayUrl, approvalMode, profile));
|
|
206
|
-
|
|
207
|
-
function buildStartupContext(agentId: string, url: string, mode: string, resolvedProfile = profile): string {
|
|
208
|
-
return [
|
|
209
|
-
"Agent Relay active.",
|
|
210
|
-
`Agent ID: ${agentId}`,
|
|
211
|
-
`Relay URL: ${url}`,
|
|
212
|
-
resolvedProfile.profileName ? `Profile: ${resolvedProfile.profileName}` : "",
|
|
213
|
-
resolvedProfile.label ? `Label: ${resolvedProfile.label}` : "",
|
|
214
|
-
resolvedProfile.tags.length ? `Tags: ${resolvedProfile.tags.join(", ")}` : "",
|
|
215
|
-
resolvedProfile.capabilities.length ? `Capabilities: ${resolvedProfile.capabilities.join(", ")}` : "",
|
|
216
|
-
resolvedProfile.channels.length ? `Channels: ${resolvedProfile.channels.join(", ")}` : "",
|
|
217
|
-
`Approval mode: ${mode} (${describeApprovalMode(parseApprovalMode(mode))})`,
|
|
218
|
-
"Incoming messages will arrive as live user turns.",
|
|
219
|
-
`To send a message, POST JSON to ${url}/api/messages with from="${agentId}", to, subject, and body.`,
|
|
220
|
-
"Targets can be an agent id, tag:name, cap:name, label:name, or broadcast.",
|
|
221
|
-
`To pair with one available session, POST JSON to ${url}/api/pairs with from="${agentId}", target, and optional objective; pair chat uses /api/pairs/{id}/messages.`,
|
|
222
|
-
"To reply to a specific incoming message, include replyTo set to that message id.",
|
|
223
|
-
"If AGENT_RELAY_TOKEN is set, include it as the X-Agent-Relay-Token header.",
|
|
224
|
-
"Message etiquette: acknowledge incoming agent messages briefly unless they are obvious noise.",
|
|
225
|
-
"Anti-loop rule: do not auto-reply to pure acknowledgements/thanks/received messages; acknowledge once, then follow up only when there is new work, a decision, or a deliverable.",
|
|
226
|
-
].filter(Boolean).join(" ");
|
|
227
|
-
}
|
|
169
|
+
outputContinue();
|
package/live-sidecar.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { CodexAppClient, type ClientEvent, type Thread, type ThreadStatus } from
|
|
|
7
7
|
import { loadAgentRelayProfile, messageMatchesProfileChannels } from "./profile";
|
|
8
8
|
import { RelayClient, RelayHttpError, type RelayAgentSession, type RelayAgentStatus, type RelayMessage } from "./relay";
|
|
9
9
|
|
|
10
|
+
type AgentControlAction = "restart" | "shutdown";
|
|
11
|
+
|
|
10
12
|
interface Config {
|
|
11
13
|
relayUrl: string;
|
|
12
14
|
appServerUrl: string;
|
|
@@ -19,8 +21,10 @@ interface Config {
|
|
|
19
21
|
profileName?: string;
|
|
20
22
|
profileMeta: Record<string, unknown>;
|
|
21
23
|
statePath: string;
|
|
24
|
+
contextPath?: string;
|
|
22
25
|
pollIntervalMs: number;
|
|
23
26
|
heartbeatIntervalMs: number;
|
|
27
|
+
threadCheckIntervalMs: number;
|
|
24
28
|
relayBackoffInitialMs: number;
|
|
25
29
|
relayBackoffMaxMs: number;
|
|
26
30
|
coalesceWindowMs: number;
|
|
@@ -34,6 +38,7 @@ interface Config {
|
|
|
34
38
|
approvalMode: ApprovalMode;
|
|
35
39
|
parentPid?: number;
|
|
36
40
|
assumeLoadedThread: boolean;
|
|
41
|
+
headless: boolean;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
interface RuntimeState {
|
|
@@ -56,6 +61,8 @@ interface DeliveryBatch {
|
|
|
56
61
|
messages: RelayMessage[];
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
class ThreadLostError extends Error {}
|
|
65
|
+
|
|
59
66
|
export function activeClaimRenewalIds(
|
|
60
67
|
messageIds: Iterable<number>,
|
|
61
68
|
activeTurnId: string | null,
|
|
@@ -65,6 +72,10 @@ export function activeClaimRenewalIds(
|
|
|
65
72
|
return [...messageIds];
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
export function canUseSyntheticLoadedThread(threadId: string, loadedThreadIds: readonly string[]): boolean {
|
|
76
|
+
return loadedThreadIds.includes(threadId);
|
|
77
|
+
}
|
|
78
|
+
|
|
68
79
|
class CodexLiveSidecar {
|
|
69
80
|
private readonly relay: RelayClient;
|
|
70
81
|
private readonly logPrefix = "[codex-live]";
|
|
@@ -77,6 +88,7 @@ class CodexLiveSidecar {
|
|
|
77
88
|
private threadStatus: ThreadStatus["type"] = "notLoaded";
|
|
78
89
|
private lastSeenMessageId = 0;
|
|
79
90
|
private lastHeartbeatAt = 0;
|
|
91
|
+
private lastThreadCheckAt = 0;
|
|
80
92
|
private hasSuccessfulPoll = false;
|
|
81
93
|
private relayBackoffMs = 0;
|
|
82
94
|
private lastClaimRenewalAt = 0;
|
|
@@ -105,6 +117,7 @@ class CodexLiveSidecar {
|
|
|
105
117
|
process.on("SIGTERM", () => void this.stop("SIGTERM"));
|
|
106
118
|
|
|
107
119
|
await this.ensureAppReady();
|
|
120
|
+
if (this.stopping) return;
|
|
108
121
|
|
|
109
122
|
await this.registerRelayAgent();
|
|
110
123
|
|
|
@@ -116,6 +129,10 @@ class CodexLiveSidecar {
|
|
|
116
129
|
break;
|
|
117
130
|
}
|
|
118
131
|
const now = Date.now();
|
|
132
|
+
if (now - this.lastThreadCheckAt >= this.config.threadCheckIntervalMs) {
|
|
133
|
+
this.lastThreadCheckAt = now;
|
|
134
|
+
await this.verifyCurrentThread();
|
|
135
|
+
}
|
|
119
136
|
if (now - this.lastHeartbeatAt >= this.config.heartbeatIntervalMs) {
|
|
120
137
|
await this.relay.heartbeat(this.agentId, this.agentSession());
|
|
121
138
|
this.lastHeartbeatAt = now;
|
|
@@ -151,6 +168,8 @@ class CodexLiveSidecar {
|
|
|
151
168
|
this.stopping = true;
|
|
152
169
|
this.writeState();
|
|
153
170
|
this.app.close();
|
|
171
|
+
} else if (error instanceof ThreadLostError) {
|
|
172
|
+
await this.stopAndUnregister(error.message);
|
|
154
173
|
} else {
|
|
155
174
|
sleepMs = this.nextRelayBackoffMs();
|
|
156
175
|
this.log(`loop error: ${describeError(error)}; retrying in ${sleepMs}ms`);
|
|
@@ -246,6 +265,10 @@ class CodexLiveSidecar {
|
|
|
246
265
|
}
|
|
247
266
|
return;
|
|
248
267
|
} catch (error) {
|
|
268
|
+
if (error instanceof ThreadLostError) {
|
|
269
|
+
await this.stopAndUnregister(error.message);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
249
272
|
this.appConnected = false;
|
|
250
273
|
this.log(`app connection failed: ${describeError(error)}; retrying in ${waitMs}ms`);
|
|
251
274
|
await delay(waitMs);
|
|
@@ -269,6 +292,28 @@ class CodexLiveSidecar {
|
|
|
269
292
|
this.app.close();
|
|
270
293
|
}
|
|
271
294
|
|
|
295
|
+
private async stopAndUnregister(reason: string): Promise<void> {
|
|
296
|
+
if (this.stopping) return;
|
|
297
|
+
this.stopping = true;
|
|
298
|
+
this.log(`${reason}; unregistering relay agent and stopping sidecar`);
|
|
299
|
+
if (this.agentId) {
|
|
300
|
+
try {
|
|
301
|
+
await this.relay.unregisterAgent(this.agentId);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
this.log(`failed to unregister relay agent ${this.agentId}: ${describeError(error)}`);
|
|
304
|
+
try {
|
|
305
|
+
await this.relay.setStatus(this.agentId, "offline", this.agentSession());
|
|
306
|
+
} catch {
|
|
307
|
+
// Best effort after unregister failure.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
this.threadStatus = "notLoaded";
|
|
312
|
+
this.activeTurnId = null;
|
|
313
|
+
this.writeState();
|
|
314
|
+
this.app.close();
|
|
315
|
+
}
|
|
316
|
+
|
|
272
317
|
private parentExited(): boolean {
|
|
273
318
|
return Boolean(this.config.parentPid && !isAlive(this.config.parentPid));
|
|
274
319
|
}
|
|
@@ -286,7 +331,7 @@ class CodexLiveSidecar {
|
|
|
286
331
|
|
|
287
332
|
private async attachKnownThread(threadId: string): Promise<Thread> {
|
|
288
333
|
if (this.config.assumeLoadedThread) {
|
|
289
|
-
return this.
|
|
334
|
+
return this.readAssumedLoadedThread(threadId);
|
|
290
335
|
}
|
|
291
336
|
|
|
292
337
|
try {
|
|
@@ -364,6 +409,25 @@ class CodexLiveSidecar {
|
|
|
364
409
|
return payload;
|
|
365
410
|
}
|
|
366
411
|
|
|
412
|
+
private async readAssumedLoadedThread(threadId: string): Promise<Thread> {
|
|
413
|
+
try {
|
|
414
|
+
return await this.readThreadWithFallback(threadId);
|
|
415
|
+
} catch {
|
|
416
|
+
const loaded = await this.app.threadLoadedList(100);
|
|
417
|
+
if (canUseSyntheticLoadedThread(threadId, loaded.data)) {
|
|
418
|
+
return syntheticLoadedThread(threadId, this.config.cwd);
|
|
419
|
+
}
|
|
420
|
+
throw new ThreadLostError(`codex thread ${threadId} is no longer loaded`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async verifyCurrentThread(): Promise<void> {
|
|
425
|
+
if (!this.threadId || !this.appConnected) return;
|
|
426
|
+
if (!this.config.assumeLoadedThread) return;
|
|
427
|
+
const thread = await this.readAssumedLoadedThread(this.threadId);
|
|
428
|
+
this.syncThreadState(thread);
|
|
429
|
+
}
|
|
430
|
+
|
|
367
431
|
private async readThreadWithFallback(threadId: string): Promise<Thread> {
|
|
368
432
|
try {
|
|
369
433
|
const read = await this.app.threadRead(threadId, true);
|
|
@@ -391,7 +455,12 @@ class CodexLiveSidecar {
|
|
|
391
455
|
|
|
392
456
|
if (method === "thread/status/changed") {
|
|
393
457
|
if (params?.threadId === this.threadId && params.status && typeof params.status === "object" && "type" in params.status) {
|
|
394
|
-
|
|
458
|
+
const nextStatus = String(params.status.type) as ThreadStatus["type"];
|
|
459
|
+
if (this.config.assumeLoadedThread && nextStatus === "notLoaded") {
|
|
460
|
+
void this.stopAndUnregister(`codex thread ${this.threadId} reported notLoaded`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
this.threadStatus = nextStatus;
|
|
395
464
|
void this.refreshRelayStatus();
|
|
396
465
|
this.writeState();
|
|
397
466
|
}
|
|
@@ -514,6 +583,12 @@ class CodexLiveSidecar {
|
|
|
514
583
|
}
|
|
515
584
|
|
|
516
585
|
private async deliverBatch(batch: DeliveryBatch): Promise<void> {
|
|
586
|
+
const control = firstAgentControlMessage(batch.messages);
|
|
587
|
+
if (control) {
|
|
588
|
+
await this.handleAgentControl(control.message, control.action);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
517
592
|
await this.ensureAppReady();
|
|
518
593
|
|
|
519
594
|
const claimedMessageIds: number[] = [];
|
|
@@ -530,7 +605,7 @@ class CodexLiveSidecar {
|
|
|
530
605
|
}
|
|
531
606
|
|
|
532
607
|
const delivery = this.pickDeliveryMode(batch.messages);
|
|
533
|
-
const prompt = formatRelayPrompt(batch.messages, { includePrimer: !this.relayPrimerDelivered });
|
|
608
|
+
const prompt = formatRelayPrompt(batch.messages, { includePrimer: !this.relayPrimerDelivered, headless: this.config.headless });
|
|
534
609
|
const ids = batch.messages.map((message) => message.id).join(", ");
|
|
535
610
|
this.log(`delivering message ${ids} via ${delivery}`);
|
|
536
611
|
|
|
@@ -567,6 +642,33 @@ class CodexLiveSidecar {
|
|
|
567
642
|
this.writeState();
|
|
568
643
|
}
|
|
569
644
|
|
|
645
|
+
private async handleAgentControl(message: RelayMessage, action: AgentControlAction): Promise<void> {
|
|
646
|
+
this.log(`agent control ${action} requested by message ${message.id}`);
|
|
647
|
+
await this.relay.markRead(message.id, this.agentId);
|
|
648
|
+
this.advanceCursor([message]);
|
|
649
|
+
|
|
650
|
+
if (action === "shutdown") {
|
|
651
|
+
await this.stopAndUnregister(`dashboard shutdown requested by message ${message.id}`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (this.agentId) {
|
|
656
|
+
try {
|
|
657
|
+
await this.relay.unregisterAgent(this.agentId);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
this.log(`failed to unregister before restart: ${describeError(error)}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
this.agentEpoch = 0;
|
|
663
|
+
this.activeTurnId = null;
|
|
664
|
+
this.activeClaimedMessageIds.clear();
|
|
665
|
+
this.app.close();
|
|
666
|
+
this.appConnected = false;
|
|
667
|
+
await this.ensureAppReady(true);
|
|
668
|
+
await this.registerRelayAgent();
|
|
669
|
+
this.writeState();
|
|
670
|
+
}
|
|
671
|
+
|
|
570
672
|
private advanceCursor(messages: RelayMessage[]): void {
|
|
571
673
|
for (const message of messages) {
|
|
572
674
|
this.lastSeenMessageId = Math.max(this.lastSeenMessageId, message.id);
|
|
@@ -598,6 +700,7 @@ class CodexLiveSidecar {
|
|
|
598
700
|
|
|
599
701
|
private writeState(): void {
|
|
600
702
|
if (!this.threadId) return;
|
|
703
|
+
const updatedAt = new Date().toISOString();
|
|
601
704
|
const state: RuntimeState = {
|
|
602
705
|
agentId: this.agentId,
|
|
603
706
|
agentInstanceId: this.agentInstanceId,
|
|
@@ -611,9 +714,24 @@ class CodexLiveSidecar {
|
|
|
611
714
|
appServerUrl: this.config.appServerUrl,
|
|
612
715
|
relayUrl: this.config.relayUrl,
|
|
613
716
|
cwd: this.config.cwd,
|
|
614
|
-
updatedAt
|
|
717
|
+
updatedAt,
|
|
615
718
|
};
|
|
616
719
|
writeFileSync(this.config.statePath, JSON.stringify(state, null, 2));
|
|
720
|
+
if (this.config.contextPath) {
|
|
721
|
+
mkdirSync(dirname(this.config.contextPath), { recursive: true });
|
|
722
|
+
writeFileSync(this.config.contextPath, JSON.stringify({
|
|
723
|
+
version: 1,
|
|
724
|
+
agentId: this.agentId,
|
|
725
|
+
provider: "codex",
|
|
726
|
+
relayUrl: this.config.relayUrl,
|
|
727
|
+
cwd: this.config.cwd,
|
|
728
|
+
statePath: this.config.statePath,
|
|
729
|
+
matchEnv: [
|
|
730
|
+
{ name: "AGENT_RELAY_CONTEXT_PATH", value: this.config.contextPath },
|
|
731
|
+
],
|
|
732
|
+
updatedAt,
|
|
733
|
+
}, null, 2));
|
|
734
|
+
}
|
|
617
735
|
}
|
|
618
736
|
|
|
619
737
|
private log(message: string): void {
|
|
@@ -659,16 +777,17 @@ function shouldIsolateMessage(message: RelayMessage): boolean {
|
|
|
659
777
|
|
|
660
778
|
export function formatRelayPrompt(
|
|
661
779
|
messages: RelayMessage[],
|
|
662
|
-
opts: { includePrimer?: boolean } = {},
|
|
780
|
+
opts: { includePrimer?: boolean; headless?: boolean } = {},
|
|
663
781
|
): string {
|
|
664
782
|
const includePrimer = opts.includePrimer === true;
|
|
665
783
|
const relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
|
|
666
784
|
const primer = [
|
|
667
785
|
"Agent Relay live-message primer:",
|
|
668
786
|
"Treat Agent Relay deliveries as live incoming messages from other agents. Respond or act on them as appropriate.",
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
787
|
+
...(opts.headless ? ["This Codex session is running headless; Agent Relay is the primary user and agent communication surface."] : []),
|
|
788
|
+
"For normal replies, prefer `agent-relay /message TARGET BODY --reply-to MSG_ID`; the CLI detects this session's Agent Relay ID.",
|
|
789
|
+
"For pair chat replies, prefer `agent-relay /pair send PAIR_ID BODY`.",
|
|
790
|
+
`If the CLI is unavailable, call ${relayUrl}/api/messages or /api/pairs/{pairId}/messages directly and include AGENT_RELAY_TOKEN as X-Agent-Relay-Token when set.`,
|
|
672
791
|
"",
|
|
673
792
|
];
|
|
674
793
|
|
|
@@ -688,16 +807,17 @@ export function formatRelayPrompt(
|
|
|
688
807
|
|
|
689
808
|
if (message.subject) lines.push(`Subject: ${message.subject}`);
|
|
690
809
|
if (message.replyTo) lines.push(`Reply To: ${message.replyTo}`);
|
|
691
|
-
|
|
692
|
-
|
|
810
|
+
const pairId = typeof message.payload?.pairId === "string" ? message.payload.pairId : undefined;
|
|
811
|
+
if (pairId) {
|
|
812
|
+
lines.push(`Pair ID: ${pairId}`);
|
|
693
813
|
}
|
|
694
814
|
lines.push(
|
|
695
815
|
"",
|
|
696
816
|
"Body:",
|
|
697
817
|
message.body,
|
|
698
818
|
"",
|
|
699
|
-
|
|
700
|
-
? `Reply routing: pairId=${JSON.stringify(
|
|
819
|
+
pairId
|
|
820
|
+
? `Reply routing: pairId=${JSON.stringify(pairId)}.`
|
|
701
821
|
: `Reply routing: to=${JSON.stringify(message.from)}, replyTo=${message.id}.`,
|
|
702
822
|
);
|
|
703
823
|
return lines.join("\n");
|
|
@@ -721,10 +841,11 @@ export function formatRelayPrompt(
|
|
|
721
841
|
);
|
|
722
842
|
if (message.subject) lines.push(`Subject: ${message.subject}`);
|
|
723
843
|
if (message.replyTo) lines.push(`Reply To: ${message.replyTo}`);
|
|
724
|
-
|
|
844
|
+
const pairId = typeof message.payload?.pairId === "string" ? message.payload.pairId : undefined;
|
|
845
|
+
if (pairId) lines.push(`Pair ID: ${pairId}`);
|
|
725
846
|
lines.push(
|
|
726
|
-
|
|
727
|
-
? `Reply routing: pairId=${JSON.stringify(
|
|
847
|
+
pairId
|
|
848
|
+
? `Reply routing: pairId=${JSON.stringify(pairId)}.`
|
|
728
849
|
: `Reply routing: to=${JSON.stringify(message.from)}, replyTo=${message.id}.`,
|
|
729
850
|
);
|
|
730
851
|
lines.push("Body:", message.body, "", "---", "");
|
|
@@ -738,10 +859,26 @@ export function formatRelayPrompt(
|
|
|
738
859
|
|
|
739
860
|
function formatMessageSummary(message: RelayMessage): string {
|
|
740
861
|
const subject = message.subject || "message";
|
|
741
|
-
if (message.
|
|
862
|
+
if (message.kind === "system" || message.kind === "control") return `SYSTEM [msg:${message.id}]: ${message.body}`;
|
|
742
863
|
return `[msg:${message.id}] ${message.from} -> ${message.to} | ${subject}: ${message.body}`;
|
|
743
864
|
}
|
|
744
865
|
|
|
866
|
+
export function agentControlActionForMessage(message: RelayMessage): AgentControlAction | null {
|
|
867
|
+
if (message.kind !== "control") return null;
|
|
868
|
+
const control = message.payload?.agentControl;
|
|
869
|
+
if (!control || typeof control !== "object" || Array.isArray(control)) return null;
|
|
870
|
+
const action = (control as { action?: unknown }).action;
|
|
871
|
+
return action === "restart" || action === "shutdown" ? action : null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function firstAgentControlMessage(messages: RelayMessage[]): { message: RelayMessage; action: AgentControlAction } | null {
|
|
875
|
+
for (const message of messages) {
|
|
876
|
+
const action = agentControlActionForMessage(message);
|
|
877
|
+
if (action) return { message, action };
|
|
878
|
+
}
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
|
|
745
882
|
function normalizeThread(thread: Thread): Thread {
|
|
746
883
|
return {
|
|
747
884
|
...thread,
|
|
@@ -811,8 +948,10 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
|
|
|
811
948
|
profileName: profile.profileName,
|
|
812
949
|
profileMeta: profile.meta,
|
|
813
950
|
statePath: env.AGENT_RELAY_CODEX_STATE_PATH || resolve(cwd, "codex/runtime/live-state.json"),
|
|
951
|
+
contextPath: env.AGENT_RELAY_CONTEXT_PATH || undefined,
|
|
814
952
|
pollIntervalMs: envNumber(env, "AGENT_RELAY_CODEX_POLL_INTERVAL_MS", 2000),
|
|
815
953
|
heartbeatIntervalMs: envNumber(env, "AGENT_RELAY_CODEX_HEARTBEAT_INTERVAL_MS", 30000),
|
|
954
|
+
threadCheckIntervalMs: envNumber(env, "AGENT_RELAY_CODEX_THREAD_CHECK_INTERVAL_MS", 30000),
|
|
816
955
|
relayBackoffInitialMs: envNumber(env, "AGENT_RELAY_CODEX_RELAY_BACKOFF_INITIAL_MS", 2000),
|
|
817
956
|
relayBackoffMaxMs: envNumber(env, "AGENT_RELAY_CODEX_RELAY_BACKOFF_MAX_MS", 60000),
|
|
818
957
|
coalesceWindowMs: envNumber(env, "AGENT_RELAY_CODEX_COALESCE_WINDOW_MS", 600),
|
|
@@ -831,6 +970,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
|
|
|
831
970
|
}),
|
|
832
971
|
parentPid: envNumber(env, "AGENT_RELAY_CODEX_PARENT_PID", 0) || undefined,
|
|
833
972
|
assumeLoadedThread: env.AGENT_RELAY_CODEX_ASSUME_LOADED_THREAD === "1" || env.AGENT_RELAY_CODEX_ASSUME_LOADED_THREAD === "true",
|
|
973
|
+
headless: env.AGENT_RELAY_CODEX_HEADLESS === "1" || env.AGENT_RELAY_CODEX_HEADLESS === "true",
|
|
834
974
|
};
|
|
835
975
|
}
|
|
836
976
|
|
package/package.json
CHANGED
package/relay.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface RelayMessage {
|
|
|
6
6
|
id: number;
|
|
7
7
|
from: string;
|
|
8
8
|
to: string;
|
|
9
|
+
kind: string;
|
|
9
10
|
channel?: string;
|
|
10
11
|
subject?: string;
|
|
11
12
|
body: string;
|
|
@@ -14,7 +15,7 @@ export interface RelayMessage {
|
|
|
14
15
|
claimedBy?: string;
|
|
15
16
|
claimedAt?: number;
|
|
16
17
|
claimExpiresAt?: number;
|
|
17
|
-
|
|
18
|
+
payload?: Record<string, unknown>;
|
|
18
19
|
meta?: Record<string, unknown>;
|
|
19
20
|
createdAt: number;
|
|
20
21
|
}
|
|
@@ -105,6 +106,10 @@ export class RelayClient {
|
|
|
105
106
|
await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/ready`, { ready, ...session });
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
async unregisterAgent(agentId: string): Promise<void> {
|
|
110
|
+
await this.json("DELETE", `/api/agents/${encodeURIComponent(agentId)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
async pollMessages(agentId: string, sinceId: number): Promise<RelayMessage[]> {
|
|
109
114
|
const url = new URL(`/api/messages`, this.baseUrl);
|
|
110
115
|
url.searchParams.set("for", agentId);
|