sneakoscope 10.7.0 → 10.7.2

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 (66) 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/command-manifest-lite.js +1 -1
  5. package/dist/cli/install-helpers-codex-lb-config.js +55 -20
  6. package/dist/commands/bridge.js +14 -1
  7. package/dist/commands/codex.js +5 -1
  8. package/dist/commands/doctor-console.js +2 -0
  9. package/dist/commands/doctor.js +8 -9
  10. package/dist/config/skills-manifest.json +1 -1
  11. package/dist/core/agents/agent-role-config.js +6 -2
  12. package/dist/core/agents/sks-model-pin-hygiene.js +150 -0
  13. package/dist/core/codex/codex-config-guard.js +18 -13
  14. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +7 -1
  15. package/dist/core/codex-app/menubar/constants.js +0 -1
  16. package/dist/core/codex-lb/bridge-contracts.js +5 -0
  17. package/dist/core/codex-lb/combined-catalog/builder.js +4 -2
  18. package/dist/core/codex-lb/combined-catalog/latest-generation.js +7 -34
  19. package/dist/core/codex-lb/combined-catalog/normalize.js +3 -1
  20. package/dist/core/codex-lb/combined-catalog/persistence.js +4 -0
  21. package/dist/core/codex-lb/combined-catalog/storage.js +4 -1
  22. package/dist/core/codex-lb/desktop-bridge/codex-lb-turn-state.js +35 -0
  23. package/dist/core/codex-lb/desktop-bridge/compaction-adapter.js +3 -0
  24. package/dist/core/codex-lb/desktop-bridge/header-policy.js +10 -4
  25. package/dist/core/codex-lb/desktop-bridge/http-forward.js +138 -36
  26. package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
  27. package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +88 -0
  28. package/dist/core/codex-lb/desktop-bridge/provider-request-compat.js +89 -0
  29. package/dist/core/codex-lb/desktop-bridge/rejection-log.js +61 -0
  30. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +108 -16
  31. package/dist/core/codex-lb/desktop-bridge/security.js +101 -55
  32. package/dist/core/codex-lb/desktop-bridge/server.js +15 -2
  33. package/dist/core/codex-lb/desktop-bridge/types.js +3 -0
  34. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +3 -0
  35. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -6
  36. package/dist/core/codex-lb/desktop-bridge-migration.js +14 -2
  37. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +125 -3
  38. package/dist/core/codex-lb/desktop-controller-v3/model-refresh.js +140 -0
  39. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +5 -5
  40. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +14 -11
  41. package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -3
  42. package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
  43. package/dist/core/codex-lb/desktop-service.js +51 -17
  44. package/dist/core/codex-lb/model-refresh-after.js +16 -0
  45. package/dist/core/codex-lb/request-route-resolver.js +1 -1
  46. package/dist/core/codex-runtime/codex-desktop-config-policy.js +0 -1
  47. package/dist/core/commands/basic-cli.js +5 -0
  48. package/dist/core/commands/menubar-command.js +5 -1
  49. package/dist/core/doctor/desktop-bridge-catalog-repair.js +20 -17
  50. package/dist/core/hooks-runtime/jev-spawn-routing.js +1 -5
  51. package/dist/core/hooks-runtime/parent-orchestration-gate.js +6 -8
  52. package/dist/core/hooks-runtime.js +13 -9
  53. package/dist/core/init/skills.js +22 -3
  54. package/dist/core/provider/provider-context.js +4 -1
  55. package/dist/core/search/context.js +2 -2
  56. package/dist/core/subagents/model-tiers.js +19 -3
  57. package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
  58. package/dist/core/update/update-migration-state.js +10 -40
  59. package/dist/core/version.js +1 -1
  60. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +0 -1
  61. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +32 -0
  62. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +15 -5
  63. package/dist/native/sks-menubar/Sources/SidebarItem.swift +0 -2
  64. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +3 -0
  65. package/package.json +2 -2
  66. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
