sneakoscope 10.7.0 → 10.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +27 -5
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/install-helpers-codex-lb-config.js +11 -8
  5. package/dist/config/skills-manifest.json +1 -1
  6. package/dist/core/agents/agent-role-config.js +6 -2
  7. package/dist/core/agents/sks-model-pin-hygiene.js +150 -0
  8. package/dist/core/codex/codex-config-guard.js +18 -13
  9. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +7 -1
  10. package/dist/core/codex-lb/bridge-contracts.js +5 -0
  11. package/dist/core/codex-lb/combined-catalog/builder.js +4 -2
  12. package/dist/core/codex-lb/combined-catalog/latest-generation.js +7 -34
  13. package/dist/core/codex-lb/combined-catalog/normalize.js +3 -1
  14. package/dist/core/codex-lb/desktop-bridge/codex-lb-turn-state.js +35 -0
  15. package/dist/core/codex-lb/desktop-bridge/compaction-adapter.js +3 -0
  16. package/dist/core/codex-lb/desktop-bridge/header-policy.js +10 -4
  17. package/dist/core/codex-lb/desktop-bridge/http-forward.js +92 -30
  18. package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +79 -0
  19. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +57 -13
  20. package/dist/core/codex-lb/desktop-bridge/security.js +101 -55
  21. package/dist/core/codex-lb/desktop-bridge/server.js +10 -2
  22. package/dist/core/codex-lb/desktop-bridge/types.js +3 -0
  23. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +3 -0
  24. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +13 -0
  25. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +0 -9
  26. package/dist/core/codex-lb/desktop-service.js +10 -13
  27. package/dist/core/codex-lb/request-route-resolver.js +1 -1
  28. package/dist/core/codex-runtime/codex-desktop-config-policy.js +0 -1
  29. package/dist/core/hooks-runtime/jev-spawn-routing.js +1 -5
  30. package/dist/core/hooks-runtime/parent-orchestration-gate.js +6 -8
  31. package/dist/core/hooks-runtime.js +13 -9
  32. package/dist/core/init/skills.js +22 -3
  33. package/dist/core/provider/provider-context.js +4 -1
  34. package/dist/core/search/context.js +2 -2
  35. package/dist/core/update/update-migration-state.js +10 -40
  36. package/dist/core/version.js +1 -1
  37. package/package.json +2 -2
@@ -2,16 +2,19 @@ import http, {} from 'node:http';
2
2
  import https from 'node:https';
3
3
  import zlib from 'node:zlib';
4
4
  import { pipeline } from 'node:stream/promises';
5
- import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
5
+ import { BRIDGE_OFFICIAL_ROUTE_ID, BRIDGE_REMOTE_COMPACTION } from '../bridge-contracts.js';
6
6
  import { buildOfficialPassthroughHeaders, buildProviderUpstreamHeaders, rewriteResponseHeaders } from './header-policy.js';
7
7
  import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
8
- import { OPENROUTER_ONLY_REFUSAL_CODES, openRouterOnlyEnabled } from './exclusive-provider-guard.js';
9
- import { ensureDesktopBridgeRemoteTarget, isNoRouteUpstreamError, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
8
+ import { OPENROUTER_ONLY_REFUSAL_CODES } from './exclusive-provider-guard.js';
9
+ import { ensureDesktopBridgeRemoteTarget, isNoRouteUpstreamError, isResponsesFamilyPath, isUnreachableUpstreamError, pinnedPublicModel, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, threadRouteMove } from './security.js';
10
+ import { admitCodexLbTurnState, forgetCodexLbTurnStates, rememberCodexLbTurnState } from './codex-lb-turn-state.js';
11
+ import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusalBody, stripProviderBoundItems, } from './provider-handoff.js';
10
12
  import { desktopBridgeListenOrigin } from './state.js';
