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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.148",
3
+ "version": "0.9.149",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -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 { _withCodexWsClientMetadata } from './openai-codex-metadata.mjs';
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', (t) => {
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
- return _cleanMetaString(sendOpts?.installationId || sendOpts?.codexInstallationId || process.env.MIXDOG_CODEX_INSTALLATION_ID)
53
- || `mixdog-${_hashText(`${process.env.USERPROFILE || process.env.HOME || ''}:${process.cwd()}`, 32)}`;
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 wireParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY === '1';
79
- const sessionId = wireParity ? _codexUuidV7(rawSessionId) : rawSessionId;
80
- const threadId = wireParity ? _codexUuidV7(rawThreadId) : rawThreadId;
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. Under wire parity the handshake IS that prewarm, so its
88
- // turn_id empties and its request_kind becomes 'prewarm' instead of
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 = wireParity && isPrewarm
127
+ const turnId = isPrewarm
94
128
  ? ''
95
- : wireParity
96
- ? _codexUuidV7(rawExplicitTurnId || `${rawSessionId}:turn`)
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. The legacy non-parity
102
- // wire kept :1 and is left alone so measured default behavior is unchanged.
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
- turn_started_at_unix_ms: startedAt,
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
- // Handshake projection of the same identity: window id, the turn-metadata
148
- // blob, the installation id, and the routing hint. The reference client sends
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
- entry.turnState = null;
196
- entry.turnStateTurnId = null;
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
- acquired = await _acquireWithRetryFn({
663
- auth,
664
- poolKey,
665
- cacheKey,
666
- codexHeaders: codexHandshakeHeaders,
667
- // Retry attempt must not reuse a pooled socket — the prior
668
- // one is either torn down or in an unknown state.
669
- forceFresh: forceFresh || attemptIndex > 0,
670
- externalSignal,
671
- // No nested connect retry budget: the outer stream loop owns
672
- // all retries for this logical request.
673
- maxAttempts: 1,
674
- retry429,
675
- onRetry: (info) => {
676
- handshakeRetries += 1;
677
- sendSpan.handshakeRetries += 1;
678
- if (info?.classifier) handshakeRetryClassifiers.push(info.classifier);
679
- const attempt = Number(info?.attempt) || handshakeRetries;
680
- const max = Number(info?.max) || MAX_MIDSTREAM_RETRIES;
681
- emitReconnectProgress({ attempt, max, classifier: info?.classifier });
682
- },
683
- onBackoffSlept: (ms) => { sendSpan.retryBackoffMs += ms; },
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 = entry?.ephemeral ? 'ephemeral' : (reused ? 'reused' : 'new');
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 = { ...warmupBody, input: [], generate: false };
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: !!warmupResult,
1398
- warmupResponseId: warmupResult?.responseId || null,
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: !!warmupResult,
1458
- warmup_response_id: warmupResult?.responseId || null,
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;