@bitkyc08/opencodex 2.81.0 → 2.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { sanitizeLogMetadataString } from "../../lib/redact";
|
|
2
|
+
import { isOpenAiOperatedResponsesDestination } from "../../providers/openai-tiers";
|
|
3
|
+
import { normalizedProviderEndpoint, PROVIDER_REGISTRY } from "../../providers/registry";
|
|
4
|
+
import type { OcxProviderConfig } from "../../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* #6675: a custom Responses provider drops replayed plaintext reasoning unless it sets
|
|
8
|
+
* `preserveResponsesReasoningContent`. That default is deliberate (the ChatGPT backend rejects
|
|
9
|
+
* echoed raw reasoning, and forwarding it costs tokens), but it is silent, and the similarly named
|
|
10
|
+
* Chat-wire `preserveReasoningContentModels` does not opt in. Say so once per destination.
|
|
11
|
+
*
|
|
12
|
+
* Only a hand-configured `openai-responses` row with key auth qualifies. Registry presets (matched
|
|
13
|
+
* by endpoint on either OpenAI wire, since a preset may be routed to Responses per model), OAuth and
|
|
14
|
+
* forward rows, Azure, and OpenAI-operated destinations are skipped: their replay contract is
|
|
15
|
+
* curated, so blanking there is a decision rather than a missing setting. The notice carries only
|
|
16
|
+
* the destination host and option names, never request content.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_NOTICED_DESTINATIONS = 64;
|
|
19
|
+
const noticedDestinations = new Set<string>();
|
|
20
|
+
|
|
21
|
+
function destinationHost(baseUrl: unknown): string | undefined {
|
|
22
|
+
if (typeof baseUrl !== "string") return undefined;
|
|
23
|
+
try {
|
|
24
|
+
return sanitizeLogMetadataString(new URL(baseUrl).host, 128);
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Azure OpenAI and Azure AI Foundry serve OpenAI-operated models under per-resource hosts. */
|
|
31
|
+
const AZURE_OPENAI_HOST_SUFFIXES = [".openai.azure.com", ".services.ai.azure.com", ".cognitiveservices.azure.com"];
|
|
32
|
+
|
|
33
|
+
function hostnameOf(url: string): string | undefined {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(url).hostname.toLowerCase();
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A templated preset (`https://{resource}.openai.azure.com/...`) matches by host pattern. */
|
|
42
|
+
function templatedHostMatches(template: string, hostname: string): boolean {
|
|
43
|
+
const host = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i.exec(template)?.[1];
|
|
44
|
+
if (!host || !/\{[^}]*\}/.test(host)) return false;
|
|
45
|
+
const pattern = host.toLowerCase().split(/\{[^}]*\}/).map(part => part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("[^.]+");
|
|
46
|
+
return new RegExp(`^${pattern}$`).test(hostname);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRegistryDestination(baseUrl: string): boolean {
|
|
50
|
+
const endpoint = normalizedProviderEndpoint(baseUrl);
|
|
51
|
+
const hostname = hostnameOf(baseUrl);
|
|
52
|
+
if (hostname && AZURE_OPENAI_HOST_SUFFIXES.some(suffix => hostname.endsWith(suffix))) return true;
|
|
53
|
+
return PROVIDER_REGISTRY.some(entry =>
|
|
54
|
+
normalizedProviderEndpoint(entry.baseUrl) === endpoint
|
|
55
|
+
|| (hostname !== undefined && templatedHostMatches(entry.baseUrl, hostname))
|
|
56
|
+
|| (entry.destinationAliases ?? []).some(alias => normalizedProviderEndpoint(alias.baseUrl) === endpoint));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isCustomResponsesProvider(provider: OcxProviderConfig): boolean {
|
|
60
|
+
if (provider.adapter !== "openai-responses") return false;
|
|
61
|
+
if (provider.authMode !== undefined && provider.authMode !== "key") return false;
|
|
62
|
+
if (typeof provider.baseUrl !== "string" || !provider.baseUrl) return false;
|
|
63
|
+
return !isOpenAiOperatedResponsesDestination(provider) && !isRegistryDestination(provider.baseUrl);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function plaintextReasoningDroppedNotice(host: string): string {
|
|
67
|
+
return `[opencodex] Replayed plaintext reasoning was dropped for the custom Responses provider at ${host}. `
|
|
68
|
+
+ "If that endpoint accepts reasoning_text replay, set \"preserveResponsesReasoningContent\": true on the provider; "
|
|
69
|
+
+ "\"preserveReasoningContentModels\" applies only to the Chat wire.";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function notePlaintextReasoningDropped(
|
|
73
|
+
provider: OcxProviderConfig,
|
|
74
|
+
warn: (message: string) => void = message => console.warn(message),
|
|
75
|
+
): void {
|
|
76
|
+
if (!isCustomResponsesProvider(provider)) return;
|
|
77
|
+
const host = destinationHost(provider.baseUrl);
|
|
78
|
+
if (!host || noticedDestinations.has(host) || noticedDestinations.size >= MAX_NOTICED_DESTINATIONS) return;
|
|
79
|
+
noticedDestinations.add(host);
|
|
80
|
+
warn(plaintextReasoningDroppedNotice(host));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resetPlaintextReasoningNoticesForTests(): void {
|
|
84
|
+
noticedDestinations.clear();
|
|
85
|
+
}
|
|
@@ -78,6 +78,11 @@ export function sanitizeReasoningInputContent(
|
|
|
78
78
|
* placeholder, rather than emitted in the shape the upstream 400s on.
|
|
79
79
|
*/
|
|
80
80
|
requirePlaintextReasoning?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Called once per sanitized body when a reasoning item that carried nonempty plaintext
|
|
83
|
+
* `reasoning_text` was blanked. Observability only; it never changes the body (#6675).
|
|
84
|
+
*/
|
|
85
|
+
onPlaintextReasoningBlanked?: () => void;
|
|
81
86
|
},
|
|
82
87
|
): unknown {
|
|
83
88
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
@@ -85,6 +90,7 @@ export function sanitizeReasoningInputContent(
|
|
|
85
90
|
if (!Array.isArray(raw.input)) return body;
|
|
86
91
|
|
|
87
92
|
let changed = false;
|
|
93
|
+
let blankedPlaintext = false;
|
|
88
94
|
const input = raw.input.map(item => {
|
|
89
95
|
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
90
96
|
const rec = item as Record<string, unknown>;
|
|
@@ -148,11 +154,15 @@ export function sanitizeReasoningInputContent(
|
|
|
148
154
|
// guide merges reasoning items into the adjacent assistant message), so providers flagged
|
|
149
155
|
// `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
|
|
150
156
|
// continuations after tool calls (issue #875 family).
|
|
151
|
-
if (blankContent)
|
|
157
|
+
if (blankContent) {
|
|
158
|
+
if (hasPlaintextReasoningContent(rec.content)) blankedPlaintext = true;
|
|
159
|
+
next.content = [];
|
|
160
|
+
}
|
|
152
161
|
if (missingPlaintextReasoning) next.content = plaintextReasoningBackfill(next);
|
|
153
162
|
return next;
|
|
154
163
|
});
|
|
155
164
|
|
|
165
|
+
if (blankedPlaintext) opts?.onPlaintextReasoningBlanked?.();
|
|
156
166
|
return changed ? { ...raw, input } : body;
|
|
157
167
|
}
|
|
158
168
|
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
|
|
3
3
|
import { isPlainObject } from "./internal";
|
|
4
4
|
import { peekBridgeSearchReplay } from "../../responses/bridge-search-replay-cache";
|
|
5
|
+
import { externalTaskInputResponsesContent } from "../../responses/task-input";
|
|
5
6
|
|
|
6
7
|
const MAX_RESPONSES_CALL_ID_LENGTH = 64;
|
|
7
8
|
|
|
@@ -169,6 +170,16 @@ export function repairUnidentifiedToolOutputItems(body: unknown): unknown {
|
|
|
169
170
|
|| (typeof item.call_id === "string" && item.call_id.length > 0)) {
|
|
170
171
|
return item;
|
|
171
172
|
}
|
|
173
|
+
// #6764: an external task envelope is user input, not an orphaned result. Recognize it
|
|
174
|
+
// before the generic repair so the summarizer and the destination see a plain user turn.
|
|
175
|
+
// Any nonempty string call_id stays a tool result, as on the parsed path: the request schema
|
|
176
|
+
// accepts a whitespace call_id as a function_call_output and strips the envelope fields
|
|
177
|
+
// (src/responses/schema.ts), so both paths must agree that it is not task input.
|
|
178
|
+
const taskInput = externalTaskInputResponsesContent(item);
|
|
179
|
+
if (taskInput) {
|
|
180
|
+
changed = true;
|
|
181
|
+
return { type: "message", role: "user", content: taskInput };
|
|
182
|
+
}
|
|
172
183
|
if (!isRepairableToolOutput(item.output)) return item;
|
|
173
184
|
changed = true;
|
|
174
185
|
return {
|
|
@@ -380,6 +391,15 @@ export function repairOrphanedInputItems(
|
|
|
380
391
|
const isCustomOutput = item.type === "custom_tool_call_output";
|
|
381
392
|
if (isFnOutput || isCustomOutput) {
|
|
382
393
|
flushPendingSyntheticOutputs();
|
|
394
|
+
// Same rule as repairUnidentifiedToolOutputItems: a nonempty string call_id is a tool result
|
|
395
|
+
// on the parsed path (the schema strips the envelope fields), so it is never task input here.
|
|
396
|
+
const hasStringCallId = typeof item.call_id === "string" && item.call_id.length > 0;
|
|
397
|
+
const taskInput = isFnOutput && !hasStringCallId ? externalTaskInputResponsesContent(item) : undefined;
|
|
398
|
+
if (taskInput) {
|
|
399
|
+
changed = true;
|
|
400
|
+
repaired.push({ type: "message", role: "user", content: taskInput });
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
383
403
|
const callId = typeof item.call_id === "string" ? item.call_id : "";
|
|
384
404
|
const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
|
|
385
405
|
const usableOutput = isRepairableToolOutput(item.output);
|
package/src/bridge/errors.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
cyberPolicyErrorType,
|
|
13
13
|
CYBER_POLICY_ERROR_CODE,
|
|
14
14
|
isCyberPolicyCode,
|
|
15
|
+
SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE,
|
|
15
16
|
type OcxErrorPayload,
|
|
16
17
|
} from "../lib/errors";
|
|
17
18
|
|
|
@@ -22,6 +23,10 @@ export function formatErrorResponse(
|
|
|
22
23
|
options?: { code?: string | null; retryAfter?: string | null },
|
|
23
24
|
): Response {
|
|
24
25
|
const error = classifyError(status, type, message);
|
|
26
|
+
if (options?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE) {
|
|
27
|
+
error.type = "server_error";
|
|
28
|
+
error.code = SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
|
|
29
|
+
}
|
|
25
30
|
if (isCyberPolicyCode(options?.code)) {
|
|
26
31
|
error.code = CYBER_POLICY_ERROR_CODE;
|
|
27
32
|
error.type = cyberPolicyErrorType(type);
|
|
@@ -36,7 +41,8 @@ export function formatErrorResponse(
|
|
|
36
41
|
// that would hand the client a 5xx it is configured to retry four times.
|
|
37
42
|
const finalStatus = error.code === CYBER_POLICY_ERROR_CODE
|
|
38
43
|
? 400
|
|
39
|
-
:
|
|
44
|
+
: error.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503
|
|
45
|
+
: isReplayRefusalCode(error.code) ? REPLAY_REFUSED_STATUS : status;
|
|
40
46
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
41
47
|
const retryAfter = options?.retryAfter?.trim();
|
|
42
48
|
if (error.code !== CYBER_POLICY_ERROR_CODE
|
|
@@ -15,7 +15,8 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileS
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { renameAtomicFile } from "../lib/windows-atomic-replace";
|
|
18
|
-
import {
|
|
18
|
+
import { assertNotRealClaudeConfigUnderTest } from "../lib/test-home-guard";
|
|
19
|
+
import { accountsAt200k, entryParts, SAFE_AGENT_MODEL_ID, withSubagentContextMarker } from "./subagent-model";
|
|
19
20
|
import { stripOneMillionMarker } from "./context-windows";
|
|
20
21
|
import { claudeConfigDir } from "./gateway-cache";
|
|
21
22
|
import { DEFAULT_SUBAGENT_MODELS } from "../config";
|
|
@@ -97,7 +98,7 @@ export function buildClaudeAgentDefs(
|
|
|
97
98
|
// Generated defs mark [1m] on the authoritative window only — never the
|
|
98
99
|
// main-session auto-context predicate (a 372K route marked [1m] would be
|
|
99
100
|
// accounted at 1M with no compaction safety net in the subagent).
|
|
100
|
-
const model = withSubagentContextMarker(alias, windows);
|
|
101
|
+
const model = withSubagentContextMarker(alias, windows, accountsAt200k(config));
|
|
101
102
|
const bare = alias.toLowerCase();
|
|
102
103
|
if (coveredModels.has(bare)) return;
|
|
103
104
|
coveredModels.add(bare);
|
|
@@ -130,7 +131,7 @@ export function buildClaudeAgentDefs(
|
|
|
130
131
|
// the next launch sync — documented limit. No resolvable default -> no self def.
|
|
131
132
|
const selfModel = pickerDefaultModel(configDir) ?? (config.claudeCode?.model?.trim() || null);
|
|
132
133
|
if (selfModel) {
|
|
133
|
-
const marked = withSubagentContextMarker(selfModel, windows);
|
|
134
|
+
const marked = withSubagentContextMarker(selfModel, windows, accountsAt200k(config));
|
|
134
135
|
defs.push({
|
|
135
136
|
file: `${OWNED_PREFIX}self.md`,
|
|
136
137
|
name: `${OWNED_PREFIX}self`,
|
|
@@ -203,6 +204,16 @@ function isOwnedFile(path: string): boolean {
|
|
|
203
204
|
* atomic (tmp + rename). Best-effort — returns null on any failure.
|
|
204
205
|
*/
|
|
205
206
|
export function syncClaudeAgentDefs(defs: readonly ClaudeAgentDef[], configDir = claudeConfigDir()): string[] | null {
|
|
207
|
+
// Outside the best-effort catch: under an armed test process a write or prune of the
|
|
208
|
+
// real Claude agents directory must throw, never degrade to "returned null" (#6775).
|
|
209
|
+
// Every path written is judged by where it resolves, including a pre-placed link named like
|
|
210
|
+
// a definition or its temporary file.
|
|
211
|
+
const agentsDir = join(configDir, "agents");
|
|
212
|
+
assertNotRealClaudeConfigUnderTest(
|
|
213
|
+
configDir,
|
|
214
|
+
agentsDir,
|
|
215
|
+
...defs.flatMap(def => [join(agentsDir, def.file), `${join(agentsDir, def.file)}.tmp-${process.pid}`]),
|
|
216
|
+
);
|
|
206
217
|
try {
|
|
207
218
|
const dir = join(configDir, "agents");
|
|
208
219
|
if (defs.length === 0) {
|
|
@@ -15,33 +15,19 @@ import { desktop3pAlias } from "./desktop-3p";
|
|
|
15
15
|
import { nativeOpenAiContextWindow, type CatalogModel, type NativeContextLimitsInput } from "../codex/catalog";
|
|
16
16
|
import { ANTHROPIC_MODEL_CONTEXT_WINDOWS } from "../providers/registry/model-seeds";
|
|
17
17
|
import type { OcxClaudeCodeConfig } from "../types";
|
|
18
|
+
import { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR, ONE_MILLION } from "./long-context";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
export { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR } from "./long-context";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
* The native id each Claude Code tier alias resolves to (2.1.
|
|
23
|
-
* `claude-opus-5-5`, `sonnet` sends `claude-sonnet-5`, `fable` sends `claude-fable-5-1`).
|
|
23
|
+
* The native id each Claude Code tier alias resolves to (2.1.296: `--model opus` sends
|
|
24
|
+
* `claude-opus-5-5`, `sonnet` sends `claude-sonnet-5-5`, `fable` sends `claude-fable-5-1`).
|
|
24
25
|
* Behind a gateway Claude Code accounts an unmarked id at 200k, so an unset tier slot is filled
|
|
25
26
|
* with this id when the map marks it (#5755). A marked alias cannot stand in: Claude Code sends
|
|
26
27
|
* `sonnet[1m]` upstream unresolved. Haiku has no entry; its 200k window never marks.
|
|
27
28
|
*/
|
|
28
|
-
const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5", fable: "claude-fable-5-1" } as const;
|
|
29
|
+
const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5-5", fable: "claude-fable-5-1" } as const;
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
* Auto-context defaults (devlog 260712 020, user-approved).
|
|
32
|
-
*
|
|
33
|
-
* The compact window is the token count at which Claude Code starts compacting, and it is
|
|
34
|
-
* also the floor `shouldMarkOneMillion` uses — a model may only carry the marker if it can
|
|
35
|
-
* host this window. 350,000 was chosen when the widest native row advertised 372,000.
|
|
36
|
-
*
|
|
37
|
-
* It now matches the auto-compaction limit the Codex catalog ships for the same models
|
|
38
|
-
* (`nativeAutoCompactLimit`: 829,800 against the 922,000 native window). Leaving the two
|
|
39
|
-
* apart meant one model compacting at 350k under Claude Code and at 829,800 under Codex.
|
|
40
|
-
* The value stays clear of the measured 922,000 ceiling by ~92k, so compaction still has
|
|
41
|
-
* room to run before the upstream refuses.
|
|
42
|
-
*/
|
|
43
|
-
export const AUTO_COMPACT_WINDOW_DEFAULT = 829_800;
|
|
44
|
-
export const AUTO_CONTEXT_FLOOR = 200_000;
|
|
45
31
|
/** Binary-verified accepted range for CLAUDE_CODE_AUTO_COMPACT_WINDOW (2.1.207: pSo=1e5, yDs=1e6). */
|
|
46
32
|
export const AUTO_COMPACT_WINDOW_MIN = 100_000;
|
|
47
33
|
export const AUTO_COMPACT_WINDOW_MAX = ONE_MILLION;
|
|
@@ -59,14 +45,27 @@ export interface AutoContextMode {
|
|
|
59
45
|
enabled: boolean;
|
|
60
46
|
/** Effective CLAUDE_CODE_AUTO_COMPACT_WINDOW value (tokens). */
|
|
61
47
|
compactWindow: number;
|
|
48
|
+
/** claudeCode.contextAccounting "200k": nothing is marked [1m] automatically, whatever its window. */
|
|
49
|
+
accounting200k?: true;
|
|
62
50
|
}
|
|
63
51
|
|
|
64
52
|
export const AUTO_CONTEXT_OFF: AutoContextMode = { enabled: false, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT };
|
|
65
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Marking mode for surfaces whose runner may not inherit the compaction env (Desktop pickers,
|
|
56
|
+
* discovery, generated subagents). shouldMarkOneMillion under it equals isLongContextWindow
|
|
57
|
+
* (long-context.ts); kept as a mode so the existing marker helpers serve every surface.
|
|
58
|
+
*/
|
|
59
|
+
export const UNPAIRED_AUTO_CONTEXT: AutoContextMode = { enabled: true, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT };
|
|
60
|
+
|
|
61
|
+
/** The "200k" opt-in: no widening, no compact-window injection, no automatic marker. */
|
|
62
|
+
export const ACCOUNTING_200K: AutoContextMode = { enabled: false, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT, accounting200k: true };
|
|
63
|
+
|
|
66
64
|
interface AutoContextConfigSlice {
|
|
67
65
|
autoContext?: boolean;
|
|
68
66
|
autoCompactWindow?: number;
|
|
69
67
|
maxContextTokens?: number;
|
|
68
|
+
contextAccounting?: string;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
function inAutoCompactRange(value: number): boolean {
|
|
@@ -90,6 +89,8 @@ function inAutoCompactRange(value: number): boolean {
|
|
|
90
89
|
* to AUTO_COMPACT_WINDOW_DEFAULT (the management API rejects them; this guards hand-edits).
|
|
91
90
|
*/
|
|
92
91
|
export function resolveAutoContext(claudeCode: AutoContextConfigSlice | undefined, envOverride?: string): AutoContextMode {
|
|
92
|
+
// The 200k opt-in wins over every other lever, a user-exported compact window included.
|
|
93
|
+
if (claudeCode?.contextAccounting === "200k") return ACCOUNTING_200K;
|
|
93
94
|
if (claudeCode?.autoContext === false) return AUTO_CONTEXT_OFF;
|
|
94
95
|
const maxCtx = claudeCode?.maxContextTokens;
|
|
95
96
|
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) return AUTO_CONTEXT_OFF;
|
|
@@ -132,6 +133,7 @@ export function claudeToolSearchEnv(value: boolean | string | undefined): string
|
|
|
132
133
|
*/
|
|
133
134
|
export function shouldMarkOneMillion(window: number | undefined, auto: AutoContextMode): boolean {
|
|
134
135
|
if (typeof window !== "number" || window <= 0) return false;
|
|
136
|
+
if (auto.accounting200k) return false;
|
|
135
137
|
if (window >= ONE_MILLION) return true;
|
|
136
138
|
return auto.enabled && window > AUTO_CONTEXT_FLOOR && window >= auto.compactWindow;
|
|
137
139
|
}
|
|
@@ -248,7 +250,7 @@ export interface ClaudeTierModels {
|
|
|
248
250
|
* value injected into BOTH haiku variables.
|
|
249
251
|
*/
|
|
250
252
|
export function effectiveModelEnv(
|
|
251
|
-
claudeCode: { model?: string; smallFastModel?: string; tierModels?: ClaudeTierModels; autoContext?: boolean; autoCompactWindow?: number; maxContextTokens?: number } | undefined,
|
|
253
|
+
claudeCode: { model?: string; smallFastModel?: string; tierModels?: ClaudeTierModels; autoContext?: boolean; autoCompactWindow?: number; maxContextTokens?: number; contextAccounting?: string } | undefined,
|
|
252
254
|
windows: Record<string, number>,
|
|
253
255
|
autoOverride?: AutoContextMode,
|
|
254
256
|
): Record<string, string> {
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -10,9 +10,10 @@ import {
|
|
|
10
10
|
} from "./desktop-3p-library";
|
|
11
11
|
export { resolveDesktop3pConfigLibraryPath, type Desktop3pConfigLibraryOptions } from "./desktop-3p-library";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
13
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, lstatSync, unlinkSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { atomicWriteFile, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
|
|
16
|
+
import { atomicWriteFileNoFollow } from "../config/atomic-write";
|
|
16
17
|
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
17
18
|
import type { OcxClaudeDesktopProfile } from "../types";
|
|
18
19
|
import {
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../codex/catalog";
|
|
25
26
|
import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
|
|
26
27
|
import { assertDesktop3pModelsValid } from "./desktop-3p-guard";
|
|
28
|
+
import { claudeSurfaceSupportsOneMillion, ONE_MILLION } from "./long-context";
|
|
27
29
|
|
|
28
30
|
export interface Desktop3pModelEntry {
|
|
29
31
|
name: string;
|
|
@@ -55,12 +57,12 @@ export interface Desktop3pRoutedModel {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
|
-
* 1M
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* The genuine-1M threshold real Anthropic rows still need. Every other row follows the
|
|
61
|
+
* long-window rule; the writer, the profile renderer and the dashboard DTO all decide through
|
|
62
|
+
* `claudeSurfaceSupportsOneMillion` (long-context.ts), so the 1M chip cannot disagree with the
|
|
63
|
+
* written config.
|
|
62
64
|
*/
|
|
63
|
-
export const DESKTOP_SUPPORTS_1M_THRESHOLD =
|
|
65
|
+
export const DESKTOP_SUPPORTS_1M_THRESHOLD = ONE_MILLION;
|
|
64
66
|
|
|
65
67
|
/** CLI arg parsing for `ocx claude desktop` mode flags (mutually exclusive). */
|
|
66
68
|
export function parseDesktop3pModeArgs(flags: string[]): { mode: Desktop3pConfigMode } | { error: string } {
|
|
@@ -251,7 +253,7 @@ function collectDesktop3pModels(
|
|
|
251
253
|
for (const { provider, id, contextWindow } of candidates) {
|
|
252
254
|
const route = `${provider}/${id}`;
|
|
253
255
|
const alias = desktop3pAlias(provider, id);
|
|
254
|
-
const supports1m =
|
|
256
|
+
const supports1m = claudeSurfaceSupportsOneMillion(provider, id, contextWindow)
|
|
255
257
|
? { supports1m: true as const }
|
|
256
258
|
: {};
|
|
257
259
|
if (alias === id) {
|
|
@@ -317,16 +319,30 @@ export function buildDesktop3pRegistry(
|
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
/** Generate Claude Desktop 3P model entries from the proxy's available models. */
|
|
322
|
+
export interface Desktop3pModelOptions {
|
|
323
|
+
/**
|
|
324
|
+
* false under claudeCode.contextAccounting "200k": a long-window model stays selectable at 1M
|
|
325
|
+
* (`supports1m`) but no longer defaults to it (`prefer1m` dropped).
|
|
326
|
+
*/
|
|
327
|
+
preferOneMillion?: boolean;
|
|
328
|
+
}
|
|
329
|
+
|
|
320
330
|
export function generateDesktop3pModels(
|
|
321
331
|
nativeSlugs: string[],
|
|
322
332
|
routedModels: Array<Desktop3pRoutedModel>,
|
|
323
333
|
profile?: OcxClaudeDesktopProfile,
|
|
324
334
|
nativeContextCap?: NativeContextLimitsInput,
|
|
335
|
+
options: Desktop3pModelOptions = {},
|
|
325
336
|
): Desktop3pModelEntry[] {
|
|
326
337
|
const { models, registry, realAnthropicIds } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
|
|
327
338
|
desktop3pRegistry = registry;
|
|
328
339
|
desktop3pRealAnthropicIds = realAnthropicIds;
|
|
329
|
-
return models;
|
|
340
|
+
return options.preferOneMillion === false ? models.map(({ prefer1m: _prefer1m, ...model }) => model) : models;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The Desktop model options a config asks for (one place, so writer and export agree). */
|
|
344
|
+
export function desktop3pModelOptions(claudeCode: { contextAccounting?: string } | undefined): Desktop3pModelOptions {
|
|
345
|
+
return { preferOneMillion: claudeCode?.contextAccounting !== "200k" };
|
|
330
346
|
}
|
|
331
347
|
|
|
332
348
|
/** Resolve an alias using the most recently generated Desktop model registry. */
|
|
@@ -382,6 +398,7 @@ export function generateDesktop3pConfig(
|
|
|
382
398
|
mode: Desktop3pConfigMode = "static",
|
|
383
399
|
profile?: OcxClaudeDesktopProfile,
|
|
384
400
|
nativeContextCap?: NativeContextLimitsInput,
|
|
401
|
+
options: Desktop3pModelOptions = {},
|
|
385
402
|
): object {
|
|
386
403
|
const base = {
|
|
387
404
|
inferenceProvider: "gateway",
|
|
@@ -398,7 +415,7 @@ export function generateDesktop3pConfig(
|
|
|
398
415
|
...base,
|
|
399
416
|
modelDiscoveryEnabled: mode === "hybrid",
|
|
400
417
|
inferenceModels: (() => {
|
|
401
|
-
const models = generateDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
|
|
418
|
+
const models = generateDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap, options);
|
|
402
419
|
// Fail loud at the write boundary rather than ship a config Desktop rejects:
|
|
403
420
|
// the output counterpart of the request-path guards.
|
|
404
421
|
assertDesktop3pModelsValid(models);
|
|
@@ -662,6 +679,7 @@ export function writeDesktop3pConfig(
|
|
|
662
679
|
profile?: OcxClaudeDesktopProfile,
|
|
663
680
|
nativeContextCap?: NativeContextLimitsInput,
|
|
664
681
|
lifecycleLockDeps?: ClientLifecycleLockDeps,
|
|
682
|
+
refreshOnly?: { appliedFingerprint: string; admit: () => boolean },
|
|
665
683
|
): { written: boolean; path: string; reason?: string; fingerprint?: string } {
|
|
666
684
|
try {
|
|
667
685
|
return withClientLifecycleSync(() => withConfigMutationLockSync(() => {
|
|
@@ -674,9 +692,29 @@ export function writeDesktop3pConfig(
|
|
|
674
692
|
if (!claudeDesktopIntegrationEnabled(latest.config)) {
|
|
675
693
|
return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desired_state_changed" };
|
|
676
694
|
}
|
|
695
|
+
if (refreshOnly && !desktopRefreshPathsSafe(resolveDesktop3pConfigLibraryPath())) {
|
|
696
|
+
return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_refresh_only_skipped" };
|
|
697
|
+
}
|
|
677
698
|
if (connection.kind === "connected" || inspectRemoteDesktopCleanup().kind !== "absent") {
|
|
678
699
|
return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_remote_store_active" };
|
|
679
700
|
}
|
|
701
|
+
let requiredId: string | undefined;
|
|
702
|
+
if (refreshOnly) {
|
|
703
|
+
const refused = { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_refresh_only_skipped" };
|
|
704
|
+
// Ownership and bytes must still match under L→C; discovery outside these locks is not authority.
|
|
705
|
+
if (!refreshOnly.admit() || latest.config.claudeCode?.desktopProfile?.appliedFingerprint !== refreshOnly.appliedFingerprint) return refused;
|
|
706
|
+
const inspected = inspectDesktop3pConfigLibrary({ appliedFingerprint: refreshOnly.appliedFingerprint });
|
|
707
|
+
if (inspected.kind !== "gateway_ours" || !inspected.appliedId || !inspected.selectedProfilePath) return refused;
|
|
708
|
+
if (!desktopRefreshPathsSafe(inspected.libraryPath, inspected.selectedProfilePath)) return refused;
|
|
709
|
+
const metadata = parseMetadata(join(inspected.libraryPath, "_meta.json"));
|
|
710
|
+
if (!isOwnedDesktopGatewayEntry(metadata.entries.find(entry => entry.id === inspected.appliedId))) return refused;
|
|
711
|
+
try {
|
|
712
|
+
const selected = JSON.parse(readFileSync(inspected.selectedProfilePath, "utf8")) as Record<string, unknown>;
|
|
713
|
+
mode = selected.modelDiscoveryEnabled !== true ? "static"
|
|
714
|
+
: Array.isArray(selected.inferenceModels) ? "hybrid" : "discovery";
|
|
715
|
+
} catch { return refused; }
|
|
716
|
+
requiredId = inspected.appliedId;
|
|
717
|
+
}
|
|
680
718
|
// Claude Desktop runs on this machine, so it dials the unauthenticated loopback listener
|
|
681
719
|
// when one is enabled — on such a hub that is the only credential-free local socket
|
|
682
720
|
// (#4236) — and otherwise the bind address, which answers but demands data-plane
|
|
@@ -700,8 +738,9 @@ export function writeDesktop3pConfig(
|
|
|
700
738
|
);
|
|
701
739
|
}
|
|
702
740
|
return writeDesktop3pConfigWithGenerator(() => (
|
|
703
|
-
generateDesktop3pConfig(destination.origin, nativeSlugs, routedModels, gatewayKey, mode, profile, nativeContextCap
|
|
704
|
-
|
|
741
|
+
generateDesktop3pConfig(destination.origin, nativeSlugs, routedModels, gatewayKey, mode, profile, nativeContextCap,
|
|
742
|
+
desktop3pModelOptions(latest.config.claudeCode))
|
|
743
|
+
), requiredId && refreshOnly ? { requiredId, expectedFingerprint: refreshOnly.appliedFingerprint, admit: refreshOnly.admit } : undefined);
|
|
705
744
|
}), lifecycleLockDeps);
|
|
706
745
|
} catch { return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_lifecycle_busy_or_unsafe" }; }
|
|
707
746
|
}
|
|
@@ -734,17 +773,38 @@ export function writeRemoteDesktop3pConfig(options: {
|
|
|
734
773
|
} catch { return { written: false, path: "", reason: "desktop_lifecycle_busy_or_unsafe" }; }
|
|
735
774
|
}
|
|
736
775
|
|
|
776
|
+
/** Refresh-only writes never follow links, including a dangling backup link. */
|
|
777
|
+
function desktopRefreshPathsSafe(libraryPath: string, configPath?: string): boolean {
|
|
778
|
+
try {
|
|
779
|
+
if (!lstatSync(libraryPath).isDirectory() || !lstatSync(join(libraryPath, "_meta.json")).isFile()) return false;
|
|
780
|
+
if (configPath) {
|
|
781
|
+
if (!lstatSync(configPath).isFile()) return false;
|
|
782
|
+
try { if (lstatSync(`${configPath}.bak`).isSymbolicLink()) return false; }
|
|
783
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") return false; }
|
|
784
|
+
}
|
|
785
|
+
return true;
|
|
786
|
+
} catch { return false; }
|
|
787
|
+
}
|
|
788
|
+
|
|
737
789
|
function writeDesktop3pConfigWithGenerator(
|
|
738
790
|
generate: () => object,
|
|
791
|
+
refreshOnly?: { requiredId: string; expectedFingerprint: string; admit: () => boolean },
|
|
739
792
|
): { written: boolean; path: string; reason?: string; fingerprint?: string } {
|
|
740
793
|
const libraryPath = resolveDesktop3pConfigLibraryPath();
|
|
741
794
|
const metadataPath = join(libraryPath, "_meta.json");
|
|
742
795
|
let configPath = libraryPath;
|
|
743
796
|
|
|
744
797
|
try {
|
|
745
|
-
|
|
798
|
+
if (refreshOnly && !desktopRefreshPathsSafe(libraryPath)) {
|
|
799
|
+
return { written: false, path: libraryPath, reason: "desktop_refresh_only_skipped" };
|
|
800
|
+
}
|
|
801
|
+
if (!refreshOnly) mkdirSync(libraryPath, { recursive: true, mode: 0o700 });
|
|
746
802
|
const metadata = parseMetadata(metadataPath);
|
|
747
803
|
const selected = metadata.entries.find(entry => entry?.id === metadata.appliedId && isOwnedDesktopGatewayEntry(entry));
|
|
804
|
+
// A refresh may reuse only the applied entry; fallback allocation would silently enroll again.
|
|
805
|
+
if (refreshOnly && selected?.id !== refreshOnly.requiredId) {
|
|
806
|
+
return { written: false, path: libraryPath, reason: "desktop_refresh_only_skipped" };
|
|
807
|
+
}
|
|
748
808
|
const existing = selected ?? metadata.entries.find(entry => isOwnedDesktopGatewayEntry(entry) && typeof entry.id === "string");
|
|
749
809
|
const id = existing?.id ?? randomUUID();
|
|
750
810
|
configPath = profilePath(libraryPath, id);
|
|
@@ -757,6 +817,22 @@ function writeDesktop3pConfigWithGenerator(
|
|
|
757
817
|
const preserved = readDesktopProfileForeignKeys(configPath);
|
|
758
818
|
const configJson = JSON.stringify({ ...preserved, ...generated }, null, 2) + "\n";
|
|
759
819
|
const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
|
|
820
|
+
if (refreshOnly) {
|
|
821
|
+
atomicWriteFileNoFollow(configPath, configJson, undefined, {
|
|
822
|
+
validateBeforeRename: () => {
|
|
823
|
+
try {
|
|
824
|
+
if (!refreshOnly.admit() || !desktopRefreshPathsSafe(libraryPath, configPath)) throw new Error();
|
|
825
|
+
const freshMetadata = parseMetadata(metadataPath);
|
|
826
|
+
if (freshMetadata.appliedId !== refreshOnly.requiredId
|
|
827
|
+
|| !isOwnedDesktopGatewayEntry(freshMetadata.entries.find(entry => entry.id === refreshOnly.requiredId))
|
|
828
|
+
|| createHash("sha256").update(readFileSync(configPath)).digest("hex").slice(0, 16) !== refreshOnly.expectedFingerprint) {
|
|
829
|
+
throw new Error();
|
|
830
|
+
}
|
|
831
|
+
} catch { throw new Error("desktop_refresh_only_skipped"); }
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
return { written: true, path: configPath, fingerprint };
|
|
835
|
+
}
|
|
760
836
|
const { backupPath } = atomicReplaceDesktopConfig(configPath, configJson);
|
|
761
837
|
try {
|
|
762
838
|
atomicWriteFile(metadataPath, JSON.stringify({ ...metadata, appliedId: id, entries }, null, 2) + "\n");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Claude Desktop picker mode: the one server-side controller for every picker mutation.
|
|
3
3
|
*
|
|
4
4
|
* While a server runs, enable, disable and Desktop mode transitions run here, serialized by one
|
|
5
|
-
* lock (devlog/
|
|
5
|
+
* lock (devlog/_fin/260924_claude_desktop_picker_mode/030_wp4_picker_activation.md, D11).
|
|
6
6
|
*/
|
|
7
7
|
import type { OcxConfig } from "../types";
|
|
8
8
|
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { routeSupportsOneMillion } from "./long-context";
|
|
2
3
|
import type {
|
|
3
4
|
OcxClaudeDesktopAssignment,
|
|
4
5
|
OcxClaudeDesktopFamily,
|
|
@@ -359,7 +360,7 @@ export function renderDesktopProfile(
|
|
|
359
360
|
name: isRealAnthropicRoute(route) ? assignment.alias : desktopProfileWireAlias(assignment.alias),
|
|
360
361
|
family: assignment.family,
|
|
361
362
|
isFamilyDefault: effectiveDefaults[assignment.family] === route,
|
|
362
|
-
supports1m:
|
|
363
|
+
supports1m: routeSupportsOneMillion(route, model.contextWindow),
|
|
363
364
|
};
|
|
364
365
|
});
|
|
365
366
|
}
|
|
@@ -17,6 +17,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
|
|
20
|
+
import { assertNotRealClaudeConfigUnderTest } from "../lib/test-home-guard";
|
|
20
21
|
import type { OcxConfig } from "../types";
|
|
21
22
|
|
|
22
23
|
export interface GatewayModelRow {
|
|
@@ -46,11 +47,27 @@ export interface GatewayModelTarget {
|
|
|
46
47
|
/** Claude Code config dir (CLAUDE_CONFIG_DIR override honored, like the CLI). */
|
|
47
48
|
export function claudeConfigDir(): string {
|
|
48
49
|
const custom = process.env.CLAUDE_CONFIG_DIR;
|
|
49
|
-
return custom && custom.length > 0 ? custom : join(
|
|
50
|
+
return custom && custom.length > 0 ? custom : join(currentUserHome(), ".claude");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The home Claude Code itself resolves. Claude Code runs on Node, whose `os.homedir()`
|
|
55
|
+
* consults HOME (POSIX) or USERPROFILE (Windows) at call time when it is set; Bun's
|
|
56
|
+
* `os.homedir()` keeps the value it read at startup. In production the two agree. Under
|
|
57
|
+
* the test preload, which rewrites HOME after Bun has started, only this one follows the
|
|
58
|
+
* sandbox — the cached value is the developer's real home (#6775). Exported for tests.
|
|
59
|
+
*/
|
|
60
|
+
export function currentUserHome(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string {
|
|
61
|
+
const fromEnv = platform === "win32" ? env.USERPROFILE : env.HOME;
|
|
62
|
+
return fromEnv !== undefined && fromEnv !== "" ? fromEnv : homedir();
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
/** Write the cache file; returns its path or null (best-effort, never throws). */
|
|
53
66
|
export function writeGatewayModelCache(baseUrl: string, models: readonly GatewayModelRow[], configDir = claudeConfigDir()): string | null {
|
|
67
|
+
// Outside the best-effort catch: an armed test process must not write here, and must
|
|
68
|
+
// fail loudly rather than degrade to "returned null".
|
|
69
|
+
// The file itself too: writeFileSync follows a link at gateway-models.json.
|
|
70
|
+
assertNotRealClaudeConfigUnderTest(configDir, join(configDir, "cache"), join(configDir, "cache", "gateway-models.json"));
|
|
54
71
|
try {
|
|
55
72
|
// Mirror the CLI's usable-id filter so our file matches what it would cache.
|
|
56
73
|
const usable = models.filter(m => /(claude|anthropic)/i.test(m.id));
|