@@ -0,0 +1,88 @@
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)) {
64
+ let next = item;
65
+ if ('encrypted_content' in next && !isNativeCompactionItem(next)) {
66
+ const { encrypted_content: _foreign, ...rest } = next;
67
+ next = rest;
68
+ changed = true;
69
+ }
70
+ if (next.type === 'function_call' && 'encrypted_function_args' in next) {
71
+ const { encrypted_function_args: _foreign, ...rest } = next;
72
+ next = rest;
73
+ changed = true;
74
+ }
75
+ input.push(next);
76
+ continue;
77
+ }
78
+ input.push(item);
79
+ }
80
+ if (changed)
81
+ payload.input = input;
82
+ }
83
+ if (moving && payload.previous_response_id !== undefined) {
84
+ delete payload.previous_response_id;
85
+ changed = true;
86
+ }
87
+ return changed;
88
+ }
@@ -0,0 +1,89 @@
1
+ import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
2
+ import { PACKAGE_VERSION } from '../../version.js';
3
+ import { DESKTOP_BRIDGE_LOG_SCHEMA, REJECTION_LOG_SUMMARY_INTERVAL_MS } from './rejection-log.js';
4
+ export const PROVIDER_INCLUDE_ALLOWLIST = {
5
+ [BRIDGE_OFFICIAL_ROUTE_ID]: null,
6
+ 'codex-lb': [
7
+ 'code_interpreter_call.outputs',
8
+ 'computer_call_output.output.image_url',
9
+ 'file_search_call.results',
10
+ 'message.input_image.image_url',
11
+ 'message.output_text.logprobs',
12
+ 'reasoning.encrypted_content',
13
+ 'web_search_call.action.sources',
14
+ ],
15
+ openrouter: [
16
+ 'code_interpreter_call.outputs',
17
+ 'computer_call_output.output.image_url',
18
+ 'file_search_call.results',
19
+ 'message.input_image.image_url',
20
+ 'reasoning.encrypted_content',
21
+ ],
22
+ };
23
+ export const REQUEST_COMPAT_INCLUDE_VALUE = /^[a-z_.]{1,64}$/;
24
+ export const REQUEST_COMPAT_MAX_EXTRA_INCLUDE = 8;
25
+ export function adaptRequestForRoute(payload, route, extraInclude = []) {
26
+ if (!payload || route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
27
+ return [];
28
+ const changes = [];
29
+ const allowlist = PROVIDER_INCLUDE_ALLOWLIST[route.provider_id];
30
+ if (allowlist && Array.isArray(payload.include)) {
31
+ const allowed = new Set([...allowlist, ...extraInclude]);
32
+ const kept = [];
33
+ for (const value of payload.include) {
34
+ if (typeof value === 'string' && allowed.has(value))
35
+ kept.push(value);
36
+ else
37
+ changes.push({ field: 'include', value: String(value) });
38
+ }
39
+ if (kept.length !== payload.include.length)
40
+ payload.include = kept;
41
+ }
42
+ return changes;
43
+ }
44
+ export function providerExtraInclude(config, route) {
45
+ if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
46
+ return [];
47
+ const values = config.providers[route.provider_id]?.request_compat?.extra_include;
48
+ return Array.isArray(values) ? values.filter((value) => REQUEST_COMPAT_INCLUDE_VALUE.test(value)) : [];
49
+ }
50
+ const adaptationCounts = new WeakMap();
51
+ function loggedValue(value) {
52
+ return /^[A-Za-z0-9_.:-]{1,64}$/.test(value) ? value : '<other>';
53
+ }
54
+ export function logRequestAdaptation(config, route, changes, transport, options = {}) {
55
+ const write = options.write ?? ((line) => process.stdout.write(line));
56
+ const now = options.now?.() ?? Date.now();
57
+ let counts = adaptationCounts.get(config);
58
+ if (!counts) {
59
+ counts = new Map();
60
+ adaptationCounts.set(config, counts);
61
+ }
62
+ for (const change of changes) {
63
+ const value = loggedValue(change.value);
64
+ const key = `${route.provider_id}\0${change.field}\0${value}`;
65
+ const entry = counts.get(key) ?? { count: 0, loggedAtMs: Number.NEGATIVE_INFINITY };
66
+ entry.count += 1;
67
+ counts.set(key, entry);
68
+ if (entry.count > 1 && now - entry.loggedAtMs < REJECTION_LOG_SUMMARY_INTERVAL_MS)
69
+ continue;
70
+ entry.loggedAtMs = now;
71
+ try {
72
+ write(`${JSON.stringify({
73
+ schema: DESKTOP_BRIDGE_LOG_SCHEMA,
74
+ sks_version: PACKAGE_VERSION,
75
+ at: new Date(now).toISOString(),
76
+ secret_fields_redacted: true,
77
+ event: 'sks.desktop_bridge.request_adapted',
78
+ code: 'bridge_request_include_dropped',
79
+ transport,
80
+ provider_id: route.provider_id,
81
+ field: change.field,
82
+ value,
83
+ count: entry.count,
84
+ })}\n`);
85
+ }
86
+ catch {
87
+ }
88
+ }
89
+ }
@@ -2,6 +2,58 @@ import { PACKAGE_VERSION } from '../../version.js';
2
2
  export const DESKTOP_BRIDGE_LOG_SCHEMA = 'sks.desktop-bridge-log.v2';
3
3
  export const REJECTION_LOG_BURST = 5;
4
4
  export const REJECTION_LOG_SUMMARY_INTERVAL_MS = 60_000;
5
+ const SHAPE_TOKEN = /^[A-Za-z0-9_.[\]-]{1,64}$/;
6
+ const MAX_SHAPE_KEYS = 64;
7
+ const MAX_SHAPE_INCLUDE = 16;
8
+ const MAX_SHAPE_INPUT_TYPES = 32;
9
+ function shapeToken(value) {
10
+ return typeof value === 'string' && SHAPE_TOKEN.test(value) ? value : '<other>';
11
+ }
12
+ export function desktopBridgeRequestShape(payload) {
13
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
14
+ return null;
15
+ const request = payload;
16
+ const inputTypes = {};
17
+ if (Array.isArray(request.input)) {
18
+ for (const item of request.input) {
19
+ const type = item && typeof item === 'object' && !Array.isArray(item) ? shapeToken(item.type) : '<other>';
20
+ if (inputTypes[type] === undefined && Object.keys(inputTypes).length >= MAX_SHAPE_INPUT_TYPES)
21
+ continue;
22
+ inputTypes[type] = (inputTypes[type] ?? 0) + 1;
23
+ }
24
+ }
25
+ else if (typeof request.input === 'string')
26
+ inputTypes['<text>'] = 1;
27
+ return {
28
+ keys: Object.keys(request).map(shapeToken).sort().slice(0, MAX_SHAPE_KEYS),
29
+ include: Array.isArray(request.include) ? request.include.slice(0, MAX_SHAPE_INCLUDE).map(shapeToken) : null,
30
+ input_types: inputTypes,
31
+ };
32
+ }
33
+ function safeRequestShape(shape) {
34
+ if (!shape || !Array.isArray(shape.keys))
35
+ return null;
36
+ const inputTypes = {};
37
+ for (const [type, count] of Object.entries(shape.input_types ?? {}).slice(0, MAX_SHAPE_INPUT_TYPES)) {
38
+ if (Number.isSafeInteger(count) && count > 0)
39
+ inputTypes[shapeToken(type)] = count;
40
+ }
41
+ return {
42
+ keys: shape.keys.slice(0, MAX_SHAPE_KEYS).map(shapeToken),
43
+ include: Array.isArray(shape.include) ? shape.include.slice(0, MAX_SHAPE_INCLUDE).map(shapeToken) : null,
44
+ input_types: inputTypes,
45
+ };
46
+ }
47
+ function safeUpstreamId(value) {
48
+ const text = String(value ?? '').trim();
49
+ return /^[A-Za-z0-9_.:-]{1,64}$/.test(text) ? text : null;
50
+ }
51
+ export function safeUpstreamParam(value) {
52
+ if (typeof value !== 'string')
53
+ return null;
54
+ const text = value.slice(0, 64);
55
+ return /^[A-Za-z0-9_.[\]-]+$/.test(text) ? text : null;
56
+ }
5
57
  function safeCatalogId(value) {
6
58
  const text = String(value ?? '').replace(/[\r\n\0]/g, '').trim().slice(0, 128);
7
59
  return text.length > 0 ? text : null;
@@ -69,6 +121,10 @@ export function createDesktopBridgeRejectionLogger(options = {}) {
69
121
  const method = safeMethod(event.method);
70
122
  const providerId = safeCatalogId(event.provider_id);
71
123
  const publicModel = safeCatalogId(event.public_model);
124
+ const upstreamCode = safeUpstreamId(event.upstream_code);
125
+ const upstreamType = safeUpstreamId(event.upstream_type);
126
+ const upstreamParam = safeUpstreamParam(event.upstream_param);
127
+ const requestShape = safeRequestShape(event.request_shape);
72
128
  emit({
73
129
  event: 'sks.desktop_bridge.rejected',
74
130
  code,
@@ -81,6 +137,11 @@ export function createDesktopBridgeRejectionLogger(options = {}) {
81
137
  ...(Number.isSafeInteger(event.max_request_body_bytes) && event.max_request_body_bytes > 0 ? { max_request_body_bytes: event.max_request_body_bytes } : {}),
82
138
  ...(Number.isSafeInteger(event.request_body_bytes) && event.request_body_bytes >= 0 ? { request_body_bytes: event.request_body_bytes } : {}),
83
139
  ...(event.request_body_stage === 'encoded' || event.request_body_stage === 'decoded' ? { request_body_stage: event.request_body_stage } : {}),
140
+ ...(Number.isInteger(event.upstream_status) && event.upstream_status >= 100 && event.upstream_status <= 599 ? { upstream_status: event.upstream_status } : {}),
141
+ ...(upstreamCode ? { upstream_code: upstreamCode } : {}),
142
+ ...(upstreamType ? { upstream_type: upstreamType } : {}),
143
+ ...(upstreamParam ? { upstream_param: upstreamParam } : {}),
144
+ ...(requestShape ? { request_shape: requestShape } : {}),
84
145
  });
85
146
  };
86
147
  }
@@ -1,14 +1,18 @@
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
- import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
7
- import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode } from './security.js';
6
+ import { createDesktopBridgeRejectionLogger, desktopBridgeRequestShape } from './rejection-log.js';
7
+ import { adaptRequestForRoute, logRequestAdaptation, providerExtraInclude } from './provider-request-compat.js';
8
+ import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, threadRouteMove } from './security.js';
9
+ import { admitCodexLbTurnState } from './codex-lb-turn-state.js';
10
+ import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusedEvent, stripProviderBoundItems, } from './provider-handoff.js';
8
11
  import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError } from './types.js';
