agent-embassy 1.0.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.
Files changed (145) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CONTRIBUTING.md +122 -0
  3. package/LICENSE +21 -0
  4. package/README.md +185 -0
  5. package/README.zh-CN.md +185 -0
  6. package/SECURITY.md +224 -0
  7. package/assets/live-dashboard/app.css +1610 -0
  8. package/assets/mark-seal.svg +7 -0
  9. package/assets/mark.svg +7 -0
  10. package/assets/social-preview.png +0 -0
  11. package/assets/vendor/react/LICENSE +21 -0
  12. package/assets/vendor/react/react-dom.production.min.js +267 -0
  13. package/assets/vendor/react/react.production.min.js +31 -0
  14. package/dist/src/errors.d.ts +5 -0
  15. package/dist/src/errors.js +11 -0
  16. package/dist/src/errors.js.map +1 -0
  17. package/dist/src/gateway/claude-compatibility-scratch.d.ts +18 -0
  18. package/dist/src/gateway/claude-compatibility-scratch.js +158 -0
  19. package/dist/src/gateway/claude-compatibility-scratch.js.map +1 -0
  20. package/dist/src/gateway/claude-helper-client.d.ts +43 -0
  21. package/dist/src/gateway/claude-helper-client.js +212 -0
  22. package/dist/src/gateway/claude-helper-client.js.map +1 -0
  23. package/dist/src/gateway/claude-helper-protocol.d.ts +172 -0
  24. package/dist/src/gateway/claude-helper-protocol.js +373 -0
  25. package/dist/src/gateway/claude-helper-protocol.js.map +1 -0
  26. package/dist/src/gateway/claude-helper-supervisor.d.ts +74 -0
  27. package/dist/src/gateway/claude-helper-supervisor.js +497 -0
  28. package/dist/src/gateway/claude-helper-supervisor.js.map +1 -0
  29. package/dist/src/gateway/claude-helper.d.ts +1 -0
  30. package/dist/src/gateway/claude-helper.js +269 -0
  31. package/dist/src/gateway/claude-helper.js.map +1 -0
  32. package/dist/src/gateway/claude-peer.d.ts +273 -0
  33. package/dist/src/gateway/claude-peer.js +2359 -0
  34. package/dist/src/gateway/claude-peer.js.map +1 -0
  35. package/dist/src/gateway/claude-runtime.d.ts +40 -0
  36. package/dist/src/gateway/claude-runtime.js +302 -0
  37. package/dist/src/gateway/claude-runtime.js.map +1 -0
  38. package/dist/src/gateway/cli-copy.d.ts +8 -0
  39. package/dist/src/gateway/cli-copy.en.d.ts +13 -0
  40. package/dist/src/gateway/cli-copy.en.js +43 -0
  41. package/dist/src/gateway/cli-copy.en.js.map +1 -0
  42. package/dist/src/gateway/cli-copy.js +19 -0
  43. package/dist/src/gateway/cli-copy.js.map +1 -0
  44. package/dist/src/gateway/cli-copy.zh-CN.d.ts +13 -0
  45. package/dist/src/gateway/cli-copy.zh-CN.js +43 -0
  46. package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -0
  47. package/dist/src/gateway/cli.d.ts +44 -0
  48. package/dist/src/gateway/cli.js +969 -0
  49. package/dist/src/gateway/cli.js.map +1 -0
  50. package/dist/src/gateway/codex-app-server.d.ts +310 -0
  51. package/dist/src/gateway/codex-app-server.js +1746 -0
  52. package/dist/src/gateway/codex-app-server.js.map +1 -0
  53. package/dist/src/gateway/codex-local-transport.d.ts +81 -0
  54. package/dist/src/gateway/codex-local-transport.js +617 -0
  55. package/dist/src/gateway/codex-local-transport.js.map +1 -0
  56. package/dist/src/gateway/codex-registration-generation.d.ts +5 -0
  57. package/dist/src/gateway/codex-registration-generation.js +19 -0
  58. package/dist/src/gateway/codex-registration-generation.js.map +1 -0
  59. package/dist/src/gateway/codex-registration-succession.d.ts +209 -0
  60. package/dist/src/gateway/codex-registration-succession.js +588 -0
  61. package/dist/src/gateway/codex-registration-succession.js.map +1 -0
  62. package/dist/src/gateway/compatibility.d.ts +70 -0
  63. package/dist/src/gateway/compatibility.js +252 -0
  64. package/dist/src/gateway/compatibility.js.map +1 -0
  65. package/dist/src/gateway/config.d.ts +24 -0
  66. package/dist/src/gateway/config.js +130 -0
  67. package/dist/src/gateway/config.js.map +1 -0
  68. package/dist/src/gateway/control.d.ts +308 -0
  69. package/dist/src/gateway/control.js +1573 -0
  70. package/dist/src/gateway/control.js.map +1 -0
  71. package/dist/src/gateway/dashboard-copy.d.ts +7 -0
  72. package/dist/src/gateway/dashboard-copy.en.d.ts +506 -0
  73. package/dist/src/gateway/dashboard-copy.en.js +507 -0
  74. package/dist/src/gateway/dashboard-copy.en.js.map +1 -0
  75. package/dist/src/gateway/dashboard-copy.js +516 -0
  76. package/dist/src/gateway/dashboard-copy.js.map +1 -0
  77. package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +506 -0
  78. package/dist/src/gateway/dashboard-copy.zh-CN.js +507 -0
  79. package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -0
  80. package/dist/src/gateway/dashboard-model.d.ts +207 -0
  81. package/dist/src/gateway/dashboard-model.js +720 -0
  82. package/dist/src/gateway/dashboard-model.js.map +1 -0
  83. package/dist/src/gateway/dashboard.d.ts +26 -0
  84. package/dist/src/gateway/dashboard.js +941 -0
  85. package/dist/src/gateway/dashboard.js.map +1 -0
  86. package/dist/src/gateway/delivery-machine.d.ts +235 -0
  87. package/dist/src/gateway/delivery-machine.js +540 -0
  88. package/dist/src/gateway/delivery-machine.js.map +1 -0
  89. package/dist/src/gateway/instance-lease.d.ts +21 -0
  90. package/dist/src/gateway/instance-lease.js +549 -0
  91. package/dist/src/gateway/instance-lease.js.map +1 -0
  92. package/dist/src/gateway/live-dashboard-app/app.js +2914 -0
  93. package/dist/src/gateway/live-dashboard-assets.d.ts +10 -0
  94. package/dist/src/gateway/live-dashboard-assets.js +73 -0
  95. package/dist/src/gateway/live-dashboard-assets.js.map +1 -0
  96. package/dist/src/gateway/live-dashboard-bootstrap.d.ts +36 -0
  97. package/dist/src/gateway/live-dashboard-bootstrap.js +176 -0
  98. package/dist/src/gateway/live-dashboard-bootstrap.js.map +1 -0
  99. package/dist/src/gateway/live-dashboard-command.d.ts +54 -0
  100. package/dist/src/gateway/live-dashboard-command.js +324 -0
  101. package/dist/src/gateway/live-dashboard-command.js.map +1 -0
  102. package/dist/src/gateway/live-dashboard-http.d.ts +41 -0
  103. package/dist/src/gateway/live-dashboard-http.js +445 -0
  104. package/dist/src/gateway/live-dashboard-http.js.map +1 -0
  105. package/dist/src/gateway/live-dashboard-protocol.d.ts +38 -0
  106. package/dist/src/gateway/live-dashboard-protocol.js +156 -0
  107. package/dist/src/gateway/live-dashboard-protocol.js.map +1 -0
  108. package/dist/src/gateway/live-dashboard-server.d.ts +33 -0
  109. package/dist/src/gateway/live-dashboard-server.js +130 -0
  110. package/dist/src/gateway/live-dashboard-server.js.map +1 -0
  111. package/dist/src/gateway/live-dashboard-stream.d.ts +46 -0
  112. package/dist/src/gateway/live-dashboard-stream.js +233 -0
  113. package/dist/src/gateway/live-dashboard-stream.js.map +1 -0
  114. package/dist/src/gateway/live-dashboard.d.ts +31 -0
  115. package/dist/src/gateway/live-dashboard.js +190 -0
  116. package/dist/src/gateway/live-dashboard.js.map +1 -0
  117. package/dist/src/gateway/locale.d.ts +4 -0
  118. package/dist/src/gateway/locale.js +10 -0
  119. package/dist/src/gateway/locale.js.map +1 -0
  120. package/dist/src/gateway/progress-watch-machine.d.ts +97 -0
  121. package/dist/src/gateway/progress-watch-machine.js +172 -0
  122. package/dist/src/gateway/progress-watch-machine.js.map +1 -0
  123. package/dist/src/gateway/providers.d.ts +287 -0
  124. package/dist/src/gateway/providers.js +2451 -0
  125. package/dist/src/gateway/providers.js.map +1 -0
  126. package/dist/src/gateway/server.d.ts +57 -0
  127. package/dist/src/gateway/server.js +264 -0
  128. package/dist/src/gateway/server.js.map +1 -0
  129. package/dist/src/gateway/service.d.ts +449 -0
  130. package/dist/src/gateway/service.js +4831 -0
  131. package/dist/src/gateway/service.js.map +1 -0
  132. package/dist/src/gateway/store.d.ts +340 -0
  133. package/dist/src/gateway/store.js +4047 -0
  134. package/dist/src/gateway/store.js.map +1 -0
  135. package/dist/src/gateway/types.d.ts +514 -0
  136. package/dist/src/gateway/types.js +414 -0
  137. package/dist/src/gateway/types.js.map +1 -0
  138. package/dist/src/mutex.d.ts +4 -0
  139. package/dist/src/mutex.js +23 -0
  140. package/dist/src/mutex.js.map +1 -0
  141. package/docs/DESIGN.md +134 -0
  142. package/docs/GATEWAY-ARCHITECTURE.md +858 -0
  143. package/package.json +80 -0
  144. package/skills/embassy-peer/SKILL.md +198 -0
  145. package/skills/embassy-peer/agents/openai.yaml +4 -0
