mixdog 0.9.148 → 0.9.149
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 +1 -1
- package/src/rules/shared/30-exploration.md +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +92 -29
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +7 -27
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -2
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +4 -4
package/package.json
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
wildcard or recursive file paths→`glob`;
|
|
14
14
|
known directory's immediate entries→`list`;
|
|
15
15
|
unknown file or directory location→`find`.
|
|
16
|
+
- For `code_graph`, location-only lookup uses `body:false`; use `body:true`
|
|
17
|
+
only for the smallest exact implementation symbol needed, and never use
|
|
18
|
+
`overview` and `symbols` for the same evidence.
|
|
16
19
|
- Use a path locator only when the owner's required target is unknown. Paths
|
|
17
20
|
reachable by expanding an environment variable or the home directory are
|
|
18
21
|
resolved locations, not unknowns.
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
mintUuidV7,
|
|
9
9
|
} from '../session/manager/session-id.mjs';
|
|
10
10
|
import { buildStableProviderPromptCacheKey } from '../agent-runtime/cache-strategy.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
_codexWsCompatibilityHeaders,
|
|
13
|
+
_withCodexWsClientMetadata,
|
|
14
|
+
} from './openai-codex-metadata.mjs';
|
|
12
15
|
import { codexOriginator, codexUserAgent } from './codex-client-meta.mjs';
|
|
13
16
|
|
|
14
17
|
test('Codex wire identity is a real time-based UUIDv7 and remains session-stable', () => {
|
|
@@ -26,14 +29,7 @@ test('Codex wire identity is a real time-based UUIDv7 and remains session-stable
|
|
|
26
29
|
assert.equal(ensureCodexWireSessionId(session), sessionId);
|
|
27
30
|
});
|
|
28
31
|
|
|
29
|
-
test('Codex cache key and every wire session identity use the same UUIDv7', (
|
|
30
|
-
const previousParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY;
|
|
31
|
-
process.env.MIXDOG_OAI_CODEX_WIRE_PARITY = '1';
|
|
32
|
-
t.after(() => {
|
|
33
|
-
if (previousParity == null) delete process.env.MIXDOG_OAI_CODEX_WIRE_PARITY;
|
|
34
|
-
else process.env.MIXDOG_OAI_CODEX_WIRE_PARITY = previousParity;
|
|
35
|
-
});
|
|
36
|
-
|
|
32
|
+
test('Codex cache key and every wire session identity use the same UUIDv7', () => {
|
|
37
33
|
const sessionId = mintUuidV7();
|
|
38
34
|
const turnStartedAtUnixMs = Date.now();
|
|
39
35
|
const turnId = mintUuidV7(turnStartedAtUnixMs);
|
|
@@ -80,11 +76,38 @@ test('Codex cache key and every wire session identity use the same UUIDv7', (t)
|
|
|
80
76
|
assert.equal(turnMetadata.session_id, sessionId);
|
|
81
77
|
assert.equal(turnMetadata.thread_id, sessionId);
|
|
82
78
|
assert.equal(turnMetadata.turn_id, turnId);
|
|
79
|
+
assert.match(turnMetadata.installation_id, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
83
80
|
assert.equal(turnMetadata.agent_name, '/root');
|
|
84
81
|
assert.equal(turnMetadata.sandbox, 'none');
|
|
85
82
|
assert.equal(turnMetadata.sandbox_mode, 'danger-full-access');
|
|
86
83
|
assert.equal(turnMetadata.auto_review_enabled, false);
|
|
84
|
+
assert.equal(turnMetadata.node_repl_auto_review_required, false);
|
|
85
|
+
assert.equal(turnMetadata.node_repl_disabled, false);
|
|
87
86
|
assert.equal(turnMetadata.turn_started_at_unix_ms, turnStartedAtUnixMs);
|
|
87
|
+
|
|
88
|
+
const prewarmHeaders = _codexWsCompatibilityHeaders({
|
|
89
|
+
cacheKey: promptCacheKey,
|
|
90
|
+
poolKey: session.id,
|
|
91
|
+
model: 'gpt-5.6-sol',
|
|
92
|
+
handshake: true,
|
|
93
|
+
useResponsesLite: true,
|
|
94
|
+
sendOpts: {
|
|
95
|
+
codexSessionId: sessionId,
|
|
96
|
+
codexThreadId: sessionId,
|
|
97
|
+
requestKind: 'prewarm',
|
|
98
|
+
session,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
assert.equal(prewarmHeaders['session-id'], sessionId);
|
|
102
|
+
assert.equal(prewarmHeaders['thread-id'], sessionId);
|
|
103
|
+
assert.equal(prewarmHeaders['x-client-request-id'], sessionId);
|
|
104
|
+
assert.equal('x-codex-installation-id' in prewarmHeaders, false);
|
|
105
|
+
assert.equal('x-openai-internal-codex-responses-lite' in prewarmHeaders, false);
|
|
106
|
+
const prewarmMetadata = JSON.parse(prewarmHeaders['x-codex-turn-metadata']);
|
|
107
|
+
assert.equal(prewarmMetadata.request_kind, 'prewarm');
|
|
108
|
+
assert.equal(prewarmMetadata.turn_id, '');
|
|
109
|
+
assert.equal(prewarmMetadata.installation_id, turnMetadata.installation_id);
|
|
110
|
+
assert.equal('turn_started_at_unix_ms' in prewarmMetadata, false);
|
|
88
111
|
});
|
|
89
112
|
|
|
90
113
|
// A compaction summary is a request of the same session: same thread identity,
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
// installation/session/thread/turn identity block that rides every frame, its
|
|
3
3
|
// handshake-header projection, and the per-turn x-codex-turn-state guard.
|
|
4
4
|
// Extracted from openai-oauth-ws.mjs, which now owns transport flow only.
|
|
5
|
-
import { createHash } from 'crypto';
|
|
5
|
+
import { createHash, randomUUID } from 'crypto';
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { getPluginData } from '../config.mjs';
|
|
9
|
+
|
|
10
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
11
|
+
let _installationId = null;
|
|
6
12
|
|
|
7
13
|
function _cleanMetaString(value) {
|
|
8
14
|
return typeof value === 'string' ? value.trim() : '';
|
|
@@ -49,8 +55,38 @@ function _codexRequestKind(sendOpts, sessionId) {
|
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
function _codexInstallationId(sendOpts) {
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const explicit = _cleanMetaString(
|
|
59
|
+
sendOpts?.installationId
|
|
60
|
+
|| sendOpts?.codexInstallationId
|
|
61
|
+
|| process.env.MIXDOG_CODEX_INSTALLATION_ID,
|
|
62
|
+
).toLowerCase();
|
|
63
|
+
if (UUID_RE.test(explicit)) return explicit;
|
|
64
|
+
if (_installationId) return _installationId;
|
|
65
|
+
const dir = getPluginData();
|
|
66
|
+
const file = join(dir, 'installation_id');
|
|
67
|
+
try {
|
|
68
|
+
const existing = existsSync(file) ? readFileSync(file, 'utf8').trim().toLowerCase() : '';
|
|
69
|
+
if (UUID_RE.test(existing)) {
|
|
70
|
+
_installationId = existing;
|
|
71
|
+
return _installationId;
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
const generated = randomUUID();
|
|
75
|
+
try {
|
|
76
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
77
|
+
try {
|
|
78
|
+
writeFileSync(file, generated, { encoding: 'utf8', flag: 'wx', mode: 0o644 });
|
|
79
|
+
} catch {
|
|
80
|
+
const winner = readFileSync(file, 'utf8').trim().toLowerCase();
|
|
81
|
+
if (UUID_RE.test(winner)) {
|
|
82
|
+
_installationId = winner;
|
|
83
|
+
return _installationId;
|
|
84
|
+
}
|
|
85
|
+
writeFileSync(file, generated, { encoding: 'utf8', mode: 0o644 });
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
_installationId = generated;
|
|
89
|
+
return _installationId;
|
|
54
90
|
}
|
|
55
91
|
|
|
56
92
|
// The identity block is rebuilt per request: never cached on the pooled
|
|
@@ -75,51 +111,42 @@ function _codexMetadataBase(entry, { poolKey, cacheKey, sendOpts, handshake = fa
|
|
|
75
111
|
)
|
|
76
112
|
|| rawSessionId;
|
|
77
113
|
const rawInstallationId = _codexInstallationId(sendOpts);
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const installationId = wireParity ? _codexUuidV7(rawInstallationId) : rawInstallationId;
|
|
114
|
+
const sessionId = _codexUuidV7(rawSessionId);
|
|
115
|
+
const threadId = _codexUuidV7(rawThreadId);
|
|
116
|
+
const installationId = rawInstallationId;
|
|
82
117
|
const startedAt = Number.isFinite(Number(sendOpts?.turnStartedAtUnixMs))
|
|
83
118
|
? Math.floor(Number(sendOpts.turnStartedAtUnixMs))
|
|
84
119
|
: _sessionStartedAtUnixMs(rawSessionId);
|
|
85
120
|
const requestKind = _codexRequestKind(sendOpts, rawSessionId);
|
|
86
121
|
// The reference client opens the WS with a prewarm (empty turn_id) BEFORE
|
|
87
|
-
// the real turn
|
|
88
|
-
//
|
|
89
|
-
// presenting the handshake as a live turn. Parity off is unchanged.
|
|
122
|
+
// the real turn, so the handshake is always identified as a prewarm rather
|
|
123
|
+
// than as a live turn.
|
|
90
124
|
const isPrewarm = requestKind === 'prewarm' || handshake === true;
|
|
91
125
|
const rawExplicitTurnId = _cleanMetaString(sendOpts?.turnId || sendOpts?.codexTurnId || sendOpts?.session?.turnId);
|
|
92
126
|
const explicitWindowId = _cleanMetaString(sendOpts?.windowId || sendOpts?.codexWindowId || sendOpts?.session?.windowId);
|
|
93
|
-
const turnId =
|
|
127
|
+
const turnId = isPrewarm
|
|
94
128
|
? ''
|
|
95
|
-
:
|
|
96
|
-
|
|
97
|
-
: (rawExplicitTurnId || sessionId);
|
|
98
|
-
const effectiveRequestKind = wireParity && isPrewarm ? 'prewarm' : requestKind;
|
|
129
|
+
: _codexUuidV7(rawExplicitTurnId || `${rawSessionId}:turn`);
|
|
130
|
+
const effectiveRequestKind = isPrewarm ? 'prewarm' : requestKind;
|
|
99
131
|
// Window id is `<thread-id>:<auto-compact window number>`, and that counter
|
|
100
132
|
// starts at 0: a thread that never auto-compacted reports generation 0 and
|
|
101
|
-
// only advances when a new context window opens.
|
|
102
|
-
|
|
103
|
-
const windowId = explicitWindowId || `${threadId}:${wireParity ? 0 : 1}`;
|
|
133
|
+
// only advances when a new context window opens.
|
|
134
|
+
const windowId = explicitWindowId || `${threadId}:0`;
|
|
104
135
|
const turnMetadata = {
|
|
105
136
|
installation_id: installationId,
|
|
106
137
|
session_id: sessionId,
|
|
107
138
|
thread_id: threadId,
|
|
139
|
+
agent_name: '/root',
|
|
108
140
|
turn_id: turnId,
|
|
109
141
|
window_id: windowId,
|
|
110
142
|
request_kind: effectiveRequestKind,
|
|
111
|
-
// Turn-metadata fields the reference client fills on every request.
|
|
112
|
-
// They were behind a probe knob after a 2026-07-04 A/B showed no
|
|
113
|
-
// isolated effect; they are unconditional now because a partial blob is
|
|
114
|
-
// a shape no real client sends. Absolute agent path, not a bare name;
|
|
115
|
-
// the sandbox pair reports this runtime honestly (tools run with full
|
|
116
|
-
// host access, so there is no sandbox to declare).
|
|
117
|
-
agent_name: '/root',
|
|
118
143
|
thread_source: 'user',
|
|
119
144
|
sandbox: 'none',
|
|
120
145
|
sandbox_mode: 'danger-full-access',
|
|
121
146
|
auto_review_enabled: false,
|
|
122
|
-
|
|
147
|
+
node_repl_auto_review_required: false,
|
|
148
|
+
node_repl_disabled: false,
|
|
149
|
+
...(!isPrewarm ? { turn_started_at_unix_ms: startedAt } : {}),
|
|
123
150
|
};
|
|
124
151
|
return {
|
|
125
152
|
'x-codex-installation-id': installationId,
|
|
@@ -144,16 +171,18 @@ export function _metadataTrace(metadata) {
|
|
|
144
171
|
};
|
|
145
172
|
}
|
|
146
173
|
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// all of them on every request, and a 2026-07-04 A/B measured the blob alone
|
|
150
|
-
// lifting prefix-cache hits.
|
|
174
|
+
// The WebSocket handshake carries compatibility identity and routing fields.
|
|
175
|
+
// Installation and Responses Lite data stay in per-request client_metadata.
|
|
151
176
|
export function _codexWsCompatibilityHeaders(context = {}) {
|
|
152
177
|
const metadata = _codexMetadataBase(null, context);
|
|
153
178
|
const headers = {};
|
|
179
|
+
if (metadata.session_id) headers['session-id'] = metadata.session_id;
|
|
180
|
+
if (metadata.thread_id) {
|
|
181
|
+
headers['thread-id'] = metadata.thread_id;
|
|
182
|
+
headers['x-client-request-id'] = metadata.thread_id;
|
|
183
|
+
}
|
|
154
184
|
if (metadata['x-codex-window-id']) headers['x-codex-window-id'] = metadata['x-codex-window-id'];
|
|
155
185
|
if (metadata['x-codex-turn-metadata']) headers['x-codex-turn-metadata'] = metadata['x-codex-turn-metadata'];
|
|
156
|
-
if (metadata['x-codex-installation-id']) headers['x-codex-installation-id'] = metadata['x-codex-installation-id'];
|
|
157
186
|
// Routing hint. The reference client attaches this to EVERY request whose
|
|
158
187
|
// auth is the ChatGPT backend — no flag, no mode, and with `model=` alone
|
|
159
188
|
// when no service tier is selected. It is how the backend lands the request
|
|
@@ -177,9 +206,14 @@ export function _codexWsCompatibilityHeaders(context = {}) {
|
|
|
177
206
|
export function _withCodexWsClientMetadata(frame, entry, enabled, context = {}) {
|
|
178
207
|
if (!enabled || !frame || typeof frame !== 'object') return frame;
|
|
179
208
|
const base = _codexMetadataBase(entry, context);
|
|
209
|
+
const requestKind = _codexRequestKind(context?.sendOpts, context?.poolKey || '');
|
|
210
|
+
const isPrewarmRequest = requestKind === 'prewarm';
|
|
180
211
|
const metadata = {
|
|
181
212
|
...base,
|
|
182
213
|
...(frame.client_metadata && typeof frame.client_metadata === 'object' ? frame.client_metadata : {}),
|
|
214
|
+
...(context?.useResponsesLite === true
|
|
215
|
+
? { ws_request_header_x_openai_internal_codex_responses_lite: 'true' }
|
|
216
|
+
: {}),
|
|
183
217
|
'x-codex-ws-stream-request-start-ms': String(Date.now()),
|
|
184
218
|
};
|
|
185
219
|
if (entry && typeof entry === 'object') {
|
|
@@ -192,10 +226,21 @@ export function _withCodexWsClientMetadata(frame, entry, enabled, context = {})
|
|
|
192
226
|
if (entry.turnStateTurnId == null) {
|
|
193
227
|
entry.turnStateTurnId = base.turn_id;
|
|
194
228
|
} else if (entry.turnStateTurnId !== base.turn_id) {
|
|
195
|
-
|
|
196
|
-
|
|
229
|
+
// Codex startup prewarm owns the handshake turn-state until
|
|
230
|
+
// the first real turn consumes that prewarmed client session.
|
|
231
|
+
// Adopt it once across prewarm→turn; ordinary turn changes
|
|
232
|
+
// still retire the old token.
|
|
233
|
+
if (entry.turnStateFromPrewarm === true && !isPrewarmRequest) {
|
|
234
|
+
entry.turnStateTurnId = base.turn_id;
|
|
235
|
+
} else {
|
|
236
|
+
entry.turnState = null;
|
|
237
|
+
entry.turnStateTurnId = null;
|
|
238
|
+
}
|
|
197
239
|
}
|
|
198
240
|
}
|
|
241
|
+
if (!isPrewarmRequest && entry.turnStateFromPrewarm === true) {
|
|
242
|
+
entry.turnStateFromPrewarm = false;
|
|
243
|
+
}
|
|
199
244
|
entry.currentTurnId = base.turn_id;
|
|
200
245
|
}
|
|
201
246
|
if (entry?.turnState) metadata['x-codex-turn-state'] = String(entry.turnState);
|
|
@@ -7,6 +7,23 @@
|
|
|
7
7
|
* _displayCodexModel for existing importers.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const CODEX_RESPONSES_LITE_CAPABILITY = Object.freeze({
|
|
11
|
+
'gpt-5.6-sol': true,
|
|
12
|
+
'gpt-5.6-terra': true,
|
|
13
|
+
'gpt-5.6-luna': true,
|
|
14
|
+
'gpt-5.5': false,
|
|
15
|
+
'gpt-5.4': false,
|
|
16
|
+
'gpt-5.4-mini': false,
|
|
17
|
+
'gpt-5.2': false,
|
|
18
|
+
'codex-auto-review': false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function _codexUsesResponsesLite(id, modelInfo = null) {
|
|
22
|
+
const explicit = modelInfo?.useResponsesLite ?? modelInfo?.use_responses_lite;
|
|
23
|
+
if (typeof explicit === 'boolean') return explicit;
|
|
24
|
+
return CODEX_RESPONSES_LITE_CAPABILITY[String(id || '').trim()] === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
// OAuth catalog returns dated ids (gpt-5.4-mini-2026-03-17). Strip the trailing
|
|
11
28
|
// -YYYY-MM-DD to get the version alias (gpt-5.4-mini). Unknown shapes pass
|
|
12
29
|
// through unchanged.
|
|
@@ -82,6 +99,10 @@ export function _normalizeCodexModel(m) {
|
|
|
82
99
|
latest: false,
|
|
83
100
|
description: m?.description || '',
|
|
84
101
|
reasoningLevels: (m?.supported_reasoning_levels || []).map(r => r.effort),
|
|
102
|
+
supportVerbosity: m?.support_verbosity === true,
|
|
103
|
+
defaultVerbosity: m?.default_verbosity || null,
|
|
104
|
+
useResponsesLite: m?.use_responses_lite === true,
|
|
105
|
+
supportsReasoningSummaries: m?.supports_reasoning_summaries === true,
|
|
85
106
|
serviceTiers,
|
|
86
107
|
defaultServiceTier: m?.default_service_tier || null,
|
|
87
108
|
additionalSpeedTiers,
|
|
@@ -251,6 +251,9 @@ export async function sendViaHttpSse({
|
|
|
251
251
|
const statelessConversation = opts?.statelessConversation === true
|
|
252
252
|
|| _envFlag('MIXDOG_OAI_STATELESS_HTTP', false);
|
|
253
253
|
const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey, statelessConversation });
|
|
254
|
+
if (auth?.type !== 'openai-direct' && body?.input?.[0]?.type === 'additional_tools') {
|
|
255
|
+
headers['x-openai-internal-codex-responses-lite'] = 'true';
|
|
256
|
+
}
|
|
254
257
|
const fetchStartedAt = Date.now();
|
|
255
258
|
const responsesUrl = auth?.type === 'openai-direct'
|
|
256
259
|
? OPENAI_DIRECT_RESPONSES_URL
|
|
@@ -515,6 +515,7 @@ export async function sendViaWebSocket({
|
|
|
515
515
|
_sendSpanTraceFn = appendAgentTrace,
|
|
516
516
|
_agentTraceFn = appendAgentTrace,
|
|
517
517
|
_carriedWarmup = null,
|
|
518
|
+
_prewarmedHandle = null,
|
|
518
519
|
}) {
|
|
519
520
|
// One bounded Codex stream retry budget covers transient handshake and
|
|
520
521
|
// pre-output stream failures. Every retry acquires a fresh connection.
|
|
@@ -583,6 +584,7 @@ export async function sendViaWebSocket({
|
|
|
583
584
|
sendOpts,
|
|
584
585
|
model: useModel,
|
|
585
586
|
serviceTier: body?.service_tier || '',
|
|
587
|
+
useResponsesLite: body?.input?.[0]?.type === 'additional_tools',
|
|
586
588
|
};
|
|
587
589
|
const codexHandshakeHeaders = useCodexWsClientMetadata
|
|
588
590
|
? _codexWsCompatibilityHeaders({ ...codexMetadataContext, handshake: true })
|
|
@@ -650,6 +652,7 @@ export async function sendViaWebSocket({
|
|
|
650
652
|
// Armed by the rejection safety net below: once a server rejects a
|
|
651
653
|
// replayed reasoning item, every later attempt of THIS send strips them.
|
|
652
654
|
let suppressReasoningReplay = false;
|
|
655
|
+
let prewarmedHandle = _prewarmedHandle;
|
|
653
656
|
|
|
654
657
|
for (let attemptIndex = 0; attemptIndex <= MAX_MIDSTREAM_RETRIES; attemptIndex++) {
|
|
655
658
|
const handshakeStart = performance.now();
|
|
@@ -659,29 +662,43 @@ export async function sendViaWebSocket({
|
|
|
659
662
|
sendSpan.acquireAttempts += 1;
|
|
660
663
|
try { onStageChange?.('requesting'); } catch {}
|
|
661
664
|
try {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
665
|
+
const reserved = attemptIndex === 0
|
|
666
|
+
&& forceFresh !== true
|
|
667
|
+
&& prewarmedHandle?.entry
|
|
668
|
+
&& prewarmedHandle.poolKey === poolKey
|
|
669
|
+
&& prewarmedHandle.cacheKey === cacheKey;
|
|
670
|
+
if (reserved) {
|
|
671
|
+
acquired = {
|
|
672
|
+
entry: prewarmedHandle.entry,
|
|
673
|
+
reused: true,
|
|
674
|
+
prewarmed: true,
|
|
675
|
+
};
|
|
676
|
+
prewarmedHandle = null;
|
|
677
|
+
} else {
|
|
678
|
+
acquired = await _acquireWithRetryFn({
|
|
679
|
+
auth,
|
|
680
|
+
poolKey,
|
|
681
|
+
cacheKey,
|
|
682
|
+
codexHeaders: codexHandshakeHeaders,
|
|
683
|
+
// Retry attempt must not reuse a pooled socket — the prior
|
|
684
|
+
// one is either torn down or in an unknown state.
|
|
685
|
+
forceFresh: forceFresh || attemptIndex > 0,
|
|
686
|
+
externalSignal,
|
|
687
|
+
// No nested connect retry budget: the outer stream loop owns
|
|
688
|
+
// all retries for this logical request.
|
|
689
|
+
maxAttempts: 1,
|
|
690
|
+
retry429,
|
|
691
|
+
onRetry: (info) => {
|
|
692
|
+
handshakeRetries += 1;
|
|
693
|
+
sendSpan.handshakeRetries += 1;
|
|
694
|
+
if (info?.classifier) handshakeRetryClassifiers.push(info.classifier);
|
|
695
|
+
const attempt = Number(info?.attempt) || handshakeRetries;
|
|
696
|
+
const max = Number(info?.max) || MAX_MIDSTREAM_RETRIES;
|
|
697
|
+
emitReconnectProgress({ attempt, max, classifier: info?.classifier });
|
|
698
|
+
},
|
|
699
|
+
onBackoffSlept: (ms) => { sendSpan.retryBackoffMs += ms; },
|
|
700
|
+
});
|
|
701
|
+
}
|
|
685
702
|
} catch (err) {
|
|
686
703
|
_stampWarmup(err);
|
|
687
704
|
sendSpan.poolAcquireMs += performance.now() - handshakeStart;
|
|
@@ -780,7 +797,9 @@ export async function sendViaWebSocket({
|
|
|
780
797
|
}
|
|
781
798
|
const { entry, reused } = acquired;
|
|
782
799
|
sendSpan.poolAcquireMs += performance.now() - handshakeStart;
|
|
783
|
-
sendSpan.acquireMode =
|
|
800
|
+
sendSpan.acquireMode = acquired.prewarmed
|
|
801
|
+
? 'prewarmed'
|
|
802
|
+
: entry?.ephemeral ? 'ephemeral' : (reused ? 'reused' : 'new');
|
|
784
803
|
// Re-seed the retry attempt's fresh entry with the prior attempt's
|
|
785
804
|
// last successful anchor so _computeDelta sees a non-null
|
|
786
805
|
// lastInputPrefixHash and prev_response_id, keeping the same xAI
|
|
@@ -819,6 +838,9 @@ export async function sendViaWebSocket({
|
|
|
819
838
|
// entry.lastRequestInput always records the post-policy input.
|
|
820
839
|
requestBody = _applyReasoningReplayPolicy(entry, requestBody, { suppress: suppressReasoningReplay });
|
|
821
840
|
let warmupResult = null;
|
|
841
|
+
const startupWarmupResponseId = typeof entry?.startupWarmupResponseId === 'string'
|
|
842
|
+
? entry.startupWarmupResponseId
|
|
843
|
+
: null;
|
|
822
844
|
// midState is shared between warmup and the main stream so warmup
|
|
823
845
|
// failures (first-byte timeout, send-failure, ws_4000) flow through
|
|
824
846
|
// the SAME mid-stream classifier as the main send. A wedged warmup
|
|
@@ -864,7 +886,11 @@ export async function sendViaWebSocket({
|
|
|
864
886
|
// cannot accidentally duplicate the transcript. Keep the same
|
|
865
887
|
// request properties as the real turn; Codex changes only the
|
|
866
888
|
// input tail and adds generate:false.
|
|
867
|
-
const parityWarmupBody = {
|
|
889
|
+
const parityWarmupBody = {
|
|
890
|
+
...warmupBody,
|
|
891
|
+
input: Array.isArray(warmupBody.input) ? warmupBody.input : [],
|
|
892
|
+
generate: false,
|
|
893
|
+
};
|
|
868
894
|
const warmupFrame = _buildResponseCreateFrame(parityWarmupBody);
|
|
869
895
|
const warmupMetadataContext = {
|
|
870
896
|
...codexMetadataContext,
|
|
@@ -927,6 +953,7 @@ export async function sendViaWebSocket({
|
|
|
927
953
|
entry.lastInputPrefixHash = createHash('sha256')
|
|
928
954
|
.update(JSON.stringify(warmupInputArr))
|
|
929
955
|
.digest('hex');
|
|
956
|
+
entry.turnStateFromPrewarm = true;
|
|
930
957
|
try {
|
|
931
958
|
const warmupPayload = {
|
|
932
959
|
provider: traceProvider,
|
|
@@ -949,6 +976,36 @@ export async function sendViaWebSocket({
|
|
|
949
976
|
} catch {}
|
|
950
977
|
}
|
|
951
978
|
|
|
979
|
+
// Codex performs generate:false during session startup, then hands
|
|
980
|
+
// this live client session to the first real turn. Startup callers
|
|
981
|
+
// stop here; the pooled entry retains its response id, request
|
|
982
|
+
// snapshot, socket, and turn-state for the later real send.
|
|
983
|
+
if (sendOpts?._startupPrewarmOnly === true) {
|
|
984
|
+
const responseId = warmupResult?.responseId
|
|
985
|
+
|| startupWarmupResponseId
|
|
986
|
+
|| entry.lastResponseId
|
|
987
|
+
|| null;
|
|
988
|
+
if (responseId) entry.startupWarmupResponseId = responseId;
|
|
989
|
+
else releaseWebSocket({ entry, poolKey, keep: false });
|
|
990
|
+
emitSendSpan('ok');
|
|
991
|
+
return {
|
|
992
|
+
content: '',
|
|
993
|
+
model: warmupResult?.model || useModel,
|
|
994
|
+
toolCalls: [],
|
|
995
|
+
usage: warmupResult?.usage || {
|
|
996
|
+
inputTokens: 0,
|
|
997
|
+
outputTokens: 0,
|
|
998
|
+
cachedTokens: 0,
|
|
999
|
+
cacheWriteTokens: 0,
|
|
1000
|
+
promptTokens: 0,
|
|
1001
|
+
},
|
|
1002
|
+
startupPrewarm: !!responseId,
|
|
1003
|
+
startupPrewarmHandle: responseId
|
|
1004
|
+
? { entry, poolKey, cacheKey }
|
|
1005
|
+
: null,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
952
1009
|
// A completed generate:false prewarm is a valid continuation
|
|
953
1010
|
// anchor. Compute against its retained empty-input snapshot so the
|
|
954
1011
|
// first real request sends previous_response_id plus exactly the
|
|
@@ -1393,9 +1450,10 @@ export async function sendViaWebSocket({
|
|
|
1393
1450
|
cacheObservation.actualMiss ? (cacheObservation.missReason || 'miss') : false,
|
|
1394
1451
|
);
|
|
1395
1452
|
}
|
|
1453
|
+
const effectiveWarmupResponseId = warmupResult?.responseId || startupWarmupResponseId || null;
|
|
1396
1454
|
const warmupContinuity = _warmupContinuityTrace({
|
|
1397
|
-
warmupUsed: !!
|
|
1398
|
-
warmupResponseId:
|
|
1455
|
+
warmupUsed: !!effectiveWarmupResponseId,
|
|
1456
|
+
warmupResponseId: effectiveWarmupResponseId,
|
|
1399
1457
|
priorEntryResponseId,
|
|
1400
1458
|
sentPrevResponseId,
|
|
1401
1459
|
earlyCacheMisses: entry.earlyCacheMisses,
|
|
@@ -1454,8 +1512,10 @@ export async function sendViaWebSocket({
|
|
|
1454
1512
|
body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
|
|
1455
1513
|
frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
|
|
1456
1514
|
frame_has_instructions: typeof frame.instructions === 'string' && frame.instructions.length > 0,
|
|
1457
|
-
warmup_used: !!
|
|
1458
|
-
warmup_response_id:
|
|
1515
|
+
warmup_used: !!effectiveWarmupResponseId,
|
|
1516
|
+
warmup_response_id: effectiveWarmupResponseId,
|
|
1517
|
+
warmup_first_real_cache_hit: !!effectiveWarmupResponseId
|
|
1518
|
+
&& cacheObservation.cachedTokens > 0,
|
|
1459
1519
|
...warmupContinuity,
|
|
1460
1520
|
tool_call_count: resultToolCallCount,
|
|
1461
1521
|
keep_socket: keepSocket,
|
|
@@ -1510,6 +1570,9 @@ export async function sendViaWebSocket({
|
|
|
1510
1570
|
}
|
|
1511
1571
|
} catch {}
|
|
1512
1572
|
|
|
1573
|
+
if (startupWarmupResponseId) {
|
|
1574
|
+
try { delete entry.startupWarmupResponseId; } catch {}
|
|
1575
|
+
}
|
|
1513
1576
|
releaseWebSocket({ entry, poolKey, keep: keepSocket });
|
|
1514
1577
|
const { responseId: _ignored, responseItems: _responseItemsIgnored, closeSocket: _closeSocketIgnored, ...out } = result;
|
|
1515
1578
|
if (includeResponseId && result.responseId) out.responseId = result.responseId;
|