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,140 @@
1
+ import http from 'node:http';
2
+ import path from 'node:path';
3
+ import { readText, writeJsonAtomic } from '../../fsx.js';
4
+ import { safeWriteCodexConfigToml } from '../../codex-runtime/codex-desktop-config-policy.js';
5
+ import { readTopLevelTomlString } from '../../codex-app/codex-model-catalog.js';
6
+ import { codexReadsBridgeModels, hasUserOwnedModelCatalog, setDesktopBridgeManagedModelCatalog } from '../../../cli/install-helpers-codex-lb-config.js';
7
+ import { captureCodexAuthSnapshot } from '../desktop-auth-invariant.js';
8
+ import { desktopBridgeModelsEtag } from '../desktop-bridge/models-endpoint.js';
9
+ export const MODEL_REFRESH_STATE_SCHEMA = 'sks.bridge-model-refresh-state.v1';
10
+ export const MODEL_REFRESH_DEBOUNCE_MS = 60_000;
11
+ export const MODEL_REFRESH_RECOVERY_COMMAND = 'sks bridge models refresh';
12
+ export function modelRefreshStatePath(home) {
13
+ return path.join(path.resolve(home), '.codex', 'sks', 'sks-bridge-model-refresh.json');
14
+ }
15
+ export async function readModelRefreshState(home) {
16
+ try {
17
+ const parsed = JSON.parse(await readText(modelRefreshStatePath(home), ''));
18
+ return parsed?.schema === MODEL_REFRESH_STATE_SCHEMA && typeof parsed.refreshed_at === 'string'
19
+ && typeof parsed.provider_registry_generation === 'string' && typeof parsed.generation === 'string'
20
+ && Number.isSafeInteger(parsed.total)
21
+ ? parsed
22
+ : null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export async function writeModelRefreshState(home, state) {
29
+ await writeJsonAtomic(modelRefreshStatePath(home), { schema: MODEL_REFRESH_STATE_SCHEMA, ...state }, { mode: 0o600 });
30
+ }
31
+ export function refreshedRecently(state, registryGeneration, nowMs) {
32
+ if (!state || state.provider_registry_generation !== registryGeneration)
33
+ return false;
34
+ const age = nowMs - Date.parse(state.refreshed_at);
35
+ return Number.isFinite(age) && age >= 0 && age < MODEL_REFRESH_DEBOUNCE_MS;
36
+ }
37
+ export function listedModelSlugs(catalog) {
38
+ if (!catalog)
39
+ return [];
40
+ return [...new Set(catalog.models
41
+ .filter((model) => !/^hid(?:e|den)$/i.test(String(model.visibility || 'list')))
42
+ .map((model) => String(model.slug || ''))
43
+ .filter(Boolean))].sort();
44
+ }
45
+ export function modelListDiff(before, after) {
46
+ const was = new Set(before);
47
+ const is = new Set(after);
48
+ return { added: after.filter((slug) => !was.has(slug)), removed: before.filter((slug) => !is.has(slug)) };
49
+ }
50
+ export function modelRefreshLine(input) {
51
+ const line = `Model list refreshed: ${input.total} models (+${input.added} −${input.removed}) generation ${input.generation.slice(0, 8)}`;
52
+ return input.restartCodex ? `${line}. Restart Codex to see the new model list.` : line;
53
+ }
54
+ export async function bridgeServesModels(baseUrl, catalog, options = {}) {
55
+ const expected = desktopBridgeModelsEtag(catalog.generation);
56
+ let failure = 'bridge_models_self_check_unreachable';
57
+ for (let attempt = 0; attempt < (options.attempts ?? 3); attempt += 1) {
58
+ if (attempt > 0)
59
+ await new Promise((resolve) => setTimeout(resolve, 500));
60
+ failure = await new Promise((resolve) => {
61
+ let url;
62
+ try {
63
+ url = new URL(`${baseUrl.replace(/\/+$/, '')}/models`);
64
+ }
65
+ catch {
66
+ resolve('bridge_models_self_check_url_invalid');
67
+ return;
68
+ }
69
+ if (url.protocol !== 'http:' || !['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)) {
70
+ resolve('bridge_models_self_check_not_loopback');
71
+ return;
72
+ }
73
+ const request = http.get(url, { timeout: options.timeoutMs ?? 3_000 }, (response) => {
74
+ const chunks = [];
75
+ response.on('data', (chunk) => chunks.push(chunk));
76
+ response.once('end', () => {
77
+ if (response.statusCode !== 200) {
78
+ resolve(`bridge_models_self_check_status_${response.statusCode}`);
79
+ return;
80
+ }
81
+ if (response.headers.etag !== expected) {
82
+ resolve('bridge_models_self_check_generation_mismatch');
83
+ return;
84
+ }
85
+ try {
86
+ const models = JSON.parse(Buffer.concat(chunks).toString('utf8')).models;
87
+ resolve(Array.isArray(models) && models.length === catalog.models.length ? null : 'bridge_models_self_check_rows_mismatch');
88
+ }
89
+ catch {
90
+ resolve('bridge_models_self_check_body_invalid');
91
+ }
92
+ });
93
+ response.once('error', () => resolve('bridge_models_self_check_unreachable'));
94
+ });
95
+ request.once('timeout', () => request.destroy());
96
+ request.once('error', () => resolve('bridge_models_self_check_unreachable'));
97
+ });
98
+ if (failure === null)
99
+ return null;
100
+ }
101
+ return failure;
102
+ }
103
+ export async function alignCodexModelSource(input) {
104
+ const current = await readText(input.configPath, '');
105
+ if (hasUserOwnedModelCatalog(current)) {
106
+ return { source: 'user_model_catalog_json', changed: false, warnings: ['codex_model_list_pinned_by_user_model_catalog_json'] };
107
+ }
108
+ const baseUrl = readTopLevelTomlString(current, 'openai_base_url');
109
+ if (!baseUrl || (!codexReadsBridgeModels(current) && !readTopLevelTomlString(current, 'model_catalog_json'))) {
110
+ return { source: 'not_managed', changed: false, warnings: [] };
111
+ }
112
+ const auth = await captureCodexAuthSnapshot({ home: input.home, authPath: input.authPath }).catch(() => null);
113
+ const warnings = [];
114
+ let wantBridge = auth?.mode === 'chatgpt_oauth';
115
+ if (wantBridge) {
116
+ const failure = await input.selfCheck(baseUrl, input.catalog);
117
+ if (failure) {
118
+ warnings.push(failure);
119
+ wantBridge = false;
120
+ }
121
+ }
122
+ if (!wantBridge && !input.catalogPath) {
123
+ return { source: codexReadsBridgeModels(current) ? 'bridge_models' : 'model_catalog_json', changed: false, warnings: [...warnings, 'codex_model_catalog_path_unavailable'] };
124
+ }
125
+ const next = setDesktopBridgeManagedModelCatalog(current, wantBridge ? null : input.catalogPath);
126
+ const source = wantBridge ? 'bridge_models' : 'model_catalog_json';
127
+ if (next === current)
128
+ return { source, changed: false, warnings };
129
+ if (!input.allowConfigWrites) {
130
+ return {
131
+ source: codexReadsBridgeModels(current) ? 'bridge_models' : 'model_catalog_json',
132
+ changed: false,
133
+ warnings: [...warnings, `codex_model_source_change_deferred:${source}`]
134
+ };
135
+ }
136
+ const write = await safeWriteCodexConfigToml(input.configPath, current, next, 'desktop-bridge-model-source', { verifyUnchangedBeforeWrite: true });
137
+ if (!write.ok)
138
+ return { source: codexReadsBridgeModels(current) ? 'bridge_models' : 'model_catalog_json', changed: false, warnings: [...warnings, `codex_model_source_write_refused:${write.status}`] };
139
+ return { source, changed: true, warnings };
140
+ }
@@ -9,7 +9,7 @@ import { readActiveCombinedBridgeCatalog } from '../combined-catalog.js';
9
9
  import { MAX_SELECTED_OPENROUTER_MODELS, bridgeAvailableModelsPath, readBridgeModelSelectionState, writeBridgeModelSelection } from '../combined-catalog/model-selection.js';
10
10
  import { ensureUserReadOnlyListRole } from '../../subagents/read-only-list-role.js';
11
11
  import { canonicalizeBridgeModelId } from '../route-index.js';
12
- import { syncCatalogInternal } from './catalog.js';
12
+ import { syncAsRefresh } from './catalog.js';
13
13
  import { hasOpenRouterRoute, openRouterOnlyStoreLocation } from './openrouter-only-catalog.js';
14
14
  import { applyOpenRouterOnlyMainModel, openRouterOnlyMainModelTarget, restoreOpenRouterOnlyMainModel } from './openrouter-only-main-model.js';
15
15
  import { openRouterOnlyProviderBlocker, openRouterOnlyStatusFromCore } from './openrouter-only-status.js';
@@ -116,10 +116,10 @@ async function enableOpenRouterOnly(noRestart, options) {
116
116
  if (entries.length === 0) {
117
117
  return finish('openrouter-only.set', false, ['openrouter_only_no_openrouter_models'], {}, options);
118
118
  }
119
- const sync = await syncCatalogInternal({
119
+ const sync = await syncAsRefresh({
120
120
  ...options,
121
121
  settings: { ...(options.settings || {}), auth_priority_enabled: false }
122
- }, {
122
+ }, 'openrouter-only.set', {
123
123
  openRouterOnly: { ...before, enabled: true, subagent_models: entries },
124
124
  onActivated: async () => {
125
125
  await requireRoutableEntry(core.paths, { ...before, enabled: true, subagent_models: entries });
@@ -204,7 +204,7 @@ export async function turnOffOpenRouterOnly(options, behavior = {}) {
204
204
  return { ok: true, changed: false, catalog_sync: null, main_model: null, blockers: [] };
205
205
  }
206
206
  const off = { ...before, enabled: false };
207
- const sync = (commit) => syncCatalogInternal(options, {
207
+ const sync = (commit) => syncAsRefresh(options, 'openrouter-only.off', {
208
208
  ...(behavior.restartService === undefined ? {} : { restartService: behavior.restartService }),
209
209
  openRouterOnly: off,
210
210
  ...(commit ? { onActivated: () => writeOpenRouterOnlyState({ enabled: false }, location) } : {})
@@ -319,7 +319,7 @@ export async function setSubagentModels(raw, noRestart, options, expectedProfile
319
319
  }, options);
320
320
  }
321
321
  const exposedBefore = await exposedModelIds(paths);
322
- const sync = await syncCatalogInternal(options, {
322
+ const sync = await syncAsRefresh(options, 'subagent-models.set', {
323
323
  openRouterOnly: { ...before, subagent_models: entries },
324
324
  onActivated: async () => {
325
325
  if (entries.length > 0)
@@ -1,6 +1,3 @@
1
- import { bindCodexClientProvider } from '../../../cli/install-helpers-codex-lb-config.js';
2
- import { safeWriteCodexConfigToml } from '../../codex-runtime/codex-desktop-config-policy.js';
3
- import { readText } from '../../fsx.js';
4
1
  import { testOpenRouterConnection } from '../../providers/openrouter/openrouter-account.js';
5
2
  import { openRouterSecretPaths } from '../../providers/openrouter/openrouter-secret-store.js';
6
3
  import { codexLbEnvPath, codexLbMetadataPath, readCodexLbModelCatalog } from '../codex-lb-env.js';
@@ -8,6 +5,14 @@ import { configureProviderCredential, recordProviderCredentialValidation, remove
8
5
  import { configureBridgeProviderProfile, resolveBridgeProviderRegistry, setBridgeProviderEnabled } from '../provider-registry.js';
9
6
  import { commandResult, activeProviderIds, controllerEnv, controllerPaths, nowIso, persistRuntimeSettings, providerCode, quiesceRunningBridge, resolveRawCredentials, stringArray, timeoutMs } from './shared.js';
10
7
  import { desktopBridgeStatusV3, loadCore } from './status.js';
8
+ import { refreshModelCatalog } from './catalog.js';
9
+ async function refreshAfterProviderChange(reason, options) {
10
+ const core = await loadCore(options);
11
+ if (!core.activeCatalog.ok || !core.policy)
12
+ return null;
13
+ const { sync: _sync, ...summary } = await refreshModelCatalog(options, { reason, force: true, allowConfigWrites: true });
14
+ return summary;
15
+ }
11
16
  export async function configureProvider(request, options) {
12
17
  const paths = controllerPaths(options);
13
18
  const restartDeferred = await quiesceRunningBridge(await loadCore(options), options);
@@ -37,12 +42,14 @@ export async function configureProvider(request, options) {
37
42
  if (core.activeCatalog.ok && core.policy) {
38
43
  await persistRuntimeSettings({ ...core, registry }, options, { restartService: false });
39
44
  }
45
+ const modelRefresh = restartDeferred ? null : await refreshAfterProviderChange('provider.configure', options);
40
46
  const status = await desktopBridgeStatusV3(options);
41
47
  const blockers = restartDeferred ? ['desktop_bridge_restart_deferred_until_provider_validation'] : [];
42
48
  return commandResult('provider.configure', true, status, {
43
49
  configuration: configured,
44
50
  restart_deferred: restartDeferred,
45
51
  recovery_action: restartDeferred ? `bridge provider validate ${request.provider_id}` : null,
52
+ model_refresh: modelRefresh,
46
53
  }, blockers, options);
47
54
  }
48
55
  export async function validateProvider(providerId, options) {
@@ -85,8 +92,9 @@ export async function validateProvider(providerId, options) {
85
92
  });
86
93
  }
87
94
  }
95
+ const modelRefresh = ok ? await refreshAfterProviderChange('provider.validate', options) : null;
88
96
  const status = await desktopBridgeStatusV3(options);
89
- return commandResult('provider.validate', true, status, { provider_id: providerId, validated: ok, validation: publicValidationResult(result) }, ok ? [] : blockers, options);
97
+ return commandResult('provider.validate', true, status, { provider_id: providerId, validated: ok, validation: publicValidationResult(result), model_refresh: modelRefresh }, ok ? [] : blockers, options);
90
98
  }
91
99
  export async function setProviderState(providerId, enabled, operation, options) {
92
100
  const paths = controllerPaths(options);
@@ -111,14 +119,9 @@ export async function setProviderState(providerId, enabled, operation, options)
111
119
  });
112
120
  }
113
121
  }
114
- if (providerId === 'codex-lb') {
115
- const current = await readText(paths.configPath, '');
116
- const next = bindCodexClientProvider(current, enabled ? 'codex-lb' : 'openai');
117
- if (next !== current)
118
- await safeWriteCodexConfigToml(paths.configPath, current, next, 'codex-lb-client-provider');
119
- }
122
+ const modelRefresh = await refreshAfterProviderChange(operation, options);
120
123
  const status = await desktopBridgeStatusV3(options);
121
- return commandResult(operation, true, status, { provider_id: providerId, enabled }, [], options);
124
+ return commandResult(operation, true, status, { provider_id: providerId, enabled, model_refresh: modelRefresh }, [], options);
122
125
  }
123
126
  export async function removeCredential(providerId, options) {
124
127
  const paths = controllerPaths(options);
@@ -1,4 +1,4 @@
1
- import { DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, DESKTOP_BRIDGE_MANAGED_MARKER, DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER } from '../../../cli/install-helpers-codex-lb-config.js';
1
+ import { DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, DESKTOP_BRIDGE_MANAGED_MARKER } from '../../../cli/install-helpers-codex-lb-config.js';
2
2
  import { readActiveCombinedBridgeCatalog } from '../combined-catalog.js';
3
3
  import { desktopBridgeServiceStatus, desktopBridgeServicePaths, readDesktopBridgeServiceSettings } from '../desktop-service.js';
4
4
  import { captureCodexAuthSnapshot } from '../desktop-auth-invariant.js';
@@ -276,8 +276,7 @@ function isManagedConfig(config) {
276
276
  const top = String(config || '').split(/\n\s*\[/)[0] || '';
277
277
  return [
278
278
  DESKTOP_BRIDGE_MANAGED_MARKER,
279
- DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER,
280
- DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER
279
+ DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER
281
280
  ].every((marker) => top.split(/\r?\n/).some((line) => line.trim() === marker))
282
281
  && /^\s*model_provider\s*=\s*"openai"\s*(?:#.*)?$/m.test(top);
283
282
  }
@@ -1,4 +1,4 @@
1
- import { listSelectableModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
1
+ import { listSelectableModels, refreshModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
2
2
  import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, setOfficialModelsMode, setAuthPriority, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
3
3
  import { configureProvider, removeCredential, setProviderState, validateProvider } from './desktop-controller-v3/provider-commands.js';
4
4
  import { listSubagentModels, openRouterOnlyStatusCommand, setOpenRouterOnly, setSubagentModels } from './desktop-controller-v3/openrouter-only-commands.js';
@@ -55,6 +55,8 @@ export async function executeDesktopBridgeCommandV3(request, options = {}) {
55
55
  return listSelectableModels(options);
56
56
  if (request.operation === 'models.select')
57
57
  return selectExposedModels(request.public_ids, options);
58
+ if (request.operation === 'models.refresh')
59
+ return refreshModels(request, options);
58
60
  if (request.operation === 'catalog.sync')
59
61
  return syncCatalog(options);
60
62
  if (request.operation === 'catalog.status') {
@@ -13,6 +13,8 @@ import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, sha256Stable
13
13
  import { captureCodexAuthSnapshot } from './desktop-auth-invariant.js';
14
14
  import { applyOfficialModelPassthrough, bridgeRoutePolicyPath, writeBridgeRoutingPolicy } from './provider-route-policy.js';
15
15
  import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale } from './desktop-bridge/state.js';
16
+ import { REQUEST_COMPAT_INCLUDE_VALUE, REQUEST_COMPAT_MAX_EXTRA_INCLUDE } from './desktop-bridge/provider-request-compat.js';
17
+ import { bridgeRouteIndexPath, combinedBridgeCatalogPath, readActiveCombinedBridgeCatalog } from './combined-catalog.js';
16
18
  import { PACKAGE_VERSION } from '../version.js';
17
19
  import { canonicalChildModelId, readOpenRouterOnlyStateSync } from '../subagents/child-model-allowlist.js';
18
20
  import { BRIDGE_OFFICIAL_ROUTE_ID } from './bridge-contracts.js';
@@ -133,7 +135,7 @@ export async function resolveDesktopBridgeActivationSettings(options = {}) {
133
135
  const merged = { ...(persisted || {}), ...(options.settings || {}) };
134
136
  const host = merged.listen_host || DEFAULT_DESKTOP_BRIDGE_HOST;
135
137
  const port = merged.listen_port ?? await (options.selectAvailablePort || selectAvailableDesktopBridgePort)(host);
136
- return defaultDesktopBridgeServiceSettings({ ...merged, ...(options.providerRegistry ? { provider_registry: options.providerRegistry } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}), client_capability_sha256: sha256(clientCapability), listen_host: host, listen_port: port });
138
+ return defaultDesktopBridgeServiceSettings({ ...merged, ...(options.providerRegistry ? { provider_registry: withPersistedRequestCompat(options.providerRegistry, persisted?.provider_registry) } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}), client_capability_sha256: sha256(clientCapability), listen_host: host, listen_port: port });
137
139
  }
138
140
  export async function readDesktopBridgeServiceSettings(file) {
139
141
  let stat;
@@ -297,7 +299,8 @@ function normalizeEndpointForBinding(value) {
297
299
  export async function resolveDesktopBridgeRuntimeConfig(options = {}) {
298
300
  const home = options.home || options.env?.HOME || process.env.HOME || os.homedir();
299
301
  const paths = overridePaths(desktopBridgeServicePaths(home), options);
300
- let settings = defaultDesktopBridgeServiceSettings({ ...((await readDesktopBridgeServiceSettings(paths.settings_path)) || {}), ...(options.settings || {}), ...(options.providerRegistry ? { provider_registry: options.providerRegistry } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}) });
302
+ const persisted = await readDesktopBridgeServiceSettings(paths.settings_path);
303
+ let settings = defaultDesktopBridgeServiceSettings({ ...(persisted || {}), ...(options.settings || {}), ...(options.providerRegistry ? { provider_registry: withPersistedRequestCompat(options.providerRegistry, persisted?.provider_registry) } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}) });
301
304
  const clientCapability = options.clientCapability
302
305
  ? validateClientCapability(options.clientCapability)
303
306
  : await readDesktopBridgeClientCapability(paths.client_capability_path);
@@ -313,23 +316,17 @@ export async function resolveDesktopBridgeRuntimeConfig(options = {}) {
313
316
  if (!officialOnly && !providerRouteIds.some((id) => credentials.registry.providers[id].credential_state === 'ready'))
314
317
  throw new Error('desktop_bridge_provider_credentials_unavailable');
315
318
  const persistProviderSessionPins = options.persistProviderSessionPins
316
- || ((pins) => persistDesktopBridgeSessionPins(paths.settings_path, settings, pins));
319
+ || ((pins) => persistDesktopBridgeSessionPins(paths.settings_path, pins));
317
320
  const openRouterOnly = resolveDesktopBridgeOpenRouterOnly({ home, ...(options.env ? { env: options.env } : {}) });
318
321
  const config = { providerRegistry: settings.provider_registry, routePolicy: settings.route_policy, providerSessionPins: settings.provider_session_pins, ...(options.resolveRequestRoute ? { resolveRequestRoute: options.resolveRequestRoute } : {}), persistProviderSessionPins, resolveProviderCredential: credentials.resolver, clientCapabilitySha256: settings.client_capability_sha256, listenHost: settings.listen_host, listenPort: settings.listen_port, allowedPathPrefixes: DESKTOP_BRIDGE_ALLOWED_PATH_PREFIXES, allowedOrigins: settings.allowed_origins, connectTimeoutMs: settings.connect_timeout_ms, idleTimeoutMs: settings.idle_timeout_ms, officialPassthrough: settings.official_passthrough.enabled ? { baseUrl: settings.official_passthrough.base_url } : null, ...(openRouterOnly ? { openRouterOnly } : {}) };
319
322
  const primary = credentials.sources['codex-lb'] || credentials.sources.openrouter;
320
323
  return { config, settings, loaded_env: credentials.loaded, credential_source: primary || null, credential_sources: credentials.sources, paths };
321
324
  }
322
- async function persistDesktopBridgeSessionPins(file, expected, pins) {
325
+ async function persistDesktopBridgeSessionPins(file, pins) {
323
326
  await withFileLock(settingsLockOptions(file), async () => {
324
327
  const current = await readDesktopBridgeServiceSettings(file);
325
- if (!current
326
- || current.listen_host !== expected.listen_host
327
- || current.listen_port !== expected.listen_port
328
- || current.provider_registry.generation !== expected.provider_registry.generation
329
- || current.route_policy.policy_generation !== expected.route_policy.policy_generation
330
- || current.route_policy.catalog_generation !== expected.route_policy.catalog_generation) {
331
- throw new DesktopBridgeError('session_pin_route_unavailable');
332
- }
328
+ if (!current)
329
+ return;
333
330
  await writeDesktopBridgeServiceSettingsUnlocked(file, {
334
331
  ...current,
335
332
  provider_session_pins: [...pins],
@@ -557,12 +554,20 @@ async function autoApplyOfficialModelsAtServe(runtime, home, options) {
557
554
  runtime.config.routePolicy = converged;
558
555
  process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.official_models_auto_applied', at: new Date().toISOString(), sks_version: PACKAGE_VERSION, mode, policy_generation: converged.policy_generation, secret_fields_redacted: true })}\n`);
559
556
  }
557
+ async function activeModelsCatalog(home) {
558
+ const codexHome = path.join(path.resolve(home), '.codex');
559
+ const active = await readActiveCombinedBridgeCatalog(combinedBridgeCatalogPath(codexHome), bridgeRouteIndexPath(codexHome)).catch(() => null);
560
+ if (!active?.ok || active.catalog.models.length === 0)
561
+ return null;
562
+ return { generation: active.catalog.generation, models: active.catalog.models };
563
+ }
560
564
  export async function serveDesktopBridge(options = {}) {
561
565
  let handle = null;
562
566
  try {
563
567
  const runtime = await resolveDesktopBridgeRuntimeConfig(options);
564
568
  const serveHome = options.home || options.env?.HOME || process.env.HOME || os.homedir();
565
569
  await autoApplyOfficialModelsAtServe(runtime, serveHome, options);
570
+ runtime.config.modelsCatalog = await activeModelsCatalog(serveHome);
566
571
  const supervised = desktopBridgeIsSupervised();
567
572
  const skewMarkerPath = path.join(path.dirname(runtime.paths.state_path), 'desktop-bridge-skew-restart.json');
568
573
  const prepared = await preflightDesktopBridge(runtime.config);
@@ -701,8 +706,9 @@ function validateProviderSessionPins(value) {
701
706
  const pin = value;
702
707
  const allowed = new Set([
703
708
  'thread_id', 'provider_id', 'public_model', 'upstream_model',
704
- 'catalog_generation', 'route_policy_generation', 'created_at'
709
+ 'catalog_generation', 'route_policy_generation', 'created_at', 'handoff_from'
705
710
  ]);
711
+ const target = (value) => value === 'codex-lb' || value === 'openrouter' || value === BRIDGE_OFFICIAL_ROUTE_ID;
706
712
  const threadId = typeof pin.thread_id === 'string' ? pin.thread_id.trim() : '';
707
713
  if (Object.keys(pin).some((key) => !allowed.has(key))
708
714
  || !threadId
@@ -710,7 +716,8 @@ function validateProviderSessionPins(value) {
710
716
  || threadId.length > 256
711
717
  || !/^[A-Za-z0-9._:/-]+$/.test(threadId)
712
718
  || seen.has(threadId)
713
- || (pin.provider_id !== 'codex-lb' && pin.provider_id !== 'openrouter')
719
+ || !target(pin.provider_id)
720
+ || (pin.handoff_from !== undefined && (!target(pin.handoff_from) || pin.handoff_from === pin.provider_id))
714
721
  || canonicalizeBridgeModelId(pin.public_model) !== pin.public_model
715
722
  || normalizeBridgeUpstreamModelId(pin.upstream_model) !== pin.upstream_model
716
723
  || typeof pin.catalog_generation !== 'string'
@@ -729,7 +736,8 @@ function validateProviderSessionPins(value) {
729
736
  upstream_model: pin.upstream_model,
730
737
  catalog_generation: pin.catalog_generation,
731
738
  route_policy_generation: pin.route_policy_generation,
732
- created_at: pin.created_at
739
+ created_at: pin.created_at,
740
+ ...(pin.handoff_from === undefined ? {} : { handoff_from: pin.handoff_from })
733
741
  };
734
742
  });
735
743
  }
@@ -781,11 +789,37 @@ function normalizeProviderRegistrySnapshot(value) {
781
789
  const providers = Object.fromEntries(['codex-lb', 'openrouter'].map((providerId) => {
782
790
  const provider = registry.providers[providerId];
783
791
  const sourceCatalogGeneration = provider.source_catalog_generation ?? provider.catalog_generation ?? null;
784
- const { catalog_generation: _legacyCatalogGeneration, ...rest } = provider;
785
- return [providerId, { ...rest, source_catalog_generation: sourceCatalogGeneration }];
792
+ const { catalog_generation: _legacyCatalogGeneration, request_compat: requestCompat, ...rest } = provider;
793
+ const normalized = { ...rest, source_catalog_generation: sourceCatalogGeneration };
794
+ if (requestCompat !== undefined)
795
+ normalized.request_compat = validateRequestCompat(requestCompat);
796
+ return [providerId, normalized];
786
797
  }));
787
798
  return { ...registry, providers };
788
799
  }
800
+ function validateRequestCompat(value) {
801
+ if (!value || typeof value !== 'object' || Array.isArray(value))
802
+ throw new Error('desktop_bridge_settings_request_compat_invalid');
803
+ const row = value;
804
+ if (Object.keys(row).some((key) => key !== 'extra_include'))
805
+ throw new Error('desktop_bridge_settings_request_compat_invalid');
806
+ const values = row.extra_include ?? [];
807
+ if (!Array.isArray(values) || values.length > REQUEST_COMPAT_MAX_EXTRA_INCLUDE
808
+ || values.some((entry) => typeof entry !== 'string' || !REQUEST_COMPAT_INCLUDE_VALUE.test(entry))) {
809
+ throw new Error('desktop_bridge_settings_request_compat_invalid');
810
+ }
811
+ return { extra_include: [...new Set(values)] };
812
+ }
813
+ function withPersistedRequestCompat(registry, persisted) {
814
+ let providers = null;
815
+ for (const id of ['codex-lb', 'openrouter']) {
816
+ const compat = persisted?.providers?.[id]?.request_compat;
817
+ if (!compat || registry.providers[id].request_compat)
818
+ continue;
819
+ providers = { ...(providers ?? registry.providers), [id]: { ...registry.providers[id], request_compat: compat } };
820
+ }
821
+ return providers ? { ...registry, generation: sha256Stable(providers), providers } : registry;
822
+ }
789
823
  function overridePaths(base, options) { return { settings_path: options.settingsPath || base.settings_path, state_path: options.statePath || base.state_path, client_capability_path: options.clientCapabilityPath || base.client_capability_path, launch_agent_path: options.launchAgentPath || base.launch_agent_path, stdout_log_path: options.stdoutLogPath || base.stdout_log_path, stderr_log_path: options.stderrLogPath || base.stderr_log_path }; }
790
824
  function launchDomain(uid = typeof process.getuid === 'function' ? process.getuid() : 0) { return `gui/${uid}`; }
791
825
  function launchService(uid) { return `${launchDomain(uid)}/${DESKTOP_BRIDGE_LAUNCHD_LABEL}`; }
@@ -0,0 +1,16 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { exists } from '../fsx.js';
4
+ import { desktopBridgeServicePaths } from './desktop-service.js';
5
+ export async function refreshModelListAfter(reason) {
6
+ if (process.platform !== 'darwin')
7
+ return null;
8
+ if (process.env.NODE_TEST_CONTEXT !== undefined || process.env.SKS_TEST_ISOLATION === '1' || process.env.SKS_RELEASE_UPGRADE_SMOKE === '1')
9
+ return null;
10
+ const home = path.resolve(process.env.HOME || os.homedir());
11
+ if (!(await exists(desktopBridgeServicePaths(home).settings_path)))
12
+ return null;
13
+ const { executeDesktopBridgeCommandV3 } = await import('./desktop-controller-v3.js');
14
+ const command = await executeDesktopBridgeCommandV3({ operation: 'models.refresh', reason, force: false, allow_config_writes: true }, { home, env: process.env }).catch(() => null);
15
+ return command?.result?.model_refresh ?? null;
16
+ }
@@ -23,7 +23,7 @@ export function resolveBridgeRequestRoute(request, policy, options) {
23
23
  return blocked(base, 'catalog_route_index_stale');
24
24
  }
25
25
  const threadPin = request.thread_id ? findSessionPin(options.session_pins, request.thread_id) : null;
26
- const pin = threadPin && canonicalizeBridgeModelId(threadPin.public_model) === model ? threadPin : null;
26
+ const pin = threadPin && threadPin.provider_id !== 'openai' && canonicalizeBridgeModelId(threadPin.public_model) === model ? threadPin : null;
27
27
  const pinAffinity = pin ? sessionPinAffinity(pin, policy, options.route_index, options.registry) : null;
28
28
  if (pinAffinity === 'unprovable')
29
29
  return blocked(base, 'session_pin_route_unavailable');
@@ -197,7 +197,6 @@ export async function safeWriteCodexConfigToml(configPath, current, next, tag =
197
197
  configPath,
198
198
  before: String(current || ''),
199
199
  cause: tag,
200
- removeTopLevelModeLocks: true,
201
200
  ...(opts.preserveFastUiKeys === undefined ? {} : { preserveFastUiKeys: opts.preserveFastUiKeys }),
202
201
  ...(opts.verifyUnchangedBeforeWrite === undefined ? {} : { verifyUnchangedBeforeWrite: opts.verifyUnchangedBeforeWrite }),
203
202
  ...(opts.expectedBeforeExists === undefined ? {} : { expectedBeforeExists: opts.expectedBeforeExists }),
@@ -330,6 +330,9 @@ export async function setupCommand(args = []) {
330
330
  ...[...(codexHooks?.activation?.blockers || []), ...(codexHooks?.project?.blockers || [])].map((blocker) => `codex_hooks:${blocker}`)
331
331
  ]
332
332
  };
333
+ const modelRefresh = result.ok ? await (await import('../codex-lb/model-refresh-after.js')).refreshModelListAfter('setup') : null;
334
+ if (modelRefresh)
335
+ result.model_refresh = modelRefresh;
333
336
  if (!result.ok)
334
337
  process.exitCode = 1;
335
338
  if (flag(args, '--json')) {
@@ -337,6 +340,8 @@ export async function setupCommand(args = []) {
337
340
  return result;
338
341
  }
339
342
  console.log(`${result.ok ? 'Setup complete' : 'Setup blocked'}: ${root}`);
343
+ if (typeof modelRefresh?.line === 'string')
344
+ console.log(modelRefresh.line);
340
345
  console.log(`Install scope: ${installScope}`);
341
346
  console.log(`Codex CLI: ${cliTools.codex.status}${cliTools.codex.version ? ` ${cliTools.codex.version}` : ''}`);
342
347
  console.log(`SKS Codex hooks: ${codexHooks?.active ? 'active for every Codex project' : installScope === 'project' ? 'this project only' : 'not active (run: sks doctor --fix)'}`);
@@ -31,7 +31,9 @@ export async function menubarCommand(subcommand = 'status', args = []) {
31
31
  return result;
32
32
  }
33
33
  if (action === 'install') {
34
- const result = await installSksMenuBar({ root, ...(home ? { home } : {}), apply: true, launch: !flag(args, '--no-launch'), quiet: flag(args, '--json') });
34
+ const installed = await installSksMenuBar({ root, ...(home ? { home } : {}), apply: true, launch: !flag(args, '--no-launch'), quiet: flag(args, '--json') });
35
+ const modelRefresh = await (await import('../codex-lb/model-refresh-after.js')).refreshModelListAfter('menubar-install');
36
+ const result = modelRefresh ? { ...installed, model_refresh: modelRefresh } : installed;
35
37
  if (flag(args, '--json')) {
36
38
  printJson(result);
37
39
  if (!result.ok)
@@ -46,6 +48,8 @@ export async function menubarCommand(subcommand = 'status', args = []) {
46
48
  console.log(`SKS menu bar install: ${result.status}`);
47
49
  for (const actionLine of result.actions)
48
50
  console.log(`- ${actionLine}`);
51
+ if (typeof modelRefresh?.line === 'string')
52
+ console.log(modelRefresh.line);
49
53
  for (const warning of result.warnings)
50
54
  console.log(`warning: ${warning}`);
51
55
  for (const blocker of result.blockers)
@@ -101,10 +101,8 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
101
101
  fix: input.fix
102
102
  });
103
103
  const status = await statusImpl({ home: bridgeHome, env: process.env });
104
- const blockers = (status?.readiness?.blockers || []).map(String);
105
- const stale = blockers.filter((blocker) => blocker.endsWith('_catalog_stale'));
106
104
  const reverify = { fix: input.fix, bridgeHome, restarted: restarted.restarted };
107
- if (!status?.management?.managed || stale.length === 0) {
105
+ if (!status?.management?.managed) {
108
106
  return reverifyTransportIfDegraded({
109
107
  ...base,
110
108
  ok: restarted.blockers.length === 0,
@@ -114,28 +112,33 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
114
112
  }, reverify, statusImpl, executeImpl);
115
113
  }
116
114
  const attempts = [];
117
- let syncedAndVerified = false;
115
+ let refresh = null;
116
+ let stale = [];
118
117
  for (let attempt = 1; attempt <= CATALOG_REPAIR_MAX_ATTEMPTS; attempt += 1) {
119
- const sync = await executeImpl({ operation: 'catalog.sync' }, { home: bridgeHome, env: process.env });
120
- const commandOk = sync?.ok === true && sync?.execution?.ok === true;
118
+ const command = await executeImpl({
119
+ operation: 'models.refresh',
120
+ reason: input.reason || (input.fix ? 'doctor-fix' : 'doctor'),
121
+ force: false,
122
+ allow_config_writes: input.fix
123
+ }, { home: bridgeHome, env: process.env });
124
+ refresh = command?.result?.model_refresh && typeof command.result.model_refresh === 'object' ? command.result.model_refresh : null;
121
125
  const after = await statusImpl({ home: bridgeHome, env: process.env });
122
- const stillStale = (after?.readiness?.blockers || [])
123
- .map(String).filter((blocker) => blocker.endsWith('_catalog_stale'));
124
- if (commandOk && stillStale.length === 0) {
125
- syncedAndVerified = true;
126
+ stale = (after?.readiness?.blockers || []).map(String).filter((blocker) => blocker.endsWith('_catalog_stale'));
127
+ if (refresh?.ok === true && stale.length === 0)
126
128
  break;
127
- }
128
- attempts.push(`catalog_sync_attempt_${attempt}:${commandOk ? `still_stale:${stillStale.join(',')}` : (sync?.execution?.blockers || ['failed']).map(String).join(',')}`);
129
+ attempts.push(`model_refresh_attempt_${attempt}:${refresh?.ok === true ? `still_stale:${stale.join(',')}` : (refresh?.blockers || command?.execution?.blockers || ['failed']).map(String).join(',')}`);
129
130
  }
131
+ const refreshed = refresh?.ok === true;
130
132
  return reverifyTransportIfDegraded({
131
133
  ...base,
132
- ok: syncedAndVerified && restarted.blockers.length === 0,
133
- repaired: syncedAndVerified || restarted.restarted,
134
- warnings: [...restarted.warnings, ...(syncedAndVerified ? attempts : [])],
134
+ ok: stale.length === 0 && restarted.blockers.length === 0,
135
+ repaired: (refreshed && refresh?.skipped !== true) || restarted.restarted,
136
+ warnings: [...restarted.warnings, ...(refresh?.warnings || []).map(String), ...(stale.length === 0 ? attempts : [])],
135
137
  blockers: [
136
- ...(syncedAndVerified ? [] : ['desktop_bridge_catalog_still_stale_after_repair', ...attempts]),
138
+ ...(stale.length === 0 ? [] : ['desktop_bridge_catalog_still_stale_after_repair', ...attempts]),
137
139
  ...restarted.blockers
138
- ]
140
+ ],
141
+ model_refresh: refresh
139
142
  }, reverify, statusImpl, executeImpl);
140
143
  }
141
144
  catch (error) {
@@ -75,7 +75,7 @@ async function openRouterOnlySpawnRouting(root, parentState, input, allowlist) {
75
75
  const requestedEffort = String(input.reasoning_effort || '') || null;
76
76
  const agent = String(input.agent_type || input.agentType || '').trim() || null;
77
77
  const task = spawnTask(input, { items: true });
78
- const choice = task && !parentState?.jev_execution_plan
78
+ const choice = task
79
79
  ? await chooseChildModel({ root, task, role: agent, requestedModel: requested, requestedEffort, state, profile: childModelListProfile(allowlist) })
80
80
  .catch(() => fallbackChildModel(state, requested, 'consult_failed', requestedEffort))
81
81
  : fallbackChildModel(state, requested, 'empty_task', requestedEffort);
@@ -144,10 +144,6 @@ async function tierSpawnRewrite(root, state, input) {
144
144
  return next;
145
145
  }
146
146
  }
147
- if (boundPlan) {
148
- const fallback = roleTierFallback(agent);
149
- return { ...input, model: fallback.model, reasoning_effort: fallback.effort, ...forkTurns };
150
- }
151
147
  const decision = await consultJevTurnModel({ root, prompt: task, roleId: 'spawn' }).catch(() => null);
152
148
  if (!decision?.called)
153
149
  return null;
@@ -328,14 +328,12 @@ export async function evaluateParentOrchestrationGate(input) {
328
328
  const message = ledger.escapes === 0 ? escapeMessage(missionId, intent, next) : null;
329
329
  return { action: 'escape', reason: 'max_blocks_reached', message, jev: null };
330
330
  }
331
- const jev = state?.jev_execution_plan
332
- ? { called: false, choice: null, reason: 'turn_binding_keeps_delegation_baseline' }
333
- : await consultJevToolDelegation({
334
- root,
335
- missionGoal: missionGoal(state),
336
- toolName: intent.toolName,
337
- targets: intent.targets
338
- }).catch(() => ({ called: false, choice: null, reason: 'consult_failed' }));
331
+ const jev = await consultJevToolDelegation({
332
+ root,
333
+ missionGoal: missionGoal(state),
334
+ toolName: intent.toolName,
335
+ targets: intent.targets
336
+ }).catch(() => ({ called: false, choice: null, reason: 'consult_failed' }));
339
337
  const base = {
340
338
  ...ledger,
341
339
  last_tool: intent.toolName,