@@ -0,0 +1,1746 @@
1
+ import { Buffer } from "node:buffer";
2
+ import WebSocket from "ws";
3
+ import { sharesCompatibilityMajor } from "./compatibility.js";
4
+ /**
5
+ * Stable App Server methods reviewed for the gateway's first writable version.
6
+ *
7
+ * `turn/steer` is exposed only through the exact Claude-to-Codex `STEER:`
8
+ * contract. It queues input at the attested next tool-call boundary and never
9
+ * authorizes an interrupt or a generic/public JSON-RPC escape hatch.
10
+ */
11
+ export const CODEX_APP_SERVER_V1_METHODS = [
12
+ "thread/loaded/list",
13
+ "thread/resume",
14
+ "thread/unsubscribe",
15
+ "turn/start",
16
+ "turn/steer",
17
+ "turn/interrupt",
18
+ ];
19
+ /** Exact App Server builds whose writable v2 schema was reviewed. */
20
+ export const CODEX_APP_SERVER_WRITABLE_VERSIONS = ["0.147.0"];
21
+ export class CodexConnectorError extends Error {
22
+ ambiguous;
23
+ code;
24
+ constructor(code, ambiguous = false) {
25
+ super(code);
26
+ this.name = "CodexConnectorError";
27
+ this.code = code;
28
+ this.ambiguous = ambiguous;
29
+ }
30
+ }
31
+ const DEFAULT_MAX_FRAME_BYTES = 1024 * 1024;
32
+ const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
33
+ const DEFAULT_TURN_WATCHDOG_MS = 2 * 60_000;
34
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;
35
+ const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5_000;
36
+ const DEFAULT_MAX_INPUT_BYTES = 64 * 1024;
37
+ const DEFAULT_MAX_QUEUE_DEPTH = 32;
38
+ const DEFAULT_MAX_MESSAGE_IDS = 4_096;
39
+ const DEFAULT_MAX_DEADLINE_MS = 24 * 60 * 60 * 1_000;
40
+ function isRecord(value) {
41
+ return typeof value === "object" && value !== null && !Array.isArray(value);
42
+ }
43
+ function rawDataToBuffer(data) {
44
+ if (Buffer.isBuffer(data))
45
+ return data;
46
+ if (data instanceof ArrayBuffer)
47
+ return Buffer.from(data);
48
+ return Buffer.concat(data);
49
+ }
50
+ /**
51
+ * WebSocket client over a caller-owned, socket-compatible Duplex. The caller
52
+ * remains responsible for spawning, bounding, and terminating its exact local
53
+ * or SSH proxy process; this adapter never discovers or signals App Server.
54
+ */
55
+ export class WebSocketDuplexTransport {
56
+ socket;
57
+ maxFrameBytes;
58
+ closeTimeoutMs;
59
+ heartbeatTimeoutMs;
60
+ static async connect(stream, options = {}) {
61
+ const maxFrameBytes = positiveInteger(options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES, "INVALID_CONFIGURATION");
62
+ const handshakeTimeoutMs = positiveInteger(options.handshakeTimeoutMs ?? 5_000, "INVALID_CONFIGURATION");
63
+ const closeTimeoutMs = positiveInteger(options.closeTimeoutMs ?? 2_000, "INVALID_CONFIGURATION");
64
+ const heartbeatIntervalMs = positiveInteger(options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS, "INVALID_CONFIGURATION");
65
+ const heartbeatTimeoutMs = positiveInteger(options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS, "INVALID_CONFIGURATION");
66
+ if (heartbeatTimeoutMs >= heartbeatIntervalMs) {
67
+ throw new CodexConnectorError("INVALID_CONFIGURATION");
68
+ }
69
+ const socket = new WebSocket("ws://localhost/rpc", {
70
+ createConnection: () => {
71
+ stream.setNoDelay(true);
72
+ stream.setKeepAlive(true, heartbeatIntervalMs);
73
+ queueMicrotask(() => stream.emit("connect"));
74
+ return stream;
75
+ },
76
+ followRedirects: false,
77
+ handshakeTimeout: handshakeTimeoutMs,
78
+ maxPayload: maxFrameBytes,
79
+ perMessageDeflate: false,
80
+ });
81
+ // A permanent sink prevents an EventEmitter error from becoming an
82
+ // uncaught exception before/after connector listeners are installed.
83
+ socket.on("error", () => undefined);
84
+ await new Promise((resolve, reject) => {
85
+ let settled = false;
86
+ const finish = (error) => {
87
+ if (settled)
88
+ return;
89
+ settled = true;
90
+ socket.off("open", onOpen);
91
+ socket.off("error", onError);
92
+ socket.off("unexpected-response", onUnexpectedResponse);
93
+ if (error === undefined)
94
+ resolve();
95
+ else {
96
+ socket.terminate();
97
+ reject(error);
98
+ }
99
+ };
100
+ const onOpen = () => finish();
101
+ const onError = () => finish(new CodexConnectorError("TRANSPORT_CLOSED"));
102
+ const onUnexpectedResponse = () => finish(new CodexConnectorError("PROTOCOL_ERROR"));
103
+ socket.once("open", onOpen);
104
+ socket.once("error", onError);
105
+ socket.once("unexpected-response", onUnexpectedResponse);
106
+ });
107
+ return new WebSocketDuplexTransport(socket, maxFrameBytes, closeTimeoutMs, heartbeatIntervalMs, heartbeatTimeoutMs);
108
+ }
109
+ closeListeners = new Set();
110
+ errorListeners = new Set();
111
+ messageListeners = new Set();
112
+ heartbeatInterval;
113
+ heartbeatTimeout;
114
+ heartbeatFailed = false;
115
+ constructor(socket, maxFrameBytes, closeTimeoutMs, heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS) {
116
+ this.socket = socket;
117
+ this.maxFrameBytes = maxFrameBytes;
118
+ this.closeTimeoutMs = closeTimeoutMs;
119
+ this.heartbeatTimeoutMs = heartbeatTimeoutMs;
120
+ socket.on("message", (data, isBinary) => {
121
+ if (isBinary) {
122
+ this.emitError();
123
+ socket.terminate();
124
+ return;
125
+ }
126
+ const bytes = rawDataToBuffer(data);
127
+ if (bytes.length > this.maxFrameBytes) {
128
+ this.emitError();
129
+ socket.terminate();
130
+ return;
131
+ }
132
+ const payload = bytes.toString("utf8");
133
+ for (const listener of this.messageListeners)
134
+ listener(payload);
135
+ });
136
+ socket.on("pong", () => {
137
+ if (this.heartbeatTimeout !== undefined) {
138
+ clearTimeout(this.heartbeatTimeout);
139
+ this.heartbeatTimeout = undefined;
140
+ }
141
+ });
142
+ socket.on("error", () => this.failHeartbeat());
143
+ socket.on("close", () => {
144
+ this.stopHeartbeat();
145
+ for (const listener of this.closeListeners)
146
+ listener();
147
+ });
148
+ this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), heartbeatIntervalMs);
149
+ this.heartbeatInterval.unref();
150
+ }
151
+ onMessage(listener) {
152
+ this.messageListeners.add(listener);
153
+ return () => this.messageListeners.delete(listener);
154
+ }
155
+ onClose(listener) {
156
+ this.closeListeners.add(listener);
157
+ return () => this.closeListeners.delete(listener);
158
+ }
159
+ onError(listener) {
160
+ this.errorListeners.add(listener);
161
+ return () => this.errorListeners.delete(listener);
162
+ }
163
+ async send(payload) {
164
+ if (this.socket.readyState !== WebSocket.OPEN) {
165
+ throw new CodexConnectorError("TRANSPORT_CLOSED");
166
+ }
167
+ if (Buffer.byteLength(payload, "utf8") > this.maxFrameBytes) {
168
+ throw new CodexConnectorError("INPUT_INVALID");
169
+ }
170
+ await new Promise((resolve, reject) => {
171
+ this.socket.send(payload, (error) => {
172
+ if (error == null)
173
+ resolve();
174
+ else
175
+ reject(new CodexConnectorError("TRANSPORT_WRITE_FAILED", true));
176
+ });
177
+ });
178
+ }
179
+ async close() {
180
+ this.stopHeartbeat();
181
+ if (this.socket.readyState === WebSocket.CLOSED)
182
+ return;
183
+ await new Promise((resolve) => {
184
+ let settled = false;
185
+ const finish = () => {
186
+ if (settled)
187
+ return;
188
+ settled = true;
189
+ clearTimeout(timer);
190
+ this.socket.off("close", finish);
191
+ resolve();
192
+ };
193
+ const timer = setTimeout(() => {
194
+ this.socket.terminate();
195
+ finish();
196
+ }, this.closeTimeoutMs);
197
+ this.socket.once("close", finish);
198
+ this.socket.close(1000);
199
+ });
200
+ }
201
+ emitError() {
202
+ for (const listener of this.errorListeners)
203
+ listener();
204
+ }
205
+ sendHeartbeat() {
206
+ if (this.socket.readyState !== WebSocket.OPEN ||
207
+ this.heartbeatFailed ||
208
+ this.heartbeatTimeout !== undefined) {
209
+ return;
210
+ }
211
+ try {
212
+ this.socket.ping(undefined, undefined, (error) => {
213
+ if (error != null)
214
+ this.failHeartbeat();
215
+ });
216
+ }
217
+ catch {
218
+ this.failHeartbeat();
219
+ return;
220
+ }
221
+ if (this.heartbeatFailed)
222
+ return;
223
+ this.heartbeatTimeout = setTimeout(() => this.failHeartbeat(), this.heartbeatTimeoutMs);
224
+ this.heartbeatTimeout.unref();
225
+ }
226
+ failHeartbeat() {
227
+ if (this.heartbeatFailed)
228
+ return;
229
+ this.heartbeatFailed = true;
230
+ this.stopHeartbeat();
231
+ this.emitError();
232
+ this.socket.terminate();
233
+ }
234
+ stopHeartbeat() {
235
+ if (this.heartbeatInterval !== undefined) {
236
+ clearInterval(this.heartbeatInterval);
237
+ this.heartbeatInterval = undefined;
238
+ }
239
+ if (this.heartbeatTimeout !== undefined) {
240
+ clearTimeout(this.heartbeatTimeout);
241
+ this.heartbeatTimeout = undefined;
242
+ }
243
+ }
244
+ }
245
+ const V1_METHOD_SET = new Set(CODEX_APP_SERVER_V1_METHODS);
246
+ const OUTPUT_NOTIFICATION_OPT_OUTS = [
247
+ "item/started",
248
+ "item/agentMessage/delta",
249
+ "item/reasoning/textDelta",
250
+ "item/reasoning/summaryTextDelta",
251
+ "item/commandExecution/outputDelta",
252
+ "turn/diff/updated",
253
+ "turn/plan/updated",
254
+ ];
255
+ const APPROVAL_REQUEST_METHODS = new Set([
256
+ "item/commandExecution/requestApproval",
257
+ "item/fileChange/requestApproval",
258
+ "item/permissions/requestApproval",
259
+ "item/tool/requestUserInput",
260
+ "mcpServer/elicitation/request",
261
+ ]);
262
+ function positiveInteger(value, code) {
263
+ if (!Number.isSafeInteger(value) || value <= 0) {
264
+ throw new CodexConnectorError(code);
265
+ }
266
+ return value;
267
+ }
268
+ function validOpaqueId(value, maximumLength = 256) {
269
+ return (value.length > 0 &&
270
+ value.length <= maximumLength &&
271
+ /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(value));
272
+ }
273
+ function validateRoute(route) {
274
+ if (!validOpaqueId(route.threadId) ||
275
+ !validOpaqueId(route.endpointGeneration, 128)) {
276
+ throw new CodexConnectorError("INVALID_ROUTE");
277
+ }
278
+ return { ...route };
279
+ }
280
+ function validateClientInfo(clientInfo) {
281
+ const values = [clientInfo.name, clientInfo.title, clientInfo.version];
282
+ if (values.some((value) => value.length === 0 ||
283
+ value.length > 128 ||
284
+ value.includes("\0") ||
285
+ /[\r\n]/u.test(value))) {
286
+ throw new CodexConnectorError("INVALID_CONFIGURATION");
287
+ }
288
+ return { ...clientInfo };
289
+ }
290
+ function validateCompatibility(compatibility, route) {
291
+ if (compatibility.endpointGeneration !== route.endpointGeneration ||
292
+ compatibility.protocol !== "app-server-v2-stable" ||
293
+ compatibility.steering?.method !== "turn/steer" ||
294
+ compatibility.steering.requestSchema !== "expected-turn-id-text-v1" ||
295
+ compatibility.steering.deliveryBoundary !== "next-tool-call-boundary" ||
296
+ !(CODEX_APP_SERVER_WRITABLE_VERSIONS.some((version) => version === compatibility.appServerVersion) ||
297
+ (compatibility.observedSchemaCandidate === true &&
298
+ CODEX_APP_SERVER_WRITABLE_VERSIONS.some((version) => sharesCompatibilityMajor(version, compatibility.appServerVersion))))) {
299
+ throw new CodexConnectorError("INVALID_CONFIGURATION");
300
+ }
301
+ return { ...compatibility, steering: { ...compatibility.steering } };
302
+ }
303
+ function parseRouteStatus(value) {
304
+ if (!isRecord(value) || typeof value.type !== "string")
305
+ return null;
306
+ if (value.type === "notLoaded")
307
+ return "not_loaded";
308
+ if (value.type === "idle")
309
+ return "idle";
310
+ if (value.type === "systemError")
311
+ return "system_error";
312
+ if (value.type !== "active")
313
+ return null;
314
+ if (value.activeFlags === undefined)
315
+ return "active";
316
+ if (!Array.isArray(value.activeFlags) ||
317
+ value.activeFlags.length > 32 ||
318
+ !value.activeFlags.every((flag) => typeof flag === "string" && flag.length <= 128)) {
319
+ return null;
320
+ }
321
+ return value.activeFlags.includes("waitingOnApproval")
322
+ ? "waiting_approval"
323
+ : "active";
324
+ }
325
+ function parseTurn(value) {
326
+ if (!isRecord(value) ||
327
+ typeof value.id !== "string" ||
328
+ !validOpaqueId(value.id) ||
329
+ typeof value.status !== "string" ||
330
+ value.status.length > 64) {
331
+ return null;
332
+ }
333
+ return { id: value.id, status: value.status };
334
+ }
335
+ /**
336
+ * One connection, one exact opted-in Codex route, one endpoint generation.
337
+ * Reconnection creates a new connector/generation; this object never retries
338
+ * an ambiguous write or silently rebinds a stale route.
339
+ */
340
+ export class CodexAppServerConnector {
341
+ static async connect(options) {
342
+ const connector = new CodexAppServerConnector(options);
343
+ await connector.initialize();
344
+ return connector;
345
+ }
346
+ acceptedMessageIds = new Set();
347
+ activeMessageId = null;
348
+ activeTurnId = null;
349
+ connection = "connecting";
350
+ drainScheduled = false;
351
+ eventSequence = 0;
352
+ expectedClose = false;
353
+ faulting = false;
354
+ consecutiveRequestTimeouts = 0;
355
+ inFlightMethod = null;
356
+ initializeRequested = false;
357
+ initialized = false;
358
+ lastCompletedTurnId = null;
359
+ nextRequestId = 1;
360
+ ownsActiveTurn = false;
361
+ pending = new Map();
362
+ queue = [];
363
+ revision = 0;
364
+ routeStatus = "unknown";
365
+ selectedThreadObserved = false;
366
+ statusEpoch = 0;
367
+ transientReply = null;
368
+ transientReplyTooLarge = false;
369
+ turnWatchdogTimer;
370
+ unlisten = [];
371
+ clientInfo;
372
+ compatibility;
373
+ maxDeadlineMs;
374
+ maxFrameBytes;
375
+ maxInputBytes;
376
+ maxMessageIds;
377
+ maxQueueDepth;
378
+ maxReplyBytes;
379
+ now;
380
+ onEvent;
381
+ onTurnResult;
382
+ requestTimeoutMs;
383
+ turnWatchdogMs;
384
+ route;
385
+ transport;
386
+ writesEnabled;
387
+ constructor(options) {
388
+ this.route = validateRoute(options.route);
389
+ this.compatibility = validateCompatibility(options.compatibility, this.route);
390
+ if (typeof options.writesEnabled !== "boolean") {
391
+ throw new CodexConnectorError("INVALID_CONFIGURATION");
392
+ }
393
+ this.writesEnabled = options.writesEnabled;
394
+ this.transport = options.transport;
395
+ this.clientInfo = validateClientInfo(options.clientInfo ?? {
396
+ name: "agent_embassy_gateway",
397
+ title: "Embassy Gateway",
398
+ version: "1.0.0",
399
+ });
400
+ this.maxFrameBytes = positiveInteger(options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES, "INVALID_CONFIGURATION");
401
+ this.maxDeadlineMs = positiveInteger(options.maxDeadlineMs ?? DEFAULT_MAX_DEADLINE_MS, "INVALID_CONFIGURATION");
402
+ this.maxInputBytes = positiveInteger(options.maxInputBytes ?? DEFAULT_MAX_INPUT_BYTES, "INVALID_CONFIGURATION");
403
+ this.maxMessageIds = positiveInteger(options.maxMessageIds ?? DEFAULT_MAX_MESSAGE_IDS, "INVALID_CONFIGURATION");
404
+ this.maxQueueDepth = positiveInteger(options.maxQueueDepth ?? DEFAULT_MAX_QUEUE_DEPTH, "INVALID_CONFIGURATION");
405
+ this.maxReplyBytes = positiveInteger(options.maxReplyBytes ?? DEFAULT_MAX_INPUT_BYTES, "INVALID_CONFIGURATION");
406
+ this.requestTimeoutMs = positiveInteger(options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, "INVALID_CONFIGURATION");
407
+ this.turnWatchdogMs = positiveInteger(options.turnWatchdogMs ?? DEFAULT_TURN_WATCHDOG_MS, "INVALID_CONFIGURATION");
408
+ this.now = options.now ?? (() => new Date());
409
+ this.onEvent = options.onEvent;
410
+ this.onTurnResult = options.onTurnResult;
411
+ this.unlisten.push(this.transport.onMessage((payload) => this.handlePayload(payload)), this.transport.onClose(() => this.handleDisconnect()), this.transport.onError(() => this.protocolFault("TRANSPORT_CLOSED")));
412
+ }
413
+ observation() {
414
+ const writeBlockCode = this.currentWriteBlockCode();
415
+ return {
416
+ connection: this.connection,
417
+ eventSequence: this.eventSequence,
418
+ hasActiveTurn: this.activeTurnId !== null,
419
+ queueDepth: this.queue.length,
420
+ requestInFlight: this.inFlightMethod !== null,
421
+ routeStatus: this.routeStatus,
422
+ writableReady: this.isWritableReady(),
423
+ writeBlockCode,
424
+ };
425
+ }
426
+ guard() {
427
+ const writableReady = this.isWritableReady();
428
+ return {
429
+ ...this.route,
430
+ activeTurnId: this.activeTurnId,
431
+ revision: this.revision,
432
+ status: this.routeStatus,
433
+ writableReady,
434
+ };
435
+ }
436
+ async observeLoadedThread(guard) {
437
+ this.assertGuard(guard);
438
+ this.assertReady();
439
+ this.assertNoRequest();
440
+ this.beginRequest("thread/loaded/list");
441
+ try {
442
+ const result = await this.request("thread/loaded/list", {});
443
+ if (!isRecord(result) || !Array.isArray(result.data)) {
444
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
445
+ }
446
+ const data = result.data;
447
+ if (data.length > 100_000 ||
448
+ !data.every((item) => typeof item === "string" && validOpaqueId(item))) {
449
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
450
+ }
451
+ const selectedThreadLoaded = data.includes(this.route.threadId);
452
+ if (this.selectedThreadObserved !== selectedThreadLoaded) {
453
+ this.selectedThreadObserved = selectedThreadLoaded;
454
+ this.bumpRevision();
455
+ }
456
+ if (!selectedThreadLoaded) {
457
+ if (this.routeStatus === "active" ||
458
+ this.routeStatus === "waiting_approval" ||
459
+ this.routeStatus === "interrupting" ||
460
+ this.activeTurnId !== null) {
461
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
462
+ }
463
+ this.setRouteStatus("not_loaded");
464
+ }
465
+ const loadedObservation = {
466
+ loadedThreadCount: data.length,
467
+ selectedThreadLoaded,
468
+ };
469
+ this.emit("thread_observed", {
470
+ loadedThreadCount: data.length,
471
+ selectedThreadLoaded,
472
+ });
473
+ this.finishRequest("thread/loaded/list");
474
+ return { ...loadedObservation, observation: this.observation() };
475
+ }
476
+ catch (error) {
477
+ const normalized = this.normalizeError(error, true);
478
+ if (normalized.ambiguous && this.connection === "ready") {
479
+ this.settleActiveDelivery("ambiguous");
480
+ this.setRouteStatus("uncertain");
481
+ }
482
+ this.handleActionError(normalized);
483
+ throw normalized;
484
+ }
485
+ finally {
486
+ this.finishRequest("thread/loaded/list");
487
+ }
488
+ }
489
+ async resumeThread(guard) {
490
+ this.assertGuard(guard);
491
+ this.assertReady();
492
+ this.assertNoRequest();
493
+ if (!this.selectedThreadObserved) {
494
+ throw new CodexConnectorError("THREAD_NOT_OBSERVED");
495
+ }
496
+ if (this.routeStatus !== "unknown" &&
497
+ this.routeStatus !== "not_loaded" &&
498
+ !(this.currentWriteBlockCode() !== null &&
499
+ this.routeStatus === "idle" &&
500
+ this.activeTurnId === null)) {
501
+ throw new CodexConnectorError("ROUTE_BUSY");
502
+ }
503
+ const statusEpoch = this.statusEpoch;
504
+ this.beginRequest("thread/resume");
505
+ try {
506
+ const result = await this.request("thread/resume", {
507
+ excludeTurns: true,
508
+ threadId: this.route.threadId,
509
+ });
510
+ if (!isRecord(result) ||
511
+ !isRecord(result.thread) ||
512
+ !Array.isArray(result.thread.turns) ||
513
+ result.thread.turns.length !== 0) {
514
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
515
+ }
516
+ if (result.thread.id !== this.route.threadId) {
517
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
518
+ }
519
+ if (this.statusEpoch === statusEpoch) {
520
+ const status = parseRouteStatus(result.thread.status);
521
+ if (status === null) {
522
+ // A response without status proves subscription but not idleness. Do
523
+ // not guess; queued/model actions remain fail-closed until status is
524
+ // observed.
525
+ this.setRouteStatus("unknown");
526
+ }
527
+ else {
528
+ this.setRouteStatus(status);
529
+ }
530
+ }
531
+ this.emit("thread_resumed");
532
+ const writeBlockCode = this.currentWriteBlockCode();
533
+ if (writeBlockCode !== null) {
534
+ this.emit("route_write_blocked", { errorCode: writeBlockCode });
535
+ }
536
+ }
537
+ catch (error) {
538
+ const normalized = this.normalizeError(error, true);
539
+ if (normalized.ambiguous && this.connection === "ready") {
540
+ this.setRouteStatus("uncertain");
541
+ }
542
+ this.handleActionError(normalized);
543
+ throw normalized;
544
+ }
545
+ finally {
546
+ this.finishRequest("thread/resume");
547
+ }
548
+ return this.observation();
549
+ }
550
+ /**
551
+ * Revalidate the exact selected thread/resume schema without creating a
552
+ * turn. Unlike initial route setup, certification is permitted while the
553
+ * route is already idle; every other state remains fail-closed.
554
+ */
555
+ async certifyThreadOperations(guard) {
556
+ this.assertGuard(guard);
557
+ this.assertReady();
558
+ this.assertNoRequest();
559
+ if (!this.selectedThreadObserved) {
560
+ throw new CodexConnectorError("THREAD_NOT_OBSERVED");
561
+ }
562
+ if (this.routeStatus !== "idle") {
563
+ throw new CodexConnectorError("ROUTE_BUSY");
564
+ }
565
+ const statusEpoch = this.statusEpoch;
566
+ this.beginRequest("thread/resume");
567
+ try {
568
+ const result = await this.request("thread/resume", {
569
+ excludeTurns: true,
570
+ threadId: this.route.threadId,
571
+ });
572
+ if (!isRecord(result) ||
573
+ !isRecord(result.thread) ||
574
+ result.thread.id !== this.route.threadId ||
575
+ !Array.isArray(result.thread.turns) ||
576
+ result.thread.turns.length !== 0) {
577
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
578
+ }
579
+ if (this.statusEpoch === statusEpoch) {
580
+ const status = parseRouteStatus(result.thread.status);
581
+ if (status !== "idle") {
582
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
583
+ }
584
+ this.setRouteStatus(status);
585
+ }
586
+ this.emit("thread_resumed");
587
+ }
588
+ catch (error) {
589
+ const normalized = this.normalizeError(error, true);
590
+ if (normalized.ambiguous && this.connection === "ready") {
591
+ this.setRouteStatus("uncertain");
592
+ }
593
+ this.handleActionError(normalized);
594
+ throw normalized;
595
+ }
596
+ finally {
597
+ this.finishRequest("thread/resume");
598
+ }
599
+ return this.observation();
600
+ }
601
+ async unsubscribeThread(guard) {
602
+ this.assertGuard(guard);
603
+ this.assertReady();
604
+ this.assertNoRequest();
605
+ if (this.activeTurnId !== null ||
606
+ this.routeStatus === "active" ||
607
+ this.routeStatus === "waiting_approval" ||
608
+ this.routeStatus === "starting" ||
609
+ this.routeStatus === "interrupting") {
610
+ throw new CodexConnectorError("ROUTE_BUSY");
611
+ }
612
+ this.dropQueuedMessages();
613
+ this.beginRequest("thread/unsubscribe");
614
+ try {
615
+ const result = await this.request("thread/unsubscribe", {
616
+ threadId: this.route.threadId,
617
+ });
618
+ if (!isRecord(result) ||
619
+ (result.status !== "unsubscribed" &&
620
+ result.status !== "notSubscribed" &&
621
+ result.status !== "notLoaded")) {
622
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
623
+ }
624
+ this.setRouteStatus("not_loaded");
625
+ this.selectedThreadObserved = false;
626
+ this.emit("thread_unsubscribed");
627
+ const status = result.status;
628
+ this.finishRequest("thread/unsubscribe");
629
+ return { observation: this.observation(), status };
630
+ }
631
+ catch (error) {
632
+ const normalized = this.normalizeError(error, true);
633
+ if (normalized.ambiguous && this.connection === "ready") {
634
+ this.setRouteStatus("uncertain");
635
+ }
636
+ this.handleActionError(normalized);
637
+ throw normalized;
638
+ }
639
+ finally {
640
+ this.finishRequest("thread/unsubscribe");
641
+ }
642
+ }
643
+ /**
644
+ * Drop only this exact route's transient queued bodies. Route disable and
645
+ * unregister paths use this before releasing broker ownership.
646
+ */
647
+ cancelQueuedMessages(guard) {
648
+ this.assertGuard(guard);
649
+ this.assertReady();
650
+ const droppedMessages = this.dropQueuedMessages();
651
+ this.emit("queued_messages_cancelled", { droppedMessages });
652
+ return this.observation();
653
+ }
654
+ async submitMessage(guard, input) {
655
+ this.assertGuard(guard);
656
+ this.assertReady();
657
+ if (!this.selectedThreadObserved) {
658
+ throw new CodexConnectorError("THREAD_NOT_OBSERVED");
659
+ }
660
+ if (input.steer === true && this.routeStatus === "active") {
661
+ this.assertWritableReady();
662
+ const message = this.validateMessage(input);
663
+ if (this.activeTurnId === null || this.inFlightMethod !== null) {
664
+ this.acceptedMessageIds.delete(message.messageId);
665
+ return { disposition: "deferred", observation: this.observation() };
666
+ }
667
+ const disposition = await this.steerMessage(message);
668
+ return { disposition, observation: this.observation() };
669
+ }
670
+ if (this.routeStatus === "starting" ||
671
+ this.routeStatus === "active" ||
672
+ this.routeStatus === "waiting_approval" ||
673
+ this.routeStatus === "interrupting") {
674
+ this.assertWritableReady();
675
+ const message = this.validateMessage(input);
676
+ if (input.steer === true) {
677
+ this.acceptedMessageIds.delete(message.messageId);
678
+ return { disposition: "deferred", observation: this.observation() };
679
+ }
680
+ this.enqueue(message);
681
+ return { disposition: "queued", observation: this.observation() };
682
+ }
683
+ this.assertWritableReady();
684
+ const message = this.validateMessage(input);
685
+ if (this.routeStatus !== "idle") {
686
+ this.acceptedMessageIds.delete(message.messageId);
687
+ throw new CodexConnectorError("ROUTE_NOT_READY");
688
+ }
689
+ const disposition = await this.startMessage(message);
690
+ return { disposition, observation: this.observation() };
691
+ }
692
+ /**
693
+ * Interrupt only a connector-originated turn whose exact ID is present in a
694
+ * fresh CAS guard. The method clears queued work and never targets a Desktop
695
+ * or user-originated active turn.
696
+ */
697
+ async interruptOwnedTurn(guard) {
698
+ this.assertGuard(guard);
699
+ this.assertReady();
700
+ this.assertNoRequest();
701
+ if (!this.selectedThreadObserved) {
702
+ throw new CodexConnectorError("THREAD_NOT_OBSERVED");
703
+ }
704
+ if (!this.ownsActiveTurn ||
705
+ this.activeTurnId === null ||
706
+ guard.activeTurnId !== this.activeTurnId ||
707
+ (this.routeStatus !== "active" &&
708
+ this.routeStatus !== "waiting_approval")) {
709
+ throw new CodexConnectorError("TURN_NOT_OWNED");
710
+ }
711
+ const turnId = this.activeTurnId;
712
+ const droppedMessages = this.dropQueuedMessages();
713
+ this.beginRequest("turn/interrupt");
714
+ try {
715
+ const result = await this.request("turn/interrupt", {
716
+ threadId: this.route.threadId,
717
+ turnId,
718
+ });
719
+ if (!isRecord(result)) {
720
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
721
+ }
722
+ // A completion notification can win the race with the response.
723
+ if (this.activeTurnId === turnId && this.ownsActiveTurn) {
724
+ this.setRouteStatus("interrupting");
725
+ }
726
+ this.emit("turn_interrupt_requested", { droppedMessages });
727
+ }
728
+ catch (error) {
729
+ const normalized = this.normalizeError(error, true);
730
+ if (normalized.ambiguous && this.connection === "ready") {
731
+ this.settleActiveDelivery("ambiguous");
732
+ this.activeTurnId = null;
733
+ this.setRouteStatus("uncertain");
734
+ }
735
+ this.handleActionError(normalized);
736
+ throw normalized;
737
+ }
738
+ finally {
739
+ this.finishRequest("turn/interrupt");
740
+ }
741
+ return this.observation();
742
+ }
743
+ async close() {
744
+ if (this.connection === "closed" || this.connection === "faulted")
745
+ return;
746
+ this.clearTurnWatchdog();
747
+ this.expectedClose = true;
748
+ try {
749
+ await this.transport.close();
750
+ }
751
+ finally {
752
+ this.handleDisconnect();
753
+ }
754
+ }
755
+ async initialize() {
756
+ try {
757
+ const result = await this.request("initialize", {
758
+ capabilities: {
759
+ // `thread/resume.excludeTurns` is field-gated behind this capability
760
+ // in the exactly attested 0.147.0 schema. The client method allowlist
761
+ // remains closed and exposes no experimental method.
762
+ experimentalApi: true,
763
+ optOutNotificationMethods: [...OUTPUT_NOTIFICATION_OPT_OUTS],
764
+ },
765
+ clientInfo: this.clientInfo,
766
+ });
767
+ if (!isRecord(result)) {
768
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH");
769
+ }
770
+ await this.transport.send(JSON.stringify({ method: "initialized", params: {} }));
771
+ this.initialized = true;
772
+ this.connection = "ready";
773
+ this.bumpRevision();
774
+ this.emit("connection_ready");
775
+ }
776
+ catch (error) {
777
+ const normalized = this.normalizeError(error, true);
778
+ this.protocolFault(normalized.code);
779
+ throw normalized;
780
+ }
781
+ }
782
+ validateMessage(input) {
783
+ if (!validOpaqueId(input.messageId, 128) ||
784
+ typeof input.text !== "string" ||
785
+ input.text.trim().length === 0 ||
786
+ input.text.includes("\0")) {
787
+ throw new CodexConnectorError("INPUT_INVALID");
788
+ }
789
+ const nowMs = this.now().getTime();
790
+ const deadlineAtMs = typeof input.deadlineAt === "string"
791
+ ? Date.parse(input.deadlineAt)
792
+ : Number.NaN;
793
+ if (!Number.isFinite(nowMs) ||
794
+ !Number.isFinite(deadlineAtMs) ||
795
+ new Date(deadlineAtMs).toISOString() !== input.deadlineAt ||
796
+ deadlineAtMs <= nowMs ||
797
+ deadlineAtMs > nowMs + this.maxDeadlineMs) {
798
+ throw new CodexConnectorError("INPUT_INVALID");
799
+ }
800
+ const byteLength = Buffer.byteLength(input.text, "utf8");
801
+ if (byteLength === 0 || byteLength > this.maxInputBytes) {
802
+ throw new CodexConnectorError("INPUT_INVALID");
803
+ }
804
+ if (this.acceptedMessageIds.has(input.messageId)) {
805
+ throw new CodexConnectorError("MESSAGE_DUPLICATE");
806
+ }
807
+ if (this.acceptedMessageIds.size >= this.maxMessageIds) {
808
+ throw new CodexConnectorError("MESSAGE_CAPACITY");
809
+ }
810
+ this.acceptedMessageIds.add(input.messageId);
811
+ return {
812
+ byteLength,
813
+ deadlineAtMs,
814
+ messageId: input.messageId,
815
+ text: input.text,
816
+ };
817
+ }
818
+ enqueue(message) {
819
+ if (this.queue.length >= this.maxQueueDepth) {
820
+ this.acceptedMessageIds.delete(message.messageId);
821
+ throw new CodexConnectorError("QUEUE_FULL");
822
+ }
823
+ this.queue.push(message);
824
+ this.bumpRevision();
825
+ this.emit("message_queued", { messageBytes: message.byteLength });
826
+ }
827
+ async startMessage(message) {
828
+ this.assertNoRequest();
829
+ this.assertWritableReady();
830
+ if (this.messageExpired(message)) {
831
+ this.expireMessage(message);
832
+ return "expired";
833
+ }
834
+ // Reserve queue order while the host-local lease is being revalidated,
835
+ // but do not claim turn ownership until immediately before the RPC write.
836
+ this.activeMessageId = message.messageId;
837
+ this.ownsActiveTurn = false;
838
+ this.transientReply = null;
839
+ this.transientReplyTooLarge = false;
840
+ this.setRouteStatus("starting");
841
+ let requestBegan = false;
842
+ try {
843
+ await this.refreshExactRouteBoundary();
844
+ if (this.messageExpired(message)) {
845
+ this.activeMessageId = null;
846
+ this.acceptedMessageIds.delete(message.messageId);
847
+ if (this.routeStatus === "starting")
848
+ this.setRouteStatus("idle");
849
+ this.settleDelivery(message.messageId, "expired");
850
+ return "expired";
851
+ }
852
+ if (this.connection !== "ready" ||
853
+ this.routeStatus !== "starting" ||
854
+ this.activeMessageId !== message.messageId) {
855
+ throw new CodexConnectorError("ROUTE_NOT_READY");
856
+ }
857
+ const statusEpoch = this.statusEpoch;
858
+ this.ownsActiveTurn = true;
859
+ this.emit("turn_starting", { messageBytes: message.byteLength });
860
+ this.beginRequest("turn/start");
861
+ requestBegan = true;
862
+ const result = await this.request("turn/start", {
863
+ input: [{ text: message.text, type: "text" }],
864
+ threadId: this.route.threadId,
865
+ });
866
+ if (!isRecord(result)) {
867
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
868
+ }
869
+ const turn = parseTurn(result.turn);
870
+ if (turn === null || turn.status !== "inProgress") {
871
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
872
+ }
873
+ if (this.activeTurnId !== null && this.activeTurnId !== turn.id) {
874
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
875
+ }
876
+ if (this.lastCompletedTurnId === turn.id) {
877
+ // A very fast turn may complete before its start response is handled.
878
+ // Never resurrect it or retry; the completion notification is final.
879
+ return "started";
880
+ }
881
+ this.activeTurnId = turn.id;
882
+ this.ownsActiveTurn = true;
883
+ if (this.statusEpoch === statusEpoch || this.routeStatus === "starting") {
884
+ this.setRouteStatus("active");
885
+ }
886
+ this.emit("turn_started", { messageBytes: message.byteLength });
887
+ this.armTurnWatchdog();
888
+ return "started";
889
+ }
890
+ catch (error) {
891
+ const normalized = this.normalizeError(error, true);
892
+ const noObservedTurn = this.activeTurnId === null;
893
+ if (this.connection !== "ready") {
894
+ // The transport close/fault already settled the reserved delivery.
895
+ }
896
+ else if (!normalized.ambiguous && noObservedTurn) {
897
+ this.settleDelivery(message.messageId, "failed", null, "REPLY_UNAVAILABLE");
898
+ this.activeMessageId = null;
899
+ this.ownsActiveTurn = false;
900
+ this.acceptedMessageIds.delete(message.messageId);
901
+ if (normalized.code !== "ROUTE_NOT_READY" ||
902
+ this.routeStatus === "starting") {
903
+ // A clean JSON-RPC rejection may represent a concurrent Desktop
904
+ // state change. Stop the queue until authoritative state is observed.
905
+ this.setRouteStatus("system_error");
906
+ }
907
+ }
908
+ else {
909
+ this.settleActiveDelivery("ambiguous");
910
+ this.activeTurnId = null;
911
+ this.setRouteStatus("uncertain");
912
+ }
913
+ this.handleActionError(normalized);
914
+ throw normalized;
915
+ }
916
+ finally {
917
+ if (requestBegan)
918
+ this.finishRequest("turn/start");
919
+ }
920
+ }
921
+ /**
922
+ * Add one exact input to the currently observed active turn. App Server owns
923
+ * the temporal boundary: the attested method applies it at the next tool-call
924
+ * boundary, never by interrupting an in-progress generation.
925
+ */
926
+ async steerMessage(message) {
927
+ this.assertNoRequest();
928
+ this.assertWritableReady();
929
+ if (this.messageExpired(message)) {
930
+ this.expireMessage(message);
931
+ return "expired";
932
+ }
933
+ if (this.routeStatus !== "active" || this.activeTurnId === null) {
934
+ this.acceptedMessageIds.delete(message.messageId);
935
+ return "deferred";
936
+ }
937
+ const expectedTurnId = this.activeTurnId;
938
+ this.beginRequest("turn/steer");
939
+ try {
940
+ const result = await this.request("turn/steer", {
941
+ expectedTurnId,
942
+ input: [{ text: message.text, type: "text" }],
943
+ threadId: this.route.threadId,
944
+ });
945
+ if (!isRecord(result) ||
946
+ Object.keys(result).length !== 1 ||
947
+ typeof result.turnId !== "string" ||
948
+ result.turnId !== expectedTurnId) {
949
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
950
+ }
951
+ if (this.activeTurnId !== null &&
952
+ this.activeTurnId !== expectedTurnId) {
953
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
954
+ }
955
+ this.acceptedMessageIds.delete(message.messageId);
956
+ this.emit("turn_steered", { messageBytes: message.byteLength });
957
+ return "steered";
958
+ }
959
+ catch (error) {
960
+ const normalized = this.normalizeError(error, true);
961
+ this.acceptedMessageIds.delete(message.messageId);
962
+ if (!normalized.ambiguous && normalized.code === "RPC_REJECTED") {
963
+ return "deferred";
964
+ }
965
+ this.handleActionError(normalized);
966
+ throw normalized;
967
+ }
968
+ finally {
969
+ this.finishRequest("turn/steer");
970
+ }
971
+ }
972
+ scheduleDrain() {
973
+ this.dropExpiredQueuedMessages();
974
+ if (this.drainScheduled ||
975
+ this.connection !== "ready" ||
976
+ this.routeStatus !== "idle" ||
977
+ this.currentWriteBlockCode() !== null ||
978
+ this.inFlightMethod !== null ||
979
+ this.queue.length === 0) {
980
+ return;
981
+ }
982
+ // Leave the body in the queue until the microtask commits the start. This
983
+ // keeps it visible to exact-route cancellation/close in the completion ->
984
+ // drain scheduling window.
985
+ this.drainScheduled = true;
986
+ queueMicrotask(() => {
987
+ this.drainScheduled = false;
988
+ this.dropExpiredQueuedMessages();
989
+ if (this.connection !== "ready" ||
990
+ this.routeStatus !== "idle" ||
991
+ this.currentWriteBlockCode() !== null ||
992
+ this.inFlightMethod !== null ||
993
+ this.queue.length === 0) {
994
+ return;
995
+ }
996
+ const message = this.queue.shift();
997
+ if (message === undefined)
998
+ return;
999
+ this.bumpRevision();
1000
+ void this.startMessage(message).then((disposition) => {
1001
+ if (disposition === "expired")
1002
+ this.scheduleDrain();
1003
+ }, () => undefined);
1004
+ });
1005
+ }
1006
+ assertGuard(guard) {
1007
+ if (guard.threadId !== this.route.threadId ||
1008
+ guard.endpointGeneration !== this.route.endpointGeneration) {
1009
+ throw new CodexConnectorError("STALE_ENDPOINT");
1010
+ }
1011
+ if (guard.revision !== this.revision ||
1012
+ guard.status !== this.routeStatus ||
1013
+ guard.activeTurnId !== this.activeTurnId ||
1014
+ guard.writableReady !== this.isWritableReady()) {
1015
+ throw new CodexConnectorError("ROUTE_CAS_MISMATCH");
1016
+ }
1017
+ }
1018
+ assertReady() {
1019
+ if (!this.initialized ||
1020
+ this.connection !== "ready" ||
1021
+ this.routeStatus === "stale") {
1022
+ throw new CodexConnectorError("CONNECTOR_CLOSED");
1023
+ }
1024
+ }
1025
+ assertNoRequest() {
1026
+ if (this.inFlightMethod !== null) {
1027
+ throw new CodexConnectorError("ROUTE_BUSY");
1028
+ }
1029
+ }
1030
+ currentWriteBlockCode() {
1031
+ return this.writesEnabled ? null : "WRITES_DISABLED";
1032
+ }
1033
+ isWritableReady() {
1034
+ return this.connection === "ready" && this.currentWriteBlockCode() === null;
1035
+ }
1036
+ assertWritableReady() {
1037
+ const code = this.currentWriteBlockCode();
1038
+ if (code !== null)
1039
+ throw new CodexConnectorError(code);
1040
+ }
1041
+ /**
1042
+ * Refresh the exact thread on this same connection immediately before a
1043
+ * `turn/start`. This confirms the registered task is still the observed idle
1044
+ * route without reading history or changing its native approval/sandbox
1045
+ * policy.
1046
+ */
1047
+ async refreshExactRouteBoundary() {
1048
+ this.assertWritableReady();
1049
+ if (this.routeStatus !== "starting" ||
1050
+ this.activeMessageId === null ||
1051
+ this.activeTurnId !== null ||
1052
+ this.ownsActiveTurn) {
1053
+ throw new CodexConnectorError("ROUTE_NOT_READY");
1054
+ }
1055
+ const statusEpoch = this.statusEpoch;
1056
+ this.beginRequest("thread/resume");
1057
+ try {
1058
+ const result = await this.request("thread/resume", {
1059
+ excludeTurns: true,
1060
+ threadId: this.route.threadId,
1061
+ });
1062
+ if (!isRecord(result) ||
1063
+ !isRecord(result.thread) ||
1064
+ result.thread.id !== this.route.threadId ||
1065
+ !Array.isArray(result.thread.turns) ||
1066
+ result.thread.turns.length !== 0) {
1067
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
1068
+ }
1069
+ const refreshedStatus = parseRouteStatus(result.thread.status);
1070
+ if (refreshedStatus === null) {
1071
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
1072
+ }
1073
+ if (refreshedStatus !== "idle") {
1074
+ this.setRouteStatus(refreshedStatus);
1075
+ throw new CodexConnectorError("ROUTE_NOT_READY");
1076
+ }
1077
+ this.assertWritableReady();
1078
+ if (this.statusEpoch !== statusEpoch ||
1079
+ this.routeStatus !== "starting" ||
1080
+ this.activeMessageId === null ||
1081
+ this.activeTurnId !== null ||
1082
+ this.ownsActiveTurn) {
1083
+ throw new CodexConnectorError("ROUTE_NOT_READY");
1084
+ }
1085
+ }
1086
+ finally {
1087
+ this.finishRequest("thread/resume");
1088
+ }
1089
+ }
1090
+ messageExpired(message) {
1091
+ const nowMs = this.now().getTime();
1092
+ return !Number.isFinite(nowMs) || nowMs >= message.deadlineAtMs;
1093
+ }
1094
+ expireMessage(message) {
1095
+ this.acceptedMessageIds.delete(message.messageId);
1096
+ this.settleDelivery(message.messageId, "expired");
1097
+ }
1098
+ dropExpiredQueuedMessages() {
1099
+ let dropped = 0;
1100
+ for (let index = this.queue.length - 1; index >= 0; index -= 1) {
1101
+ const message = this.queue[index];
1102
+ if (message === undefined || !this.messageExpired(message))
1103
+ continue;
1104
+ this.queue.splice(index, 1);
1105
+ this.expireMessage(message);
1106
+ dropped += 1;
1107
+ }
1108
+ if (dropped > 0)
1109
+ this.bumpRevision();
1110
+ }
1111
+ beginRequest(method) {
1112
+ if (!V1_METHOD_SET.has(method) || this.inFlightMethod !== null) {
1113
+ throw new CodexConnectorError("ROUTE_BUSY");
1114
+ }
1115
+ this.inFlightMethod = method;
1116
+ this.bumpRevision();
1117
+ this.emit("request_started", { operation: method });
1118
+ }
1119
+ finishRequest(method) {
1120
+ if (this.inFlightMethod !== method)
1121
+ return;
1122
+ this.inFlightMethod = null;
1123
+ this.bumpRevision();
1124
+ if (this.routeStatus === "idle")
1125
+ this.scheduleDrain();
1126
+ }
1127
+ request(method, params) {
1128
+ if (method !== "initialize" && !V1_METHOD_SET.has(method)) {
1129
+ return Promise.reject(new CodexConnectorError("METHOD_NOT_ALLOWED"));
1130
+ }
1131
+ if (method === "initialize") {
1132
+ if (this.initializeRequested || this.connection !== "connecting") {
1133
+ return Promise.reject(new CodexConnectorError("METHOD_NOT_ALLOWED"));
1134
+ }
1135
+ this.initializeRequested = true;
1136
+ }
1137
+ else if (!this.initialized) {
1138
+ return Promise.reject(new CodexConnectorError("CONNECTOR_CLOSED"));
1139
+ }
1140
+ if (this.connection === "closed" || this.connection === "faulted") {
1141
+ return Promise.reject(new CodexConnectorError("CONNECTOR_CLOSED"));
1142
+ }
1143
+ const id = this.nextRequestId;
1144
+ this.nextRequestId += 1;
1145
+ if (!Number.isSafeInteger(this.nextRequestId)) {
1146
+ return Promise.reject(new CodexConnectorError("PROTOCOL_ERROR"));
1147
+ }
1148
+ const payload = JSON.stringify({ id, method, params });
1149
+ if (Buffer.byteLength(payload, "utf8") > this.maxFrameBytes) {
1150
+ return Promise.reject(new CodexConnectorError("INPUT_INVALID"));
1151
+ }
1152
+ return new Promise((resolve, reject) => {
1153
+ const timer = setTimeout(() => {
1154
+ const pending = this.pending.get(id);
1155
+ if (pending === undefined)
1156
+ return;
1157
+ this.pending.delete(id);
1158
+ this.consecutiveRequestTimeouts += 1;
1159
+ pending.reject(new CodexConnectorError("REQUEST_TIMEOUT", true));
1160
+ if (this.consecutiveRequestTimeouts >= 2) {
1161
+ queueMicrotask(() => {
1162
+ if (this.connection === "ready") {
1163
+ this.protocolFault("REQUEST_TIMEOUT");
1164
+ }
1165
+ });
1166
+ }
1167
+ }, this.requestTimeoutMs);
1168
+ this.pending.set(id, { method, reject, resolve, timer });
1169
+ let write;
1170
+ try {
1171
+ write = this.transport.send(payload);
1172
+ }
1173
+ catch {
1174
+ write = Promise.reject(new CodexConnectorError("TRANSPORT_WRITE_FAILED", true));
1175
+ }
1176
+ void write.catch(() => {
1177
+ const pending = this.pending.get(id);
1178
+ if (pending === undefined)
1179
+ return;
1180
+ this.pending.delete(id);
1181
+ clearTimeout(pending.timer);
1182
+ pending.reject(new CodexConnectorError("TRANSPORT_WRITE_FAILED", true));
1183
+ });
1184
+ });
1185
+ }
1186
+ handlePayload(payload) {
1187
+ if (Buffer.byteLength(payload, "utf8") > this.maxFrameBytes) {
1188
+ this.protocolFault("PROTOCOL_ERROR");
1189
+ return;
1190
+ }
1191
+ let parsed;
1192
+ try {
1193
+ parsed = JSON.parse(payload);
1194
+ }
1195
+ catch {
1196
+ this.protocolFault("PROTOCOL_ERROR");
1197
+ return;
1198
+ }
1199
+ if (!isRecord(parsed)) {
1200
+ this.protocolFault("PROTOCOL_ERROR");
1201
+ return;
1202
+ }
1203
+ if (typeof parsed.method === "string") {
1204
+ if (parsed.id === undefined)
1205
+ this.handleNotification(parsed.method, parsed.params);
1206
+ else
1207
+ this.handleServerRequest(parsed.method, parsed.params);
1208
+ return;
1209
+ }
1210
+ if (parsed.id === undefined) {
1211
+ this.protocolFault("PROTOCOL_ERROR");
1212
+ return;
1213
+ }
1214
+ if (!Number.isSafeInteger(parsed.id) || typeof parsed.id !== "number") {
1215
+ this.protocolFault("PROTOCOL_ERROR");
1216
+ return;
1217
+ }
1218
+ const pending = this.pending.get(parsed.id);
1219
+ if (pending === undefined) {
1220
+ this.protocolFault("PROTOCOL_ERROR");
1221
+ return;
1222
+ }
1223
+ const hasResult = Object.hasOwn(parsed, "result");
1224
+ const hasError = Object.hasOwn(parsed, "error");
1225
+ if (hasResult === hasError) {
1226
+ this.protocolFault("PROTOCOL_ERROR");
1227
+ return;
1228
+ }
1229
+ this.pending.delete(parsed.id);
1230
+ clearTimeout(pending.timer);
1231
+ this.consecutiveRequestTimeouts = 0;
1232
+ if (hasError) {
1233
+ if (!isRecord(parsed.error) ||
1234
+ typeof parsed.error.code !== "number" ||
1235
+ !Number.isSafeInteger(parsed.error.code)) {
1236
+ pending.reject(new CodexConnectorError("PROTOCOL_ERROR", true));
1237
+ this.protocolFault("PROTOCOL_ERROR");
1238
+ return;
1239
+ }
1240
+ // Deliberately discard the server's free-form error message/data.
1241
+ pending.reject(new CodexConnectorError("RPC_REJECTED"));
1242
+ return;
1243
+ }
1244
+ pending.resolve(parsed.result);
1245
+ }
1246
+ handleNotification(method, params) {
1247
+ if (method === "item/completed") {
1248
+ this.captureCompletedAgentMessage(params);
1249
+ return;
1250
+ }
1251
+ if (method === "thread/settings/updated") {
1252
+ if (!isRecord(params) || typeof params.threadId !== "string") {
1253
+ this.protocolFault("PROTOCOL_ERROR");
1254
+ return;
1255
+ }
1256
+ if (params.threadId !== this.route.threadId)
1257
+ return;
1258
+ // Native settings remain owned by Codex. Registration is the Embassy
1259
+ // reachability boundary, so a settings update neither blocks the route
1260
+ // nor drops already accepted messages.
1261
+ return;
1262
+ }
1263
+ if (method === "thread/status/changed") {
1264
+ if (!isRecord(params) || typeof params.threadId !== "string") {
1265
+ this.protocolFault("PROTOCOL_ERROR");
1266
+ return;
1267
+ }
1268
+ if (params.threadId !== this.route.threadId)
1269
+ return;
1270
+ const status = parseRouteStatus(params.status);
1271
+ if (status === null) {
1272
+ this.protocolFault("PROTOCOL_ERROR");
1273
+ return;
1274
+ }
1275
+ const reservedRefreshInFlight = this.routeStatus === "starting" &&
1276
+ this.activeMessageId !== null &&
1277
+ this.activeTurnId === null &&
1278
+ !this.ownsActiveTurn &&
1279
+ this.inFlightMethod === "thread/resume";
1280
+ const ownedStartInFlight = this.activeMessageId !== null &&
1281
+ this.activeTurnId === null &&
1282
+ this.ownsActiveTurn &&
1283
+ this.inFlightMethod === "turn/start";
1284
+ if (status === "idle" &&
1285
+ (reservedRefreshInFlight || ownedStartInFlight)) {
1286
+ // This idle belongs to the external turn whose completion released the
1287
+ // queue. Do not let it invalidate the exact refresh epoch or reopen the
1288
+ // drain while the reserved message crosses the refresh/start boundary.
1289
+ return;
1290
+ }
1291
+ this.statusEpoch += 1;
1292
+ if (status === "idle" && this.activeTurnId !== null) {
1293
+ // Wait for the correlated turn/completed notification before draining.
1294
+ return;
1295
+ }
1296
+ if (status === "not_loaded") {
1297
+ this.clearTurnWatchdog();
1298
+ this.selectedThreadObserved = false;
1299
+ this.settleActiveDelivery("ambiguous");
1300
+ this.activeTurnId = null;
1301
+ this.activeMessageId = null;
1302
+ this.ownsActiveTurn = false;
1303
+ this.clearTransientReply();
1304
+ this.dropQueuedMessages();
1305
+ }
1306
+ if (status === "active" &&
1307
+ this.routeStatus === "interrupting" &&
1308
+ this.activeTurnId !== null) {
1309
+ return;
1310
+ }
1311
+ this.setRouteStatus(status);
1312
+ if ((status === "active" || status === "waiting_approval") &&
1313
+ this.ownsActiveTurn &&
1314
+ this.activeTurnId !== null) {
1315
+ this.armTurnWatchdog();
1316
+ }
1317
+ this.emit(status === "waiting_approval"
1318
+ ? "approval_waiting"
1319
+ : "route_status_changed");
1320
+ if (status === "idle")
1321
+ this.scheduleDrain();
1322
+ return;
1323
+ }
1324
+ if (method === "thread/closed") {
1325
+ if (!isRecord(params) || typeof params.threadId !== "string") {
1326
+ this.protocolFault("PROTOCOL_ERROR");
1327
+ return;
1328
+ }
1329
+ if (params.threadId !== this.route.threadId)
1330
+ return;
1331
+ this.settleActiveDelivery("ambiguous");
1332
+ this.clearTurnWatchdog();
1333
+ this.activeTurnId = null;
1334
+ this.selectedThreadObserved = false;
1335
+ this.activeMessageId = null;
1336
+ this.ownsActiveTurn = false;
1337
+ this.clearTransientReply();
1338
+ this.dropQueuedMessages();
1339
+ this.setRouteStatus("not_loaded");
1340
+ this.emit("route_status_changed");
1341
+ return;
1342
+ }
1343
+ if (method === "turn/started") {
1344
+ if (!isRecord(params) ||
1345
+ typeof params.threadId !== "string") {
1346
+ this.protocolFault("PROTOCOL_ERROR");
1347
+ return;
1348
+ }
1349
+ if (params.threadId !== this.route.threadId)
1350
+ return;
1351
+ const turn = parseTurn(params.turn);
1352
+ if (turn === null ||
1353
+ turn.status !== "inProgress") {
1354
+ this.protocolFault("PROTOCOL_ERROR");
1355
+ return;
1356
+ }
1357
+ if (!this.ownsActiveTurn && this.activeMessageId === null) {
1358
+ if (this.lastCompletedTurnId === turn.id)
1359
+ return;
1360
+ if (this.activeTurnId !== null && this.activeTurnId !== turn.id) {
1361
+ this.protocolFault("PROTOCOL_ERROR");
1362
+ return;
1363
+ }
1364
+ this.activeTurnId = turn.id;
1365
+ this.setRouteStatus("active");
1366
+ this.emit("turn_started");
1367
+ return;
1368
+ }
1369
+ if (!this.ownsActiveTurn || this.activeMessageId === null) {
1370
+ return;
1371
+ }
1372
+ if (this.lastCompletedTurnId === turn.id)
1373
+ return;
1374
+ if (this.activeTurnId !== null && this.activeTurnId !== turn.id) {
1375
+ this.protocolFault("PROTOCOL_ERROR");
1376
+ return;
1377
+ }
1378
+ this.activeTurnId = turn.id;
1379
+ this.ownsActiveTurn =
1380
+ this.ownsActiveTurn && this.activeMessageId !== null;
1381
+ this.setRouteStatus("active");
1382
+ this.emit("turn_started");
1383
+ this.armTurnWatchdog();
1384
+ return;
1385
+ }
1386
+ if (method === "turn/completed") {
1387
+ if (!isRecord(params) ||
1388
+ typeof params.threadId !== "string") {
1389
+ this.protocolFault("PROTOCOL_ERROR");
1390
+ return;
1391
+ }
1392
+ if (params.threadId !== this.route.threadId)
1393
+ return;
1394
+ const turn = parseTurn(params.turn);
1395
+ if (turn === null ||
1396
+ (turn.status !== "completed" &&
1397
+ turn.status !== "failed" &&
1398
+ turn.status !== "interrupted")) {
1399
+ this.protocolFault("PROTOCOL_ERROR");
1400
+ return;
1401
+ }
1402
+ const outcome = turn.status;
1403
+ this.clearTurnWatchdog();
1404
+ if (!this.ownsActiveTurn || this.activeMessageId === null) {
1405
+ // An approval request from another client records the external turn ID
1406
+ // so Embassy can wait without answering it. Its terminal notification
1407
+ // must release that observation and any queued Embassy work, while
1408
+ // never claiming or settling the external turn as gateway-owned.
1409
+ if (this.ownsActiveTurn || this.activeMessageId !== null) {
1410
+ this.protocolFault("PROTOCOL_ERROR");
1411
+ return;
1412
+ }
1413
+ if (this.activeTurnId === null)
1414
+ return;
1415
+ if (this.activeTurnId !== turn.id) {
1416
+ this.protocolFault("PROTOCOL_ERROR");
1417
+ return;
1418
+ }
1419
+ this.activeTurnId = null;
1420
+ this.lastCompletedTurnId = turn.id;
1421
+ this.clearTransientReply();
1422
+ this.setRouteStatus(outcome === "failed" ? "system_error" : "idle");
1423
+ this.emit("turn_completed", { turnOutcome: outcome });
1424
+ if (outcome !== "failed")
1425
+ this.scheduleDrain();
1426
+ return;
1427
+ }
1428
+ if (this.activeTurnId === null)
1429
+ return;
1430
+ if (this.activeTurnId !== turn.id) {
1431
+ this.protocolFault("PROTOCOL_ERROR");
1432
+ return;
1433
+ }
1434
+ const messageId = this.activeMessageId;
1435
+ const reply = this.transientReply;
1436
+ const replyTooLarge = this.transientReplyTooLarge;
1437
+ this.activeTurnId = null;
1438
+ this.lastCompletedTurnId = turn.id;
1439
+ this.activeMessageId = null;
1440
+ this.ownsActiveTurn = false;
1441
+ this.clearTransientReply();
1442
+ this.setRouteStatus(outcome === "failed" ? "system_error" : "idle");
1443
+ this.emit("turn_completed", { turnOutcome: outcome });
1444
+ if (messageId !== null) {
1445
+ const text = outcome === "completed" && !replyTooLarge ? reply : null;
1446
+ const replyCode = replyTooLarge
1447
+ ? "REPLY_TOO_LARGE"
1448
+ : text === null
1449
+ ? "REPLY_UNAVAILABLE"
1450
+ : null;
1451
+ this.settleDelivery(messageId, outcome, text, replyCode);
1452
+ }
1453
+ if (outcome !== "failed")
1454
+ this.scheduleDrain();
1455
+ return;
1456
+ }
1457
+ if (method === "serverRequest/resolved") {
1458
+ if (!isRecord(params) || typeof params.threadId !== "string") {
1459
+ this.protocolFault("PROTOCOL_ERROR");
1460
+ return;
1461
+ }
1462
+ if (params.threadId === this.route.threadId &&
1463
+ this.routeStatus === "waiting_approval") {
1464
+ this.setRouteStatus("active");
1465
+ this.emit("route_status_changed");
1466
+ }
1467
+ return;
1468
+ }
1469
+ if (method === "warning" || method === "configWarning") {
1470
+ this.emit("server_warning");
1471
+ }
1472
+ // Every other notification is ignored after the global frame bound. Raw
1473
+ // item/model/tool payloads are neither normalized nor retained.
1474
+ }
1475
+ handleServerRequest(method, params) {
1476
+ if (!APPROVAL_REQUEST_METHODS.has(method)) {
1477
+ this.emit("server_request_ignored");
1478
+ return;
1479
+ }
1480
+ if (!isRecord(params) ||
1481
+ typeof params.threadId !== "string" ||
1482
+ typeof params.turnId !== "string" ||
1483
+ !validOpaqueId(params.turnId)) {
1484
+ this.protocolFault("PROTOCOL_ERROR");
1485
+ return;
1486
+ }
1487
+ if (params.threadId !== this.route.threadId)
1488
+ return;
1489
+ if (this.activeTurnId !== null && this.activeTurnId !== params.turnId) {
1490
+ this.protocolFault("PROTOCOL_ERROR");
1491
+ return;
1492
+ }
1493
+ this.activeTurnId = params.turnId;
1494
+ // Do not acquire ownership merely by observing another client's approval.
1495
+ this.setRouteStatus("waiting_approval");
1496
+ this.emit("approval_waiting");
1497
+ // Intentionally no JSON-RPC response: the gateway is not an approval UI or
1498
+ // authority. The owning Desktop client must resolve the request.
1499
+ }
1500
+ captureCompletedAgentMessage(params) {
1501
+ if (!this.ownsActiveTurn ||
1502
+ this.activeMessageId === null ||
1503
+ this.activeTurnId === null) {
1504
+ return;
1505
+ }
1506
+ if (!isRecord(params) ||
1507
+ typeof params.threadId !== "string" ||
1508
+ typeof params.turnId !== "string" ||
1509
+ !isRecord(params.item)) {
1510
+ this.protocolFault("PROTOCOL_ERROR");
1511
+ return;
1512
+ }
1513
+ if (params.threadId !== this.route.threadId)
1514
+ return;
1515
+ if (params.turnId !== this.activeTurnId) {
1516
+ return;
1517
+ }
1518
+ const item = params.item;
1519
+ if (item.type !== "agentMessage")
1520
+ return;
1521
+ if (item.phase !== undefined && item.phase !== "final_answer")
1522
+ return;
1523
+ if (typeof item.text !== "string" || item.text.includes("\0"))
1524
+ return;
1525
+ const replyBytes = Buffer.byteLength(item.text, "utf8");
1526
+ if (replyBytes === 0)
1527
+ return;
1528
+ if (replyBytes > this.maxReplyBytes) {
1529
+ this.transientReply = null;
1530
+ this.transientReplyTooLarge = true;
1531
+ return;
1532
+ }
1533
+ this.transientReply = item.text;
1534
+ this.transientReplyTooLarge = false;
1535
+ this.armTurnWatchdog();
1536
+ }
1537
+ clearTransientReply() {
1538
+ this.transientReply = null;
1539
+ this.transientReplyTooLarge = false;
1540
+ }
1541
+ settleDelivery(messageId, outcome, text = null, replyCode = "REPLY_UNAVAILABLE") {
1542
+ try {
1543
+ this.onTurnResult?.({ messageId, outcome, replyCode, text });
1544
+ }
1545
+ catch {
1546
+ // Delivery consumers cannot alter transport or route state. Any reply
1547
+ // text is dropped immediately after this synchronous handoff.
1548
+ }
1549
+ }
1550
+ settleActiveDelivery(outcome) {
1551
+ this.clearTurnWatchdog();
1552
+ if (this.activeMessageId !== null) {
1553
+ this.settleDelivery(this.activeMessageId, outcome);
1554
+ }
1555
+ this.activeMessageId = null;
1556
+ this.ownsActiveTurn = false;
1557
+ this.clearTransientReply();
1558
+ }
1559
+ setRouteStatus(status) {
1560
+ if (this.routeStatus === status)
1561
+ return;
1562
+ this.routeStatus = status;
1563
+ this.bumpRevision();
1564
+ }
1565
+ bumpRevision() {
1566
+ this.revision += 1;
1567
+ if (!Number.isSafeInteger(this.revision)) {
1568
+ this.protocolFault("PROTOCOL_ERROR");
1569
+ }
1570
+ }
1571
+ emit(kind, details) {
1572
+ this.eventSequence += 1;
1573
+ const event = {
1574
+ ...this.observation(),
1575
+ kind,
1576
+ timestamp: this.now().toISOString(),
1577
+ ...(details === undefined ? {} : { details }),
1578
+ };
1579
+ try {
1580
+ this.onEvent?.(event);
1581
+ }
1582
+ catch {
1583
+ // Monitoring consumers cannot affect routing or protocol state.
1584
+ }
1585
+ }
1586
+ dropQueuedMessages() {
1587
+ const dropped = this.queue.length;
1588
+ for (const message of this.queue) {
1589
+ this.settleDelivery(message.messageId, "abandoned");
1590
+ }
1591
+ this.queue.length = 0;
1592
+ if (dropped > 0)
1593
+ this.bumpRevision();
1594
+ return dropped;
1595
+ }
1596
+ normalizeError(error, ambiguousFallback) {
1597
+ return error instanceof CodexConnectorError
1598
+ ? error
1599
+ : new CodexConnectorError("PROTOCOL_ERROR", ambiguousFallback);
1600
+ }
1601
+ handleActionError(error) {
1602
+ const normalized = this.normalizeError(error, true);
1603
+ if (normalized.code === "PROTOCOL_ERROR" ||
1604
+ normalized.code === "RESULT_SCHEMA_MISMATCH" ||
1605
+ normalized.code === "TRANSPORT_WRITE_FAILED" ||
1606
+ normalized.code === "TRANSPORT_CLOSED") {
1607
+ this.protocolFault(normalized.code);
1608
+ }
1609
+ }
1610
+ rejectPending(code) {
1611
+ for (const [id, pending] of this.pending) {
1612
+ this.pending.delete(id);
1613
+ clearTimeout(pending.timer);
1614
+ pending.reject(new CodexConnectorError(code, true));
1615
+ }
1616
+ }
1617
+ protocolFault(code) {
1618
+ if (this.faulting || this.connection === "faulted")
1619
+ return;
1620
+ this.faulting = true;
1621
+ this.connection = "faulted";
1622
+ this.initialized = false;
1623
+ this.routeStatus = "stale";
1624
+ this.selectedThreadObserved = false;
1625
+ this.clearTurnWatchdog();
1626
+ this.settleActiveDelivery("ambiguous");
1627
+ this.activeTurnId = null;
1628
+ const droppedMessages = this.dropQueuedMessages();
1629
+ this.inFlightMethod = null;
1630
+ this.bumpRevision();
1631
+ this.rejectPending(code);
1632
+ this.emit("protocol_fault", { droppedMessages, errorCode: code });
1633
+ void this.transport.close().catch(() => undefined);
1634
+ for (const remove of this.unlisten.splice(0))
1635
+ remove();
1636
+ this.faulting = false;
1637
+ }
1638
+ handleDisconnect() {
1639
+ if (this.connection === "closed" || this.connection === "faulted")
1640
+ return;
1641
+ this.connection = "closed";
1642
+ this.initialized = false;
1643
+ this.routeStatus = "stale";
1644
+ this.selectedThreadObserved = false;
1645
+ this.clearTurnWatchdog();
1646
+ this.settleActiveDelivery("ambiguous");
1647
+ this.activeTurnId = null;
1648
+ const droppedMessages = this.dropQueuedMessages();
1649
+ this.inFlightMethod = null;
1650
+ this.bumpRevision();
1651
+ this.rejectPending("TRANSPORT_CLOSED");
1652
+ this.emit("connection_closed", {
1653
+ droppedMessages,
1654
+ ...(!this.expectedClose ? { errorCode: "TRANSPORT_CLOSED" } : {}),
1655
+ });
1656
+ for (const remove of this.unlisten.splice(0))
1657
+ remove();
1658
+ }
1659
+ armTurnWatchdog(delayMs = this.turnWatchdogMs) {
1660
+ this.clearTurnWatchdog();
1661
+ if (this.connection !== "ready" ||
1662
+ !this.ownsActiveTurn ||
1663
+ this.activeMessageId === null ||
1664
+ this.activeTurnId === null) {
1665
+ return;
1666
+ }
1667
+ this.turnWatchdogTimer = setTimeout(() => {
1668
+ this.turnWatchdogTimer = undefined;
1669
+ void this.probeOwnedTurnStatus();
1670
+ }, delayMs);
1671
+ this.turnWatchdogTimer.unref();
1672
+ }
1673
+ clearTurnWatchdog() {
1674
+ if (this.turnWatchdogTimer === undefined)
1675
+ return;
1676
+ clearTimeout(this.turnWatchdogTimer);
1677
+ this.turnWatchdogTimer = undefined;
1678
+ }
1679
+ async probeOwnedTurnStatus() {
1680
+ if (this.connection !== "ready" ||
1681
+ !this.ownsActiveTurn ||
1682
+ this.activeMessageId === null ||
1683
+ this.activeTurnId === null) {
1684
+ return;
1685
+ }
1686
+ if (this.inFlightMethod !== null) {
1687
+ this.armTurnWatchdog(Math.min(this.turnWatchdogMs, this.requestTimeoutMs));
1688
+ return;
1689
+ }
1690
+ const watchedMessageId = this.activeMessageId;
1691
+ const watchedTurnId = this.activeTurnId;
1692
+ this.beginRequest("thread/resume");
1693
+ try {
1694
+ const result = await this.request("thread/resume", {
1695
+ excludeTurns: true,
1696
+ threadId: this.route.threadId,
1697
+ });
1698
+ if (!isRecord(result) ||
1699
+ !isRecord(result.thread) ||
1700
+ result.thread.id !== this.route.threadId ||
1701
+ !Array.isArray(result.thread.turns) ||
1702
+ result.thread.turns.length !== 0) {
1703
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
1704
+ }
1705
+ const status = parseRouteStatus(result.thread.status);
1706
+ if (status === null) {
1707
+ throw new CodexConnectorError("RESULT_SCHEMA_MISMATCH", true);
1708
+ }
1709
+ if (!this.ownsActiveTurn ||
1710
+ this.activeMessageId !== watchedMessageId ||
1711
+ this.activeTurnId !== watchedTurnId) {
1712
+ return;
1713
+ }
1714
+ if (status === "active" || status === "waiting_approval") {
1715
+ this.setRouteStatus(status);
1716
+ this.emit(status === "waiting_approval"
1717
+ ? "approval_waiting"
1718
+ : "route_status_changed");
1719
+ this.armTurnWatchdog();
1720
+ return;
1721
+ }
1722
+ this.settleActiveDelivery("ambiguous");
1723
+ this.activeTurnId = null;
1724
+ if (status === "not_loaded")
1725
+ this.selectedThreadObserved = false;
1726
+ this.setRouteStatus(status);
1727
+ this.emit("route_status_changed");
1728
+ if (status === "idle")
1729
+ this.scheduleDrain();
1730
+ }
1731
+ catch (error) {
1732
+ const normalized = this.normalizeError(error, true);
1733
+ this.handleActionError(normalized);
1734
+ if (this.connection === "ready" &&
1735
+ this.ownsActiveTurn &&
1736
+ this.activeMessageId === watchedMessageId &&
1737
+ this.activeTurnId === watchedTurnId) {
1738
+ this.armTurnWatchdog();
1739
+ }
1740
+ }
1741
+ finally {
1742
+ this.finishRequest("thread/resume");
1743
+ }
1744
+ }
1745
+ }
1746
+ //# sourceMappingURL=codex-app-server.js.map