agent-relay-server 0.121.5 → 0.122.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.
Files changed (179) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +6 -6
  3. package/public/assets/{MessageBubble-CLqxCG9b.js → MessageBubble-DRdUD-kW.js} +2 -2
  4. package/public/assets/{MessageBubble-CLqxCG9b.js.map → MessageBubble-DRdUD-kW.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BkkToFEb.js → PlanGraphPanel-siKMPCln.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BkkToFEb.js.map → PlanGraphPanel-siKMPCln.js.map} +1 -1
  7. package/public/assets/{activity-D-ocGQGs.js → activity-D3MNcSaV.js} +2 -2
  8. package/public/assets/{activity-D-ocGQGs.js.map → activity-D3MNcSaV.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CRPgYTyb.js → agent-profiles-CKzttq6G.js} +2 -2
  10. package/public/assets/{agent-profiles-CRPgYTyb.js.map → agent-profiles-CKzttq6G.js.map} +1 -1
  11. package/public/assets/{agent-type-icon-CeEoMouY.js → agent-type-icon-0ZJRP1Vy.js} +2 -2
  12. package/public/assets/{agent-type-icon-CeEoMouY.js.map → agent-type-icon-0ZJRP1Vy.js.map} +1 -1
  13. package/public/assets/{agents-Chw5E_2q.js → agents-CuVj_rfG.js} +2 -2
  14. package/public/assets/{agents-Chw5E_2q.js.map → agents-CuVj_rfG.js.map} +1 -1
  15. package/public/assets/{analytics-BmNDG0hR.js → analytics-iob6C27d.js} +2 -2
  16. package/public/assets/{analytics-BmNDG0hR.js.map → analytics-iob6C27d.js.map} +1 -1
  17. package/public/assets/{automation-D7g90kTv.js → automation-B2ixxs4q.js} +2 -2
  18. package/public/assets/{automation-D7g90kTv.js.map → automation-B2ixxs4q.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-Bv7DhLBZ.js → branch-state-badge-CafBJWKo.js} +2 -2
  20. package/public/assets/{branch-state-badge-Bv7DhLBZ.js.map → branch-state-badge-CafBJWKo.js.map} +1 -1
  21. package/public/assets/{channels-CLwPeEPi.js → channels-C-WsbL2H.js} +2 -2
  22. package/public/assets/{channels-CLwPeEPi.js.map → channels-C-WsbL2H.js.map} +1 -1
  23. package/public/assets/{chat-Cgj7VKzc.js → chat-DeoVTVob.js} +2 -2
  24. package/public/assets/{chat-Cgj7VKzc.js.map → chat-DeoVTVob.js.map} +1 -1
  25. package/public/assets/{connectors-Br6fiTny.js → connectors-C2Ac03LJ.js} +2 -2
  26. package/public/assets/{connectors-Br6fiTny.js.map → connectors-C2Ac03LJ.js.map} +1 -1
  27. package/public/assets/display-CJzSoTMq.js +3 -0
  28. package/public/assets/display-CJzSoTMq.js.map +1 -0
  29. package/public/assets/{formatted-body-CmkuD23M.js → formatted-body-35XBOY5L.js} +3 -3
  30. package/public/assets/{formatted-body-CmkuD23M.js.map → formatted-body-35XBOY5L.js.map} +1 -1
  31. package/public/assets/{formatted-body-impl-DbdFmbwW.js → formatted-body-impl-RmDxI5Ux.js} +2 -2
  32. package/public/assets/{formatted-body-impl-DbdFmbwW.js.map → formatted-body-impl-RmDxI5Ux.js.map} +1 -1
  33. package/public/assets/index-D0xMQQb_.js +25 -0
  34. package/public/assets/{index-D9OogaB5.js.map → index-D0xMQQb_.js.map} +1 -1
  35. package/public/assets/{insights-qvaPqWCV.js → insights-B643SFvm.js} +2 -2
  36. package/public/assets/{insights-qvaPqWCV.js.map → insights-B643SFvm.js.map} +1 -1
  37. package/public/assets/{integrations-DYEGSqq_.js → integrations-hFIHeCF4.js} +2 -2
  38. package/public/assets/{integrations-DYEGSqq_.js.map → integrations-hFIHeCF4.js.map} +1 -1
  39. package/public/assets/{maintenance-C0HIEB-J.js → maintenance-CURvdp_Z.js} +2 -2
  40. package/public/assets/{maintenance-C0HIEB-J.js.map → maintenance-CURvdp_Z.js.map} +1 -1
  41. package/public/assets/{managed-agents-DdS7aI38.js → managed-agents-DrZtdlSn.js} +2 -2
  42. package/public/assets/{managed-agents-DdS7aI38.js.map → managed-agents-DrZtdlSn.js.map} +1 -1
  43. package/public/assets/{markdown-preview-impl-BnXMH1z1.js → markdown-preview-impl-CVDO--ek.js} +2 -2
  44. package/public/assets/{markdown-preview-impl-BnXMH1z1.js.map → markdown-preview-impl-CVDO--ek.js.map} +1 -1
  45. package/public/assets/{memory-CSwx7bz8.js → memory-Bm7dt_Qf.js} +2 -2
  46. package/public/assets/{memory-CSwx7bz8.js.map → memory-Bm7dt_Qf.js.map} +1 -1
  47. package/public/assets/{messages-Be9CmvDv.js → messages-DTq4i2KZ.js} +2 -2
  48. package/public/assets/{messages-Be9CmvDv.js.map → messages-DTq4i2KZ.js.map} +1 -1
  49. package/public/assets/{orchestrators-BgtvvHvo.js → orchestrators-Dra318Ap.js} +2 -2
  50. package/public/assets/{orchestrators-BgtvvHvo.js.map → orchestrators-Dra318Ap.js.map} +1 -1
  51. package/public/assets/{overview-BvhbPWtF.js → overview-yr1Vmk64.js} +2 -2
  52. package/public/assets/{overview-BvhbPWtF.js.map → overview-yr1Vmk64.js.map} +1 -1
  53. package/public/assets/{pairs-CpXSOMx0.js → pairs-mnlKDBGp.js} +2 -2
  54. package/public/assets/{pairs-CpXSOMx0.js.map → pairs-mnlKDBGp.js.map} +1 -1
  55. package/public/assets/projects-Dzdf-gkK.js +2 -0
  56. package/public/assets/{projects-DdPNPFJI.js.map → projects-Dzdf-gkK.js.map} +1 -1
  57. package/public/assets/prompt-settings-ByL8SNCi.js +2 -0
  58. package/public/assets/{prompt-settings-BpZse7-X.js.map → prompt-settings-ByL8SNCi.js.map} +1 -1
  59. package/public/assets/{registry-pb1gTZEg.js → registry-CXWyOtpr.js} +2 -2
  60. package/public/assets/{registry-pb1gTZEg.js.map → registry-CXWyOtpr.js.map} +1 -1
  61. package/public/assets/{security-BgcX1n9D.js → security-0SBsc_4W.js} +2 -2
  62. package/public/assets/{security-BgcX1n9D.js.map → security-0SBsc_4W.js.map} +1 -1
  63. package/public/assets/{select-DZPVpKPv.js → select-Bw5z2KJ_.js} +2 -2
  64. package/public/assets/{select-DZPVpKPv.js.map → select-Bw5z2KJ_.js.map} +1 -1
  65. package/public/assets/{settings-hd5kzdRB.js → settings-78Cyq_tA.js} +4 -4
  66. package/public/assets/{settings-hd5kzdRB.js.map → settings-78Cyq_tA.js.map} +1 -1
  67. package/public/assets/{store-DKTLubBT.js → store-BPrVuKiv.js} +4 -4
  68. package/public/assets/store-BPrVuKiv.js.map +1 -0
  69. package/public/assets/{tasks-C5d2LU5E.js → tasks-Cg2MMeri.js} +2 -2
  70. package/public/assets/{tasks-C5d2LU5E.js.map → tasks-Cg2MMeri.js.map} +1 -1
  71. package/public/assets/teams-DWv6UwFV.js +2 -0
  72. package/public/assets/{teams-B8f2pGJe.js.map → teams-DWv6UwFV.js.map} +1 -1
  73. package/public/assets/{terminal-viewer-impl-CxAscH0U.js → terminal-viewer-impl-DBLDjbIg.js} +2 -2
  74. package/public/assets/{terminal-viewer-impl-CxAscH0U.js.map → terminal-viewer-impl-DBLDjbIg.js.map} +1 -1
  75. package/public/assets/{types-uVOXgvMT.js → types-BeJUSfDj.js} +2 -2
  76. package/public/assets/types-BeJUSfDj.js.map +1 -0
  77. package/public/assets/{user-avatar-Dw6mzWhv.js → user-avatar-BsOLyVJj.js} +2 -2
  78. package/public/assets/{user-avatar-Dw6mzWhv.js.map → user-avatar-BsOLyVJj.js.map} +1 -1
  79. package/public/assets/{work-queue-9imc7aNK.js → work-queue-DmwvnF6F.js} +2 -2
  80. package/public/assets/{work-queue-9imc7aNK.js.map → work-queue-DmwvnF6F.js.map} +1 -1
  81. package/public/assets/{workspaces-CLYc3yF3.js → workspaces-dIBivJN8.js} +2 -2
  82. package/public/assets/{workspaces-CLYc3yF3.js.map → workspaces-dIBivJN8.js.map} +1 -1
  83. package/public/index.html +6 -6
  84. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  85. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
  86. package/runner/src/adapter.ts +53 -18
  87. package/runner/src/adapters/claude-delivery.ts +127 -0
  88. package/runner/src/adapters/claude-quota-harvest.ts +92 -0
  89. package/runner/src/adapters/claude-session-probe.ts +225 -0
  90. package/runner/src/adapters/claude-status-detectors.ts +147 -0
  91. package/runner/src/adapters/claude-transcript-tail.ts +128 -0
  92. package/runner/src/adapters/claude-transcript.ts +414 -0
  93. package/runner/src/adapters/claude.ts +672 -0
  94. package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
  95. package/runner/src/adapters/codex-client.ts +279 -0
  96. package/runner/src/adapters/codex.ts +1345 -0
  97. package/runner/src/attachment-cache.ts +189 -0
  98. package/runner/src/busy-reconciler.ts +102 -0
  99. package/runner/src/claim-tracker.ts +140 -0
  100. package/runner/src/claude-prompt-gates.ts +268 -0
  101. package/runner/src/config.ts +1 -4
  102. package/runner/src/context-probe-state.ts +6 -0
  103. package/runner/src/continuation-archive.ts +179 -0
  104. package/runner/src/control-server.ts +639 -0
  105. package/runner/src/index.ts +396 -0
  106. package/runner/src/launch-assembly.ts +508 -0
  107. package/runner/src/liveness.ts +50 -0
  108. package/runner/src/logger.ts +99 -0
  109. package/runner/src/mcp-outbox.ts +120 -0
  110. package/runner/src/message-body-config/config.ts +3 -0
  111. package/runner/src/message-body-config.ts +1 -0
  112. package/runner/src/native-self-resume.ts +202 -0
  113. package/runner/src/outbox.ts +342 -0
  114. package/runner/src/process-meta.ts +9 -0
  115. package/runner/src/profile-home.ts +260 -0
  116. package/runner/src/profile-projection.ts +305 -0
  117. package/runner/src/providers.ts +20 -0
  118. package/runner/src/provisioning.ts +314 -0
  119. package/runner/src/quota.ts +40 -0
  120. package/runner/src/rate-limit.ts +189 -0
  121. package/runner/src/relay-injection-events.ts +102 -0
  122. package/runner/src/relay-instructions.ts +60 -0
  123. package/runner/src/relay-mcp-proxy.ts +383 -0
  124. package/runner/src/relay-mcp.ts +156 -0
  125. package/runner/src/reply-obligation-cache.ts +136 -0
  126. package/runner/src/response-capture-report.ts +63 -0
  127. package/runner/src/runner-core.ts +2409 -0
  128. package/runner/src/runner-helpers.ts +435 -0
  129. package/runner/src/runner-insights.ts +105 -0
  130. package/runner/src/runner.ts +14 -0
  131. package/runner/src/session-destroy.ts +22 -0
  132. package/runner/src/session-insights.ts +118 -0
  133. package/runner/src/session-scratch.ts +271 -0
  134. package/runner/src/template-resolver.ts +29 -0
  135. package/runner/src/version.ts +43 -0
  136. package/src/agent-card-projection.ts +5 -1
  137. package/src/agent-command-activity.ts +39 -0
  138. package/src/automations/reconcile.ts +2 -2
  139. package/src/cli/workspace.ts +3 -1
  140. package/src/config.ts +10 -1
  141. package/src/db/agents.ts +118 -37
  142. package/src/db/message-reads.ts +50 -0
  143. package/src/db/messages.ts +48 -9
  144. package/src/db/migrations.ts +54 -0
  145. package/src/db/provider-quota-burn.ts +96 -67
  146. package/src/db/provider-quotas.ts +29 -7
  147. package/src/db/scheduled-timers.ts +21 -0
  148. package/src/db/schema.ts +1 -0
  149. package/src/db/tasks.ts +33 -3
  150. package/src/db/work-evidence.ts +10 -7
  151. package/src/event-loop-lag.ts +15 -0
  152. package/src/gate-resolver.ts +19 -1
  153. package/src/maintenance/jobs.ts +23 -6
  154. package/src/managed-policy.ts +5 -12
  155. package/src/mcp-plan-tools.ts +13 -1
  156. package/src/project-instructions.ts +36 -2
  157. package/src/quota-advisory.ts +6 -4
  158. package/src/quota-band-transition.ts +23 -14
  159. package/src/routes/agent-sessions.ts +39 -7
  160. package/src/routes/agents.ts +3 -2
  161. package/src/routes/tasks.ts +16 -4
  162. package/src/routes/workspaces.ts +35 -10
  163. package/src/services/plan-graph.ts +27 -9
  164. package/src/services/register-agent.ts +12 -12
  165. package/src/services/send-message.ts +43 -15
  166. package/src/services/spawn-agent.ts +3 -0
  167. package/src/spawn-command.ts +8 -0
  168. package/src/spawn-targets.ts +19 -34
  169. package/src/sse.ts +25 -3
  170. package/src/workspace-merge.ts +25 -5
  171. package/src/workspace-orphans.ts +13 -6
  172. package/public/assets/display-exQDWum3.js +0 -3
  173. package/public/assets/display-exQDWum3.js.map +0 -1
  174. package/public/assets/index-D9OogaB5.js +0 -25
  175. package/public/assets/projects-DdPNPFJI.js +0 -2
  176. package/public/assets/prompt-settings-BpZse7-X.js +0 -2
  177. package/public/assets/store-DKTLubBT.js.map +0 -1
  178. package/public/assets/teams-B8f2pGJe.js +0 -2
  179. package/public/assets/types-uVOXgvMT.js.map +0 -1
