@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
normalizeCursorClaudeId,
|
|
4
4
|
type NormalizedCursorClaudeId,
|
|
5
5
|
} from "./claude-id";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Cursor umbrella catalog — the single source of truth for cursor model
|
|
@@ -599,7 +600,7 @@ export function resolveCursorSelection(
|
|
|
599
600
|
pickedId: string,
|
|
600
601
|
reasoning: string | undefined,
|
|
601
602
|
liveMaxModeIds?: ReadonlySet<string>,
|
|
602
|
-
options: { fast?: boolean } = {},
|
|
603
|
+
options: { fast?: boolean; liveRosterScope?: string } = {},
|
|
603
604
|
): CursorResolvedSelection {
|
|
604
605
|
const parsed = parseCursorVariantId(pickedId);
|
|
605
606
|
if (!parsed.known) {
|
|
@@ -617,7 +618,10 @@ export function resolveCursorSelection(
|
|
|
617
618
|
const requested = parsed.level ?? reasoning;
|
|
618
619
|
const effort = cursorVariantEffort(spec, requested);
|
|
619
620
|
const requestedClaude = normalizeCursorClaudeId(pickedId);
|
|
620
|
-
const
|
|
621
|
+
const scopedClaudeIdentities = options.liveRosterScope
|
|
622
|
+
? liveCursorClaudeWireIdentitiesByScope.get(options.liveRosterScope)
|
|
623
|
+
: liveCursorClaudeWireIdentities;
|
|
624
|
+
const claudeIdentity = scopedClaudeIdentities?.get(parsed.baseId)
|
|
621
625
|
?? (requestedClaude
|
|
622
626
|
? { sourceBaseId: requestedClaude.sourceBaseId, spelling: requestedClaude.spelling }
|
|
623
627
|
: undefined);
|
|
@@ -626,7 +630,9 @@ export function resolveCursorSelection(
|
|
|
626
630
|
? `${capability.wirePrefix}${canonicalId}`
|
|
627
631
|
: canonicalId;
|
|
628
632
|
const ultraRequested = parsed.ultra || reasoning?.toLowerCase() === "ultra";
|
|
629
|
-
const evidence = liveMaxModeIds
|
|
633
|
+
const evidence = liveMaxModeIds
|
|
634
|
+
?? (options.liveRosterScope ? liveCursorMaxModeBasesByScope.get(options.liveRosterScope) : undefined)
|
|
635
|
+
?? liveCursorMaxModeBases;
|
|
630
636
|
const maxModeArmed = capability.maxModeVerified === true || evidence.has(parsed.baseId);
|
|
631
637
|
return { wireId, canonicalId, maxMode: ultraRequested && maxModeArmed, known: true };
|
|
632
638
|
}
|
|
@@ -639,15 +645,36 @@ export function resolveCursorSelection(
|
|
|
639
645
|
*/
|
|
640
646
|
let liveCursorMaxModeBases: ReadonlySet<string> = new Set();
|
|
641
647
|
let liveCursorClaudeWireIdentities: ReadonlyMap<string, CursorLiveClaudeWireIdentity> = new Map();
|
|
648
|
+
const liveCursorMaxModeBasesByScope = new Map<string, ReadonlySet<string>>();
|
|
649
|
+
const liveCursorClaudeWireIdentitiesByScope = new Map<string, ReadonlyMap<string, CursorLiveClaudeWireIdentity>>();
|
|
650
|
+
const liveCursorRosterScopesByProvider = new Map<string, Set<string>>();
|
|
651
|
+
|
|
652
|
+
/** Non-secret key binding live roster evidence to one upstream destination and credential. */
|
|
653
|
+
export function cursorLiveRosterScope(baseUrl: string | undefined, credential: string): string {
|
|
654
|
+
const destination = (baseUrl?.trim().replace(/\/+$/, "") || "https://api2.cursor.sh");
|
|
655
|
+
return createHash("sha256")
|
|
656
|
+
.update("ocx:cursor:live-roster\0")
|
|
657
|
+
.update(destination)
|
|
658
|
+
.update("\0")
|
|
659
|
+
.update(credential)
|
|
660
|
+
.digest("hex");
|
|
661
|
+
}
|
|
642
662
|
|
|
643
|
-
export function recordLiveCursorClaudeModels(liveIds: readonly string[]): void {
|
|
663
|
+
export function recordLiveCursorClaudeModels(liveIds: readonly string[], scope?: { provider: string; key: string }): void {
|
|
644
664
|
const next = new Map<string, CursorLiveClaudeWireIdentity>();
|
|
645
665
|
for (const rawId of liveIds) {
|
|
646
666
|
const n = normalizeCursorClaudeId(rawId.startsWith("cursor-") ? rawId.slice(7) : rawId);
|
|
647
667
|
if (!n || !CURSOR_CAPABILITIES[n.canonicalBaseId]) continue;
|
|
648
668
|
if (!next.has(n.canonicalBaseId)) next.set(n.canonicalBaseId, { sourceBaseId: n.sourceBaseId, spelling: n.spelling });
|
|
649
669
|
}
|
|
650
|
-
|
|
670
|
+
if (scope) {
|
|
671
|
+
liveCursorClaudeWireIdentitiesByScope.set(scope.key, next);
|
|
672
|
+
const scopes = liveCursorRosterScopesByProvider.get(scope.provider) ?? new Set<string>();
|
|
673
|
+
scopes.add(scope.key);
|
|
674
|
+
liveCursorRosterScopesByProvider.set(scope.provider, scopes);
|
|
675
|
+
} else {
|
|
676
|
+
liveCursorClaudeWireIdentities = next;
|
|
677
|
+
}
|
|
651
678
|
}
|
|
652
679
|
|
|
653
680
|
export function liveCursorClaudeWireIdentitiesForTests(): ReadonlyMap<string, CursorLiveClaudeWireIdentity> {
|
|
@@ -656,15 +683,32 @@ export function liveCursorClaudeWireIdentitiesForTests(): ReadonlyMap<string, Cu
|
|
|
656
683
|
|
|
657
684
|
export function resetLiveCursorClaudeWireIdentitiesForTests(): void {
|
|
658
685
|
liveCursorClaudeWireIdentities = new Map();
|
|
686
|
+
liveCursorClaudeWireIdentitiesByScope.clear();
|
|
687
|
+
liveCursorRosterScopesByProvider.clear();
|
|
659
688
|
}
|
|
660
689
|
|
|
661
|
-
export function recordLiveCursorMaxModeModels(liveIds: readonly string[]): void {
|
|
690
|
+
export function recordLiveCursorMaxModeModels(liveIds: readonly string[], scope?: { provider: string; key: string }): void {
|
|
662
691
|
const bases = new Set<string>();
|
|
663
692
|
for (const id of liveIds) {
|
|
664
693
|
const parsed = parseCursorVariantId(id);
|
|
665
694
|
if (parsed.known) bases.add(parsed.baseId);
|
|
666
695
|
}
|
|
667
|
-
|
|
696
|
+
if (scope) liveCursorMaxModeBasesByScope.set(scope.key, bases);
|
|
697
|
+
else liveCursorMaxModeBases = bases;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function clearLiveCursorRosterState(provider?: string): void {
|
|
701
|
+
if (!provider) {
|
|
702
|
+
liveCursorClaudeWireIdentitiesByScope.clear();
|
|
703
|
+
liveCursorMaxModeBasesByScope.clear();
|
|
704
|
+
liveCursorRosterScopesByProvider.clear();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
for (const scope of liveCursorRosterScopesByProvider.get(provider) ?? []) {
|
|
708
|
+
liveCursorClaudeWireIdentitiesByScope.delete(scope);
|
|
709
|
+
liveCursorMaxModeBasesByScope.delete(scope);
|
|
710
|
+
}
|
|
711
|
+
liveCursorRosterScopesByProvider.delete(provider);
|
|
668
712
|
}
|
|
669
713
|
|
|
670
714
|
export function liveCursorMaxModeBasesForTests(): ReadonlySet<string> {
|
|
@@ -42,6 +42,7 @@ export interface CursorCheckpointSnapshot {
|
|
|
42
42
|
coveredMessageCount?: number;
|
|
43
43
|
prefixDigest?: string;
|
|
44
44
|
systemDigest?: string;
|
|
45
|
+
toolSuspended?: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
interface CursorCheckpointStore {
|
|
@@ -229,6 +230,7 @@ export function commitCursorCheckpoint(input: {
|
|
|
229
230
|
coveredMessageCount?: number;
|
|
230
231
|
prefixDigest?: string;
|
|
231
232
|
systemDigest?: string;
|
|
233
|
+
toolSuspended?: boolean;
|
|
232
234
|
}): string | undefined {
|
|
233
235
|
if (!input.conversationId || !input.modelId || input.checkpointBytes.byteLength === 0) return undefined;
|
|
234
236
|
if (input.checkpointBytes.byteLength > CURSOR_CHECKPOINT_MAX_TOTAL_BYTES) return undefined;
|
|
@@ -258,6 +260,7 @@ export function commitCursorCheckpoint(input: {
|
|
|
258
260
|
...(input.coveredMessageCount !== undefined ? { coveredMessageCount: input.coveredMessageCount } : {}),
|
|
259
261
|
...(input.prefixDigest ? { prefixDigest: input.prefixDigest } : {}),
|
|
260
262
|
...(input.systemDigest ? { systemDigest: input.systemDigest } : {}),
|
|
263
|
+
...(input.toolSuspended ? { toolSuspended: true } : {}),
|
|
261
264
|
};
|
|
262
265
|
const blobIds = collectCheckpointBlobIds(input.checkpointBytes);
|
|
263
266
|
if (blobIds === undefined) return undefined;
|
|
@@ -305,15 +305,18 @@ export function isCursorRouterModelId(modelId: string): boolean {
|
|
|
305
305
|
export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: string }>(
|
|
306
306
|
configured: readonly T[],
|
|
307
307
|
liveIds: readonly string[],
|
|
308
|
+
maxModeLiveIds: readonly string[] = [],
|
|
308
309
|
): T[] {
|
|
309
|
-
return configured.filter(model =>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
310
|
+
return configured.filter(model => {
|
|
311
|
+
if (CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(model.id)) return false;
|
|
312
|
+
if (isCursorRouterModelId(model.id)) return true;
|
|
313
|
+
const ultraBase = cursorUltraBaseModelId(model.id);
|
|
314
|
+
return isCursorModelAvailableForAccount(ultraBase ?? model.id, liveIds)
|
|
315
|
+
// Successful discovery is authoritative: synthetic ultra rows additionally require the
|
|
316
|
+
// account-specific Max Mode capability. Discovery failures bypass this filter and retain
|
|
317
|
+
// the static seed under the caller's existing degraded-catalog policy.
|
|
318
|
+
&& (ultraBase === undefined || isCursorModelAvailableForAccount(ultraBase, maxModeLiveIds));
|
|
319
|
+
});
|
|
317
320
|
}
|
|
318
321
|
|
|
319
322
|
/**
|
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
type BackgroundShellTerminationReport,
|
|
88
88
|
} from "./native-exec-shell";
|
|
89
89
|
import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
|
|
90
|
-
import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
90
|
+
import type { CursorStreamHealthClock, CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
91
91
|
import { CursorHttp1BidiConnection } from "./http1-bidi";
|
|
92
92
|
import { isPinnedHttp1 } from "../../lib/upstream-http-version";
|
|
93
93
|
|
|
@@ -107,6 +107,16 @@ const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
|
|
|
107
107
|
* Reset on every decoded frame that is not liveness-only.
|
|
108
108
|
*/
|
|
109
109
|
const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000;
|
|
110
|
+
/**
|
|
111
|
+
* The production T04 clock. The watchdog reads time and schedules its one timer through this
|
|
112
|
+
* object so a test can supply a clock it advances by hand; nothing else about the transport's
|
|
113
|
+
* timers moves with it.
|
|
114
|
+
*/
|
|
115
|
+
const REAL_STREAM_HEALTH_CLOCK: CursorStreamHealthClock = {
|
|
116
|
+
now: () => Date.now(),
|
|
117
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
118
|
+
clearTimeout: timer => clearTimeout(timer),
|
|
119
|
+
};
|
|
110
120
|
/**
|
|
111
121
|
* After `turnEnded` is decoded, the application turn is complete. A server that keeps
|
|
112
122
|
* HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
|
|
@@ -494,6 +504,8 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
494
504
|
private lastInboundFrameAt = 0;
|
|
495
505
|
private lastMeaningfulFrameAt = 0;
|
|
496
506
|
private streamHealthFail?: (error: Error) => void;
|
|
507
|
+
/** Time source for the T04 watchdog only; the globals unless a test injects one. */
|
|
508
|
+
private readonly streamHealthClock: CursorStreamHealthClock;
|
|
497
509
|
private committed = false;
|
|
498
510
|
private expectedClose = false;
|
|
499
511
|
/**
|
|
@@ -542,6 +554,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
542
554
|
// so the transport-level race test can drive it deterministically.
|
|
543
555
|
this.clientToolFinalizeGraceMs = input.clientToolFinalizeGraceMs ?? CLIENT_TOOL_FINALIZE_GRACE_MS;
|
|
544
556
|
this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
|
|
557
|
+
this.streamHealthClock = input.streamHealthClock ?? REAL_STREAM_HEALTH_CLOCK;
|
|
545
558
|
// Desktop (computer-use / record-screen) executors are available even with no MCP servers.
|
|
546
559
|
this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
|
|
547
560
|
this.execContext = {
|
|
@@ -847,7 +860,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
847
860
|
|
|
848
861
|
private clearStreamHealthTimer(): void {
|
|
849
862
|
if (this.streamHealthTimer) {
|
|
850
|
-
clearTimeout(this.streamHealthTimer);
|
|
863
|
+
this.streamHealthClock.clearTimeout(this.streamHealthTimer);
|
|
851
864
|
this.streamHealthTimer = undefined;
|
|
852
865
|
}
|
|
853
866
|
this.streamHealthFail = undefined;
|
|
@@ -858,24 +871,28 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
858
871
|
* failAndClear; the timer owns nothing else. Never armed before the first decoded
|
|
859
872
|
* frame (the first-frame timer covers dial + first response), and disarmed by
|
|
860
873
|
* every settle / expected-close path alongside the other timers.
|
|
874
|
+
*
|
|
875
|
+
* Both clocks and the timer go through `streamHealthClock`, which is the globals in
|
|
876
|
+
* production. The `elapsedMs` diagnostic below deliberately stays on the wall clock,
|
|
877
|
+
* because `turnStartedAt` is stamped there and the pair has to subtract coherently.
|
|
861
878
|
*/
|
|
862
879
|
private armStreamHealthTimer(fail: (error: Error) => void): void {
|
|
863
|
-
if (this.streamHealthTimer) clearTimeout(this.streamHealthTimer);
|
|
880
|
+
if (this.streamHealthTimer) this.streamHealthClock.clearTimeout(this.streamHealthTimer);
|
|
864
881
|
if (this.expectedClose) return;
|
|
865
882
|
this.streamHealthFail = fail;
|
|
866
883
|
const silenceMs = this.input.streamSilenceFailMs ?? CURSOR_STREAM_SILENCE_FAIL_MS;
|
|
867
884
|
const heartbeatOnlyMs = this.input.streamHeartbeatOnlyFailMs ?? CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS;
|
|
868
|
-
const now =
|
|
885
|
+
const now = this.streamHealthClock.now();
|
|
869
886
|
const deadline = Math.min(
|
|
870
887
|
this.lastInboundFrameAt + silenceMs,
|
|
871
888
|
this.lastMeaningfulFrameAt + heartbeatOnlyMs,
|
|
872
889
|
);
|
|
873
|
-
this.streamHealthTimer = setTimeout(() => {
|
|
890
|
+
this.streamHealthTimer = this.streamHealthClock.setTimeout(() => {
|
|
874
891
|
this.streamHealthTimer = undefined;
|
|
875
892
|
const failFn = this.streamHealthFail;
|
|
876
893
|
if (!failFn || this.expectedClose) return;
|
|
877
|
-
const stalledFor =
|
|
878
|
-
const meaningfulStalledFor =
|
|
894
|
+
const stalledFor = this.streamHealthClock.now() - this.lastInboundFrameAt;
|
|
895
|
+
const meaningfulStalledFor = this.streamHealthClock.now() - this.lastMeaningfulFrameAt;
|
|
879
896
|
if (stalledFor < silenceMs && meaningfulStalledFor < heartbeatOnlyMs) {
|
|
880
897
|
// A frame landed between arming and firing — re-arm for the fresh deadline.
|
|
881
898
|
this.armStreamHealthTimer(failFn);
|
|
@@ -906,7 +923,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
906
923
|
* heartbeat-only threshold.
|
|
907
924
|
*/
|
|
908
925
|
private noteInboundFrame(livenessOnly: boolean): void {
|
|
909
|
-
const now =
|
|
926
|
+
const now = this.streamHealthClock.now();
|
|
910
927
|
this.lastInboundFrameAt = now;
|
|
911
928
|
if (!livenessOnly) this.lastMeaningfulFrameAt = now;
|
|
912
929
|
if (this.streamHealthFail) this.armStreamHealthTimer(this.streamHealthFail);
|
|
@@ -1288,7 +1305,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1288
1305
|
const decodedUpdate = decoded.message.case === "interactionUpdate" ? decoded.message.value.message?.case : undefined;
|
|
1289
1306
|
const livenessOnly = decodedUpdate === "heartbeat" || decoded.message.case === "conversationCheckpointUpdate";
|
|
1290
1307
|
if (!this.streamHealthFail) {
|
|
1291
|
-
const now =
|
|
1308
|
+
const now = this.streamHealthClock.now();
|
|
1292
1309
|
this.lastInboundFrameAt = now;
|
|
1293
1310
|
this.lastMeaningfulFrameAt = now;
|
|
1294
1311
|
this.streamHealthFail = failAndClear;
|
|
@@ -741,7 +741,7 @@ function contentText(message: OcxMessage): string {
|
|
|
741
741
|
if (typeof message.content === "string") return message.content;
|
|
742
742
|
return message.content
|
|
743
743
|
.map(part => {
|
|
744
|
-
if (part.type === "text") return part.text;
|
|
744
|
+
if (part.type === "text" || part.type === "document") return part.text;
|
|
745
745
|
if (part.type === "thinking") return part.thinking;
|
|
746
746
|
if (part.type === "image") return undefined;
|
|
747
747
|
return undefined;
|
|
@@ -754,7 +754,7 @@ function contentToText(content: OcxToolResultMessage["content"]): string {
|
|
|
754
754
|
if (typeof content === "string") return content;
|
|
755
755
|
return content
|
|
756
756
|
.map(part => {
|
|
757
|
-
if (part.type === "text") return part.text;
|
|
757
|
+
if (part.type === "text" || part.type === "document") return part.text;
|
|
758
758
|
if (part.type === "image") return CURSOR_VISION_IMAGE_HISTORY_MARKER;
|
|
759
759
|
return undefined;
|
|
760
760
|
})
|
|
@@ -767,7 +767,7 @@ function historyContentText(message: OcxMessage): string {
|
|
|
767
767
|
if (message.role === "toolResult" || typeof message.content === "string") return contentText(message);
|
|
768
768
|
return message.content
|
|
769
769
|
.map(part => {
|
|
770
|
-
if (part.type === "text") return part.text;
|
|
770
|
+
if (part.type === "text" || part.type === "document") return part.text;
|
|
771
771
|
if (part.type === "thinking") return part.thinking;
|
|
772
772
|
if (part.type === "image") return CURSOR_VISION_IMAGE_HISTORY_MARKER;
|
|
773
773
|
return undefined;
|
|
@@ -836,6 +836,9 @@ function decodeResultParts(message: OcxToolResultMessage): DecodedResultPart[] |
|
|
|
836
836
|
return content.map((part): DecodedResultPart => {
|
|
837
837
|
if (part.type === "text") return { kind: "text", text: part.text };
|
|
838
838
|
if (part.type === "video") return { kind: "text", text: "[video]" };
|
|
839
|
+
// Without this the document falls through to decodeInlineImage(part.imageUrl) below and is
|
|
840
|
+
// treated as an image it is not.
|
|
841
|
+
if (part.type === "document") return { kind: "text", text: part.text };
|
|
839
842
|
const decoded = decodeInlineImage(part.imageUrl);
|
|
840
843
|
return decoded ? { kind: "image", ...decoded } : { kind: "undecodable" };
|
|
841
844
|
});
|
|
@@ -213,7 +213,7 @@ export function cursorRequestEmitsFastVariant(parsed: OcxParsedRequest): boolean
|
|
|
213
213
|
* instead: current Cursor clients send the matching Grok base id plus `effort` and `fast` parameters.
|
|
214
214
|
* A fully-qualified id (one that is not a known effort base) passes through unchanged.
|
|
215
215
|
*/
|
|
216
|
-
function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: boolean): {
|
|
216
|
+
function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: boolean, liveRosterScope?: string): {
|
|
217
217
|
modelId: string;
|
|
218
218
|
requestedModelParameters?: readonly CursorRequestedModelParameter[];
|
|
219
219
|
routingLevel?: CursorRoutingLevel;
|
|
@@ -239,7 +239,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: bool
|
|
|
239
239
|
],
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
|
-
const resolved = resolveCursorSelection(id, reasoning, undefined, { fast });
|
|
242
|
+
const resolved = resolveCursorSelection(id, reasoning, undefined, { fast, liveRosterScope });
|
|
243
243
|
return {
|
|
244
244
|
...selection,
|
|
245
245
|
...(resolved.maxMode ? { maxMode: true } : {}),
|
|
@@ -251,6 +251,9 @@ function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): stri
|
|
|
251
251
|
switch (part.type) {
|
|
252
252
|
case "text":
|
|
253
253
|
return part.text;
|
|
254
|
+
case "document":
|
|
255
|
+
// Cursor has no document carrier; the marker keeps the turn from serializing to nothing.
|
|
256
|
+
return part.text;
|
|
254
257
|
case "thinking":
|
|
255
258
|
return part.thinking;
|
|
256
259
|
case "image":
|
|
@@ -407,6 +410,8 @@ export function cursorCoveredPrefixDigest(parsed: OcxParsedRequest, coveredMessa
|
|
|
407
410
|
export interface CreateCursorRequestOptions {
|
|
408
411
|
/** Force a brand-new Cursor conversation id even when remembered state exists. */
|
|
409
412
|
forceFreshConversation?: boolean;
|
|
413
|
+
/** Credential-bound scope for live Cursor model spelling and Max-Mode evidence. */
|
|
414
|
+
liveRosterScope?: string;
|
|
410
415
|
}
|
|
411
416
|
|
|
412
417
|
function lookupPrefixSnapshot(
|
|
@@ -476,7 +481,13 @@ function resolveCursorCheckpoint(
|
|
|
476
481
|
if (cursorCheckpointModelAffinityId(snapshot.modelId) !== cursorCheckpointModelAffinityId(request.modelId)) {
|
|
477
482
|
return { reason: "model_changed" };
|
|
478
483
|
}
|
|
479
|
-
|
|
484
|
+
// The continuation state only exists on the ref path; a ref-less prefix hit carries
|
|
485
|
+
// the suspension on the snapshot itself, or a non-toolResult request could resume
|
|
486
|
+
// bytes upstream serialized mid-tool-call.
|
|
487
|
+
if (
|
|
488
|
+
parsed.context.messages.at(-1)?.role !== "toolResult"
|
|
489
|
+
&& (snapshot.toolSuspended === true || cursorState?.checkpointUsable === false)
|
|
490
|
+
) {
|
|
480
491
|
return { reason: "trailing_tool_result" };
|
|
481
492
|
}
|
|
482
493
|
const lineage = lineageMismatch(parsed, snapshot);
|
|
@@ -493,7 +504,12 @@ export function createCursorRequest(
|
|
|
493
504
|
const visibleTools = cursorToolsForActivePrompt(parsed.context.tools, activeText, parsed.options.toolChoice);
|
|
494
505
|
const budget = applyCursorToolBudget(visibleTools, parsed.options.toolChoice);
|
|
495
506
|
const limitNote = catalogLimitNote(budget.tools, budget.omitted);
|
|
496
|
-
const model = normalizeCursorModelId(
|
|
507
|
+
const model = normalizeCursorModelId(
|
|
508
|
+
parsed.modelId,
|
|
509
|
+
parsed.options.reasoning,
|
|
510
|
+
cursorFastRequested(parsed),
|
|
511
|
+
options.liveRosterScope,
|
|
512
|
+
);
|
|
497
513
|
const request: CursorRunRequest = {
|
|
498
514
|
modelId: model.modelId,
|
|
499
515
|
...(model.requestedModelParameters ? { requestedModelParameters: model.requestedModelParameters } : {}),
|
|
@@ -55,6 +55,25 @@ export interface CursorTransportFactoryInput {
|
|
|
55
55
|
* client thread. Distinct from the per-transport native-exec/shell owner.
|
|
56
56
|
*/
|
|
57
57
|
sessionId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Test-only clock for the T04 inbound stream-health watchdog. Production omits it and the
|
|
60
|
+
* transport uses the global timers; injecting here must not change scheduling semantics.
|
|
61
|
+
*
|
|
62
|
+
* It exists because the watchdog's contract — a meaningful frame re-arms both deadlines — can
|
|
63
|
+
* only be asserted against real timers by keeping a synthetic server delivering frames faster
|
|
64
|
+
* than the silence budget for several multiples of it. That is an assertion about how busy the
|
|
65
|
+
* machine is, and it failed in the unsharded macOS lane for exactly that reason while the
|
|
66
|
+
* watchdog was working correctly. With the clock injected, virtual time advances only where
|
|
67
|
+
* the test says it does, so no amount of runner contention can expire a deadline.
|
|
68
|
+
*/
|
|
69
|
+
streamHealthClock?: CursorStreamHealthClock;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The three time primitives the T04 watchdog uses. Production binds all three to the globals. */
|
|
73
|
+
export interface CursorStreamHealthClock {
|
|
74
|
+
now(): number;
|
|
75
|
+
setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
|
|
76
|
+
clearTimeout(timer: ReturnType<typeof setTimeout>): void;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
export type CursorTransportFactory = (input: CursorTransportFactoryInput) => CursorTransport;
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
CursorTransportDisabledError,
|
|
62
62
|
type CursorTransportFactory,
|
|
63
63
|
} from "./cursor/transport";
|
|
64
|
+
import { cursorLiveRosterScope } from "./cursor/catalog";
|
|
64
65
|
|
|
65
66
|
export const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
66
67
|
|
|
@@ -172,9 +173,11 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
172
173
|
// Namespace thread→conversation derivation by the authenticated Cursor credential so
|
|
173
174
|
// shared-proxy tenants with different Cursor accounts cannot collide on a parent thread id.
|
|
174
175
|
// Prefer an already-set auth scope (e.g. Codex pool account) when present.
|
|
176
|
+
let liveRosterScope: string | undefined;
|
|
175
177
|
if (!_parsed._cursorIdentityScope) {
|
|
176
178
|
try {
|
|
177
179
|
const token = resolveCursorToken(provider, incoming.headers);
|
|
180
|
+
liveRosterScope = cursorLiveRosterScope(provider.baseUrl, token);
|
|
178
181
|
_parsed._cursorIdentityScope = createHash("sha256")
|
|
179
182
|
.update("ocx:cursor:acct:")
|
|
180
183
|
.update(token)
|
|
@@ -183,11 +186,17 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
183
186
|
} catch {
|
|
184
187
|
/* Missing credential is handled by the live transport path below. */
|
|
185
188
|
}
|
|
189
|
+
} else {
|
|
190
|
+
try {
|
|
191
|
+
liveRosterScope = cursorLiveRosterScope(provider.baseUrl, resolveCursorToken(provider, incoming.headers));
|
|
192
|
+
} catch {
|
|
193
|
+
/* Missing credential is handled by the live transport path below. */
|
|
194
|
+
}
|
|
186
195
|
}
|
|
187
196
|
const inheritedCheckpointRef = _parsed._providerContinuation?.cursor?.checkpointRef;
|
|
188
197
|
const previousConversationId = _parsed._cursorConversationId;
|
|
189
198
|
let request = {
|
|
190
|
-
...createCursorRequest(_parsed),
|
|
199
|
+
...createCursorRequest(_parsed, { liveRosterScope }),
|
|
191
200
|
_cursorIdentityScope: _parsed._cursorIdentityScope?.trim() || "local",
|
|
192
201
|
};
|
|
193
202
|
requestSizeContext = cursorRequestSizeContext(request);
|
|
@@ -254,6 +263,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
254
263
|
coveredMessageCount,
|
|
255
264
|
prefixDigest: cursorCoveredPrefixDigest(_parsed, coveredMessageCount),
|
|
256
265
|
systemDigest: cursorInstructionDigest(_parsed),
|
|
266
|
+
toolSuspended: toolSuspendedCommit,
|
|
257
267
|
});
|
|
258
268
|
if (!checkpointRef) return;
|
|
259
269
|
if (previousRef && previousRef !== checkpointRef) invalidateCursorCheckpoint(previousRef);
|
|
@@ -263,7 +273,8 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
263
273
|
...(_parsed._providerContinuation?.cursor ?? {}),
|
|
264
274
|
conversationId: activeRequest.conversationId,
|
|
265
275
|
// A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
|
|
266
|
-
// continuation; the request-builder guard keys on checkpointUsable=false
|
|
276
|
+
// continuation; the request-builder guard keys on this checkpointUsable=false and
|
|
277
|
+
// the snapshot's persisted toolSuspended flag for that.
|
|
267
278
|
checkpointUsable: !toolSuspendedCommit,
|
|
268
279
|
checkpointRef,
|
|
269
280
|
},
|
|
@@ -439,7 +450,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
439
450
|
lastTransport = undefined;
|
|
440
451
|
_parsed._cursorConversationId = undefined;
|
|
441
452
|
const next = {
|
|
442
|
-
...createCursorRequest(_parsed, { forceFreshConversation: true }),
|
|
453
|
+
...createCursorRequest(_parsed, { forceFreshConversation: true, liveRosterScope }),
|
|
443
454
|
_cursorIdentityScope: _parsed._cursorIdentityScope?.trim() || "local",
|
|
444
455
|
};
|
|
445
456
|
rekeyContextUsage(failedConversationId, next.conversationId);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Type-only: erased at compile time, so this does not create an import cycle with the registry.
|
|
2
|
+
import type { AdapterWire } from "./registry";
|
|
3
|
+
import type { OcxMessage, OcxParsedRequest } from "../types";
|
|
4
|
+
import { toolRestrictsCallers } from "../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wires that can actually carry a constraint, as a default-deny allowlist.
|
|
8
|
+
*
|
|
9
|
+
* A per-adapter opt-in is the wrong shape for this: an adapter that never learned about a
|
|
10
|
+
* carrier rebuilds the declaration or the message without it and returns a normal completion,
|
|
11
|
+
* which is exactly the silent widening this batch exists to remove. Listing the wires that CAN
|
|
12
|
+
* carry it means a new wire refuses until someone teaches it, and adding a member to
|
|
13
|
+
* `AdapterWire` makes the omission visible here rather than at a customer's upstream.
|
|
14
|
+
*/
|
|
15
|
+
const CALLER_RESTRICTION_WIRES: ReadonlySet<AdapterWire> = new Set<AdapterWire>(["anthropic"]);
|
|
16
|
+
const INLINE_DOCUMENT_WIRES: ReadonlySet<AdapterWire> = new Set<AdapterWire>([
|
|
17
|
+
"anthropic",
|
|
18
|
+
"openai-chat",
|
|
19
|
+
"google",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/** A fixed-vocabulary refusal, or `undefined` when this wire can hold everything the request carries. */
|
|
23
|
+
export function unrepresentableDeclaration(parsed: OcxParsedRequest, wire: AdapterWire): string | undefined {
|
|
24
|
+
if (!CALLER_RESTRICTION_WIRES.has(wire) && parsed.context.tools?.some(toolRestrictsCallers)) {
|
|
25
|
+
// The name is deliberately absent: it is caller-controlled and would put client metadata
|
|
26
|
+
// into an error body.
|
|
27
|
+
return "OpenCodex cannot express tools[].allowed_callers on this route. "
|
|
28
|
+
+ "Route the request to an Anthropic-protocol provider, or remove the caller restriction.";
|
|
29
|
+
}
|
|
30
|
+
if (!INLINE_DOCUMENT_WIRES.has(wire) && parsed.context.messages.some(carriesDocument)) {
|
|
31
|
+
return "OpenCodex cannot translate document input on this route. "
|
|
32
|
+
+ "Use a native input wire that supports the attachment, or convert it to text first.";
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Typed structurally rather than as `OcxContentPart[]`: an assistant turn's content is
|
|
39
|
+
* `OcxAssistantContentPart[]`, which carries thinking and tool-call members and is not
|
|
40
|
+
* assignable to the user-content union. Only the discriminant is read here.
|
|
41
|
+
*/
|
|
42
|
+
function carriesDocument(message: OcxMessage): boolean {
|
|
43
|
+
const content: unknown = message.content;
|
|
44
|
+
return Array.isArray(content) && (content as ReadonlyArray<{ type: string }>).some(part => part.type === "document");
|
|
45
|
+
}
|
|
@@ -1055,6 +1055,8 @@ export interface CloudChatRequest {
|
|
|
1055
1055
|
catalog?: CacheEntry | null;
|
|
1056
1056
|
/** Abort signal — closes the fetch stream. */
|
|
1057
1057
|
signal?: AbortSignal;
|
|
1058
|
+
/** Executor for the inference POST only; catalog and JWT RPCs retain their own transport. */
|
|
1059
|
+
executor?: typeof globalThis.fetch;
|
|
1058
1060
|
}
|
|
1059
1061
|
|
|
1060
1062
|
export class CloudChatError extends Error {
|
|
@@ -1216,7 +1218,7 @@ export async function* streamChatEvents(req: CloudChatRequest): AsyncGenerator<C
|
|
|
1216
1218
|
|
|
1217
1219
|
let resp: Response;
|
|
1218
1220
|
try {
|
|
1219
|
-
resp = await fetch(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
|
|
1221
|
+
resp = await (req.executor ?? globalThis.fetch)(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
|
|
1220
1222
|
method: 'POST',
|
|
1221
1223
|
headers: {
|
|
1222
1224
|
'Content-Type': 'application/connect+proto',
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* is idempotent; ambiguous transport failures still propagate unchanged.
|
|
6
6
|
*/
|
|
7
7
|
import { parseRetryAfterFromMessage } from '../../../lib/retry-delay.js';
|
|
8
|
-
import {
|
|
8
|
+
import type { AdapterFetchContext } from '../../base.js';
|
|
9
|
+
import { createAdapterPhysicalSend } from '../../physical-send.js';
|
|
10
|
+
import { abortError, SendBudgetExhaustedError, sleepWithAbort } from '../../../lib/upstream-retry.js';
|
|
9
11
|
import { CloudChatError, streamChatEvents, type CloudChatEvent, type CloudChatRequest } from './chat.js';
|
|
10
12
|
|
|
11
13
|
/** 1 initial attempt plus at most 2 replays. */
|
|
@@ -33,6 +35,11 @@ export interface StatedResetRetryOptions {
|
|
|
33
35
|
maxReplays?: number;
|
|
34
36
|
/** CUMULATIVE wait allowance, not a fresh allowance on every failure. */
|
|
35
37
|
maxWaitMs?: number;
|
|
38
|
+
/** Request-wide execution authority. Absent preserves context-free callers' unlimited fetch. */
|
|
39
|
+
execution?: Pick<
|
|
40
|
+
AdapterFetchContext,
|
|
41
|
+
"executor" | "sendBudget" | "onPhysicalSend" | "onRecoveryWithheld"
|
|
42
|
+
>;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
function replayLimit(value: number | undefined): number {
|
|
@@ -59,15 +66,34 @@ export async function* streamChatEventsWithResetRetry(
|
|
|
59
66
|
const sleep = options?.sleep ?? sleepWithAbort;
|
|
60
67
|
const maxReplays = replayLimit(options?.maxReplays);
|
|
61
68
|
const maxWaitMs = waitLimit(options?.maxWaitMs);
|
|
69
|
+
const execution = options?.execution;
|
|
70
|
+
// One sender owns the whole invocation so ordinals span the initial POST and both replays.
|
|
71
|
+
// Its executor remains lazy: replay admission happens after the provider-stated wait, never
|
|
72
|
+
// while a reservation could be held for up to an hour.
|
|
73
|
+
const send = execution
|
|
74
|
+
? createAdapterPhysicalSend({ ...execution, abortSignal: req.signal })
|
|
75
|
+
: undefined;
|
|
62
76
|
let replays = 0;
|
|
63
77
|
let waitedMs = 0;
|
|
78
|
+
let replaySourceError: CloudChatError | undefined;
|
|
64
79
|
while (true) {
|
|
65
80
|
// Check again after sleeping: cancellation can race with timer completion.
|
|
66
81
|
// A pre-aborted request must not even enter a custom transport.
|
|
67
82
|
if (req.signal?.aborted) throw abortError(req.signal);
|
|
68
83
|
let yielded = false;
|
|
69
84
|
try {
|
|
70
|
-
|
|
85
|
+
const recovery = replays > 0 ? "rate-limit-429" as const : undefined;
|
|
86
|
+
const attemptRequest = send
|
|
87
|
+
? {
|
|
88
|
+
...req,
|
|
89
|
+
executor: ((input, init) => send({
|
|
90
|
+
url: typeof input === "string" ? input : input instanceof URL ? input.href : input.url,
|
|
91
|
+
...(recovery ? { sendClass: "auth-recovery" as const, recovery } : {}),
|
|
92
|
+
dispatch: executor => executor(input, init),
|
|
93
|
+
})) as typeof globalThis.fetch,
|
|
94
|
+
}
|
|
95
|
+
: req;
|
|
96
|
+
for await (const event of stream(attemptRequest)) {
|
|
71
97
|
// Latch before yielding, so a consumer-injected error is post-output.
|
|
72
98
|
yielded = true;
|
|
73
99
|
yield event;
|
|
@@ -75,14 +101,24 @@ export async function* streamChatEventsWithResetRetry(
|
|
|
75
101
|
return;
|
|
76
102
|
} catch (error) {
|
|
77
103
|
if (req.signal?.aborted) throw abortError(req.signal);
|
|
78
|
-
|
|
104
|
+
if (!yielded && error instanceof SendBudgetExhaustedError && replaySourceError) {
|
|
105
|
+
// The provider's refusal is the real upstream answer. A local cap can withhold its
|
|
106
|
+
// recovery, but replacing the 429 would erase the status and stated reset metadata.
|
|
107
|
+
execution?.onRecoveryWithheld?.({ reason: "retry-send-budget" });
|
|
108
|
+
throw replaySourceError;
|
|
109
|
+
}
|
|
110
|
+
const retryableError = !yielded
|
|
79
111
|
&& error instanceof CloudChatError
|
|
80
112
|
&& error.status === 429
|
|
81
|
-
?
|
|
113
|
+
? error
|
|
114
|
+
: undefined;
|
|
115
|
+
const waitSec = retryableError
|
|
116
|
+
? parseRetryAfterFromMessage(retryableError.message)
|
|
82
117
|
: undefined;
|
|
83
118
|
const waitMs = waitSec === undefined ? undefined : waitSec * 1000;
|
|
84
119
|
if (
|
|
85
|
-
|
|
120
|
+
retryableError === undefined
|
|
121
|
+
|| waitMs === undefined
|
|
86
122
|
|| replays >= maxReplays
|
|
87
123
|
|| waitMs > maxWaitMs - waitedMs
|
|
88
124
|
) {
|
|
@@ -91,6 +127,7 @@ export async function* streamChatEventsWithResetRetry(
|
|
|
91
127
|
throw error;
|
|
92
128
|
}
|
|
93
129
|
replays += 1;
|
|
130
|
+
replaySourceError = retryableError;
|
|
94
131
|
// Charge the complete scheduled wait once, before sleeping. This is a
|
|
95
132
|
// sleep allowance, not a wall-clock deadline on generation or timer
|
|
96
133
|
// scheduling: waking a few milliseconds late must not reject an already
|