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.
@@ -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,
@@ -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 { describeApprovalMode, parseApprovalMode } from "../approval.ts";
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
- outputContext("Agent Relay is managed by codex-relay for this session; the launcher attached Relay and TUI to the same Codex App Server thread.");
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
- if (threadId) {
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
- if (!threadId) {
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.readThreadWithFallback(threadId).catch(() => syntheticLoadedThread(threadId, this.config.cwd));
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
- this.threadStatus = String(params.status.type) as ThreadStatus["type"];
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: new Date().toISOString(),
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
- "When calling the relay API, include AGENT_RELAY_TOKEN as the X-Agent-Relay-Token header if that environment variable is set.",
670
- `For normal replies, POST JSON to ${relayUrl}/api/messages with from set to your Agent Relay ID, to set to the sender Agent Relay ID, body set to your response, and replyTo set to the incoming message id.`,
671
- `For pair chat replies, POST JSON to ${relayUrl}/api/pairs/{pairId}/messages with from set to your Agent Relay ID and body set to your response.`,
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
- if (typeof message.meta?.pairId === "string") {
692
- lines.push(`Pair ID: ${message.meta.pairId}`);
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
- typeof message.meta?.pairId === "string"
700
- ? `Reply routing: pairId=${JSON.stringify(message.meta.pairId)}.`
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
- if (typeof message.meta?.pairId === "string") lines.push(`Pair ID: ${message.meta.pairId}`);
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
- typeof message.meta?.pairId === "string"
727
- ? `Reply routing: pairId=${JSON.stringify(message.meta.pairId)}.`
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.type === "system") return `SYSTEM [msg:${message.id}]: ${message.body}`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-codex",
3
- "version": "0.4.35",
3
+ "version": "0.6.0",
4
4
  "description": "Codex integration for Agent Relay — auto-registers sessions as agents and enables inter-agent messaging",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "0.4.35",
3
+ "version": "0.6.0",
4
4
  "description": "Agent Relay integration for Codex sessions",
5
5
  "author": {
6
6
  "name": "Edin Mujkanovic"
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
- type?: string;
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);