march-cli 0.1.19 → 0.1.21
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/package.json +3 -2
- package/src/agent/runner/codex-large-context-guard.mjs +87 -0
- package/src/agent/runner/codex-transport-compression.mjs +180 -0
- package/src/agent/runner/codex-transport-debug.mjs +41 -3
- package/src/agent/runner/codex-websocket-event-debug.mjs +130 -0
- package/src/agent/runner.mjs +8 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "march-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "March CLI — terminal-native coding agent with context reconstruction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/main.mjs",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"node-pty": "^1.1.0",
|
|
35
35
|
"typebox": "^1.0.58",
|
|
36
36
|
"undici": "^7.25.0",
|
|
37
|
-
"web-tree-sitter": "^0.26.8"
|
|
37
|
+
"web-tree-sitter": "^0.26.8",
|
|
38
|
+
"ws": "^8.20.1"
|
|
38
39
|
},
|
|
39
40
|
"optionalDependencies": {
|
|
40
41
|
"@vscode/ripgrep": "^1.18.0"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getOpenAICodexWebSocketDebugStats,
|
|
3
|
+
resetOpenAICodexWebSocketDebugStats,
|
|
4
|
+
} from "@earendil-works/pi-ai/openai-codex-responses";
|
|
5
|
+
|
|
6
|
+
const FETCH_INSTALLED = Symbol.for("march.codex.largeContextGuard.fetchInstalled");
|
|
7
|
+
const ORIGINAL_FETCH = Symbol.for("march.codex.largeContextGuard.originalFetch");
|
|
8
|
+
const DEFAULT_MAX_HTTP_FALLBACK_BYTES = 512 * 1024;
|
|
9
|
+
|
|
10
|
+
export function installCodexLargeContextGuard() {
|
|
11
|
+
if (!isGuardEnabled()) return;
|
|
12
|
+
if (globalThis[FETCH_INSTALLED]) return;
|
|
13
|
+
const originalFetch = globalThis.fetch;
|
|
14
|
+
if (typeof originalFetch !== "function") return;
|
|
15
|
+
globalThis[ORIGINAL_FETCH] = originalFetch;
|
|
16
|
+
globalThis.fetch = async function marchCodexLargeContextGuardedFetch(input, init) {
|
|
17
|
+
const bodyBytes = getBodyBytes(init?.body);
|
|
18
|
+
if (isCodexResponsesHttpRequest(input, init) && !isZstdEncoded(input, init) && bodyBytes > getMaxHttpFallbackBytes()) {
|
|
19
|
+
throw new Error(`Codex HTTP fallback blocked for uncompressed large payload (${bodyBytes} bytes); retrying WebSocket instead`);
|
|
20
|
+
}
|
|
21
|
+
return originalFetch.call(this, input, init);
|
|
22
|
+
};
|
|
23
|
+
globalThis[FETCH_INSTALLED] = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyCodexLargeContextGuardToPayload(payload, { model, session } = {}) {
|
|
27
|
+
if (!isGuardEnabled() || !isCodexModel(model)) return payload;
|
|
28
|
+
const sessionId = session?.sessionId;
|
|
29
|
+
const stats = sessionId ? getOpenAICodexWebSocketDebugStats(sessionId) : null;
|
|
30
|
+
if (stats?.websocketFallbackActive) {
|
|
31
|
+
// pi-ai marks fallback session-wide. Clear it before retry payload assembly so WS gets another chance.
|
|
32
|
+
resetOpenAICodexWebSocketDebugStats(sessionId);
|
|
33
|
+
}
|
|
34
|
+
return payload;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isGuardEnabled() {
|
|
38
|
+
const value = process.env.MARCH_CODEX_LARGE_CONTEXT_GUARD;
|
|
39
|
+
return value !== "0" && value !== "false" && value !== "no";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getMaxHttpFallbackBytes() {
|
|
43
|
+
const raw = process.env.MARCH_CODEX_HTTP_FALLBACK_MAX_BYTES;
|
|
44
|
+
const parsed = raw ? Number.parseInt(raw, 10) : DEFAULT_MAX_HTTP_FALLBACK_BYTES;
|
|
45
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_HTTP_FALLBACK_BYTES;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isCodexModel(model) {
|
|
49
|
+
return model?.provider === "openai-codex" || model?.api === "openai-codex-responses";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isCodexResponsesHttpRequest(input, init) {
|
|
53
|
+
const url = getRequestUrl(input);
|
|
54
|
+
if (!url || !url.includes("/codex/responses")) return false;
|
|
55
|
+
const method = init?.method ?? input?.method ?? "GET";
|
|
56
|
+
if (String(method).toUpperCase() !== "POST") return false;
|
|
57
|
+
return headerValue(init?.headers ?? input?.headers, "accept")?.includes("text/event-stream")
|
|
58
|
+
|| headerValue(init?.headers ?? input?.headers, "OpenAI-Beta")?.includes("responses=experimental");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getRequestUrl(input) {
|
|
62
|
+
if (typeof input === "string") return input;
|
|
63
|
+
if (input instanceof URL) return input.toString();
|
|
64
|
+
if (input && typeof input.url === "string") return input.url;
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isZstdEncoded(input, init) {
|
|
69
|
+
return headerValue(init?.headers ?? input?.headers, "content-encoding").toLowerCase() === "zstd";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getBodyBytes(body) {
|
|
73
|
+
if (typeof body === "string") return Buffer.byteLength(body);
|
|
74
|
+
if (body instanceof Uint8Array) return body.byteLength;
|
|
75
|
+
if (body instanceof ArrayBuffer) return body.byteLength;
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function headerValue(headers, name) {
|
|
80
|
+
if (!headers) return "";
|
|
81
|
+
if (typeof headers.get === "function") return headers.get(name) ?? headers.get(name.toLowerCase()) ?? "";
|
|
82
|
+
const lowerName = name.toLowerCase();
|
|
83
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
84
|
+
if (key.toLowerCase() === lowerName) return String(value ?? "");
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import zlib from "node:zlib";
|
|
2
|
+
import WsWebSocket from "ws";
|
|
3
|
+
|
|
4
|
+
const FETCH_INSTALLED = Symbol.for("march.codex.transportCompression.fetchInstalled");
|
|
5
|
+
const WEBSOCKET_INSTALLED = Symbol.for("march.codex.transportCompression.websocketInstalled");
|
|
6
|
+
const ORIGINAL_FETCH = Symbol.for("march.codex.transportCompression.originalFetch");
|
|
7
|
+
const ORIGINAL_WEBSOCKET = Symbol.for("march.codex.transportCompression.originalWebSocket");
|
|
8
|
+
const STATS = Symbol.for("march.codex.transportCompression.stats");
|
|
9
|
+
const DEFAULT_MIN_ZSTD_BYTES = 1024;
|
|
10
|
+
|
|
11
|
+
export function installCodexTransportCompression() {
|
|
12
|
+
if (!isCompressionEnabled()) return;
|
|
13
|
+
installCodexHttpCompression();
|
|
14
|
+
installCodexWebSocketCompression();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getCodexTransportCompressionStats() {
|
|
18
|
+
return globalThis[STATS] ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function installCodexHttpCompression() {
|
|
22
|
+
if (!isHttpCompressionEnabled() || globalThis[FETCH_INSTALLED]) return;
|
|
23
|
+
const originalFetch = globalThis.fetch;
|
|
24
|
+
if (typeof originalFetch !== "function" || typeof zlib.zstdCompressSync !== "function") return;
|
|
25
|
+
globalThis[ORIGINAL_FETCH] = originalFetch;
|
|
26
|
+
globalThis.fetch = async function marchCodexCompressedFetch(input, init = {}) {
|
|
27
|
+
if (!isCodexResponsesHttpRequest(input, init) || hasHeader(init.headers ?? input?.headers, "content-encoding")) {
|
|
28
|
+
return originalFetch.call(this, input, init);
|
|
29
|
+
}
|
|
30
|
+
const body = init?.body;
|
|
31
|
+
const plain = toBuffer(body);
|
|
32
|
+
if (!plain || plain.byteLength < getMinZstdBytes()) return originalFetch.call(this, input, init);
|
|
33
|
+
|
|
34
|
+
const compressed = zlib.zstdCompressSync(plain, {
|
|
35
|
+
params: { [zlib.constants.ZSTD_c_compressionLevel]: 3 },
|
|
36
|
+
});
|
|
37
|
+
const headers = new Headers(init.headers ?? input?.headers ?? {});
|
|
38
|
+
headers.set("content-encoding", "zstd");
|
|
39
|
+
headers.set("content-type", headers.get("content-type") ?? "application/json");
|
|
40
|
+
recordHttpCompression(plain.byteLength, compressed.byteLength);
|
|
41
|
+
return originalFetch.call(this, input, { ...init, headers, body: compressed });
|
|
42
|
+
};
|
|
43
|
+
globalThis[FETCH_INSTALLED] = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function installCodexWebSocketCompression() {
|
|
47
|
+
if (!isWebSocketCompressionEnabled() || globalThis[WEBSOCKET_INSTALLED]) return;
|
|
48
|
+
const OriginalWebSocket = globalThis.WebSocket;
|
|
49
|
+
if (typeof OriginalWebSocket !== "function") return;
|
|
50
|
+
globalThis[ORIGINAL_WEBSOCKET] = OriginalWebSocket;
|
|
51
|
+
|
|
52
|
+
class MarchCodexCompressedWebSocket extends WsWebSocket {
|
|
53
|
+
constructor(url, protocolsOrOptions, maybeOptions) {
|
|
54
|
+
if (!isCodexResponsesWebSocketUrl(url)) {
|
|
55
|
+
return new OriginalWebSocket(url, protocolsOrOptions, maybeOptions);
|
|
56
|
+
}
|
|
57
|
+
const { protocols, options } = normalizeWebSocketArgs(protocolsOrOptions, maybeOptions);
|
|
58
|
+
super(url, protocols, {
|
|
59
|
+
...options,
|
|
60
|
+
perMessageDeflate: options.perMessageDeflate ?? true,
|
|
61
|
+
});
|
|
62
|
+
recordWebSocketCompression();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
copyReadyStateConstants(MarchCodexCompressedWebSocket, OriginalWebSocket);
|
|
66
|
+
globalThis.WebSocket = MarchCodexCompressedWebSocket;
|
|
67
|
+
globalThis[WEBSOCKET_INSTALLED] = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeWebSocketArgs(protocolsOrOptions, maybeOptions) {
|
|
71
|
+
if (isOptionsObject(protocolsOrOptions) && maybeOptions === undefined) {
|
|
72
|
+
return { protocols: [], options: protocolsOrOptions };
|
|
73
|
+
}
|
|
74
|
+
return { protocols: protocolsOrOptions ?? [], options: maybeOptions ?? {} };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isOptionsObject(value) {
|
|
78
|
+
if (!value || typeof value !== "object") return false;
|
|
79
|
+
if (Array.isArray(value) || value instanceof String) return false;
|
|
80
|
+
return "headers" in value || "perMessageDeflate" in value || "handshakeTimeout" in value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function copyReadyStateConstants(Target, OriginalWebSocket) {
|
|
84
|
+
for (const key of ["CONNECTING", "OPEN", "CLOSING", "CLOSED"]) {
|
|
85
|
+
const value = OriginalWebSocket[key] ?? WsWebSocket[key];
|
|
86
|
+
if (typeof value === "number") Object.defineProperty(Target, key, { value, enumerable: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isCompressionEnabled() {
|
|
91
|
+
return isEnabled(process.env.MARCH_CODEX_TRANSPORT_COMPRESSION, true);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isHttpCompressionEnabled() {
|
|
95
|
+
return isEnabled(process.env.MARCH_CODEX_HTTP_COMPRESSION, true);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isWebSocketCompressionEnabled() {
|
|
99
|
+
return isEnabled(process.env.MARCH_CODEX_WS_COMPRESSION, true);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isEnabled(value, fallback) {
|
|
103
|
+
if (value === undefined) return fallback;
|
|
104
|
+
return value !== "0" && value !== "false" && value !== "no";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getMinZstdBytes() {
|
|
108
|
+
const raw = process.env.MARCH_CODEX_HTTP_COMPRESSION_MIN_BYTES;
|
|
109
|
+
const parsed = raw ? Number.parseInt(raw, 10) : DEFAULT_MIN_ZSTD_BYTES;
|
|
110
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_MIN_ZSTD_BYTES;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isCodexResponsesHttpRequest(input, init) {
|
|
114
|
+
const url = getRequestUrl(input);
|
|
115
|
+
if (!url || !url.includes("/codex/responses")) return false;
|
|
116
|
+
const method = init?.method ?? input?.method ?? "GET";
|
|
117
|
+
if (String(method).toUpperCase() !== "POST") return false;
|
|
118
|
+
return headerValue(init?.headers ?? input?.headers, "accept").includes("text/event-stream")
|
|
119
|
+
|| headerValue(init?.headers ?? input?.headers, "OpenAI-Beta").includes("responses=experimental");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isCodexResponsesWebSocketUrl(url) {
|
|
123
|
+
const raw = getRequestUrl(url);
|
|
124
|
+
if (!raw || !raw.includes("/codex/responses")) return false;
|
|
125
|
+
return raw.startsWith("ws://") || raw.startsWith("wss://");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getRequestUrl(input) {
|
|
129
|
+
if (typeof input === "string") return input;
|
|
130
|
+
if (input instanceof URL) return input.toString();
|
|
131
|
+
if (input && typeof input.url === "string") return input.url;
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function toBuffer(body) {
|
|
136
|
+
if (typeof body === "string") return Buffer.from(body);
|
|
137
|
+
if (body instanceof Uint8Array) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
138
|
+
if (body instanceof ArrayBuffer) return Buffer.from(body);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hasHeader(headers, name) {
|
|
143
|
+
return headerValue(headers, name).length > 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function headerValue(headers, name) {
|
|
147
|
+
if (!headers) return "";
|
|
148
|
+
if (typeof headers.get === "function") return headers.get(name) ?? headers.get(name.toLowerCase()) ?? "";
|
|
149
|
+
const lowerName = name.toLowerCase();
|
|
150
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
151
|
+
if (key.toLowerCase() === lowerName) return String(value ?? "");
|
|
152
|
+
}
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function recordHttpCompression(preBytes, postBytes) {
|
|
157
|
+
const stats = ensureStats();
|
|
158
|
+
stats.httpZstdRequests += 1;
|
|
159
|
+
stats.lastHttpPreBytes = preBytes;
|
|
160
|
+
stats.lastHttpPostBytes = postBytes;
|
|
161
|
+
stats.lastHttpRatio = Number((postBytes / preBytes).toFixed(4));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function recordWebSocketCompression() {
|
|
165
|
+
const stats = ensureStats();
|
|
166
|
+
stats.wsCompressedConnections += 1;
|
|
167
|
+
stats.wsPerMessageDeflate = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ensureStats() {
|
|
171
|
+
globalThis[STATS] = globalThis[STATS] ?? {
|
|
172
|
+
httpZstdRequests: 0,
|
|
173
|
+
lastHttpPreBytes: 0,
|
|
174
|
+
lastHttpPostBytes: 0,
|
|
175
|
+
lastHttpRatio: 0,
|
|
176
|
+
wsCompressedConnections: 0,
|
|
177
|
+
wsPerMessageDeflate: false,
|
|
178
|
+
};
|
|
179
|
+
return globalThis[STATS];
|
|
180
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getOpenAICodexWebSocketDebugStats } from "@earendil-works/pi-ai/openai-codex-responses";
|
|
2
|
+
import { getCodexTransportCompressionStats } from "./codex-transport-compression.mjs";
|
|
3
|
+
import { getCodexWebSocketLastEvent } from "./codex-websocket-event-debug.mjs";
|
|
2
4
|
|
|
3
5
|
export function getCodexTransportDebugSnapshot(session) {
|
|
4
6
|
if (!isCodexTransportDebugEnabled()) return null;
|
|
@@ -12,7 +14,7 @@ export function dumpCodexTransportDebug({ before, session, ui, logger }) {
|
|
|
12
14
|
const after = sessionId ? (getOpenAICodexWebSocketDebugStats(sessionId) ?? null) : null;
|
|
13
15
|
const fields = formatCodexTransportDebugFields(sessionId, before, after);
|
|
14
16
|
logger?.event("codex.transport", fields);
|
|
15
|
-
writeCodexTransportDebug(ui, formatCodexTransportDebugLines(fields));
|
|
17
|
+
writeCodexTransportDebug(ui, formatCodexTransportDebugLines(fields, logger?.path));
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function isCodexTransportDebugEnabled() {
|
|
@@ -20,6 +22,11 @@ function isCodexTransportDebugEnabled() {
|
|
|
20
22
|
return value === "1" || value === "true" || value === "yes";
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
function isCodexTransportDebugEventsVisible() {
|
|
26
|
+
const value = process.env.MARCH_CODEX_TRANSPORT_DEBUG_EVENTS;
|
|
27
|
+
return value === "1" || value === "true" || value === "yes";
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
function formatCodexTransportDebugFields(sessionId, before, after) {
|
|
24
31
|
const delta = (key) => (after?.[key] ?? 0) - (before?.[key] ?? 0);
|
|
25
32
|
return {
|
|
@@ -38,13 +45,15 @@ function formatCodexTransportDebugFields(sessionId, before, after) {
|
|
|
38
45
|
lastInputItems: after?.lastInputItems ?? 0,
|
|
39
46
|
lastDeltaInputItems: after?.lastDeltaInputItems ?? 0,
|
|
40
47
|
lastWebSocketError: after?.lastWebSocketError ?? null,
|
|
48
|
+
compression: getCodexTransportCompressionStats(),
|
|
49
|
+
lastWebSocketEvent: getCodexWebSocketLastEvent(sessionId),
|
|
41
50
|
hasStats: Boolean(after),
|
|
42
51
|
};
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
function formatCodexTransportDebugLines(fields) {
|
|
54
|
+
function formatCodexTransportDebugLines(fields, logPath) {
|
|
46
55
|
if (!fields.hasStats) return [`[codex-transport] sessionId=${fields.sessionId} no Codex WebSocket stats`];
|
|
47
|
-
|
|
56
|
+
const lines = [
|
|
48
57
|
`[codex-transport] sessionId=${fields.sessionId}`,
|
|
49
58
|
` requests=${fields.requests} totalRequests=${fields.totalRequests}`,
|
|
50
59
|
` wsConnections created=${fields.connectionsCreated} reused=${fields.connectionsReused}`,
|
|
@@ -52,7 +61,36 @@ function formatCodexTransportDebugLines(fields) {
|
|
|
52
61
|
` fallback websocketFailures=${fields.websocketFailures} sseFallbacks=${fields.sseFallbacks} active=${fields.websocketFallbackActive}`,
|
|
53
62
|
` error lastWebSocketError=${formatDebugValue(fields.lastWebSocketError)}`,
|
|
54
63
|
` lastInputItems=${fields.lastInputItems} lastDeltaInputItems=${fields.lastDeltaInputItems}`,
|
|
64
|
+
` compression httpZstd=${formatDebugValue(fields.compression?.httpZstdRequests ?? 0)} lastBytes=${formatDebugValue(fields.compression ? `${fields.compression.lastHttpPreBytes}->${fields.compression.lastHttpPostBytes}` : null)} ratio=${formatDebugValue(fields.compression?.lastHttpRatio)} wsDeflate=${formatDebugValue(fields.compression?.wsPerMessageDeflate)} wsConnections=${formatDebugValue(fields.compression?.wsCompressedConnections ?? 0)}`,
|
|
55
65
|
];
|
|
66
|
+
if (isCodexTransportDebugEventsVisible()) {
|
|
67
|
+
lines.push(...formatWebSocketEventLines(fields.lastWebSocketEvent));
|
|
68
|
+
} else if (fields.lastWebSocketEvent && logPath) {
|
|
69
|
+
lines.push(` rawWebSocketEventLog=${formatDebugValue(logPath)}`);
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatWebSocketEventLines(events) {
|
|
75
|
+
if (!events) return [" wsEvent none"];
|
|
76
|
+
return [
|
|
77
|
+
...formatSingleWebSocketEvent("wsEvent", events.lastEvent),
|
|
78
|
+
...formatSingleWebSocketEvent("wsError", events.lastErrorEvent),
|
|
79
|
+
...formatSingleWebSocketCloseEvent(events.lastCloseEvent),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatSingleWebSocketEvent(prefix, event) {
|
|
84
|
+
if (!event) return [` ${prefix} none`];
|
|
85
|
+
return [
|
|
86
|
+
` ${prefix} phase=${formatDebugValue(event.phase)} type=${formatDebugValue(event.type)} keys=${formatDebugValue(event.eventKeys)} readyState=${formatDebugValue(event.readyState)}`,
|
|
87
|
+
` ${prefix} errorName=${formatDebugValue(event.errorName)} errorMessage=${formatDebugValue(event.errorMessage)} errorCode=${formatDebugValue(event.errorCode)} errorString=${formatDebugValue(event.errorString)} eventMessage=${formatDebugValue(event.eventMessage)}`,
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatSingleWebSocketCloseEvent(event) {
|
|
92
|
+
if (!event) return [" wsClose none"];
|
|
93
|
+
return [` wsClose code=${formatDebugValue(event.closeCode)} reason=${formatDebugValue(event.closeReason)} wasClean=${formatDebugValue(event.closeWasClean)} readyState=${formatDebugValue(event.readyState)}`];
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
function formatDebugValue(value) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const SOCKET_SESSION_ID = Symbol.for("march.codex.websocket.sessionId");
|
|
2
|
+
const SOCKET_LISTENERS = Symbol.for("march.codex.websocket.listeners");
|
|
3
|
+
const DEBUG_EVENTS_KEY = Symbol.for("march.codex.websocket.debugEvents");
|
|
4
|
+
const ORIGINAL_WEBSOCKET_KEY = Symbol.for("march.codex.websocket.originalConstructor");
|
|
5
|
+
|
|
6
|
+
export function installCodexWebSocketEventDebug() {
|
|
7
|
+
if (!isCodexTransportDebugEnabled()) return;
|
|
8
|
+
const OriginalWebSocket = globalThis.WebSocket;
|
|
9
|
+
if (typeof OriginalWebSocket !== "function") return;
|
|
10
|
+
if (globalThis[ORIGINAL_WEBSOCKET_KEY]) return;
|
|
11
|
+
|
|
12
|
+
globalThis[DEBUG_EVENTS_KEY] = globalThis[DEBUG_EVENTS_KEY] ?? new Map();
|
|
13
|
+
globalThis[ORIGINAL_WEBSOCKET_KEY] = OriginalWebSocket;
|
|
14
|
+
|
|
15
|
+
class MarchDebugWebSocket extends OriginalWebSocket {
|
|
16
|
+
constructor(...args) {
|
|
17
|
+
super(...args);
|
|
18
|
+
this[SOCKET_SESSION_ID] = extractSessionId(args[1]) ?? extractSessionId(args[2]) ?? null;
|
|
19
|
+
this[SOCKET_LISTENERS] = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addEventListener(type, listener, options) {
|
|
23
|
+
if ((type !== "error" && type !== "close") || !listener) {
|
|
24
|
+
return super.addEventListener(type, listener, options);
|
|
25
|
+
}
|
|
26
|
+
const wrapped = wrapListener(this, type, listener);
|
|
27
|
+
rememberWrappedListener(this, type, listener, wrapped);
|
|
28
|
+
return super.addEventListener(type, wrapped, options);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
removeEventListener(type, listener, options) {
|
|
32
|
+
const wrapped = takeWrappedListener(this, type, listener) ?? listener;
|
|
33
|
+
return super.removeEventListener(type, wrapped, options);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
globalThis.WebSocket = MarchDebugWebSocket;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getCodexWebSocketLastEvent(sessionId) {
|
|
41
|
+
if (!sessionId) return null;
|
|
42
|
+
return globalThis[DEBUG_EVENTS_KEY]?.get(sessionId) ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isCodexTransportDebugEnabled() {
|
|
46
|
+
const value = process.env.MARCH_CODEX_TRANSPORT_DEBUG;
|
|
47
|
+
return value === "1" || value === "true" || value === "yes";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractSessionId(options) {
|
|
51
|
+
const headers = options?.headers;
|
|
52
|
+
if (!headers || typeof headers !== "object") return null;
|
|
53
|
+
const value = headers.session_id ?? headers["session_id"] ?? headers["x-client-request-id"] ?? headers["X-Client-Request-Id"];
|
|
54
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function wrapListener(socket, type, listener) {
|
|
58
|
+
return function marchCodexWebSocketDebugListener(event) {
|
|
59
|
+
recordWebSocketEvent(socket, type, event);
|
|
60
|
+
if (typeof listener === "function") return listener.call(this, event);
|
|
61
|
+
return listener.handleEvent?.(event);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function rememberWrappedListener(socket, type, listener, wrapped) {
|
|
66
|
+
let byType = socket[SOCKET_LISTENERS].get(type);
|
|
67
|
+
if (!byType) {
|
|
68
|
+
byType = new Map();
|
|
69
|
+
socket[SOCKET_LISTENERS].set(type, byType);
|
|
70
|
+
}
|
|
71
|
+
byType.set(listener, wrapped);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function takeWrappedListener(socket, type, listener) {
|
|
75
|
+
const byType = socket[SOCKET_LISTENERS]?.get(type);
|
|
76
|
+
if (!byType) return null;
|
|
77
|
+
const wrapped = byType.get(listener);
|
|
78
|
+
byType.delete(listener);
|
|
79
|
+
return wrapped ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function recordWebSocketEvent(socket, type, event) {
|
|
83
|
+
const sessionId = socket[SOCKET_SESSION_ID];
|
|
84
|
+
if (!sessionId) return;
|
|
85
|
+
const events = globalThis[DEBUG_EVENTS_KEY].get(sessionId) ?? {};
|
|
86
|
+
const summary = summarizeWebSocketEvent(socket, type, event);
|
|
87
|
+
events.lastEvent = summary;
|
|
88
|
+
if (type === "error") events.lastErrorEvent = summary;
|
|
89
|
+
if (type === "close") events.lastCloseEvent = summary;
|
|
90
|
+
globalThis[DEBUG_EVENTS_KEY].set(sessionId, events);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function summarizeWebSocketEvent(socket, type, event) {
|
|
94
|
+
const nestedError = event && typeof event === "object" && "error" in event ? event.error : null;
|
|
95
|
+
return {
|
|
96
|
+
phase: type,
|
|
97
|
+
type: readString(event, "type") ?? type,
|
|
98
|
+
eventKeys: getEventKeys(event),
|
|
99
|
+
eventMessage: readString(event, "message"),
|
|
100
|
+
errorName: readString(nestedError, "name"),
|
|
101
|
+
errorMessage: readString(nestedError, "message"),
|
|
102
|
+
errorCode: readString(nestedError, "code") ?? readNumber(nestedError, "code"),
|
|
103
|
+
errorString: nestedError ? String(nestedError) : undefined,
|
|
104
|
+
closeCode: readNumber(event, "code"),
|
|
105
|
+
closeReason: readString(event, "reason"),
|
|
106
|
+
closeWasClean: readBoolean(event, "wasClean"),
|
|
107
|
+
readyState: typeof socket.readyState === "number" ? socket.readyState : undefined,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getEventKeys(event) {
|
|
112
|
+
if (!event || typeof event !== "object") return [];
|
|
113
|
+
return [...new Set([...Object.keys(event), "type", "message", "error", "code", "reason", "wasClean"].filter((key) => key in event))];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readString(value, key) {
|
|
117
|
+
const field = value && typeof value === "object" ? value[key] : undefined;
|
|
118
|
+
return typeof field === "string" && field.length > 0 ? field : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readNumber(value, key) {
|
|
122
|
+
const field = value && typeof value === "object" ? value[key] : undefined;
|
|
123
|
+
return typeof field === "number" ? field : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readBoolean(value, key) {
|
|
127
|
+
const field = value && typeof value === "object" ? value[key] : undefined;
|
|
128
|
+
return typeof field === "boolean" ? field : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
package/src/agent/runner.mjs
CHANGED
|
@@ -13,6 +13,9 @@ import { createRuntimeUiBridge } from "./runtime/ui-event-bridge.mjs";
|
|
|
13
13
|
import { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
14
14
|
import { notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
|
|
15
15
|
import { dumpCodexTransportDebug, getCodexTransportDebugSnapshot } from "./runner/codex-transport-debug.mjs";
|
|
16
|
+
import { installCodexWebSocketEventDebug } from "./runner/codex-websocket-event-debug.mjs";
|
|
17
|
+
import { installCodexTransportCompression } from "./runner/codex-transport-compression.mjs";
|
|
18
|
+
import { applyCodexLargeContextGuardToPayload, installCodexLargeContextGuard } from "./runner/codex-large-context-guard.mjs";
|
|
16
19
|
import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
|
|
17
20
|
import { createSessionBinding } from "./session/session-binding.mjs";
|
|
18
21
|
import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
|
|
@@ -23,11 +26,13 @@ import { appendFastVariants, createFastModelEntry, fromFastEntryModel, isFastPro
|
|
|
23
26
|
import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
|
|
24
27
|
import { registerCustomProviders } from "../provider/custom-provider.mjs";
|
|
25
28
|
import { injectHostedTools } from "../provider/hosted-tools.mjs";
|
|
26
|
-
export { MARCH_BASE_TOOL_NAMES };
|
|
27
|
-
export { installModelPayloadDumper } from "./model-payload-dumper.mjs";
|
|
29
|
+
export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
|
|
28
30
|
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
29
31
|
export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
30
32
|
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {} }) {
|
|
33
|
+
installCodexLargeContextGuard();
|
|
34
|
+
installCodexTransportCompression();
|
|
35
|
+
installCodexWebSocketEventDebug();
|
|
31
36
|
if (!useRuntimeHost && extensionPaths.length > 0) {
|
|
32
37
|
throw new Error("--extension requires the default pi runtime host path");
|
|
33
38
|
}
|
|
@@ -287,6 +292,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
287
292
|
? payload
|
|
288
293
|
: replaceProviderContextMessages(payload, engine.buildProviderContext(currentPromptForContext));
|
|
289
294
|
nextPayload = injectHostedTools(nextPayload, model, hostedTools);
|
|
295
|
+
nextPayload = applyCodexLargeContextGuardToPayload(nextPayload, { model, session: sessionBinding.get() });
|
|
290
296
|
if (_currentFastEntry) nextPayload = { ...nextPayload, service_tier: "priority" };
|
|
291
297
|
return nextPayload;
|
|
292
298
|
}
|