9
- import { buildCompactionSummaryRequest, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, ResponsesContinuationChain, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
12
+ import { buildCompactionSummaryRequest, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, ResponsesContinuationChain, safeUpstreamErrorId, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
10
13
  import { requestProviderCompactionSummary } from './http-forward.js';
11
14
  import { modelCapacityAdvisedFrame } from './capacity-retry-advice.js';
15
+ import { desktopBridgeModelsEtag, frameWithModelsEtag } from './models-endpoint.js';
12
16
  const MAX_PENDING_MESSAGES = 256;
13
17
  const CLOSE_GRACE_MS = 1_000;
14
18
  const INITIAL_CREATE_TIMEOUT_MS = 60 * 60_000;
@@ -20,6 +24,25 @@ export function isResponsesWebSocketRequest(req) {
20
24
  function bytes(data) {
21
25
  return Buffer.isBuffer(data) ? data : Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
22
26
  }
27
+ function isRecord(value) {
28
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
29
+ }
30
+ function upstreamFailureEvent(frame) {
31
+ if (!frame.includes('"response.failed"') && !frame.includes('"type":"error"') && !frame.includes('"type": "error"'))
32
+ return null;
33
+ let event;
34
+ try {
35
+ event = JSON.parse(frame.toString('utf8'));
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ if (!isRecord(event) || (event.type !== 'error' && event.type !== 'response.failed'))
41
+ return null;
42
+ const error = event.type === 'error' ? event.error : isRecord(event.response) ? event.response.error : null;
43
+ const record = isRecord(error) ? error : {};
44
+ return { status: typeof event.status === 'number' ? event.status : undefined, code: record.code, type: record.type, param: record.param };
45
+ }
23
46
  function withLineage(identity, next) {
24
47
  return {
25
48
  ...identity,
@@ -32,6 +55,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
32
55
  const upgradeIdentity = resolveCodexSessionIdentity(req.headers);
33
56
  const maxBytes = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
34
57
  const server = new WebSocketServer({ noServer: true, clientTracking: false, perMessageDeflate: false, maxPayload: maxBytes });
58
+ const modelsEtag = config.modelsCatalog ? desktopBridgeModelsEtag(config.modelsCatalog.generation) : null;
35
59
  try {
36
60
  server.handleUpgrade(req, socket, head, (client) => {
37
61
  bridge(client);
@@ -45,10 +69,12 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
45
69
  let upstream = null;
46
70
  let upstreamSocket = null;
47
71
  let bound = null;
72
+ let boundByMove = false;
48
73
  let boundBaseUrl = null;
49
74
  let boundCredentialGeneration = null;
50
75
  let boundCredentialFingerprint = null;
51
76
  let routedModel = null;
77
+ let routedShape = null;
52
78
  let stopped = false;
53
79
  let processing = false;
54
80
  let pendingBytes = 0;
@@ -126,26 +152,51 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
126
152
  }
127
153
  identity = bound ? withLineage(identity, nextIdentity) : nextIdentity;
128
154
  const request = routeRequest(model);
129
- assertBinding(assertDesktopBridgeRouteContext(request, config));
130
- const route = await resolveAndBindDesktopBridgeRouteContext(request, config);
155
+ const planned = assertDesktopBridgeRouteContext(request, config);
156
+ assertProviderHandoffAllowed(message.create, threadRouteMove(request, planned, config));
157
+ assertBinding(planned);
158
+ let move = null;
159
+ const route = await resolveAndBindDesktopBridgeRouteContext(request, config, (resolved) => {
160
+ move = threadRouteMove(request, resolved, config);
161
+ assertProviderHandoffAllowed(message.create, move);
162
+ });
131
163
  if (stopped || client.readyState !== WebSocket.OPEN)
132
164
  return;
133
165
  assertBinding(route);
134
166
  routedModel = route.public_model;
135
167
  if (!bound) {
136
168
  bound = route;
169
+ boundByMove = Boolean(move);
137
170
  await connect(route);
138
171
  }
139
172
  const create = { ...message.create, model: route.upstream_model };
140
- const expanded = expandBridgeCompactionItems(create);
141
- if (route.provider_id !== BRIDGE_OFFICIAL_ROUTE_ID && requestsRemoteCompaction(create)) {
173
+ let rewritten = expandBridgeCompactionItems(create);
174
+ if (route.session_pin?.handoff_from && stripProviderBoundItems(create, Boolean(move)))
175
+ rewritten = true;
176
+ const adapted = adaptRequestForRoute(create, route, providerExtraInclude(config, route));
177
+ if (adapted.length) {
178
+ rewritten = true;
179
+ logRequestAdaptation(config, route, adapted, 'websocket');
180
+ }
181
+ if (BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary' && requestsRemoteCompaction(create)) {
142
182
  message.compaction = create;
143
183
  return;
144
184
  }
145
- if (route.upstream_model !== model || expanded)
185
+ if (route.upstream_model !== model || rewritten) {
146
186
  message.data = Buffer.from(JSON.stringify(create));
187
+ message.binary = false;
188
+ }
147
189
  message.routed = create;
148
190
  }
191
+ function refuseMove(message, error) {
192
+ const model = typeof message.create?.model === 'string' ? message.create.model : '';
193
+ logRejection({ code: error.code, transport: 'websocket', ...(req.url ? { url: req.url } : {}) });
194
+ relay(client, Buffer.from(JSON.stringify(providerHandoffRefusedEvent(error, newCompactionResponseId(), model))), false);
195
+ const index = pending.indexOf(message);
196
+ if (index >= 0)
197
+ pending.splice(index, 1);
198
+ pendingBytes -= message.originalBytes;
199
+ }
149
200
  async function answerCompaction(create) {
150
201
  const route = bound;
151
202
  const provider = config.providers[route.provider_id];
@@ -160,7 +211,9 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
160
211
  send(compactionFailedEvent(responseId, route.public_model, 'bridge_compaction_history_unavailable'));
161
212
  return;
162
213
  }
163
- const body = Buffer.from(JSON.stringify(buildCompactionSummaryRequest({ ...create, input }, route.upstream_model)));
214
+ const summaryRequest = buildCompactionSummaryRequest({ ...create, input }, route.upstream_model);
215
+ adaptRequestForRoute(summaryRequest, route, providerExtraInclude(config, route));
216
+ const body = Buffer.from(JSON.stringify(summaryRequest));
164
217
  let accepted = false;
165
218
  try {
166
219
  const call = await requestProviderCompactionSummary(config, { provider, credential, clientHeaders: req.headers, requestUrl: req.url || '/' }, body, () => {
@@ -192,6 +245,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
192
245
  }
193
246
  async function connect(route) {
194
247
  const official = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
248
+ const summarizes = BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary';
195
249
  const provider = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : config.providers[route.provider_id];
196
250
  const remote = official ? config.officialRemote : provider?.remote;
197
251
  if (!remote)
@@ -209,6 +263,10 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
209
263
  const target = resolveDesktopBridgeTarget(req.url, remote);
210
264
  const outgoing = official ? buildOfficialPassthroughWebSocketHeaders(req.headers, target.host)
211
265
  : buildProviderWebSocketHeaders(req.headers, { providerId: provider.provider_id, authTransport: provider.auth_transport, credential: credential }, target.host);
266
+ if (boundByMove)
267
+ delete outgoing['x-codex-turn-state'];
268
+ else if (provider?.provider_id === 'codex-lb')
269
+ admitCodexLbTurnState(outgoing, identity.thread_id, credential.fingerprint);
212
270
  const headers = {};
213
271
  for (const [name, value] of Object.entries(outgoing)) {
214
272
  if (name.startsWith('sec-websocket-') || name === 'connection' || name === 'upgrade' || name === 'content-length')
@@ -256,7 +314,22 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
256
314
  frame = advised;
257
315
  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
316
  }
259
- chain.received(frame);
317
+ const advertised = modelsEtag && !binary ? frameWithModelsEtag(frame, modelsEtag) : null;
318
+ if (advertised)
319
+ frame = advertised;
320
+ const failure = binary ? null : upstreamFailureEvent(frame);
321
+ if (failure) {
322
+ logRejection({
323
+ code: 'bridge_upstream_request_rejected', transport: 'websocket',
324
+ ...(failure.status === undefined ? {} : { status: failure.status, upstream_status: failure.status }),
325
+ provider_id: route.provider_id, public_model: routedModel ?? route.public_model,
326
+ upstream_code: safeUpstreamErrorId(failure.code), upstream_type: safeUpstreamErrorId(failure.type),
327
+ upstream_param: typeof failure.param === 'string' ? failure.param : null,
328
+ request_shape: routedShape, ...(req.url ? { url: req.url } : {}),
329
+ });
330
+ }
331
+ if (summarizes)
332
+ chain.received(frame);
260
333
  try {
261
334
  relay(client, frame, binary);
262
335
  }
@@ -289,24 +362,43 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
289
362
  return;
290
363
  processing = true;
291
364
  try {
292
- if (!bound) {
365
+ while (!bound) {
293
366
  const firstCreate = pending.find((message) => message.create);
294
367
  if (!firstCreate)
295
368
  return;
296
- await resolveCreate(firstCreate);
369
+ try {
370
+ await resolveCreate(firstCreate);
371
+ }
372
+ catch (error) {
373
+ if (!(error instanceof ProviderHandoffRefusedError))
374
+ throw error;
375
+ refuseMove(firstCreate, error);
376
+ continue;
377
+ }
297
378
  firstCreate.create = null;
298
379
  }
299
380
  while (!stopped && client.readyState === WebSocket.OPEN && pending.length) {
300
381
  const message = pending[0];
301
- if (message.create)
302
- await resolveCreate(message);
382
+ if (message.create) {
383
+ try {
384
+ await resolveCreate(message);
385
+ }
386
+ catch (error) {
387
+ if (!(error instanceof ProviderHandoffRefusedError))
388
+ throw error;
389
+ refuseMove(message, error);
390
+ continue;
391
+ }
392
+ }
303
393
  if (stopped || client.readyState !== WebSocket.OPEN)
304
394
  return;
305
395
  if (message.compaction)
306
396
  await answerCompaction(message.compaction);
307
397
  else {
308
398
  relay(upstream, message.data, message.binary);
309
- if (message.routed && bound?.provider_id !== BRIDGE_OFFICIAL_ROUTE_ID)
399
+ if (message.routed)
400
+ routedShape = desktopBridgeRequestShape(message.routed);
401
+ if (message.routed && bound && BRIDGE_REMOTE_COMPACTION[bound.provider_id] === 'bridge_summary')
310
402
  chain.sent(message.routed, message.data);
311
403
  }
312
404
  pending.shift();