@@ -0,0 +1,139 @@
1
+ type EmitAgentMessage = (body: string) => void;
2
+ type ResponseEntry = { itemId?: string; body: string };
3
+ type CompleteItemOptions = { includeInResponse?: boolean; phase?: unknown };
4
+
5
+ const MAX_CAPTURED_RESPONSE_CHARS = 16_000;
6
+ const TRUNCATION_MARKER_PREFIX = "[truncated:";
7
+ const TERSE_FINAL_MAX_CHARS = 280;
8
+
9
+ export class CodexAgentMessageCapture {
10
+ private segments: ResponseEntry[][] = [];
11
+ private currentSegment: ResponseEntry[] = [];
12
+ private readonly buffers = new Map<string, string>();
13
+ private readonly flushedLengths = new Map<string, number>();
14
+ private readonly responseRecordedLengths = new Map<string, number>();
15
+
16
+ reset(): void {
17
+ this.segments = [];
18
+ this.currentSegment = [];
19
+ this.buffers.clear();
20
+ this.flushedLengths.clear();
21
+ this.responseRecordedLengths.clear();
22
+ }
23
+
24
+ appendDelta(itemId: string | undefined, delta: string): void {
25
+ if (!itemId || !delta) return;
26
+ this.buffers.set(itemId, `${this.buffers.get(itemId) ?? ""}${delta}`);
27
+ }
28
+
29
+ completeItem(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage, options: CompleteItemOptions = {}): void {
30
+ const fullText = text ?? (itemId ? this.buffers.get(itemId) : undefined);
31
+ this.flushText(itemId, fullText, emit);
32
+ if (options.includeInResponse === false || options.phase === "commentary") {
33
+ this.forgetResponseItem(itemId);
34
+ } else {
35
+ this.recordResponseText(itemId, fullText);
36
+ }
37
+ if (!itemId) return;
38
+ this.buffers.delete(itemId);
39
+ this.flushedLengths.delete(itemId);
40
+ }
41
+
42
+ closeSegment(emit: EmitAgentMessage): void {
43
+ this.flushPending(emit);
44
+ if (!this.currentSegment.length) return;
45
+ this.segments.push(this.currentSegment);
46
+ this.currentSegment = [];
47
+ }
48
+
49
+ finish(emit: EmitAgentMessage): string | undefined {
50
+ this.closeSegment(emit);
51
+ const selectedSegments = selectResponseSegments(this.segments);
52
+ this.segments = [];
53
+ const text = selectedSegments.length ? selectedSegments.map(segmentBody).join("\n\n").trim() : undefined;
54
+ return truncateCapturedResponse(text);
55
+ }
56
+
57
+ private flushPending(emit: EmitAgentMessage): void {
58
+ for (const [itemId, text] of [...this.buffers.entries()]) {
59
+ this.flushText(itemId, text, emit);
60
+ this.recordResponseText(itemId, text);
61
+ }
62
+ }
63
+
64
+ private flushText(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
65
+ if (!text) return;
66
+ const offset = itemId ? (this.flushedLengths.get(itemId) ?? 0) : 0;
67
+ const chunk = text.slice(offset);
68
+ if (itemId) this.flushedLengths.set(itemId, text.length);
69
+ const body = chunk.trim();
70
+ if (!body) return;
71
+ emit(body);
72
+ }
73
+
74
+ private recordResponseText(itemId: string | undefined, text: string | undefined): void {
75
+ if (!text) return;
76
+ const body = text.trim();
77
+ if (!body) return;
78
+ if (itemId) {
79
+ const offset = this.responseRecordedLengths.get(itemId) ?? 0;
80
+ const chunk = text.slice(offset).trim();
81
+ this.responseRecordedLengths.set(itemId, text.length);
82
+ if (!chunk) return;
83
+ this.currentSegment.push({ itemId, body: chunk });
84
+ return;
85
+ }
86
+ this.currentSegment.push({ body });
87
+ }
88
+
89
+ private forgetResponseItem(itemId: string | undefined): void {
90
+ if (!itemId) return;
91
+ this.responseRecordedLengths.delete(itemId);
92
+ this.currentSegment = this.currentSegment.filter((entry) => entry.itemId !== itemId);
93
+ this.segments = this.segments
94
+ .map((segment) => segment.filter((entry) => entry.itemId !== itemId))
95
+ .filter((segment) => segment.length > 0);
96
+ }
97
+ }
98
+
99
+ function selectResponseSegments(segments: ResponseEntry[][]): ResponseEntry[][] {
100
+ const nonEmptySegments = segments.filter((segment) => segment.length > 0);
101
+ const finalSegment = nonEmptySegments.at(-1);
102
+ if (!finalSegment) return [];
103
+ const finalBody = segmentBody(finalSegment);
104
+ if (!isTerseTerminalStatus(finalBody)) return [finalSegment];
105
+
106
+ // Codex often posts the real wrap-up across multiple assistant segments before a
107
+ // final workspace-ready/status line. Keep contiguous substantive segments after
108
+ // the last procedural assistant segment, then append the terminal status.
109
+ const selectedSegments: ResponseEntry[][] = [];
110
+ for (let index = nonEmptySegments.length - 2; index >= 0; index -= 1) {
111
+ const candidate = nonEmptySegments[index];
112
+ if (!candidate || !isSubstantiveResponseSegment(segmentBody(candidate))) break;
113
+ selectedSegments.unshift(candidate);
114
+ }
115
+ return [...selectedSegments, finalSegment];
116
+ }
117
+
118
+ function segmentBody(segment: ResponseEntry[]): string {
119
+ return segment.map((entry) => entry.body).join("\n\n").trim();
120
+ }
121
+
122
+ function isTerseTerminalStatus(body: string): boolean {
123
+ if (body.length > TERSE_FINAL_MAX_CHARS) return false;
124
+ if (/\n\s*(?:[-*]|\d+\.)\s+/.test(body)) return false;
125
+ return /\b(?:landed|ready for review|handed off|no further action|done|complete|completed|committed|merged)\b/i.test(body);
126
+ }
127
+
128
+ function isSubstantiveResponseSegment(body: string): boolean {
129
+ if (body.length >= 120) return true;
130
+ if (/\n\s*(?:[-*]|\d+\.)\s+/.test(body)) return true;
131
+ if (/\*\*(?:what changed|changes?|tests?|gates?|verification|risks?|residual risk|summary)\b/i.test(body)) return true;
132
+ if (/^#{1,3}\s+\S/m.test(body)) return true;
133
+ return body.split(/\n{2,}/).length >= 2;
134
+ }
135
+
136
+ function truncateCapturedResponse(text: string | undefined): string | undefined {
137
+ if (!text || text.length <= MAX_CAPTURED_RESPONSE_CHARS) return text;
138
+ return `${text.slice(0, MAX_CAPTURED_RESPONSE_CHARS).trimEnd()}\n\n${TRUNCATION_MARKER_PREFIX} showing first ${MAX_CAPTURED_RESPONSE_CHARS} of ${text.length} chars]`;
139
+ }
@@ -0,0 +1,279 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+
3
+ type JsonRpcId = number | string;
4
+
5
+ type JsonRpcRequest = {
6
+ id: JsonRpcId;
7
+ method: string;
8
+ params?: unknown;
9
+ };
10
+
11
+ type JsonRpcResponse = {
12
+ id: JsonRpcId;
13
+ result?: unknown;
14
+ error?: { code: number; message: string; data?: unknown };
15
+ };
16
+
17
+ type JsonRpcNotification = {
18
+ method: string;
19
+ params?: Record<string, unknown>;
20
+ };
21
+
22
+ type ThreadStatus =
23
+ | { type: "notLoaded" }
24
+ | { type: "idle" }
25
+ | { type: "systemError" }
26
+ | { type: "active"; activeFlags: string[] };
27
+
28
+ type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
29
+
30
+ interface Turn {
31
+ id: string;
32
+ status: TurnStatus;
33
+ startedAt: number | null;
34
+ completedAt: number | null;
35
+ }
36
+
37
+ export interface Thread {
38
+ id: string;
39
+ cwd: string;
40
+ status: ThreadStatus;
41
+ updatedAt: number;
42
+ preview: string;
43
+ turns?: Turn[];
44
+ }
45
+
46
+ export type ClientEvent =
47
+ | { type: "notification"; message: JsonRpcNotification }
48
+ | { type: "server-request"; message: JsonRpcRequest }
49
+ | { type: "response"; message: JsonRpcResponse };
50
+
51
+ interface TurnStartResponse {
52
+ turn: Turn;
53
+ }
54
+
55
+ interface ThreadStartResponse {
56
+ thread: Thread;
57
+ }
58
+
59
+ interface ThreadResumeResponse {
60
+ thread: Thread;
61
+ }
62
+
63
+ interface ThreadReadResponse {
64
+ thread: Thread;
65
+ }
66
+
67
+ interface ThreadListResponse {
68
+ data: Thread[];
69
+ nextCursor: string | null;
70
+ }
71
+
72
+ interface ThreadLoadedListResponse {
73
+ data: string[];
74
+ nextCursor: string | null;
75
+ }
76
+
77
+ interface AccountRateLimitsResponse {
78
+ rateLimits?: unknown;
79
+ rate_limits?: unknown;
80
+ }
81
+
82
+ export const CODEX_APP_CLIENT_EVENT_CAP = 5_000;
83
+
84
+ export class CodexAppClient {
85
+ private ws!: WebSocket;
86
+ private nextId = 1;
87
+ private pending = new Map<JsonRpcId, { resolve: (value: any) => void; reject: (err: unknown) => void }>();
88
+ private events: ClientEvent[] = [];
89
+ private listeners = new Set<(event: ClientEvent) => void>();
90
+ private connected = false;
91
+ private connectionListeners = new Set<(connected: boolean) => void>();
92
+
93
+ constructor(private readonly url: string, private readonly log: (msg: string) => void = () => {}) {}
94
+
95
+ async connect(): Promise<void> {
96
+ if (this.connected) return;
97
+ await new Promise<void>((resolve, reject) => {
98
+ const ws = new WebSocket(this.url);
99
+ this.ws = ws;
100
+
101
+ ws.onopen = () => {
102
+ this.connected = true;
103
+ this.emitConnection(true);
104
+ resolve();
105
+ };
106
+ ws.onerror = (event) => reject(new Error(`websocket error: ${String((event as ErrorEvent).message || "unknown")}`));
107
+ ws.onclose = (event) => {
108
+ this.connected = false;
109
+ this.emitConnection(false);
110
+ const err = new Error(`websocket closed code=${event.code} reason=${event.reason || "(none)"}`);
111
+ for (const pending of this.pending.values()) pending.reject(err);
112
+ this.pending.clear();
113
+ };
114
+ ws.onmessage = (event) => this.handleMessage(String(event.data));
115
+ });
116
+ }
117
+
118
+ close(): void {
119
+ if (!this.ws) return;
120
+ this.ws.close();
121
+ }
122
+
123
+ isConnected(): boolean {
124
+ return this.connected;
125
+ }
126
+
127
+ async initialize(): Promise<unknown> {
128
+ return this.request("initialize", {
129
+ clientInfo: {
130
+ name: "agent-relay-runner-codex",
131
+ title: "Agent Relay Codex Runner",
132
+ version: "0.1.0",
133
+ },
134
+ capabilities: {
135
+ experimentalApi: true,
136
+ },
137
+ });
138
+ }
139
+
140
+ onEvent(listener: (event: ClientEvent) => void): () => void {
141
+ this.listeners.add(listener);
142
+ return () => this.listeners.delete(listener);
143
+ }
144
+
145
+ onConnectionChange(listener: (connected: boolean) => void): () => void {
146
+ this.connectionListeners.add(listener);
147
+ return () => this.connectionListeners.delete(listener);
148
+ }
149
+
150
+ getEvents(): ClientEvent[] {
151
+ return [...this.events];
152
+ }
153
+
154
+ async settle(ms = 150): Promise<void> {
155
+ await delay(ms);
156
+ }
157
+
158
+ async threadStart(params: Record<string, unknown>): Promise<ThreadStartResponse> {
159
+ return this.request<ThreadStartResponse>("thread/start", params);
160
+ }
161
+
162
+ async threadResume(params: Record<string, unknown>): Promise<ThreadResumeResponse> {
163
+ return this.request<ThreadResumeResponse>("thread/resume", params);
164
+ }
165
+
166
+ async threadRead(threadId: string, includeTurns = false): Promise<ThreadReadResponse> {
167
+ return this.request<ThreadReadResponse>("thread/read", { threadId, includeTurns });
168
+ }
169
+
170
+ async threadList(params: Record<string, unknown>): Promise<ThreadListResponse> {
171
+ return this.request<ThreadListResponse>("thread/list", params);
172
+ }
173
+
174
+ async threadLoadedList(limit = 20): Promise<ThreadLoadedListResponse> {
175
+ return this.request<ThreadLoadedListResponse>("thread/loaded/list", { limit });
176
+ }
177
+
178
+ async threadCompactStart(threadId: string): Promise<Record<string, never>> {
179
+ return this.request<Record<string, never>>("thread/compact/start", { threadId });
180
+ }
181
+
182
+ async turnStart(threadId: string, text: string): Promise<TurnStartResponse> {
183
+ return this.request<TurnStartResponse>("turn/start", {
184
+ threadId,
185
+ input: [{ type: "text", text }],
186
+ });
187
+ }
188
+
189
+ async turnSteer(threadId: string, turnId: string, text: string): Promise<{ turnId: string }> {
190
+ return this.request<{ turnId: string }>("turn/steer", {
191
+ threadId,
192
+ expectedTurnId: turnId,
193
+ input: [{ type: "text", text }],
194
+ });
195
+ }
196
+
197
+ async turnInterrupt(threadId: string, turnId: string): Promise<Record<string, never>> {
198
+ return this.request<Record<string, never>>("turn/interrupt", { threadId, turnId });
199
+ }
200
+
201
+ async accountRateLimitsRead(): Promise<AccountRateLimitsResponse> {
202
+ return this.request<AccountRateLimitsResponse>("account/rateLimits/read");
203
+ }
204
+
205
+ respondToServerRequest(id: JsonRpcId, result: unknown): void {
206
+ if (!this.connected) {
207
+ throw new Error("websocket not connected");
208
+ }
209
+ this.ws.send(JSON.stringify({ id, result }));
210
+ }
211
+
212
+ rejectServerRequest(id: JsonRpcId, code: number, message: string, data?: unknown): void {
213
+ if (!this.connected) {
214
+ throw new Error("websocket not connected");
215
+ }
216
+ this.ws.send(JSON.stringify({ id, error: { code, message, ...(data !== undefined ? { data } : {}) } }));
217
+ }
218
+
219
+ private async request<T = unknown>(method: string, params?: unknown): Promise<T> {
220
+ if (!this.connected) {
221
+ throw new Error("websocket not connected");
222
+ }
223
+ const id = this.nextId++;
224
+ const payload: JsonRpcRequest = { id, method, params };
225
+ const promise = new Promise<T>((resolve, reject) => {
226
+ this.pending.set(id, { resolve, reject });
227
+ });
228
+ this.ws.send(JSON.stringify(payload));
229
+ return promise;
230
+ }
231
+
232
+ private handleMessage(raw: string): void {
233
+ // The frame comes from an external process over a WebSocket; a partial or
234
+ // malformed frame must not throw out of onmessage and drop the connection.
235
+ let parsed: JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
236
+ try {
237
+ parsed = JSON.parse(raw) as JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
238
+ } catch {
239
+ this.log(`dropped malformed frame (${raw.length} bytes)`);
240
+ return;
241
+ }
242
+
243
+ if ("id" in parsed && ("result" in parsed || "error" in parsed)) {
244
+ const pending = this.pending.get(parsed.id);
245
+ if (pending) {
246
+ this.pending.delete(parsed.id);
247
+ if (parsed.error) {
248
+ pending.reject(new Error(`${parsed.error.message} (${parsed.error.code})`));
249
+ } else {
250
+ pending.resolve(parsed.result);
251
+ }
252
+ }
253
+ this.record({ type: "response", message: parsed });
254
+ return;
255
+ }
256
+
257
+ if ("id" in parsed && "method" in parsed) {
258
+ this.log(`server-request ${parsed.method}`);
259
+ this.record({ type: "server-request", message: parsed });
260
+ return;
261
+ }
262
+
263
+ if ("method" in parsed) {
264
+ this.record({ type: "notification", message: parsed });
265
+ }
266
+ }
267
+
268
+ private record(event: ClientEvent): void {
269
+ this.events.push(event);
270
+ if (this.events.length > CODEX_APP_CLIENT_EVENT_CAP) {
271
+ this.events.splice(0, this.events.length - CODEX_APP_CLIENT_EVENT_CAP);
272
+ }
273
+ for (const listener of this.listeners) listener(event);
274
+ }
275
+
276
+ private emitConnection(connected: boolean): void {
277
+ for (const listener of this.connectionListeners) listener(connected);
278
+ }
279
+ }