11
13
  import { buildCompactionSummaryRequest, compactEndpointResult, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, safeUpstreamErrorId, sseEvent, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
12
- import { isAdvisableEventStream, modelCapacityAdvisingSse } from './capacity-retry-advice.js';
13
- import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError } from './types.js';
14
+ import { MODEL_CAPACITY_ERROR_CODE, MODEL_CAPACITY_RETRY_AFTER_SECONDS, isAdvisableEventStream, modelCapacityAdvisingSse } from './capacity-retry-advice.js';
15
+ import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError, isDesktopBridgeRoutingConflict } from './types.js';
14
16
  const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
17
+ const NATIVE_COMPACTION_IDLE_FLOOR_MS = 300_000;
15
18
  function decodeBridgeRequestBody(body, encoding, maximum) {
16
19
  const options = { maxOutputLength: maximum };
17
20
  try {
@@ -61,11 +64,10 @@ async function reencodeLikeClient(plain, encoding) {
61
64
  return new Promise((resolve) => compress(plain, (error, result) => resolve(error ? null : result)));
62
65
  }
63
66
  const RESPONSES_PATHS = ['/backend-api/codex/responses', '/api/v1/responses', '/v1/responses'];
64
- function modelBodyKind(req, pathname, config) {
67
+ function modelBodyKind(req, pathname) {
65
68
  if (RESPONSES_PATHS.includes(pathname))
66
69
  return req.method === 'POST' ? 'responses' : null;
67
- return openRouterOnlyEnabled(config) && req.method === 'POST'
68
- && RESPONSES_PATHS.some((prefix) => pathname.startsWith(`${prefix}/`)) ? 'sub-endpoint' : null;
70
+ return req.method === 'POST' && RESPONSES_PATHS.some((prefix) => pathname.startsWith(`${prefix}/`)) ? 'sub-endpoint' : null;
69
71
  }
70
72
  function isCompactEndpoint(req, pathname) {
71
73
  return req.method === 'POST' && RESPONSES_PATHS.some((prefix) => pathname === `${prefix}/compact`);
@@ -105,7 +107,7 @@ export async function prepareDesktopBridgeRequest(req, config) {
105
107
  let contentEncodingStripped = false;
106
108
  let encoding = '';
107
109
  let encodedBody = null;
108
- const bodyKind = modelBodyKind(req, pathname, config);
110
+ const bodyKind = modelBodyKind(req, pathname);
109
111
  if (bodyKind) {
110
112
  const maximum = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
111
113
  body = await readBoundedBody(req, maximum);
@@ -131,12 +133,19 @@ export async function prepareDesktopBridgeRequest(req, config) {
131
133
  body = decoded;
132
134
  }
133
135
  const headerModel = singleBridgeHeader(req.headers, 'x-sks-model');
134
- const model = typeof payload?.model === 'string' ? payload.model : headerModel;
135
136
  const sessionIdentity = resolveCodexSessionIdentity(req.headers, payload);
136
- const route = await resolveAndBindDesktopBridgeRouteContext({
137
+ const model = typeof payload?.model === 'string'
138
+ ? payload.model
139
+ : headerModel || (isResponsesFamilyPath(pathname) ? pinnedPublicModel(sessionIdentity.thread_id, config) : null);
140
+ const routeRequest = {
137
141
  public_model: String(model || ''), session_id: sessionIdentity.thread_id,
138
142
  pathname, transport: 'http', headers: req.headers, identity: sessionIdentity,
139
- }, config);
143
+ };
144
+ let move = null;
145
+ const route = await resolveAndBindDesktopBridgeRouteContext(routeRequest, config, (resolved) => {
146
+ move = threadRouteMove(routeRequest, resolved, config);
147
+ assertProviderHandoffAllowed(payload, move);
148
+ });
140
149
  let rewrite = false;
141
150
  if (payload && (bodyKind === 'responses' || typeof payload.model === 'string') && payload.model !== route.upstream_model) {
142
151
  payload.model = route.upstream_model;
@@ -144,14 +153,15 @@ export async function prepareDesktopBridgeRequest(req, config) {
144
153
  }
145
154
  if (expandBridgeCompactionItems(payload))
146
155
  rewrite = true;
156
+ if (route.session_pin?.handoff_from && stripProviderBoundItems(payload, Boolean(move)))
157
+ rewrite = true;
147
158
  if (payload && rewrite)
148
159
  body = Buffer.from(JSON.stringify(payload));
149
160
  const compaction = bodyKind === 'responses' && requestsRemoteCompaction(payload)
150
161
  ? 'trigger'
151
162
  : bodyKind === 'sub-endpoint' && isCompactEndpoint(req, pathname) ? 'endpoint' : null;
152
163
  let plainBody = null;
153
- const answeredByBridge = Boolean(compaction) && route.provider_id !== BRIDGE_OFFICIAL_ROUTE_ID;
154
- if (encodedBody && body && !answeredByBridge && keepsClientEncoding(route)) {
164
+ if (encodedBody && body && !answeredByBridge(compaction, route) && keepsClientEncoding(route)) {
155
165
  const upstreamBody = rewrite ? await reencodeLikeClient(body, encoding) : encodedBody;
156
166
  if (upstreamBody) {
157
167
  plainBody = body;
@@ -160,7 +170,10 @@ export async function prepareDesktopBridgeRequest(req, config) {
160
170
  }
161
171
  }
162
172
  const credential = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : await resolveCredential(config, route);
163
- return { body, route, credential, contentEncodingStripped, plainBody, compaction, payload };
173
+ return { body, route, credential, contentEncodingStripped, plainBody, compaction, payload, threadId: sessionIdentity.thread_id, move };
174
+ }
175
+ function answeredByBridge(compaction, route) {
176
+ return Boolean(compaction) && BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary';
164
177
  }
165
178
  function connectTimeout(request, config) {
166
179
  request.once('socket', (socket) => {
@@ -191,8 +204,12 @@ function writeHttpBridgeError(res, error, req) {
191
204
  res.destroy(error instanceof Error ? error : undefined);
192
205
  return;
193
206
  }
194
- res.writeHead(code.startsWith('catalog_') || code.startsWith('session_') || code.includes('route_')
195
- || OPENROUTER_ONLY_REFUSAL_CODES.has(code) ? 409 : 502, {
207
+ if (error instanceof ProviderHandoffRefusedError) {
208
+ res.writeHead(400, { 'content-type': 'application/json', 'cache-control': 'no-store', connection: 'close' });
209
+ res.end(providerHandoffRefusalBody(error));
210
+ return;
211
+ }
212
+ res.writeHead(isDesktopBridgeRoutingConflict(code) || OPENROUTER_ONLY_REFUSAL_CODES.has(code) ? 409 : 502, {
196
213
  'content-type': 'application/json', 'cache-control': 'no-store', connection: 'close',
197
214
  });
198
215
  res.end(JSON.stringify({ error: { type: 'sks_bridge_error', code, message: code } }));
@@ -229,19 +246,32 @@ function redactedUpstreamClientMessage(statusCode, transient) {
229
246
  const UNREADABLE_BODY_HINT = /json|pars(?:e|ing)|decod|encod|compress|utf-?8/i;
230
247
  async function readRedactedUpstreamError(response) {
231
248
  let total = 0;
249
+ let complete = true;
232
250
  const chunks = [];
233
251
  for await (const raw of response) {
234
252
  const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
235
253
  total += chunk.length;
236
254
  if (total > MAX_UPSTREAM_ERROR_BODY_BYTES) {
237
255
  response.destroy();
256
+ complete = false;
238
257
  break;
239
258
  }
240
259
  chunks.push(chunk);
241
260
  }
242
261
  let upstreamCode = null;
243
262
  let upstreamType = null;
244
- const text = Buffer.concat(chunks).toString('utf8');
263
+ const raw = Buffer.concat(chunks);
264
+ const encoding = String(response.headers['content-encoding'] || '').trim().toLowerCase();
265
+ let decoded = complete ? raw : null;
266
+ if (decoded && encoding && encoding !== 'identity') {
267
+ try {
268
+ decoded = decodeBridgeRequestBody(decoded, encoding, 4 * MAX_UPSTREAM_ERROR_BODY_BYTES);
269
+ }
270
+ catch {
271
+ decoded = null;
272
+ }
273
+ }
274
+ const text = (decoded ?? raw).toString('utf8');
245
275
  try {
246
276
  const parsed = JSON.parse(text);
247
277
  upstreamCode = safeUpstreamErrorId(parsed?.error?.code) ?? safeUpstreamErrorId(parsed?.detail);
@@ -249,7 +279,21 @@ async function readRedactedUpstreamError(response) {
249
279
  }
250
280
  catch {
251
281
  }
252
- return { upstreamCode, upstreamType, unreadableBody: UNREADABLE_BODY_HINT.test(text.slice(0, 4096)) };
282
+ return {
283
+ upstreamCode,
284
+ upstreamType,
285
+ unreadableBody: UNREADABLE_BODY_HINT.test(text.slice(0, 4096)),
286
+ body: decoded,
287
+ };
288
+ }
289
+ function scrubUpstreamErrorBody(body, credential) {
290
+ let text = body.toString('utf8');
291
+ if (credential)
292
+ text = text.split(credential).join('[REDACTED]');
293
+ text = text
294
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, 'Bearer [REDACTED]')
295
+ .replace(/\bsk-[A-Za-z0-9_-]{16,}/g, '[REDACTED]');
296
+ return Buffer.from(text, 'utf8');
253
297
  }
254
298
  function buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transient) {
255
299
  return Buffer.from(JSON.stringify({
@@ -284,6 +328,7 @@ export function destroyDesktopBridgeUpstreamAgents() {
284
328
  agent.destroy();
285
329
  upstreamAgents.clear();
286
330
  encodedBodyRefusals.clear();
331
+ forgetCodexLbTurnStates();
287
332
  }
288
333
  const STALE_CONNECTION_ERROR_CODES = new Set(['ECONNRESET', 'EPIPE', 'ECONNABORTED']);
289
334
  function isStalePooledSocketFailure(request, error) {
@@ -319,7 +364,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
319
364
  if (!official && !request.credential)
320
365
  throw new DesktopBridgeError('bridge_provider_credential_invalid');
321
366
  await ensureDesktopBridgeRemoteTarget(remote, config.remoteLookup);
322
- if (!official && request.compaction && request.payload) {
367
+ if (!official && request.compaction && answeredByBridge(request.compaction, request.route) && request.payload) {
323
368
  await answerCompactionWithProviderSummary(req, res, config, {
324
369
  kind: request.compaction,
325
370
  payload: request.payload,
@@ -341,6 +386,12 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
341
386
  : buildProviderUpstreamHeaders(req.headers, {
342
387
  providerId: provider.provider_id, authTransport: provider.auth_transport, credential: request.credential,
343
388
  }, target.host);
389
+ const codexLb = provider?.provider_id === 'codex-lb';
390
+ const threadId = request.threadId ?? null;
391
+ if (request.move)
392
+ delete headers['x-codex-turn-state'];
393
+ else if (codexLb)
394
+ admitCodexLbTurnState(headers, threadId, request.credential.fingerprint);
344
395
  if (request.body)
345
396
  headers['content-length'] = String(request.body.length);
346
397
  else
@@ -348,6 +399,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
348
399
  if (request.contentEncodingStripped)
349
400
  delete headers['content-encoding'];
350
401
  const agent = upstreamAgent(remote.secure, `${remote.address}:${remote.port}`, config.idleTimeoutMs);
402
+ const idleTimeoutMs = request.compaction ? Math.max(config.idleTimeoutMs, NATIVE_COMPACTION_IDLE_FLOOR_MS) : config.idleTimeoutMs;
351
403
  const replayable = Buffer.isBuffer(request.body);
352
404
  let body = request.body;
353
405
  let plainFallbackActive = false;
@@ -371,7 +423,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
371
423
  ...(remote.tlsServername ? { servername: remote.tlsServername } : {}),
372
424
  });
373
425
  connectTimeout(upstream, config);
374
- upstream.setTimeout(config.idleTimeoutMs, () => upstream.destroy(new DesktopBridgeError('bridge_upstream_idle_timeout')));
426
+ upstream.setTimeout(idleTimeoutMs, () => upstream.destroy(new DesktopBridgeError('bridge_upstream_idle_timeout')));
375
427
  req.once('aborted', abort);
376
428
  res.once('close', abort);
377
429
  upstream.once('error', (error) => {
@@ -390,6 +442,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
390
442
  });
391
443
  upstream.once('response', (response) => {
392
444
  const statusCode = response.statusCode || 502;
445
+ if (codexLb)
446
+ rememberCodexLbTurnState(threadId, request.credential.fingerprint, response.headers);
393
447
  if (plainFallbackActive && statusCode < 400 && !official && !encodedBodyRefusals.has(provider.provider_id)) {
394
448
  encodedBodyRefusals.add(provider.provider_id);
395
449
  logHttpRejection({ code: 'bridge_upstream_encoded_body_refused', transport: 'http', ...(req.method === undefined ? {} : { method: req.method }), ...(req.url === undefined ? {} : { url: req.url }), provider_id: provider.provider_id });
@@ -425,7 +479,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
425
479
  return;
426
480
  }
427
481
  if (statusCode >= 400) {
428
- void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, unreadableBody }) => {
482
+ void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, unreadableBody, body: upstreamBody }) => {
429
483
  if (budget.plainFallback && (statusCode === 415
430
484
  || (unreadableBody && !upstreamType && !upstreamCode && (statusCode === 400 || statusCode === 422)))) {
431
485
  responseStarted = true;
@@ -440,10 +494,13 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
440
494
  : 'bridge_upstream_transient_mislabel')));
441
495
  return;
442
496
  }
443
- const clientStatus = transientMislabel ? 503 : statusCode;
444
- const body = buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transientMislabel);
497
+ const verbatim = codexLb && upstreamBody !== null;
498
+ const clientStatus = verbatim ? statusCode : transientMislabel ? 503 : statusCode;
499
+ const body = verbatim
500
+ ? scrubUpstreamErrorBody(upstreamBody, request.credential.value)
501
+ : buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transientMislabel);
445
502
  logHttpRejection({
446
- code: transientMislabel
503
+ code: transientMislabel && !verbatim
447
504
  ? `bridge_upstream_status_${statusCode}_translated_503`
448
505
  : upstreamCode ? `bridge_upstream_status_${statusCode}:${upstreamCode}` : `bridge_upstream_status_${statusCode}`,
449
506
  transport: 'http',
@@ -457,10 +514,10 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
457
514
  const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
458
515
  responseHeaders['content-length'] = String(body.length);
459
516
  delete responseHeaders['transfer-encoding'];
460
- if (transientMislabel)
461
- responseHeaders['retry-after'] = String(responseHeaders['retry-after'] || '10');
462
- if (statusCode === 429 && !responseHeaders['retry-after'])
463
- responseHeaders['retry-after'] = '10';
517
+ delete responseHeaders['content-encoding'];
518
+ if (verbatim && upstreamCode === MODEL_CAPACITY_ERROR_CODE && responseHeaders['retry-after'] === undefined) {
519
+ responseHeaders['retry-after'] = String(MODEL_CAPACITY_RETRY_AFTER_SECONDS);
520
+ }
464
521
  res.writeHead(clientStatus, responseHeaders);
465
522
  res.end(body, () => finish());
466
523
  }).catch(finish);
@@ -638,6 +695,10 @@ export async function requestProviderCompactionSummary(config, input, body, onAc
638
695
  }
639
696
  }
640
697
  }
698
+ function singleRetryAfter(value) {
699
+ const text = String(Array.isArray(value) ? value[0] ?? '' : value ?? '').trim();
700
+ return /^[A-Za-z0-9 ,:.-]{1,64}$/.test(text) ? text : null;
701
+ }
641
702
  function upstreamErrorIds(body) {
642
703
  try {
643
704
  const parsed = JSON.parse(body.toString('utf8'));
@@ -683,9 +744,10 @@ async function answerCompactionWithProviderSummary(req, res, config, input) {
683
744
  const transient = isTransientUpstreamFailure(call.status, upstreamType, upstreamCode);
684
745
  const clientStatus = transient ? 503 : call.status;
685
746
  const errorBody = buildRedactedUpstreamErrorBody(call.status, upstreamCode, upstreamType, transient);
747
+ const retryAfter = singleRetryAfter(call.headers['retry-after']);
686
748
  res.writeHead(clientStatus, {
687
749
  'content-type': 'application/json', 'cache-control': 'no-store', 'content-length': String(errorBody.length),
688
- ...(transient || call.status === 429 ? { 'retry-after': String(call.headers['retry-after'] || '10') } : {}),
750
+ ...(retryAfter ? { 'retry-after': retryAfter } : {}),
689
751
  });
690
752
  res.end(errorBody);
691
753
  return;
@@ -0,0 +1,79 @@
1
+ import { DesktopBridgeError } from './types.js';
2
+ import { isNativeCompactionItem } from './compaction-adapter.js';
3
+ export const PROVIDER_HANDOFF_NATIVE_COMPACTION_REFUSED = 'bridge_thread_handoff_after_native_compaction';
4
+ const ROUTE_TARGET_NAMES = {
5
+ openai: 'your ChatGPT account',
6
+ 'codex-lb': 'Codex-LB',
7
+ openrouter: 'OpenRouter',
8
+ };
9
+ function isObject(value) {
10
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+ function targetName(id) {
13
+ return ROUTE_TARGET_NAMES[id] ?? id;
14
+ }
15
+ export class ProviderHandoffRefusedError extends DesktopBridgeError {
16
+ move;
17
+ constructor(move) {
18
+ super(PROVIDER_HANDOFF_NATIVE_COMPACTION_REFUSED);
19
+ this.move = move;
20
+ }
21
+ userMessage() {
22
+ return `This thread was compacted on ${targetName(this.move.from)}, and that compaction can only be read there. `
23
+ + `Switch back to the model you were using, or start a new thread to continue on ${targetName(this.move.to)}.`;
24
+ }
25
+ }
26
+ export function carriesNativeCompaction(payload) {
27
+ return Boolean(payload && Array.isArray(payload.input) && payload.input.some(isNativeCompactionItem));
28
+ }
29
+ export function assertProviderHandoffAllowed(payload, move) {
30
+ if (!move || move.from === move.to || !carriesNativeCompaction(payload))
31
+ return;
32
+ throw new ProviderHandoffRefusedError(move);
33
+ }
34
+ export function providerHandoffRefusalBody(error) {
35
+ return JSON.stringify({
36
+ error: { type: 'invalid_request_error', code: 'invalid_prompt', message: error.userMessage(), param: error.code },
37
+ });
38
+ }
39
+ export function providerHandoffRefusedEvent(error, responseId, publicModel) {
40
+ return {
41
+ type: 'response.failed',
42
+ response: {
43
+ id: responseId,
44
+ object: 'response',
45
+ created_at: Math.floor(Date.now() / 1000),
46
+ model: publicModel,
47
+ status: 'failed',
48
+ error: { code: 'invalid_prompt', message: error.userMessage() },
49
+ },
50
+ };
51
+ }
52
+ export function stripProviderBoundItems(payload, moving) {
53
+ if (!payload)
54
+ return false;
55
+ let changed = false;
56
+ if (Array.isArray(payload.input)) {
57
+ const input = [];
58
+ for (const item of payload.input) {
59
+ if (isObject(item) && item.type === 'reasoning') {
60
+ changed = true;
61
+ continue;
62
+ }
63
+ if (isObject(item) && 'encrypted_content' in item && !isNativeCompactionItem(item)) {
64
+ const { encrypted_content: _foreign, ...rest } = item;
65
+ input.push(rest);
66
+ changed = true;
67
+ continue;
68
+ }
69
+ input.push(item);
70
+ }
71
+ if (changed)
72
+ payload.input = input;
73
+ }
74
+ if (moving && payload.previous_response_id !== undefined) {
75
+ delete payload.previous_response_id;
76
+ changed = true;
77
+ }
78
+ return changed;
79
+ }
@@ -1,10 +1,12 @@
1
1
  import net from 'node:net';
2
2
  import tls from 'node:tls';
3
3
  import WebSocket, { WebSocketServer } from 'ws';
4
- import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
4
+ import { BRIDGE_OFFICIAL_ROUTE_ID, BRIDGE_REMOTE_COMPACTION } from '../bridge-contracts.js';
5
5
  import { buildOfficialPassthroughWebSocketHeaders, buildProviderWebSocketHeaders } from './header-policy.js';
6
6
  import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
7
- import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode } from './security.js';
7
+ import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, threadRouteMove } from './security.js';
8
+ import { admitCodexLbTurnState } from './codex-lb-turn-state.js';
9
+ import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusedEvent, stripProviderBoundItems, } from './provider-handoff.js';
8
10
  import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError } from './types.js';
9
11
  import { buildCompactionSummaryRequest, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, ResponsesContinuationChain, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
10
12
  import { requestProviderCompactionSummary } from './http-forward.js';
@@ -45,6 +47,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
45
47
  let upstream = null;
46
48
  let upstreamSocket = null;
47
49
  let bound = null;
50
+ let boundByMove = false;
48
51
  let boundBaseUrl = null;
49
52
  let boundCredentialGeneration = null;
50
53
  let boundCredentialFingerprint = null;
@@ -126,26 +129,44 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
126
129
  }
127
130
  identity = bound ? withLineage(identity, nextIdentity) : nextIdentity;
128
131
  const request = routeRequest(model);
129
- assertBinding(assertDesktopBridgeRouteContext(request, config));
130
- const route = await resolveAndBindDesktopBridgeRouteContext(request, config);
132
+ const planned = assertDesktopBridgeRouteContext(request, config);
133
+ assertProviderHandoffAllowed(message.create, threadRouteMove(request, planned, config));
134
+ assertBinding(planned);
135
+ let move = null;
136
+ const route = await resolveAndBindDesktopBridgeRouteContext(request, config, (resolved) => {
137
+ move = threadRouteMove(request, resolved, config);
138
+ assertProviderHandoffAllowed(message.create, move);
139
+ });
131
140
  if (stopped || client.readyState !== WebSocket.OPEN)
132
141
  return;
133
142
  assertBinding(route);
134
143
  routedModel = route.public_model;
135
144
  if (!bound) {
136
145
  bound = route;
146
+ boundByMove = Boolean(move);
137
147
  await connect(route);
138
148
  }
139
149
  const create = { ...message.create, model: route.upstream_model };
140
- const expanded = expandBridgeCompactionItems(create);
141
- if (route.provider_id !== BRIDGE_OFFICIAL_ROUTE_ID && requestsRemoteCompaction(create)) {
150
+ let rewritten = expandBridgeCompactionItems(create);
151
+ if (route.session_pin?.handoff_from && stripProviderBoundItems(create, Boolean(move)))
152
+ rewritten = true;
153
+ if (BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary' && requestsRemoteCompaction(create)) {
142
154
  message.compaction = create;
143
155
  return;
144
156
  }
145
- if (route.upstream_model !== model || expanded)
157
+ if (route.upstream_model !== model || rewritten)
146
158
  message.data = Buffer.from(JSON.stringify(create));
147
159
  message.routed = create;
148
160
  }
161
+ function refuseMove(message, error) {
162
+ const model = typeof message.create?.model === 'string' ? message.create.model : '';
163
+ logRejection({ code: error.code, transport: 'websocket', ...(req.url ? { url: req.url } : {}) });
164
+ relay(client, Buffer.from(JSON.stringify(providerHandoffRefusedEvent(error, newCompactionResponseId(), model))), false);
165
+ const index = pending.indexOf(message);
166
+ if (index >= 0)
167
+ pending.splice(index, 1);
168
+ pendingBytes -= message.originalBytes;
169
+ }
149
170
  async function answerCompaction(create) {
150
171
  const route = bound;
151
172
  const provider = config.providers[route.provider_id];
@@ -192,6 +213,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
192
213
  }
193
214
  async function connect(route) {
194
215
  const official = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
216
+ const summarizes = BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary';
195
217
  const provider = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : config.providers[route.provider_id];
196
218
  const remote = official ? config.officialRemote : provider?.remote;
197
219
  if (!remote)
@@ -209,6 +231,10 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
209
231
  const target = resolveDesktopBridgeTarget(req.url, remote);
210
232
  const outgoing = official ? buildOfficialPassthroughWebSocketHeaders(req.headers, target.host)
211
233
  : buildProviderWebSocketHeaders(req.headers, { providerId: provider.provider_id, authTransport: provider.auth_transport, credential: credential }, target.host);
234
+ if (boundByMove)
235
+ delete outgoing['x-codex-turn-state'];
236
+ else if (provider?.provider_id === 'codex-lb')
237
+ admitCodexLbTurnState(outgoing, identity.thread_id, credential.fingerprint);
212
238
  const headers = {};
213
239
  for (const [name, value] of Object.entries(outgoing)) {
214
240
  if (name.startsWith('sec-websocket-') || name === 'connection' || name === 'upgrade' || name === 'content-length')
@@ -256,7 +282,8 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
256
282
  frame = advised;
257
283
  logRejection({ code: 'bridge_upstream_model_capacity_retry_advised', transport: 'websocket', provider_id: route.provider_id, public_model: routedModel ?? route.public_model, ...(req.url ? { url: req.url } : {}) });
258
284
  }
259
- chain.received(frame);
285
+ if (summarizes)
286
+ chain.received(frame);
260
287
  try {
261
288
  relay(client, frame, binary);
262
289
  }
@@ -289,24 +316,41 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
289
316
  return;
290
317
  processing = true;
291
318
  try {
292
- if (!bound) {
319
+ while (!bound) {
293
320
  const firstCreate = pending.find((message) => message.create);
294
321
  if (!firstCreate)
295
322
  return;
296
- await resolveCreate(firstCreate);
323
+ try {
324
+ await resolveCreate(firstCreate);
325
+ }
326
+ catch (error) {
327
+ if (!(error instanceof ProviderHandoffRefusedError))
328
+ throw error;
329
+ refuseMove(firstCreate, error);
330
+ continue;
331
+ }
297
332
  firstCreate.create = null;
298
333
  }
299
334
  while (!stopped && client.readyState === WebSocket.OPEN && pending.length) {
300
335
  const message = pending[0];
301
- if (message.create)
302
- await resolveCreate(message);
336
+ if (message.create) {
337
+ try {
338
+ await resolveCreate(message);
339
+ }
340
+ catch (error) {
341
+ if (!(error instanceof ProviderHandoffRefusedError))
342
+ throw error;
343
+ refuseMove(message, error);
344
+ continue;
345
+ }
346
+ }
303
347
  if (stopped || client.readyState !== WebSocket.OPEN)
304
348
  return;
305
349
  if (message.compaction)
306
350
  await answerCompaction(message.compaction);
307
351
  else {
308
352
  relay(upstream, message.data, message.binary);
309
- if (message.routed && bound?.provider_id !== BRIDGE_OFFICIAL_ROUTE_ID)
353
+ if (message.routed && bound && BRIDGE_REMOTE_COMPACTION[bound.provider_id] === 'bridge_summary')
310
354
  chain.sent(message.routed, message.data);
311
355
  }
312
356
  pending.shift();