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.
- package/CHANGELOG.md +61 -0
- package/CONTRIBUTING.md +122 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/README.zh-CN.md +185 -0
- package/SECURITY.md +224 -0
- package/assets/live-dashboard/app.css +1610 -0
- package/assets/mark-seal.svg +7 -0
- package/assets/mark.svg +7 -0
- package/assets/social-preview.png +0 -0
- package/assets/vendor/react/LICENSE +21 -0
- package/assets/vendor/react/react-dom.production.min.js +267 -0
- package/assets/vendor/react/react.production.min.js +31 -0
- package/dist/src/errors.d.ts +5 -0
- package/dist/src/errors.js +11 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/gateway/claude-compatibility-scratch.d.ts +18 -0
- package/dist/src/gateway/claude-compatibility-scratch.js +158 -0
- package/dist/src/gateway/claude-compatibility-scratch.js.map +1 -0
- package/dist/src/gateway/claude-helper-client.d.ts +43 -0
- package/dist/src/gateway/claude-helper-client.js +212 -0
- package/dist/src/gateway/claude-helper-client.js.map +1 -0
- package/dist/src/gateway/claude-helper-protocol.d.ts +172 -0
- package/dist/src/gateway/claude-helper-protocol.js +373 -0
- package/dist/src/gateway/claude-helper-protocol.js.map +1 -0
- package/dist/src/gateway/claude-helper-supervisor.d.ts +74 -0
- package/dist/src/gateway/claude-helper-supervisor.js +497 -0
- package/dist/src/gateway/claude-helper-supervisor.js.map +1 -0
- package/dist/src/gateway/claude-helper.d.ts +1 -0
- package/dist/src/gateway/claude-helper.js +269 -0
- package/dist/src/gateway/claude-helper.js.map +1 -0
- package/dist/src/gateway/claude-peer.d.ts +273 -0
- package/dist/src/gateway/claude-peer.js +2359 -0
- package/dist/src/gateway/claude-peer.js.map +1 -0
- package/dist/src/gateway/claude-runtime.d.ts +40 -0
- package/dist/src/gateway/claude-runtime.js +302 -0
- package/dist/src/gateway/claude-runtime.js.map +1 -0
- package/dist/src/gateway/cli-copy.d.ts +8 -0
- package/dist/src/gateway/cli-copy.en.d.ts +13 -0
- package/dist/src/gateway/cli-copy.en.js +43 -0
- package/dist/src/gateway/cli-copy.en.js.map +1 -0
- package/dist/src/gateway/cli-copy.js +19 -0
- package/dist/src/gateway/cli-copy.js.map +1 -0
- package/dist/src/gateway/cli-copy.zh-CN.d.ts +13 -0
- package/dist/src/gateway/cli-copy.zh-CN.js +43 -0
- package/dist/src/gateway/cli-copy.zh-CN.js.map +1 -0
- package/dist/src/gateway/cli.d.ts +44 -0
- package/dist/src/gateway/cli.js +969 -0
- package/dist/src/gateway/cli.js.map +1 -0
- package/dist/src/gateway/codex-app-server.d.ts +310 -0
- package/dist/src/gateway/codex-app-server.js +1746 -0
- package/dist/src/gateway/codex-app-server.js.map +1 -0
- package/dist/src/gateway/codex-local-transport.d.ts +81 -0
- package/dist/src/gateway/codex-local-transport.js +617 -0
- package/dist/src/gateway/codex-local-transport.js.map +1 -0
- package/dist/src/gateway/codex-registration-generation.d.ts +5 -0
- package/dist/src/gateway/codex-registration-generation.js +19 -0
- package/dist/src/gateway/codex-registration-generation.js.map +1 -0
- package/dist/src/gateway/codex-registration-succession.d.ts +209 -0
- package/dist/src/gateway/codex-registration-succession.js +588 -0
- package/dist/src/gateway/codex-registration-succession.js.map +1 -0
- package/dist/src/gateway/compatibility.d.ts +70 -0
- package/dist/src/gateway/compatibility.js +252 -0
- package/dist/src/gateway/compatibility.js.map +1 -0
- package/dist/src/gateway/config.d.ts +24 -0
- package/dist/src/gateway/config.js +130 -0
- package/dist/src/gateway/config.js.map +1 -0
- package/dist/src/gateway/control.d.ts +308 -0
- package/dist/src/gateway/control.js +1573 -0
- package/dist/src/gateway/control.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.d.ts +7 -0
- package/dist/src/gateway/dashboard-copy.en.d.ts +506 -0
- package/dist/src/gateway/dashboard-copy.en.js +507 -0
- package/dist/src/gateway/dashboard-copy.en.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.js +516 -0
- package/dist/src/gateway/dashboard-copy.js.map +1 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.d.ts +506 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js +507 -0
- package/dist/src/gateway/dashboard-copy.zh-CN.js.map +1 -0
- package/dist/src/gateway/dashboard-model.d.ts +207 -0
- package/dist/src/gateway/dashboard-model.js +720 -0
- package/dist/src/gateway/dashboard-model.js.map +1 -0
- package/dist/src/gateway/dashboard.d.ts +26 -0
- package/dist/src/gateway/dashboard.js +941 -0
- package/dist/src/gateway/dashboard.js.map +1 -0
- package/dist/src/gateway/delivery-machine.d.ts +235 -0
- package/dist/src/gateway/delivery-machine.js +540 -0
- package/dist/src/gateway/delivery-machine.js.map +1 -0
- package/dist/src/gateway/instance-lease.d.ts +21 -0
- package/dist/src/gateway/instance-lease.js +549 -0
- package/dist/src/gateway/instance-lease.js.map +1 -0
- package/dist/src/gateway/live-dashboard-app/app.js +2914 -0
- package/dist/src/gateway/live-dashboard-assets.d.ts +10 -0
- package/dist/src/gateway/live-dashboard-assets.js +73 -0
- package/dist/src/gateway/live-dashboard-assets.js.map +1 -0
- package/dist/src/gateway/live-dashboard-bootstrap.d.ts +36 -0
- package/dist/src/gateway/live-dashboard-bootstrap.js +176 -0
- package/dist/src/gateway/live-dashboard-bootstrap.js.map +1 -0
- package/dist/src/gateway/live-dashboard-command.d.ts +54 -0
- package/dist/src/gateway/live-dashboard-command.js +324 -0
- package/dist/src/gateway/live-dashboard-command.js.map +1 -0
- package/dist/src/gateway/live-dashboard-http.d.ts +41 -0
- package/dist/src/gateway/live-dashboard-http.js +445 -0
- package/dist/src/gateway/live-dashboard-http.js.map +1 -0
- package/dist/src/gateway/live-dashboard-protocol.d.ts +38 -0
- package/dist/src/gateway/live-dashboard-protocol.js +156 -0
- package/dist/src/gateway/live-dashboard-protocol.js.map +1 -0
- package/dist/src/gateway/live-dashboard-server.d.ts +33 -0
- package/dist/src/gateway/live-dashboard-server.js +130 -0
- package/dist/src/gateway/live-dashboard-server.js.map +1 -0
- package/dist/src/gateway/live-dashboard-stream.d.ts +46 -0
- package/dist/src/gateway/live-dashboard-stream.js +233 -0
- package/dist/src/gateway/live-dashboard-stream.js.map +1 -0
- package/dist/src/gateway/live-dashboard.d.ts +31 -0
- package/dist/src/gateway/live-dashboard.js +190 -0
- package/dist/src/gateway/live-dashboard.js.map +1 -0
- package/dist/src/gateway/locale.d.ts +4 -0
- package/dist/src/gateway/locale.js +10 -0
- package/dist/src/gateway/locale.js.map +1 -0
- package/dist/src/gateway/progress-watch-machine.d.ts +97 -0
- package/dist/src/gateway/progress-watch-machine.js +172 -0
- package/dist/src/gateway/progress-watch-machine.js.map +1 -0
- package/dist/src/gateway/providers.d.ts +287 -0
- package/dist/src/gateway/providers.js +2451 -0
- package/dist/src/gateway/providers.js.map +1 -0
- package/dist/src/gateway/server.d.ts +57 -0
- package/dist/src/gateway/server.js +264 -0
- package/dist/src/gateway/server.js.map +1 -0
- package/dist/src/gateway/service.d.ts +449 -0
- package/dist/src/gateway/service.js +4831 -0
- package/dist/src/gateway/service.js.map +1 -0
- package/dist/src/gateway/store.d.ts +340 -0
- package/dist/src/gateway/store.js +4047 -0
- package/dist/src/gateway/store.js.map +1 -0
- package/dist/src/gateway/types.d.ts +514 -0
- package/dist/src/gateway/types.js +414 -0
- package/dist/src/gateway/types.js.map +1 -0
- package/dist/src/mutex.d.ts +4 -0
- package/dist/src/mutex.js +23 -0
- package/dist/src/mutex.js.map +1 -0
- package/docs/DESIGN.md +134 -0
- package/docs/GATEWAY-ARCHITECTURE.md +858 -0
- package/package.json +80 -0
- package/skills/embassy-peer/SKILL.md +198 -0
- 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
|