@yansigit/opencodex 2.31.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/AGENTS_INSTALL.md +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +108 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +76 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +112 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +205 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +14 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +225 -0
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +955 -0
- package/src/images/plan.ts +143 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +403 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +586 -0
- package/src/server/management/model-routes.ts +560 -0
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +25 -0
- package/src/server/search.ts +201 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- package/src/web-search/xai-executor.ts +219 -0
|
@@ -0,0 +1,1653 @@
|
|
|
1
|
+
import http2 from "node:http2";
|
|
2
|
+
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
3
|
+
import { namespacedToolName, type OcxProviderConfig, type OcxUsage } from "../../types";
|
|
4
|
+
import { CONNECT_FLAG_END_STREAM, ConnectFrameError, consumeConnectFrames, encodeConnectFrame } from "./framing";
|
|
5
|
+
import {
|
|
6
|
+
CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
|
|
7
|
+
CURSOR_MAX_CONNECT_FRAME_BYTES,
|
|
8
|
+
CURSOR_MAX_PENDING_FRAMES,
|
|
9
|
+
CURSOR_PENDING_FRAMES_RESUME,
|
|
10
|
+
CURSOR_TRANSPORT_MAX_BUFFERED_BYTES,
|
|
11
|
+
CURSOR_TRANSPORT_RESUME_BYTES,
|
|
12
|
+
TranslatorBudgetExceededError,
|
|
13
|
+
type TranslatorBudget,
|
|
14
|
+
} from "../../lib/translator-budget";
|
|
15
|
+
import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
|
|
16
|
+
import { prepareCursorRawMessages, resolveActiveCursorImages } from "./images";
|
|
17
|
+
import { cursorRequestMessagesFromRaw } from "./request-builder";
|
|
18
|
+
import {
|
|
19
|
+
createCursorContextUsageTracker,
|
|
20
|
+
createCursorProtobufEventState,
|
|
21
|
+
finalizeTurnEvents,
|
|
22
|
+
mapCursorProtobufServerMessage,
|
|
23
|
+
mapSyntheticMcpExecToToolEvents,
|
|
24
|
+
reportableContextTokens,
|
|
25
|
+
resolvedTurnUsage,
|
|
26
|
+
usageFromContextTokens,
|
|
27
|
+
} from "./protobuf-events";
|
|
28
|
+
import {
|
|
29
|
+
AgentClientMessageSchema,
|
|
30
|
+
AgentServerMessageSchema,
|
|
31
|
+
AskQuestionInteractionResponseSchema,
|
|
32
|
+
AskQuestionRejectedSchema,
|
|
33
|
+
AskQuestionResultSchema,
|
|
34
|
+
ClientHeartbeatSchema,
|
|
35
|
+
CreatePlanRequestResponseSchema,
|
|
36
|
+
CreatePlanResultSchema,
|
|
37
|
+
CreatePlanSuccessSchema,
|
|
38
|
+
ConversationStateStructureSchema,
|
|
39
|
+
ExaFetchRequestResponseSchema,
|
|
40
|
+
ExaFetchRequestResponse_ApprovedSchema,
|
|
41
|
+
ExaSearchRequestResponseSchema,
|
|
42
|
+
ExaSearchRequestResponse_ApprovedSchema,
|
|
43
|
+
InteractionResponseSchema,
|
|
44
|
+
SwitchModeRequestResponseSchema,
|
|
45
|
+
SwitchModeRequestResponse_RejectedSchema,
|
|
46
|
+
WebSearchRequestResponseSchema,
|
|
47
|
+
WebSearchRequestResponse_ApprovedSchema,
|
|
48
|
+
type AgentServerMessage,
|
|
49
|
+
type ExecServerMessage,
|
|
50
|
+
type InteractionQuery,
|
|
51
|
+
type InteractionResponse,
|
|
52
|
+
} from "./gen/agent_pb";
|
|
53
|
+
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
54
|
+
import { classifyCursorError, CursorUnexpectedCancelError, isCursorAbortError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
|
|
55
|
+
import { mcpArgsFromToolCall } from "./protobuf-events";
|
|
56
|
+
import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
|
|
57
|
+
import {
|
|
58
|
+
handleCursorNativeExec,
|
|
59
|
+
handleCursorNativeKv,
|
|
60
|
+
releaseCursorBlobRequestScope,
|
|
61
|
+
type CursorBlobRequestScopeToken,
|
|
62
|
+
type CursorNativeExecContext,
|
|
63
|
+
} from "./native-exec";
|
|
64
|
+
import { effectiveCursorNativeExecAllow } from "./exec-policy";
|
|
65
|
+
import { resolveMcpServers } from "./mcp-config";
|
|
66
|
+
import { CursorMcpManager } from "./mcp-manager";
|
|
67
|
+
import { buildMcpToolDefinitions, mcpDepsFromManager } from "./native-exec-mcp";
|
|
68
|
+
import { desktopDepsFromConfig } from "./native-exec-desktop";
|
|
69
|
+
import {
|
|
70
|
+
buildCursorToolDefinitions,
|
|
71
|
+
cursorRequestAdvertisesApplyPatch,
|
|
72
|
+
cursorRequestHasShellAlias,
|
|
73
|
+
cursorToolArgNormalizeSchema,
|
|
74
|
+
cursorToolWireName,
|
|
75
|
+
cursorToolsForActivePrompt,
|
|
76
|
+
isCursorSyntheticStructuredEditTool,
|
|
77
|
+
isGenericToolUseCountDemoPrompt,
|
|
78
|
+
requestedCursorToolUseCount,
|
|
79
|
+
} from "./tool-definitions";
|
|
80
|
+
import type { CursorNativeToolDeps } from "./native-exec-tools";
|
|
81
|
+
import {
|
|
82
|
+
terminateBackgroundShellsForSession,
|
|
83
|
+
type BackgroundShellTerminationReport,
|
|
84
|
+
} from "./native-exec-shell";
|
|
85
|
+
import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
|
|
86
|
+
import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
87
|
+
import { CursorHttp1BidiConnection } from "./http1-bidi";
|
|
88
|
+
import { isPinnedHttp1 } from "../../lib/upstream-http-version";
|
|
89
|
+
|
|
90
|
+
const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
|
|
91
|
+
const CURSOR_CLIENT_VERSION = "cli-2026.07.08-0c04a8a";
|
|
92
|
+
const HEARTBEAT_MS = 5_000;
|
|
93
|
+
const CURSOR_FIRST_FRAME_TIMEOUT_MS = 30_000;
|
|
94
|
+
/**
|
|
95
|
+
* T04 (senpi #1062 second half): after the first frame, a turn with NO inbound decoded
|
|
96
|
+
* frames for this long is failed instead of waiting for the 300s bridge stall watchdog
|
|
97
|
+
* (issue #2210). Reset on every decoded AgentServerMessage.
|
|
98
|
+
*/
|
|
99
|
+
const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
|
|
100
|
+
/**
|
|
101
|
+
* A stream that produces ONLY liveness frames (server heartbeat / conversationCheckpointUpdate)
|
|
102
|
+
* for this long is equally stuck — the server is alive but the turn is not progressing.
|
|
103
|
+
* Reset on every decoded frame that is not liveness-only.
|
|
104
|
+
*/
|
|
105
|
+
const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000;
|
|
106
|
+
/**
|
|
107
|
+
* After `turnEnded` is decoded, the application turn is complete. A server that keeps
|
|
108
|
+
* HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
|
|
109
|
+
* after a short grace so any trailing frames (late usage, checkpoint) still land.
|
|
110
|
+
*/
|
|
111
|
+
const TURN_ENDED_CLOSE_GRACE_MS = 500;
|
|
112
|
+
const CURSOR_TIMEOUT_DESTROY_GRACE_MS = 1_000;
|
|
113
|
+
const CLIENT_TOOL_FINALIZE_GRACE_MS = 50;
|
|
114
|
+
const GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS = 750;
|
|
115
|
+
const GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS = 1_800;
|
|
116
|
+
const GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS = 125;
|
|
117
|
+
const cursorContextUsageTracker = createCursorContextUsageTracker();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Single-shot terminal settlement for one Cursor turn: whichever of fail/finish wins first owns
|
|
121
|
+
* the terminal; later calls are no-ops. Prevents double-terminal mutation when multiple sources
|
|
122
|
+
* race (stream error + session error, end + late session error, timeout + destroy error).
|
|
123
|
+
* Exported for direct unit testing — the callbacks are otherwise private to run()/open().
|
|
124
|
+
*/
|
|
125
|
+
export function createTerminalSettler(hooks: {
|
|
126
|
+
fail: (error: Error) => void;
|
|
127
|
+
finish: () => void;
|
|
128
|
+
clearTimer: () => void;
|
|
129
|
+
}): { settleFail: (error: Error) => void; settleFinish: () => void; settled: () => boolean } {
|
|
130
|
+
let settled = false;
|
|
131
|
+
return {
|
|
132
|
+
settleFail(error) {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
hooks.clearTimer();
|
|
136
|
+
hooks.fail(error);
|
|
137
|
+
},
|
|
138
|
+
settleFinish() {
|
|
139
|
+
if (settled) return;
|
|
140
|
+
settled = true;
|
|
141
|
+
hooks.clearTimer();
|
|
142
|
+
hooks.finish();
|
|
143
|
+
},
|
|
144
|
+
settled: () => settled,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Arm the post-close destroy fallback for a timed-out turn: close() waits for in-flight frames,
|
|
150
|
+
* but a dead socket can ignore it, leaving a stalled TLS session past the timeout. Exported for
|
|
151
|
+
* unit testing with fakes. The timer is unref'd so it never holds the process open.
|
|
152
|
+
*/
|
|
153
|
+
export function armTimeoutDestroyFallback(
|
|
154
|
+
stream: { destroyed: boolean; destroy: () => void },
|
|
155
|
+
session: { destroyed: boolean; destroy: () => void },
|
|
156
|
+
graceMs: number,
|
|
157
|
+
): ReturnType<typeof setTimeout> {
|
|
158
|
+
const timer = setTimeout(() => {
|
|
159
|
+
try { if (!stream.destroyed) stream.destroy(); } catch { /* gone */ }
|
|
160
|
+
try { if (!session.destroyed) session.destroy(); } catch { /* gone */ }
|
|
161
|
+
}, graceMs);
|
|
162
|
+
timer.unref?.();
|
|
163
|
+
return timer;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Carry context-usage totals across conversation-id rotation for external-model replay. */
|
|
167
|
+
export function rekeyCursorContextUsage(fromConversationId: string, toConversationId: string): void {
|
|
168
|
+
cursorContextUsageTracker.rekey(fromConversationId, toConversationId);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class CursorMissingCredentialError extends Error {
|
|
172
|
+
readonly code = "cursor_missing_credential";
|
|
173
|
+
|
|
174
|
+
constructor() {
|
|
175
|
+
super("Cursor live transport requires a Cursor access token in provider.apiKey, Authorization, or OPENCODEX_CURSOR_TEST_TOKEN.");
|
|
176
|
+
this.name = "CursorMissingCredentialError";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function resolveCursorToken(provider: OcxProviderConfig, headers?: Headers): string {
|
|
181
|
+
const providerKey = provider.apiKey?.trim();
|
|
182
|
+
if (providerKey) return providerKey;
|
|
183
|
+
|
|
184
|
+
const forwarded = headers?.get("authorization") ?? headers?.get("Authorization");
|
|
185
|
+
if (forwarded?.toLowerCase().startsWith("bearer ")) return forwarded.slice("bearer ".length).trim();
|
|
186
|
+
|
|
187
|
+
const envToken = process.env.OPENCODEX_CURSOR_TEST_TOKEN?.trim();
|
|
188
|
+
if (envToken) return envToken;
|
|
189
|
+
throw new CursorMissingCredentialError();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Classify a Connect end-stream (trailer) frame. Cursor terminates EVERY stream with this
|
|
194
|
+
* frame; success is signalled by the ABSENCE of an `error` field (typically `{}`), not by the
|
|
195
|
+
* absence of the frame. Returns null on success, an Error only on a real Connect error.
|
|
196
|
+
* Mirrors jawcode `parseConnectEndStream` (see devlog 350.98). Exported for unit testing.
|
|
197
|
+
*/
|
|
198
|
+
export function parseConnectEndStreamError(payload: Uint8Array): Error | null {
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(new TextDecoder().decode(payload)) as { error?: { code?: string; message?: string } };
|
|
201
|
+
if (parsed?.error) {
|
|
202
|
+
return new Error(`Cursor Connect error ${parsed.error.code ?? "unknown"}: ${parsed.error.message ?? "Unknown error"}`);
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
} catch {
|
|
206
|
+
return new Error("Cursor Connect end-stream error");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function encodeClientMessage(message: Parameters<typeof create<typeof AgentClientMessageSchema>>[1]): Uint8Array {
|
|
211
|
+
return encodeConnectFrame(toBinary(AgentClientMessageSchema, create(AgentClientMessageSchema, message)));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Decide how to handle an `execServerMessage.mcpArgs` frame for a client (Responses-provider) tool.
|
|
216
|
+
*
|
|
217
|
+
* A stateless Responses proxy cannot send Cursor a real `mcpResult` later (Cursor's MCP exec is
|
|
218
|
+
* synchronous on the live h2 stream; there is no deferred-result signal). So when Cursor asks us to
|
|
219
|
+
* run a client Responses tool we must:
|
|
220
|
+
* 1. surface the tool call to Codex (tool_call_start/delta/end),
|
|
221
|
+
* 2. deliberately END turn 1 as `done`/completed — Cursor will never send `turnEnded` because it
|
|
222
|
+
* is waiting for an `mcpResult` that never comes, so relying on the stall watchdog would make
|
|
223
|
+
* turn 1 `response.incomplete` and drop the conversation id (continuation dies at step 1), and
|
|
224
|
+
* 3. cancel the Cursor run WITHOUT writing any fake `mcpResult`.
|
|
225
|
+
* The real tool result arrives on the NEXT /v1/responses request as structured history.
|
|
226
|
+
*
|
|
227
|
+
* Pure (no I/O) so the decision is unit-testable. `handleServerMessage` performs the side effects.
|
|
228
|
+
*/
|
|
229
|
+
export interface McpArgsPlan {
|
|
230
|
+
handledByResponsesBridge: boolean;
|
|
231
|
+
events: CursorServerMessage[];
|
|
232
|
+
cancelCursorRun: boolean;
|
|
233
|
+
/**
|
|
234
|
+
* The Responses bridge owns this exec and every known client tool call is committed, but turn 1 is
|
|
235
|
+
* NOT ended synchronously: a sibling call may still be announced in a later receive chunk. The
|
|
236
|
+
* transport arms a revocable grace timer and only ends the turn (see finalizeAfterDrain) if the set
|
|
237
|
+
* is still drained when it fires.
|
|
238
|
+
*/
|
|
239
|
+
finalizeWhenDrained: boolean;
|
|
240
|
+
writeMcpResult?: never;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function planMcpArgsHandling(
|
|
244
|
+
execMsg: ExecServerMessage,
|
|
245
|
+
state: ReturnType<typeof createCursorProtobufEventState>,
|
|
246
|
+
): McpArgsPlan {
|
|
247
|
+
if (execMsg.message.case !== "mcpArgs") {
|
|
248
|
+
return { handledByResponsesBridge: false, events: [], cancelCursorRun: false, finalizeWhenDrained: false };
|
|
249
|
+
}
|
|
250
|
+
const args = execMsg.message.value;
|
|
251
|
+
if (args.providerIdentifier !== OCX_RESPONSES_TOOL_PROVIDER) {
|
|
252
|
+
// A real MCP server tool: native exec handles it (executed locally, real mcpResult written).
|
|
253
|
+
return { handledByResponsesBridge: false, events: [], cancelCursorRun: false, finalizeWhenDrained: false };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// From here on the Responses bridge owns the exec: never fall through to native exec, which would
|
|
257
|
+
// send Cursor a bogus "bridge suspension not implemented" mcpResult error.
|
|
258
|
+
const toolEvents = mapSyntheticMcpExecToToolEvents(args, `exec_${execMsg.id}`, {
|
|
259
|
+
allowEmptyArgs: true,
|
|
260
|
+
state,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (toolEvents.some(event => event.type === "error")) {
|
|
264
|
+
// The error is itself the terminal signal; do not also emit `done`.
|
|
265
|
+
return { handledByResponsesBridge: true, events: toolEvents, cancelCursorRun: true, finalizeWhenDrained: false };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Parallel safety ("tool use N"): Cursor sends one exec mcpArgs per client tool call. An empty
|
|
269
|
+
// openToolCalls set proves only that every KNOWN call is committed, not that Cursor has finished
|
|
270
|
+
// announcing siblings — a sibling's toolCallStarted can still arrive in a later receive chunk. So
|
|
271
|
+
// never end turn 1 synchronously here: surface this call's events, and when the set is drained flag
|
|
272
|
+
// finalizeWhenDrained so the transport arms a revocable grace timer (finalizeAfterDrain re-checks
|
|
273
|
+
// the guard when it fires). While siblings are still open, just keep the stream open.
|
|
274
|
+
return {
|
|
275
|
+
handledByResponsesBridge: true,
|
|
276
|
+
events: toolEvents,
|
|
277
|
+
cancelCursorRun: false,
|
|
278
|
+
finalizeWhenDrained: state.openToolCalls.size === 0,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Build the `interactionResponse` reply for a server `interactionQuery`. Cursor's server-side agent
|
|
284
|
+
* BLOCKS on these queries until the client answers (matching `id`); an unanswered query is the
|
|
285
|
+
* proven cause of the heartbeat-only stall → watchdog `upstream_stall_timeout` → upstream 502 loop
|
|
286
|
+
* (devlog 260702_cursor-live-stability-rca). ocx is a headless non-interactive client, so:
|
|
287
|
+
* - createPlan: acknowledge success (the agent proceeds to execute); the plan text is surfaced to
|
|
288
|
+
* Codex as visible output so the user still sees it.
|
|
289
|
+
* - askQuestion: reject with a reason — the agent must proceed autonomously; there is no human to
|
|
290
|
+
* answer mid-turn. (Future: bridge to a Codex user-input request.)
|
|
291
|
+
* - webSearch / exaSearch / exaFetch: APPROVE (empty approval). These are approve/reject
|
|
292
|
+
* permission gates, not client-run requests — the response schema has no result field, so
|
|
293
|
+
* approval delegates the search to Cursor's SERVER, which runs it and injects results into the
|
|
294
|
+
* model server-side (the answer then streams back as textDelta; the display-plane
|
|
295
|
+
* web_search_tool_call/exa_*_tool_call result frames are native, non-mcp, and safely dropped by
|
|
296
|
+
* the event mapper). Rejecting them (the old default) killed the model's web capability on the
|
|
297
|
+
* Cursor path. Tradeoff: approval consumes the user's Cursor web-search/Exa quota. The synthetic
|
|
298
|
+
* web_search sidecar (src/web-search) is an orthogonal proxy-side path used only when the client
|
|
299
|
+
* sends a hosted web_search tool; it does not cover Cursor-native web search.
|
|
300
|
+
* - switchMode: reject (deterministic default; no non-interactive mode switch).
|
|
301
|
+
* - setupVmEnvironment: the result schema has no error case — reply success so the agent is not
|
|
302
|
+
* left waiting; the command itself was never run locally.
|
|
303
|
+
* Pure (no I/O) for unit testing; `handleServerMessage` writes the frame and emits liveness.
|
|
304
|
+
*/
|
|
305
|
+
export function planInteractionQueryReply(query: InteractionQuery): { response: InteractionResponse; replyCase: string; planText?: string } {
|
|
306
|
+
const NON_INTERACTIVE_REASON = "opencodex bridge is non-interactive; proceed without this interaction.";
|
|
307
|
+
const q = query.query;
|
|
308
|
+
const respond = (result: InteractionResponse["result"]): InteractionResponse =>
|
|
309
|
+
create(InteractionResponseSchema, { id: query.id, result });
|
|
310
|
+
|
|
311
|
+
if (q.case === "createPlanRequestQuery") {
|
|
312
|
+
const args = q.value.args;
|
|
313
|
+
const parts = [
|
|
314
|
+
args?.name ? `Plan: ${args.name}` : undefined,
|
|
315
|
+
args?.overview?.trim() ? args.overview.trim() : undefined,
|
|
316
|
+
args?.plan?.trim() ? args.plan.trim() : undefined,
|
|
317
|
+
].filter((part): part is string => typeof part === "string" && part.length > 0);
|
|
318
|
+
return {
|
|
319
|
+
response: respond({
|
|
320
|
+
case: "createPlanRequestResponse",
|
|
321
|
+
value: create(CreatePlanRequestResponseSchema, {
|
|
322
|
+
result: create(CreatePlanResultSchema, { result: { case: "success", value: create(CreatePlanSuccessSchema, {}) } }),
|
|
323
|
+
}),
|
|
324
|
+
}),
|
|
325
|
+
replyCase: "createPlanRequestResponse:success",
|
|
326
|
+
planText: parts.length > 0 ? `${parts.join("\n\n")}\n` : undefined,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (q.case === "askQuestionInteractionQuery") {
|
|
330
|
+
return {
|
|
331
|
+
response: respond({
|
|
332
|
+
case: "askQuestionInteractionResponse",
|
|
333
|
+
value: create(AskQuestionInteractionResponseSchema, {
|
|
334
|
+
result: create(AskQuestionResultSchema, {
|
|
335
|
+
result: { case: "rejected", value: create(AskQuestionRejectedSchema, { reason: NON_INTERACTIVE_REASON }) },
|
|
336
|
+
}),
|
|
337
|
+
}),
|
|
338
|
+
}),
|
|
339
|
+
replyCase: "askQuestionInteractionResponse:rejected",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (q.case === "switchModeRequestQuery") {
|
|
343
|
+
return {
|
|
344
|
+
response: respond({
|
|
345
|
+
case: "switchModeRequestResponse",
|
|
346
|
+
value: create(SwitchModeRequestResponseSchema, {
|
|
347
|
+
result: { case: "rejected", value: create(SwitchModeRequestResponse_RejectedSchema, { reason: NON_INTERACTIVE_REASON }) },
|
|
348
|
+
}),
|
|
349
|
+
}),
|
|
350
|
+
replyCase: "switchModeRequestResponse:rejected",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (q.case === "webSearchRequestQuery") {
|
|
354
|
+
return {
|
|
355
|
+
response: respond({
|
|
356
|
+
case: "webSearchRequestResponse",
|
|
357
|
+
value: create(WebSearchRequestResponseSchema, {
|
|
358
|
+
result: { case: "approved", value: create(WebSearchRequestResponse_ApprovedSchema, {}) },
|
|
359
|
+
}),
|
|
360
|
+
}),
|
|
361
|
+
replyCase: "webSearchRequestResponse:approved",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (q.case === "exaSearchRequestQuery") {
|
|
365
|
+
return {
|
|
366
|
+
response: respond({
|
|
367
|
+
case: "exaSearchRequestResponse",
|
|
368
|
+
value: create(ExaSearchRequestResponseSchema, {
|
|
369
|
+
result: { case: "approved", value: create(ExaSearchRequestResponse_ApprovedSchema, {}) },
|
|
370
|
+
}),
|
|
371
|
+
}),
|
|
372
|
+
replyCase: "exaSearchRequestResponse:approved",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (q.case === "exaFetchRequestQuery") {
|
|
376
|
+
return {
|
|
377
|
+
response: respond({
|
|
378
|
+
case: "exaFetchRequestResponse",
|
|
379
|
+
value: create(ExaFetchRequestResponseSchema, {
|
|
380
|
+
result: { case: "approved", value: create(ExaFetchRequestResponse_ApprovedSchema, {}) },
|
|
381
|
+
}),
|
|
382
|
+
}),
|
|
383
|
+
replyCase: "exaFetchRequestResponse:approved",
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (q.case === "setupVmEnvironmentArgs") {
|
|
387
|
+
// setupVmEnvironment is not supported — reply with an empty InteractionResponse so the stream
|
|
388
|
+
// stays alive instead of throwing (which kills the entire gRPC connection via failAndClear).
|
|
389
|
+
return {
|
|
390
|
+
response: respond({ case: undefined, value: undefined }),
|
|
391
|
+
replyCase: "unsupported:setupVmEnvironment",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// Unknown interaction query case — Cursor added a new query type that our protobuf definition
|
|
395
|
+
// does not include yet. Gracefully reply with an empty InteractionResponse (matching id, no
|
|
396
|
+
// result) so the server unblocks and the stream stays alive. Previously this threw, which
|
|
397
|
+
// propagated through .catch → failAndClear and killed the entire connection (#116).
|
|
398
|
+
return {
|
|
399
|
+
response: respond({ case: undefined, value: undefined }),
|
|
400
|
+
replyCase: `unsupported:${q.case ?? "unknown"}`,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Re-check the drain guard at grace-timer fire time and finalize turn 1 only if still drained. A
|
|
406
|
+
* sibling client tool call announced after the timer was armed reopens `openToolCalls`, so this
|
|
407
|
+
* returns `[]` (the pending finalize is revoked); a later drain re-arms it. Pure for unit testing.
|
|
408
|
+
*/
|
|
409
|
+
export function finalizeAfterDrain(state: ReturnType<typeof createCursorProtobufEventState>): CursorServerMessage[] {
|
|
410
|
+
if (state.terminated) return [];
|
|
411
|
+
if (state.openToolCalls.size > 0) return [];
|
|
412
|
+
return finalizeTurnEvents(state);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function clientToolFinalizeGraceMsForRequest(request: CursorRunRequest, baseGraceMs = CLIENT_TOOL_FINALIZE_GRACE_MS): number {
|
|
416
|
+
if (request.rawMessages?.at(-1)?.role === "toolResult") return baseGraceMs;
|
|
417
|
+
const text = activePromptText(request);
|
|
418
|
+
if (!cursorRequestHasShellAlias(request.tools) || !isGenericToolUseCountDemoPrompt(text)) return baseGraceMs;
|
|
419
|
+
const requestedCount = requestedCursorToolUseCount(text);
|
|
420
|
+
const expandedGraceMs = requestedCount
|
|
421
|
+
? Math.min(
|
|
422
|
+
GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS,
|
|
423
|
+
Math.max(GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS, requestedCount * GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS),
|
|
424
|
+
)
|
|
425
|
+
: GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS;
|
|
426
|
+
return Math.max(baseGraceMs, expandedGraceMs);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
class LiveCursorTransport implements CursorTransport {
|
|
430
|
+
private session?: http2.ClientHttp2Session;
|
|
431
|
+
private stream?: http2.ClientHttp2Stream;
|
|
432
|
+
private http1Connection?: CursorHttp1BidiConnection;
|
|
433
|
+
private heartbeat?: ReturnType<typeof setInterval>;
|
|
434
|
+
private firstFrameTimer?: ReturnType<typeof setTimeout>;
|
|
435
|
+
private turnEndedCloseTimer?: ReturnType<typeof setTimeout>;
|
|
436
|
+
/**
|
|
437
|
+
* T04 inbound stream-health watchdog. Armed after the request is on the wire, reset by
|
|
438
|
+
* every DECODED frame (raw chunks deliberately do not count — TLS keepalive noise must not
|
|
439
|
+
* defeat it), disarmed by any settle/expected-close path. One timer covers both thresholds:
|
|
440
|
+
* it always fires at min(lastInbound + silence, lastMeaningful + heartbeatOnly) and re-arms
|
|
441
|
+
* when neither deadline has actually elapsed.
|
|
442
|
+
*/
|
|
443
|
+
private streamHealthTimer?: ReturnType<typeof setTimeout>;
|
|
444
|
+
private lastInboundFrameAt = 0;
|
|
445
|
+
private lastMeaningfulFrameAt = 0;
|
|
446
|
+
private streamHealthFail?: (error: Error) => void;
|
|
447
|
+
private committed = false;
|
|
448
|
+
private expectedClose = false;
|
|
449
|
+
/**
|
|
450
|
+
* True once a terminal (`done` or `error`) has been admitted to the outbound queue. Read only
|
|
451
|
+
* by the EOF branch below: after a mapper error the bridge has already failed the turn, so
|
|
452
|
+
* failing again on EOF would add a duplicate adapter error for no benefit.
|
|
453
|
+
*/
|
|
454
|
+
private emittedTerminal = false;
|
|
455
|
+
private pendingFinalize?: ReturnType<typeof setTimeout>;
|
|
456
|
+
private readonly clientToolFinalizeGraceMs: number;
|
|
457
|
+
private activeClientToolFinalizeGraceMs: number;
|
|
458
|
+
private readonly token: string;
|
|
459
|
+
private readonly mcpManager?: CursorMcpManager;
|
|
460
|
+
private readonly translatorBudget: TranslatorBudget;
|
|
461
|
+
private pendingTransportFrames = 0;
|
|
462
|
+
private transportBufferedBytes = 0;
|
|
463
|
+
private readonly desktopDeps: CursorNativeToolDeps;
|
|
464
|
+
private execContext: CursorNativeExecContext = {};
|
|
465
|
+
private mcpPrepared?: Promise<void>;
|
|
466
|
+
private releaseMcpObservation?: () => void;
|
|
467
|
+
private blobRequestScope?: CursorBlobRequestScopeToken;
|
|
468
|
+
private shellCleanup?: Promise<BackgroundShellTerminationReport>;
|
|
469
|
+
// Per-turn diagnostic counters/timestamps when provider debug is on (`ocx debug provider on`). Stamped in open(), cleared on
|
|
470
|
+
// close; safe to read after a stream failure because open() owns the only writer before run().
|
|
471
|
+
private turnStartedAt = 0;
|
|
472
|
+
private framesReceived = 0;
|
|
473
|
+
private sawAssistantText = false;
|
|
474
|
+
private firstFrameAt?: number;
|
|
475
|
+
private firstFrameLogged = false;
|
|
476
|
+
/** Stable session identifier sent as x-session-id; mirrors IDE session semantics. */
|
|
477
|
+
private readonly sessionId: string;
|
|
478
|
+
/** Per-transport owner for native-exec / background shells. Must not share conversationId. */
|
|
479
|
+
private readonly shellOwnerId = crypto.randomUUID();
|
|
480
|
+
private capturedCheckpointBytes?: Uint8Array;
|
|
481
|
+
|
|
482
|
+
constructor(private readonly input: CursorTransportFactoryInput) {
|
|
483
|
+
this.sessionId = input.sessionId?.trim() || crypto.randomUUID();
|
|
484
|
+
this.translatorBudget = input.translatorBudget;
|
|
485
|
+
this.token = resolveCursorToken(input.provider, input.headers);
|
|
486
|
+
// Grace window before a drained client-tool turn is finalized. Small enough not to look like a
|
|
487
|
+
// stall, large enough to catch a sibling tool call announced in the next receive chunk. Injectable
|
|
488
|
+
// so the transport-level race test can drive it deterministically.
|
|
489
|
+
this.clientToolFinalizeGraceMs = input.clientToolFinalizeGraceMs ?? CLIENT_TOOL_FINALIZE_GRACE_MS;
|
|
490
|
+
this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
|
|
491
|
+
// Desktop (computer-use / record-screen) executors are available even with no MCP servers.
|
|
492
|
+
this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
|
|
493
|
+
this.execContext = {
|
|
494
|
+
...this.desktopDeps,
|
|
495
|
+
sessionId: this.shellOwnerId,
|
|
496
|
+
unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(input.provider, input.requestDeclaresFullAccess === true),
|
|
497
|
+
};
|
|
498
|
+
const servers = resolveMcpServers(input.provider);
|
|
499
|
+
if (servers.length > 0) {
|
|
500
|
+
this.mcpManager = new CursorMcpManager(servers, {
|
|
501
|
+
log: message => console.warn(message),
|
|
502
|
+
maxTools: input.provider.mcpMaxTools,
|
|
503
|
+
maxSchemaBytes: input.provider.mcpMaxSchemaBytes,
|
|
504
|
+
maxResultBytes: input.provider.mcpMaxResultBytes,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Connect MCP servers and compute the tool definitions advertised to the Cursor server.
|
|
511
|
+
* MUST complete before the first `requestContextArgs` (the server only calls MCP tools it was
|
|
512
|
+
* told about), so `run()` awaits this before opening the stream. Preparation failures reject the
|
|
513
|
+
* turn instead of silently running with MCP disabled.
|
|
514
|
+
*/
|
|
515
|
+
private prepareMcp(): Promise<void> {
|
|
516
|
+
if (!this.mcpManager) return Promise.resolve();
|
|
517
|
+
if (!this.mcpPrepared) {
|
|
518
|
+
this.mcpPrepared = (async () => {
|
|
519
|
+
try {
|
|
520
|
+
const mcpToolDefs = await buildMcpToolDefinitions(this.mcpManager!);
|
|
521
|
+
this.releaseMcpObservation?.();
|
|
522
|
+
this.releaseMcpObservation = this.translatorBudget.observeExternallyCapped(
|
|
523
|
+
"mcp_payload",
|
|
524
|
+
new TextEncoder().encode(JSON.stringify(mcpToolDefs)).byteLength,
|
|
525
|
+
);
|
|
526
|
+
this.execContext = {
|
|
527
|
+
...this.desktopDeps,
|
|
528
|
+
...mcpDepsFromManager(this.mcpManager!),
|
|
529
|
+
mcpToolDefs,
|
|
530
|
+
sessionId: this.shellOwnerId,
|
|
531
|
+
unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(this.input.provider, this.input.requestDeclaresFullAccess === true),
|
|
532
|
+
};
|
|
533
|
+
} catch (err) {
|
|
534
|
+
throw new Error(`Cursor MCP preparation failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
535
|
+
}
|
|
536
|
+
})();
|
|
537
|
+
}
|
|
538
|
+
return this.mcpPrepared;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
toJSON(): Record<string, string> {
|
|
542
|
+
return { type: "LiveCursorTransport", credential: "redacted" };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async *run(request: CursorRunRequest, signal?: AbortSignal): AsyncIterable<CursorServerMessage> {
|
|
546
|
+
const queue: Array<{ message: CursorServerMessage; bytes: number }> = [];
|
|
547
|
+
let notify: (() => void) | undefined;
|
|
548
|
+
let done = false;
|
|
549
|
+
let failure: Error | undefined;
|
|
550
|
+
let state = createCursorProtobufEventState({ translatorBudget: this.translatorBudget });
|
|
551
|
+
let failureLogged = false;
|
|
552
|
+
// One per-turn summary of the failure path (end-stream error, socket reset, abort) so the
|
|
553
|
+
// operator can see how far the turn got and how it was classified without re-scanning every
|
|
554
|
+
// frame. Gated behind provider debug (`ocx debug provider on`).
|
|
555
|
+
const summarizeFailure = (err: Error): Error => {
|
|
556
|
+
if (!failureLogged && !(this.expectedClose && isCursorBenignCancelError(err))) {
|
|
557
|
+
failureLogged = true;
|
|
558
|
+
debugProviderDiagnostic("cursor", "turn-failed", {
|
|
559
|
+
committed: this.committed,
|
|
560
|
+
framesReceived: this.framesReceived,
|
|
561
|
+
outputTokens: state.usage.outputTokens,
|
|
562
|
+
contextTokens: state.contextTokens,
|
|
563
|
+
firstFrameMs: this.firstFrameAt ? this.firstFrameAt - this.turnStartedAt : undefined,
|
|
564
|
+
elapsedMs: this.turnStartedAt ? Date.now() - this.turnStartedAt : undefined,
|
|
565
|
+
classified: classifyCursorError(err.message),
|
|
566
|
+
errorCode: (err as { code?: unknown }).code ?? undefined,
|
|
567
|
+
message: redactCursorForLog(err.message),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return err;
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* A cancel we did not request is a real transport failure, but as a raw `NGHTTP2_CANCEL` it
|
|
574
|
+
* gets swallowed twice over: the adapter re-decides "benign" from the error code alone
|
|
575
|
+
* (`cursor.ts:181`) and drops the turn, and any message that survives is re-matched
|
|
576
|
+
* downstream and labelled an intentional "Cursor stream suspended". Raising a typed error
|
|
577
|
+
* carries the provenance this class already holds.
|
|
578
|
+
*
|
|
579
|
+
* Suppressed once a terminal was emitted: the turn already ended, and a second terminal flips
|
|
580
|
+
* a completed buffered response to failed.
|
|
581
|
+
*/
|
|
582
|
+
const classifyTurnFailure = (err: Error): Error => {
|
|
583
|
+
if (!this.expectedClose && !this.emittedTerminal && isCursorBenignCancelError(err)) {
|
|
584
|
+
return summarizeFailure(new CursorUnexpectedCancelError(err));
|
|
585
|
+
}
|
|
586
|
+
return summarizeFailure(err);
|
|
587
|
+
};
|
|
588
|
+
const wake = () => {
|
|
589
|
+
const fn = notify;
|
|
590
|
+
notify = undefined;
|
|
591
|
+
fn?.();
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const push = (message: CursorServerMessage) => {
|
|
595
|
+
const bytes = new TextEncoder().encode(JSON.stringify(message)).byteLength;
|
|
596
|
+
this.reserveTransportBytes(bytes);
|
|
597
|
+
if (message.type === "done" || message.type === "error") this.emittedTerminal = true;
|
|
598
|
+
queue.push({ message, bytes });
|
|
599
|
+
wake();
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// Advertise MCP tools before the stream opens — the server only calls tools it was told about.
|
|
603
|
+
await this.prepareMcp();
|
|
604
|
+
// JPEG soft-cap rewrite for active-turn data: images before encode. Rebuild text
|
|
605
|
+
// messages from the prepared raw channel so omission markers replace stale
|
|
606
|
+
// pre-rewrite content that activePromptText and the tool filter would otherwise see.
|
|
607
|
+
const preparedRaw = await prepareCursorRawMessages(request.rawMessages, signal);
|
|
608
|
+
const preparedRawMessages = preparedRaw.messages;
|
|
609
|
+
const selectedImages = await resolveActiveCursorImages(
|
|
610
|
+
preparedRawMessages,
|
|
611
|
+
signal,
|
|
612
|
+
preparedRaw.images,
|
|
613
|
+
);
|
|
614
|
+
const preparedMessages = preparedRawMessages === request.rawMessages
|
|
615
|
+
? request.messages
|
|
616
|
+
: cursorRequestMessagesFromRaw(preparedRawMessages);
|
|
617
|
+
const activeRequest: CursorRunRequest = {
|
|
618
|
+
...request,
|
|
619
|
+
messages: preparedMessages,
|
|
620
|
+
rawMessages: preparedRawMessages,
|
|
621
|
+
selectedImages,
|
|
622
|
+
};
|
|
623
|
+
const activeText = activePromptText(activeRequest);
|
|
624
|
+
this.activeClientToolFinalizeGraceMs = clientToolFinalizeGraceMsForRequest(activeRequest, this.clientToolFinalizeGraceMs);
|
|
625
|
+
const cursorVisibleTools = cursorToolsForActivePrompt(activeRequest.tools, activeText, activeRequest.toolChoice);
|
|
626
|
+
const clientToolDefs = buildCursorToolDefinitions(cursorVisibleTools, activeRequest.toolChoice);
|
|
627
|
+
// `request.tools` is the catalog already filtered and budgeted by request-builder. Derive
|
|
628
|
+
// conversion provenance only from tagged synthetic tools that also survive this final prompt
|
|
629
|
+
// filter; a client tool with the same wire name can never opt into conversion by collision.
|
|
630
|
+
const syntheticStructuredEditToolNames = new Set(
|
|
631
|
+
(cursorVisibleTools ?? [])
|
|
632
|
+
.filter(isCursorSyntheticStructuredEditTool)
|
|
633
|
+
.map(cursorToolWireName),
|
|
634
|
+
);
|
|
635
|
+
const freeformToolNames = new Set(
|
|
636
|
+
(cursorVisibleTools ?? [])
|
|
637
|
+
.filter(tool => tool.freeform)
|
|
638
|
+
.map(tool => namespacedToolName(tool.namespace, tool.name)),
|
|
639
|
+
);
|
|
640
|
+
this.execContext = {
|
|
641
|
+
...this.execContext,
|
|
642
|
+
clientToolDefs,
|
|
643
|
+
rejectNativeFileMutations: cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice),
|
|
644
|
+
structuredEditAvailable: syntheticStructuredEditToolNames.size > 0,
|
|
645
|
+
};
|
|
646
|
+
const toolSchemas = new Map<string, unknown>();
|
|
647
|
+
const cursorToolNameMap = new Map<string, string>();
|
|
648
|
+
for (const tool of cursorVisibleTools ?? []) {
|
|
649
|
+
const cursorWireName = cursorToolWireName(tool);
|
|
650
|
+
// Normalize against Responses/Codex field names, not the Cursor advertisement schema.
|
|
651
|
+
// Advertising `cmd` while also storing that schema here left `cmd` unmapped and Codex
|
|
652
|
+
// rejected shell_command with "missing field `command`" (#399).
|
|
653
|
+
toolSchemas.set(cursorWireName, cursorToolArgNormalizeSchema(tool));
|
|
654
|
+
cursorToolNameMap.set(cursorWireName, namespacedToolName(tool.namespace, tool.name));
|
|
655
|
+
}
|
|
656
|
+
const contextUsage = cursorContextUsageTracker.controlsForConversation(request.conversationId, {
|
|
657
|
+
clearPrior: request.contextUsageReset === true,
|
|
658
|
+
storeCheckpoints: request.contextUsageStoreCheckpoints !== false,
|
|
659
|
+
});
|
|
660
|
+
// Build the payload once. The estimate is only worth deriving when there is no
|
|
661
|
+
// carry-forward to fall back on — with a carry present it would never be used (#373).
|
|
662
|
+
const prepared = prepareCursorRunRequest(activeRequest, {
|
|
663
|
+
estimateInputTokens: contextUsage.carryForwardTokens === undefined,
|
|
664
|
+
});
|
|
665
|
+
this.blobRequestScope = prepared.blobRequestScope;
|
|
666
|
+
try {
|
|
667
|
+
state = createCursorProtobufEventState({
|
|
668
|
+
clientToolNames: clientToolDefs.map(tool => tool.toolName || tool.name),
|
|
669
|
+
freeformToolNames,
|
|
670
|
+
parallelToolCalls: request.parallelToolCalls,
|
|
671
|
+
toolSchemas,
|
|
672
|
+
cursorToolNameMap,
|
|
673
|
+
syntheticStructuredEditToolNames,
|
|
674
|
+
translatorBudget: this.translatorBudget,
|
|
675
|
+
contextUsage,
|
|
676
|
+
...(prepared.estimatedInputTokens !== undefined
|
|
677
|
+
? { estimatedInputTokens: prepared.estimatedInputTokens }
|
|
678
|
+
: {}),
|
|
679
|
+
});
|
|
680
|
+
this.open(prepared.bytes, signal, state, push, err => {
|
|
681
|
+
this.releaseBlobRequestScope();
|
|
682
|
+
failure = err;
|
|
683
|
+
wake();
|
|
684
|
+
}, () => {
|
|
685
|
+
this.releaseBlobRequestScope();
|
|
686
|
+
done = true;
|
|
687
|
+
wake();
|
|
688
|
+
});
|
|
689
|
+
} catch (error) {
|
|
690
|
+
this.releaseBlobRequestScope();
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
while (!done || queue.length > 0) {
|
|
695
|
+
while (queue.length > 0) {
|
|
696
|
+
const queued = queue.shift();
|
|
697
|
+
if (queued) {
|
|
698
|
+
this.releaseTransportBytes(queued.bytes);
|
|
699
|
+
yield queued.message;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (failure) {
|
|
703
|
+
// A CANCEL is benign only on the client-tool suspend path (expectedClose); an
|
|
704
|
+
// unexpected server-side NGHTTP2_CANCEL must surface as a real transport error.
|
|
705
|
+
if (this.expectedClose && isCursorBenignCancelError(failure)) return;
|
|
706
|
+
// A teardown error arriving AFTER the turn's terminal frame describes the connection,
|
|
707
|
+
// not the turn: the answer is committed and every queued message has been yielded.
|
|
708
|
+
//
|
|
709
|
+
// Narrow on purpose. A benign cancel after a terminal is already swallowed one layer
|
|
710
|
+
// up (`cursor.ts:183`), so widening this to every post-terminal error would change
|
|
711
|
+
// what the adapter sees for genuine faults. What it does cover is the abort case
|
|
712
|
+
// from #1527: `signal.abort` fires `failAndClear(new Error("Cursor request was
|
|
713
|
+
// aborted"))`, which is NOT benign (`cursor-errors.ts:74`), so an ordinary completed
|
|
714
|
+
// turn that is then torn down still surfaced as `turn-failed` with
|
|
715
|
+
// `expectedClose:false`. Only `cancelCursorRun()` sets `expectedClose`, so a normal
|
|
716
|
+
// completion never qualified for the branch above.
|
|
717
|
+
if (this.emittedTerminal && isCursorAbortError(failure)) return;
|
|
718
|
+
throw attachPartialUsage(classifyTurnFailure(failure), state);
|
|
719
|
+
}
|
|
720
|
+
if (done) break;
|
|
721
|
+
await new Promise<void>(resolve => {
|
|
722
|
+
notify = resolve;
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
if (failure) {
|
|
726
|
+
if (this.expectedClose && isCursorBenignCancelError(failure)) return;
|
|
727
|
+
if (this.emittedTerminal && isCursorAbortError(failure)) return;
|
|
728
|
+
throw attachPartialUsage(classifyTurnFailure(failure), state);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
writeClient(_message: CursorClientMessage): void {}
|
|
733
|
+
|
|
734
|
+
private reserveTransportBytes(bytes: number): void {
|
|
735
|
+
if (this.transportBufferedBytes + bytes > CURSOR_TRANSPORT_MAX_BUFFERED_BYTES) {
|
|
736
|
+
throw new TranslatorBudgetExceededError("cursor_transport", CURSOR_TRANSPORT_MAX_BUFFERED_BYTES);
|
|
737
|
+
}
|
|
738
|
+
this.translatorBudget.chargeRetained(bytes, { kind: "cursor_transport" });
|
|
739
|
+
this.transportBufferedBytes += bytes;
|
|
740
|
+
this.updateTransportFlowControl();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private releaseTransportBytes(bytes: number): void {
|
|
744
|
+
this.transportBufferedBytes = Math.max(0, this.transportBufferedBytes - bytes);
|
|
745
|
+
this.translatorBudget.releaseRetained(bytes, { kind: "cursor_transport" });
|
|
746
|
+
this.updateTransportFlowControl();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private updateTransportFlowControl(): void {
|
|
750
|
+
if (
|
|
751
|
+
this.transportBufferedBytes >= CURSOR_TRANSPORT_MAX_BUFFERED_BYTES
|
|
752
|
+
|| this.pendingTransportFrames >= CURSOR_MAX_PENDING_FRAMES
|
|
753
|
+
) {
|
|
754
|
+
this.stream?.pause();
|
|
755
|
+
this.http1Connection?.pause();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (
|
|
759
|
+
this.transportBufferedBytes <= CURSOR_TRANSPORT_RESUME_BYTES
|
|
760
|
+
&& this.pendingTransportFrames <= CURSOR_PENDING_FRAMES_RESUME
|
|
761
|
+
) {
|
|
762
|
+
this.stream?.resume();
|
|
763
|
+
this.http1Connection?.resume();
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private writeConnectFrame(frame: Uint8Array): void {
|
|
768
|
+
if (this.http1Connection) {
|
|
769
|
+
this.http1Connection.write(frame);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.stream?.write(frame);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
requestCommitted(): boolean {
|
|
776
|
+
return this.committed;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private clearFirstFrameTimer(): void {
|
|
780
|
+
if (this.firstFrameTimer) {
|
|
781
|
+
clearTimeout(this.firstFrameTimer);
|
|
782
|
+
this.firstFrameTimer = undefined;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private clearStreamHealthTimer(): void {
|
|
787
|
+
if (this.streamHealthTimer) {
|
|
788
|
+
clearTimeout(this.streamHealthTimer);
|
|
789
|
+
this.streamHealthTimer = undefined;
|
|
790
|
+
}
|
|
791
|
+
this.streamHealthFail = undefined;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* T04: arm (or re-arm) the inbound stream-health watchdog. `fail` is the turn's
|
|
796
|
+
* failAndClear; the timer owns nothing else. Never armed before the first decoded
|
|
797
|
+
* frame (the first-frame timer covers dial + first response), and disarmed by
|
|
798
|
+
* every settle / expected-close path alongside the other timers.
|
|
799
|
+
*/
|
|
800
|
+
private armStreamHealthTimer(fail: (error: Error) => void): void {
|
|
801
|
+
if (this.streamHealthTimer) clearTimeout(this.streamHealthTimer);
|
|
802
|
+
if (this.expectedClose) return;
|
|
803
|
+
this.streamHealthFail = fail;
|
|
804
|
+
const silenceMs = this.input.streamSilenceFailMs ?? CURSOR_STREAM_SILENCE_FAIL_MS;
|
|
805
|
+
const heartbeatOnlyMs = this.input.streamHeartbeatOnlyFailMs ?? CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS;
|
|
806
|
+
const now = Date.now();
|
|
807
|
+
const deadline = Math.min(
|
|
808
|
+
this.lastInboundFrameAt + silenceMs,
|
|
809
|
+
this.lastMeaningfulFrameAt + heartbeatOnlyMs,
|
|
810
|
+
);
|
|
811
|
+
this.streamHealthTimer = setTimeout(() => {
|
|
812
|
+
this.streamHealthTimer = undefined;
|
|
813
|
+
const failFn = this.streamHealthFail;
|
|
814
|
+
if (!failFn || this.expectedClose) return;
|
|
815
|
+
const stalledFor = Date.now() - this.lastInboundFrameAt;
|
|
816
|
+
const meaningfulStalledFor = Date.now() - this.lastMeaningfulFrameAt;
|
|
817
|
+
if (stalledFor < silenceMs && meaningfulStalledFor < heartbeatOnlyMs) {
|
|
818
|
+
// A frame landed between arming and firing — re-arm for the fresh deadline.
|
|
819
|
+
this.armStreamHealthTimer(failFn);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const heartbeatOnly = stalledFor < silenceMs;
|
|
823
|
+
debugProviderDiagnostic("cursor", "stream-health-timeout", {
|
|
824
|
+
stalledMs: stalledFor,
|
|
825
|
+
meaningfulStalledMs: meaningfulStalledFor,
|
|
826
|
+
heartbeatOnly,
|
|
827
|
+
framesReceived: this.framesReceived,
|
|
828
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
829
|
+
});
|
|
830
|
+
const reason = heartbeatOnly
|
|
831
|
+
? `Cursor stream stalled: heartbeat-only traffic for ${Math.round(meaningfulStalledFor / 1000)}s without turn progress`
|
|
832
|
+
: `Cursor stream stalled: no inbound frames for ${Math.round(stalledFor / 1000)}s before turnEnded`;
|
|
833
|
+
failFn(new Error(reason));
|
|
834
|
+
try { this.stream?.close(); } catch { this.stream?.destroy(); }
|
|
835
|
+
this.session?.close();
|
|
836
|
+
this.http1Connection?.close();
|
|
837
|
+
}, Math.max(0, deadline - now));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* T04: record a decoded inbound frame. Liveness-only frames (server heartbeat,
|
|
842
|
+
* conversationCheckpointUpdate) keep the silence clock fresh but not the progress
|
|
843
|
+
* clock — matching senpi's split so a server that only pings still fails at the
|
|
844
|
+
* heartbeat-only threshold.
|
|
845
|
+
*/
|
|
846
|
+
private noteInboundFrame(livenessOnly: boolean): void {
|
|
847
|
+
const now = Date.now();
|
|
848
|
+
this.lastInboundFrameAt = now;
|
|
849
|
+
if (!livenessOnly) this.lastMeaningfulFrameAt = now;
|
|
850
|
+
if (this.streamHealthFail) this.armStreamHealthTimer(this.streamHealthFail);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* A clean Connect END_STREAM owns the turn terminal even when Cursor keeps the
|
|
855
|
+
* HTTP body open or tears it down with an abort/reset immediately afterward.
|
|
856
|
+
* Stop client-side liveness work and classify that later transport close as
|
|
857
|
+
* expected without actively sending an RST_STREAM back to Cursor.
|
|
858
|
+
*/
|
|
859
|
+
private markProtocolComplete(): void {
|
|
860
|
+
this.expectedClose = true;
|
|
861
|
+
this.clearPendingFinalize();
|
|
862
|
+
if (this.heartbeat) {
|
|
863
|
+
clearInterval(this.heartbeat);
|
|
864
|
+
this.heartbeat = undefined;
|
|
865
|
+
}
|
|
866
|
+
this.clearFirstFrameTimer();
|
|
867
|
+
this.clearStreamHealthTimer();
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private startShellCleanup(): Promise<BackgroundShellTerminationReport> {
|
|
871
|
+
return this.shellCleanup ??= terminateBackgroundShellsForSession(this.shellOwnerId);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async close(): Promise<void> {
|
|
875
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
876
|
+
if (this.turnEndedCloseTimer) clearTimeout(this.turnEndedCloseTimer);
|
|
877
|
+
this.clearPendingFinalize();
|
|
878
|
+
this.clearFirstFrameTimer();
|
|
879
|
+
this.clearStreamHealthTimer();
|
|
880
|
+
this.stream?.close();
|
|
881
|
+
this.session?.close();
|
|
882
|
+
this.http1Connection?.close();
|
|
883
|
+
this.releaseBlobRequestScope();
|
|
884
|
+
this.releaseMcpObservation?.();
|
|
885
|
+
this.releaseMcpObservation = undefined;
|
|
886
|
+
void this.mcpManager?.dispose();
|
|
887
|
+
await this.startShellCleanup();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private cancelCursorRun(): void {
|
|
891
|
+
this.expectedClose = true;
|
|
892
|
+
this.clearPendingFinalize();
|
|
893
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
894
|
+
this.clearFirstFrameTimer();
|
|
895
|
+
this.clearStreamHealthTimer();
|
|
896
|
+
if (this.http1Connection) {
|
|
897
|
+
this.http1Connection.close();
|
|
898
|
+
} else {
|
|
899
|
+
try {
|
|
900
|
+
this.stream?.close(http2.constants.NGHTTP2_CANCEL);
|
|
901
|
+
} catch {
|
|
902
|
+
this.stream?.destroy();
|
|
903
|
+
}
|
|
904
|
+
this.session?.close();
|
|
905
|
+
}
|
|
906
|
+
this.releaseBlobRequestScope();
|
|
907
|
+
this.releaseMcpObservation?.();
|
|
908
|
+
this.releaseMcpObservation = undefined;
|
|
909
|
+
void this.mcpManager?.dispose();
|
|
910
|
+
void this.startShellCleanup().catch(() => { /* close() observes the same cleanup promise */ });
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* T03 (#1062): after the server sends `turnEnded`, the application turn is complete.
|
|
915
|
+
* A server that keeps the HTTP/2 stream open past this point cannot hold the turn
|
|
916
|
+
* hostage until a 300s bridge idle timeout. Close our side after a short grace so any
|
|
917
|
+
* trailing frames (late usage, checkpoint) still land before we release the socket.
|
|
918
|
+
*/
|
|
919
|
+
private closeAfterTurnEnded(): void {
|
|
920
|
+
if (this.turnEndedCloseTimer) return;
|
|
921
|
+
// The application turn is over: the T03 grace timer owns the socket from here.
|
|
922
|
+
// The T04 watchdog must disarm NOW, not at the grace close — a watchdog shorter
|
|
923
|
+
// than the grace would otherwise fail a completed turn.
|
|
924
|
+
this.clearStreamHealthTimer();
|
|
925
|
+
this.turnEndedCloseTimer = setTimeout(() => {
|
|
926
|
+
this.turnEndedCloseTimer = undefined;
|
|
927
|
+
// Only expectedClose (client-tool suspend cancel) blocks the close.
|
|
928
|
+
// emittedTerminal is intentionally NOT checked here: finalizeTurnEvents sets it
|
|
929
|
+
// synchronously during turnEnded mapping, ~500ms before this timer fires, so
|
|
930
|
+
// checking it would make the close unreachable on every real path (the exact
|
|
931
|
+
// scenario this PR exists to fix — senpi #1062).
|
|
932
|
+
if (this.expectedClose) return;
|
|
933
|
+
debugProviderDiagnostic("cursor", "turn-ended-close", {
|
|
934
|
+
committed: this.committed,
|
|
935
|
+
framesReceived: this.framesReceived,
|
|
936
|
+
});
|
|
937
|
+
this.expectedClose = true;
|
|
938
|
+
this.clearFirstFrameTimer();
|
|
939
|
+
this.clearStreamHealthTimer();
|
|
940
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
941
|
+
if (this.http1Connection) {
|
|
942
|
+
this.http1Connection.close();
|
|
943
|
+
} else {
|
|
944
|
+
try {
|
|
945
|
+
this.stream?.close();
|
|
946
|
+
} catch {
|
|
947
|
+
this.stream?.destroy();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}, TURN_ENDED_CLOSE_GRACE_MS);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private releaseBlobRequestScope(): void {
|
|
954
|
+
const scope = this.blobRequestScope;
|
|
955
|
+
if (!scope) return;
|
|
956
|
+
this.blobRequestScope = undefined;
|
|
957
|
+
releaseCursorBlobRequestScope(scope);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
private clearPendingFinalize(): void {
|
|
961
|
+
if (this.pendingFinalize) {
|
|
962
|
+
clearTimeout(this.pendingFinalize);
|
|
963
|
+
this.pendingFinalize = undefined;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Any frame that records or commits a client tool call revokes a pending finalize: the call set is
|
|
969
|
+
* about to change, so the drain that armed the timer is no longer authoritative. The timer re-arms
|
|
970
|
+
* when the set drains again (see scheduleClientToolFinalize).
|
|
971
|
+
*/
|
|
972
|
+
private noteClientToolActivity(): void {
|
|
973
|
+
this.clearPendingFinalize();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Arm the revocable grace timer that ends a drained client-tool turn. On fire it re-checks the
|
|
978
|
+
* drain guard (finalizeAfterDrain): a sibling announced during the window reopened the set, so it
|
|
979
|
+
* emits nothing and waits for the next drain; otherwise it pushes the terminal `done` and cancels
|
|
980
|
+
* the Cursor run with RST_STREAM. No fake mcpResult is ever written.
|
|
981
|
+
*/
|
|
982
|
+
private scheduleClientToolFinalize(
|
|
983
|
+
state: ReturnType<typeof createCursorProtobufEventState>,
|
|
984
|
+
push: (message: CursorServerMessage) => void,
|
|
985
|
+
): void {
|
|
986
|
+
this.clearPendingFinalize();
|
|
987
|
+
this.pendingFinalize = setTimeout(() => {
|
|
988
|
+
this.pendingFinalize = undefined;
|
|
989
|
+
if (this.expectedClose) return;
|
|
990
|
+
const terminal = finalizeAfterDrain(state);
|
|
991
|
+
if (terminal.length === 0) return;
|
|
992
|
+
for (const event of terminal) push(event);
|
|
993
|
+
debugProviderDiagnostic("cursor", "client-tool-suspend", {
|
|
994
|
+
reason: "Responses bridge owns client tools; ending turn without fake mcpResult",
|
|
995
|
+
framesReceived: this.framesReceived,
|
|
996
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
997
|
+
});
|
|
998
|
+
this.cancelCursorRun();
|
|
999
|
+
}, this.activeClientToolFinalizeGraceMs);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
private open(
|
|
1003
|
+
encodedRequest: Uint8Array,
|
|
1004
|
+
signal: AbortSignal | undefined,
|
|
1005
|
+
state: ReturnType<typeof createCursorProtobufEventState>,
|
|
1006
|
+
push: (message: CursorServerMessage) => void,
|
|
1007
|
+
fail: (error: Error) => void,
|
|
1008
|
+
finish: () => void,
|
|
1009
|
+
): void {
|
|
1010
|
+
if (signal?.aborted) {
|
|
1011
|
+
fail(signal.reason instanceof Error ? signal.reason : new Error("Cursor request was aborted"));
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
this.turnStartedAt = Date.now();
|
|
1015
|
+
this.framesReceived = 0;
|
|
1016
|
+
this.sawAssistantText = false;
|
|
1017
|
+
this.emittedTerminal = false;
|
|
1018
|
+
this.firstFrameAt = undefined;
|
|
1019
|
+
this.firstFrameLogged = false;
|
|
1020
|
+
const baseUrl = this.input.provider.baseUrl || "https://api2.cursor.sh";
|
|
1021
|
+
const useHttp1 = isPinnedHttp1(this.input.provider.upstreamHttpVersion);
|
|
1022
|
+
const requestId = crypto.randomUUID();
|
|
1023
|
+
const dialHost = cursorHostLabel(baseUrl);
|
|
1024
|
+
debugProviderDiagnostic("cursor", "dial", { host: dialHost, transport: useHttp1 ? "http1.1" : "http2" });
|
|
1025
|
+
|
|
1026
|
+
let session: http2.ClientHttp2Session | undefined;
|
|
1027
|
+
let stream: http2.ClientHttp2Stream | undefined;
|
|
1028
|
+
if (!useHttp1) {
|
|
1029
|
+
session = http2.connect(baseUrl);
|
|
1030
|
+
this.session = session;
|
|
1031
|
+
// The run request is buffered until the HTTP/2 session connects. Failures before `connect`
|
|
1032
|
+
// (DNS, ECONNREFUSED, TLS, connect timeout) mean the server never received the request, so they
|
|
1033
|
+
// are safe to retry. Once connected, bytes flush to the server and the turn must not be replayed.
|
|
1034
|
+
session.on("connect", () => {
|
|
1035
|
+
this.committed = true;
|
|
1036
|
+
debugProviderDiagnostic("cursor", "connected", {
|
|
1037
|
+
transport: "http2",
|
|
1038
|
+
connectMs: Date.now() - this.turnStartedAt,
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
stream = session.request({
|
|
1042
|
+
":method": "POST",
|
|
1043
|
+
":path": CURSOR_RUN_PATH,
|
|
1044
|
+
"content-type": "application/connect+proto",
|
|
1045
|
+
"connect-protocol-version": "1",
|
|
1046
|
+
te: "trailers",
|
|
1047
|
+
authorization: `Bearer ${this.token}`,
|
|
1048
|
+
"x-ghost-mode": "true",
|
|
1049
|
+
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
1050
|
+
"x-cursor-client-type": "cli",
|
|
1051
|
+
"x-request-id": requestId,
|
|
1052
|
+
"x-session-id": this.sessionId,
|
|
1053
|
+
});
|
|
1054
|
+
this.stream = stream;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Single-shot terminal owner for this turn (createTerminalSettler): stream error, session
|
|
1058
|
+
// error, trailers, end, abort, and the first-frame timeout all race into it, and only the
|
|
1059
|
+
// first wins. The first-frame timer is cleared by any settlement so it can never leak.
|
|
1060
|
+
const settler = createTerminalSettler({
|
|
1061
|
+
fail,
|
|
1062
|
+
finish,
|
|
1063
|
+
clearTimer: () => {
|
|
1064
|
+
this.clearFirstFrameTimer();
|
|
1065
|
+
this.clearStreamHealthTimer();
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
const failAndClear = (error: Error) => {
|
|
1069
|
+
releaseBacklogLease();
|
|
1070
|
+
if (this.expectedClose) {
|
|
1071
|
+
// We already emitted a terminal `done` and cancelled the run (client-tool suspension). The
|
|
1072
|
+
// RST_STREAM CANCEL surfaces here as a stream error/abort; it is expected, not a failure.
|
|
1073
|
+
debugProviderDiagnostic("cursor", "stream-cancel-expected", {
|
|
1074
|
+
code: (error as { code?: unknown }).code,
|
|
1075
|
+
message: redactCursorForLog(error.message),
|
|
1076
|
+
framesReceived: this.framesReceived,
|
|
1077
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
1078
|
+
});
|
|
1079
|
+
settler.settleFinish();
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
settler.settleFail(error);
|
|
1083
|
+
};
|
|
1084
|
+
// Session-level errors (TLS/socket/GOAWAY) do not always propagate to the stream listener;
|
|
1085
|
+
// without this handler they could bypass orderly failure reporting entirely.
|
|
1086
|
+
const onSessionError = (err: unknown) => {
|
|
1087
|
+
const realErr = err instanceof Error ? err : new Error(String(err));
|
|
1088
|
+
debugProviderDiagnostic("cursor", "session-error", {
|
|
1089
|
+
code: String((realErr as { code?: unknown }).code ?? ""),
|
|
1090
|
+
message: redactCursorForLog(realErr.message),
|
|
1091
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
1092
|
+
});
|
|
1093
|
+
failAndClear(realErr);
|
|
1094
|
+
};
|
|
1095
|
+
this.firstFrameTimer = setTimeout(() => {
|
|
1096
|
+
this.firstFrameTimer = undefined;
|
|
1097
|
+
debugProviderDiagnostic("cursor", "first-frame-timeout", { timeoutMs: this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS });
|
|
1098
|
+
try { stream?.close(); } catch { /* already closing */ }
|
|
1099
|
+
try { session?.close(); } catch { /* already closing */ }
|
|
1100
|
+
this.http1Connection?.close();
|
|
1101
|
+
if (stream && session) {
|
|
1102
|
+
// close() waits for in-flight frames; a dead socket can ignore it — force-destroy shortly
|
|
1103
|
+
// after so a stalled TLS session cannot linger past the timeout.
|
|
1104
|
+
armTimeoutDestroyFallback(stream, session, this.input.timeoutDestroyGraceMs ?? CURSOR_TIMEOUT_DESTROY_GRACE_MS);
|
|
1105
|
+
}
|
|
1106
|
+
releaseBacklogLease();
|
|
1107
|
+
settler.settleFail(new Error("Cursor transport timed out before first response"));
|
|
1108
|
+
}, this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS);
|
|
1109
|
+
|
|
1110
|
+
// Raw Connect backlog with a parse cursor: appends copy only the incoming
|
|
1111
|
+
// chunk (amortized capacity growth), the consumed prefix is reclaimed
|
|
1112
|
+
// lazily, and the RAW used length (headers included) is what the 32 MiB
|
|
1113
|
+
// transport cap bounds — payload-only accounting let tiny-frame/header
|
|
1114
|
+
// floods slip through.
|
|
1115
|
+
let backlog = new Uint8Array();
|
|
1116
|
+
let backlogStart = 0;
|
|
1117
|
+
let backlogEnd = 0;
|
|
1118
|
+
const BACKLOG_COMPACT_MIN_SAVINGS = 64 * 1024;
|
|
1119
|
+
const appendBacklog = (chunk: Uint8Array): void => {
|
|
1120
|
+
const used = backlogEnd - backlogStart;
|
|
1121
|
+
let start = backlogStart;
|
|
1122
|
+
let end = backlogEnd;
|
|
1123
|
+
// Reclaim the consumed prefix when it is large or needed for capacity.
|
|
1124
|
+
if (start > 0 && (start >= BACKLOG_COMPACT_MIN_SAVINGS || end + chunk.byteLength > backlog.byteLength)) {
|
|
1125
|
+
backlog = backlog.slice(start, end);
|
|
1126
|
+
start = 0;
|
|
1127
|
+
end = used;
|
|
1128
|
+
}
|
|
1129
|
+
if (end + chunk.byteLength > backlog.byteLength) {
|
|
1130
|
+
const capacity = Math.max(8192, backlog.byteLength * 2, end + chunk.byteLength);
|
|
1131
|
+
const next = new Uint8Array(Math.min(CURSOR_TRANSPORT_MAX_BUFFERED_BYTES, capacity));
|
|
1132
|
+
next.set(backlog.subarray(start, end), 0);
|
|
1133
|
+
backlog = next;
|
|
1134
|
+
}
|
|
1135
|
+
backlog.set(chunk, end);
|
|
1136
|
+
backlogStart = start;
|
|
1137
|
+
backlogEnd = end + chunk.byteLength;
|
|
1138
|
+
};
|
|
1139
|
+
let frameWork: Promise<void> = Promise.resolve();
|
|
1140
|
+
// Idempotent terminal owner for the backlog lease: every settle/close path
|
|
1141
|
+
// must leave the raw charge at zero instead of relying on budget disposal.
|
|
1142
|
+
let backlogLeaseReleased = false;
|
|
1143
|
+
const releaseBacklogLease = () => {
|
|
1144
|
+
if (backlogLeaseReleased) return;
|
|
1145
|
+
backlogLeaseReleased = true;
|
|
1146
|
+
const leftover = backlogEnd - backlogStart;
|
|
1147
|
+
if (leftover > 0) this.releaseTransportBytes(leftover);
|
|
1148
|
+
backlog = new Uint8Array();
|
|
1149
|
+
backlogStart = 0;
|
|
1150
|
+
backlogEnd = 0;
|
|
1151
|
+
};
|
|
1152
|
+
const handleFrame = async (frame: ReturnType<typeof consumeConnectFrames>["frames"][number]) => {
|
|
1153
|
+
this.framesReceived++;
|
|
1154
|
+
if ((frame.flags & CONNECT_FLAG_END_STREAM) === CONNECT_FLAG_END_STREAM) {
|
|
1155
|
+
const endError = parseConnectEndStreamError(frame.payload);
|
|
1156
|
+
debugProviderDiagnostic("cursor", "connect-end-stream", endError ? {
|
|
1157
|
+
code: cursorConnectErrorCode(frame.payload),
|
|
1158
|
+
message: redactCursorForLog(endError.message),
|
|
1159
|
+
classified: classifyCursorError(endError.message),
|
|
1160
|
+
framesReceived: this.framesReceived,
|
|
1161
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
1162
|
+
} : { framesReceived: this.framesReceived, elapsedMs: Date.now() - this.turnStartedAt });
|
|
1163
|
+
if (endError) {
|
|
1164
|
+
failAndClear(endError);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
// Connect's clean END_STREAM envelope is the protocol terminal. Cursor's RunSSE body can
|
|
1168
|
+
// remain open after this frame (or close through an AbortError), so waiting for HTTP EOF
|
|
1169
|
+
// strands an otherwise completed turn until the outer bridge stall watchdog fires.
|
|
1170
|
+
//
|
|
1171
|
+
// Earlier frames in this serialized frameWork chain have already run. Preserve their real
|
|
1172
|
+
// turnEnded terminal when present; otherwise finalize the clean protocol end once so open
|
|
1173
|
+
// tool calls still fail closed, a text-only turn receives its normal done event, and a
|
|
1174
|
+
// drained client-tool turn does not lose the pending terminal when protocol cleanup clears
|
|
1175
|
+
// its grace timer.
|
|
1176
|
+
const hasPendingClientToolFinalization = this.pendingFinalize !== undefined;
|
|
1177
|
+
if (
|
|
1178
|
+
!this.expectedClose
|
|
1179
|
+
&& !state.terminated
|
|
1180
|
+
&& !this.emittedTerminal
|
|
1181
|
+
&& (
|
|
1182
|
+
state.openToolCalls.size > 0
|
|
1183
|
+
|| this.sawAssistantText
|
|
1184
|
+
|| hasPendingClientToolFinalization
|
|
1185
|
+
)
|
|
1186
|
+
) {
|
|
1187
|
+
const terminal = hasPendingClientToolFinalization && state.openToolCalls.size === 0
|
|
1188
|
+
? finalizeAfterDrain(state)
|
|
1189
|
+
: finalizeTurnEvents(state);
|
|
1190
|
+
for (const event of terminal) push(event);
|
|
1191
|
+
}
|
|
1192
|
+
this.markProtocolComplete();
|
|
1193
|
+
releaseBacklogLease();
|
|
1194
|
+
settler.settleFinish();
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const decoded = fromBinary(AgentServerMessageSchema, frame.payload);
|
|
1198
|
+
// T04: every decoded frame refreshes the silence clock; only non-liveness frames
|
|
1199
|
+
// refresh the progress clock. First decoded frame arms the watchdog (the first-frame
|
|
1200
|
+
// timer owned everything before this point).
|
|
1201
|
+
const decodedUpdate = decoded.message.case === "interactionUpdate" ? decoded.message.value.message?.case : undefined;
|
|
1202
|
+
const livenessOnly = decodedUpdate === "heartbeat" || decoded.message.case === "conversationCheckpointUpdate";
|
|
1203
|
+
if (!this.streamHealthFail) {
|
|
1204
|
+
const now = Date.now();
|
|
1205
|
+
this.lastInboundFrameAt = now;
|
|
1206
|
+
this.lastMeaningfulFrameAt = now;
|
|
1207
|
+
this.streamHealthFail = failAndClear;
|
|
1208
|
+
}
|
|
1209
|
+
this.noteInboundFrame(livenessOnly);
|
|
1210
|
+
await this.handleServerMessage(decoded, state, push);
|
|
1211
|
+
};
|
|
1212
|
+
const drainPendingFrames = () => {
|
|
1213
|
+
const availableSlots = CURSOR_MAX_PENDING_FRAMES - this.pendingTransportFrames;
|
|
1214
|
+
const used = backlogEnd - backlogStart;
|
|
1215
|
+
if (availableSlots <= 0 || used === 0) {
|
|
1216
|
+
this.updateTransportFlowControl();
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
// Cursor decode, zero-copy: frame payloads are views into the backlog, so
|
|
1220
|
+
// the charge TRANSFERS — only consumed header bytes leave the counter
|
|
1221
|
+
// (payload bytes stay charged and are released when each frame's work
|
|
1222
|
+
// finishes). An exact 16 MiB payload therefore peaks at 16 MiB + 5, not
|
|
1223
|
+
// at double its size.
|
|
1224
|
+
const decoded = consumeConnectFrames(
|
|
1225
|
+
backlog.subarray(backlogStart, backlogEnd),
|
|
1226
|
+
CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
|
|
1227
|
+
availableSlots,
|
|
1228
|
+
);
|
|
1229
|
+
if (decoded.frames.length > 0) {
|
|
1230
|
+
backlogStart += decoded.consumedBytes;
|
|
1231
|
+
this.releaseTransportBytes(decoded.consumedBytes - decoded.frames.reduce((n, frame) => n + frame.payload.byteLength, 0));
|
|
1232
|
+
}
|
|
1233
|
+
for (const frame of decoded.frames) {
|
|
1234
|
+
this.pendingTransportFrames += 1;
|
|
1235
|
+
this.updateTransportFlowControl();
|
|
1236
|
+
frameWork = frameWork
|
|
1237
|
+
.then(() => handleFrame(frame))
|
|
1238
|
+
.catch(err => failAndClear(err instanceof Error ? err : new Error(String(err))))
|
|
1239
|
+
.finally(() => {
|
|
1240
|
+
this.releaseTransportBytes(frame.payload.byteLength);
|
|
1241
|
+
this.pendingTransportFrames = Math.max(0, this.pendingTransportFrames - 1);
|
|
1242
|
+
this.updateTransportFlowControl();
|
|
1243
|
+
drainPendingFrames();
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
const onData = (chunk: string | Uint8Array) => {
|
|
1248
|
+
this.clearFirstFrameTimer();
|
|
1249
|
+
// Once the turn has settled, late network bytes must never be charged —
|
|
1250
|
+
// the backlog lease is already released and nobody would own these.
|
|
1251
|
+
if (settler.settled()) return;
|
|
1252
|
+
if (!this.firstFrameLogged) {
|
|
1253
|
+
this.firstFrameLogged = true;
|
|
1254
|
+
this.firstFrameAt = Date.now();
|
|
1255
|
+
debugProviderDiagnostic("cursor", "first-frame", { latencyMs: this.firstFrameAt - this.turnStartedAt });
|
|
1256
|
+
}
|
|
1257
|
+
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
1258
|
+
let charged = false;
|
|
1259
|
+
let appended = false;
|
|
1260
|
+
try {
|
|
1261
|
+
// RAW chunk bytes (headers included) join the backlog charge; consumed
|
|
1262
|
+
// bytes leave it at drain. No whole-backlog replacement copy anymore.
|
|
1263
|
+
this.reserveTransportBytes(bytes.byteLength);
|
|
1264
|
+
charged = true;
|
|
1265
|
+
appendBacklog(bytes);
|
|
1266
|
+
appended = true;
|
|
1267
|
+
drainPendingFrames();
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
// Release ONLY when the reservation succeeded but the append never
|
|
1270
|
+
// happened. A failed reservation charged nothing — releasing here
|
|
1271
|
+
// would debit unrelated existing ownership; an appended chunk is owned
|
|
1272
|
+
// by the terminal backlog cleanup.
|
|
1273
|
+
if (charged && !appended) this.releaseTransportBytes(bytes.byteLength);
|
|
1274
|
+
failAndClear(err instanceof Error ? err : new Error(String(err)));
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
const onTrailers = (trailers: http2.IncomingHttpHeaders) => {
|
|
1278
|
+
const status = trailers["grpc-status"];
|
|
1279
|
+
if (status !== undefined) debugProviderDiagnostic("cursor", "trailers", { grpcStatus: String(status) });
|
|
1280
|
+
if (status && status !== "0") failAndClear(new Error(`Cursor gRPC error ${status}`));
|
|
1281
|
+
};
|
|
1282
|
+
const onStreamError = (err: unknown) => {
|
|
1283
|
+
const realErr = err instanceof Error ? err : new Error(String(err));
|
|
1284
|
+
if (this.expectedClose) {
|
|
1285
|
+
failAndClear(realErr);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const code = (realErr as { code?: unknown }).code;
|
|
1289
|
+
const errno = (realErr as { errno?: unknown }).errno;
|
|
1290
|
+
debugProviderDiagnostic("cursor", "stream-error", {
|
|
1291
|
+
code: typeof code === "string" || typeof code === "number" ? String(code) : undefined,
|
|
1292
|
+
errno: typeof errno === "string" || typeof errno === "number" ? String(errno) : undefined,
|
|
1293
|
+
name: realErr.name,
|
|
1294
|
+
message: redactCursorForLog(realErr.message),
|
|
1295
|
+
committed: this.committed,
|
|
1296
|
+
framesReceived: this.framesReceived,
|
|
1297
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
1298
|
+
});
|
|
1299
|
+
failAndClear(realErr);
|
|
1300
|
+
};
|
|
1301
|
+
const onStreamEnd = () => {
|
|
1302
|
+
this.clearFirstFrameTimer();
|
|
1303
|
+
debugProviderDiagnostic("cursor", "stream-end", {
|
|
1304
|
+
committed: this.committed,
|
|
1305
|
+
framesReceived: this.framesReceived,
|
|
1306
|
+
expectedClose: this.expectedClose,
|
|
1307
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
1308
|
+
});
|
|
1309
|
+
// Settle only after queued frame work drains to quiescence (the chain can
|
|
1310
|
+
// extend itself while draining), then classify the terminal state:
|
|
1311
|
+
// a trailing incomplete frame is a typed failure, and a zero-frame,
|
|
1312
|
+
// zero-byte end without an expected close stays the existing unexpected
|
|
1313
|
+
// EOF — both beat the old silent success.
|
|
1314
|
+
void (async () => {
|
|
1315
|
+
let previous: Promise<void>;
|
|
1316
|
+
do {
|
|
1317
|
+
previous = frameWork;
|
|
1318
|
+
await previous;
|
|
1319
|
+
} while (previous !== frameWork);
|
|
1320
|
+
})().then(() => {
|
|
1321
|
+
if (settler.settled()) return;
|
|
1322
|
+
const leftover = backlogEnd - backlogStart;
|
|
1323
|
+
if (leftover > 0 && !this.expectedClose) {
|
|
1324
|
+
releaseBacklogLease();
|
|
1325
|
+
settler.settleFail(new ConnectFrameError(
|
|
1326
|
+
"frame_incomplete",
|
|
1327
|
+
`Cursor Connect stream ended with ${leftover} unconsumed bytes (incomplete frame)`,
|
|
1328
|
+
));
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (this.framesReceived === 0 && !this.expectedClose) {
|
|
1332
|
+
releaseBacklogLease();
|
|
1333
|
+
settler.settleFail(new Error("Cursor stream ended before any response frame (unexpected EOF)"));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
// `emittedTerminal` joins dev's two conditions so EOF finalization cannot append a
|
|
1337
|
+
// second terminal after a mapper error already failed the turn (integration 010).
|
|
1338
|
+
if (state.terminated || this.expectedClose || this.emittedTerminal) {
|
|
1339
|
+
releaseBacklogLease();
|
|
1340
|
+
settler.settleFinish();
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
// Open tools fail-closed as a truncation *event* (finalizeTurnEvents), not a thrown
|
|
1344
|
+
// transport error. settleFail here would hide that typed message as adapter_eof.
|
|
1345
|
+
if (state.openToolCalls.size > 0) {
|
|
1346
|
+
for (const event of finalizeTurnEvents(state)) push(event);
|
|
1347
|
+
releaseBacklogLease();
|
|
1348
|
+
settler.settleFinish();
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (this.framesReceived > 0 && this.sawAssistantText) {
|
|
1352
|
+
for (const event of finalizeTurnEvents(state)) push(event);
|
|
1353
|
+
releaseBacklogLease();
|
|
1354
|
+
settler.settleFinish();
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
releaseBacklogLease();
|
|
1358
|
+
settler.settleFinish();
|
|
1359
|
+
}).catch((err) => {
|
|
1360
|
+
failAndClear(err instanceof Error ? err : new Error(String(err)));
|
|
1361
|
+
});
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
if (useHttp1) {
|
|
1365
|
+
const providerFetch = this.input.fetch
|
|
1366
|
+
?? (this.input.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch;
|
|
1367
|
+
this.http1Connection = new CursorHttp1BidiConnection({
|
|
1368
|
+
baseUrl,
|
|
1369
|
+
token: this.token,
|
|
1370
|
+
clientVersion: CURSOR_CLIENT_VERSION,
|
|
1371
|
+
sessionId: this.sessionId,
|
|
1372
|
+
requestId,
|
|
1373
|
+
translatorBudget: this.translatorBudget,
|
|
1374
|
+
callbacks: {
|
|
1375
|
+
onCommitted: () => {
|
|
1376
|
+
this.committed = true;
|
|
1377
|
+
debugProviderDiagnostic("cursor", "connected", {
|
|
1378
|
+
transport: "http1.1",
|
|
1379
|
+
connectMs: Date.now() - this.turnStartedAt,
|
|
1380
|
+
});
|
|
1381
|
+
},
|
|
1382
|
+
onData,
|
|
1383
|
+
onEnd: onStreamEnd,
|
|
1384
|
+
onError: onStreamError,
|
|
1385
|
+
},
|
|
1386
|
+
...(providerFetch ? { fetch: providerFetch } : {}),
|
|
1387
|
+
});
|
|
1388
|
+
this.http1Connection.start();
|
|
1389
|
+
} else {
|
|
1390
|
+
session!.on("error", onSessionError);
|
|
1391
|
+
stream!.on("data", onData);
|
|
1392
|
+
stream!.on("trailers", onTrailers);
|
|
1393
|
+
stream!.on("error", onStreamError);
|
|
1394
|
+
stream!.on("end", onStreamEnd);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
signal?.addEventListener("abort", () => {
|
|
1398
|
+
this.close();
|
|
1399
|
+
failAndClear(new Error("Cursor request was aborted"));
|
|
1400
|
+
}, { once: true });
|
|
1401
|
+
// Close the race between the preflight above and listener installation. No request payload is
|
|
1402
|
+
// written until after this check.
|
|
1403
|
+
if (signal?.aborted) {
|
|
1404
|
+
this.close();
|
|
1405
|
+
failAndClear(signal.reason instanceof Error ? signal.reason : new Error("Cursor request was aborted"));
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
this.writeConnectFrame(encodeConnectFrame(encodedRequest));
|
|
1410
|
+
this.heartbeat = setInterval(() => {
|
|
1411
|
+
this.writeConnectFrame(encodeClientMessage({
|
|
1412
|
+
message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
|
|
1413
|
+
}));
|
|
1414
|
+
}, HEARTBEAT_MS);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
capturedConversationCheckpoint(): Uint8Array | undefined {
|
|
1418
|
+
return this.capturedCheckpointBytes;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
private async handleServerMessage(
|
|
1422
|
+
message: AgentServerMessage,
|
|
1423
|
+
state: ReturnType<typeof createCursorProtobufEventState>,
|
|
1424
|
+
push: (message: CursorServerMessage) => void,
|
|
1425
|
+
): Promise<void> {
|
|
1426
|
+
if (!this.stream && !this.http1Connection) return;
|
|
1427
|
+
debugProviderDiagnostic("cursor", "frame", describeCursorServerFrame(message));
|
|
1428
|
+
if (message.message.case === "conversationCheckpointUpdate") {
|
|
1429
|
+
try {
|
|
1430
|
+
this.capturedCheckpointBytes = toBinary(ConversationStateStructureSchema, message.message.value);
|
|
1431
|
+
} catch {
|
|
1432
|
+
this.capturedCheckpointBytes = undefined;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (message.message.case === "kvServerMessage") {
|
|
1436
|
+
this.writeConnectFrame(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (message.message.case === "execServerMessage") {
|
|
1440
|
+
const execMsg = message.message.value;
|
|
1441
|
+
if (execMsg.message.case === "mcpArgs") {
|
|
1442
|
+
const plan = planMcpArgsHandling(execMsg, state);
|
|
1443
|
+
if (plan.handledByResponsesBridge) {
|
|
1444
|
+
this.noteClientToolActivity();
|
|
1445
|
+
for (const event of plan.events) push(event);
|
|
1446
|
+
if (plan.cancelCursorRun) this.cancelCursorRun();
|
|
1447
|
+
else if (plan.finalizeWhenDrained) this.scheduleClientToolFinalize(state, push);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
// Native exec/MCP is handled inside this transport and can mutate files/process state
|
|
1452
|
+
// without emitting a Responses tool event. Mark the turn replay-unsafe before executing so
|
|
1453
|
+
// an eventual invalid_argument cannot cause the adapter's fresh-conversation fallback to
|
|
1454
|
+
// run the same local action twice.
|
|
1455
|
+
push({ type: "local_side_effect" });
|
|
1456
|
+
const replies = await handleCursorNativeExec(message.message.value, this.execContext);
|
|
1457
|
+
for (const reply of replies) this.writeConnectFrame(encodeConnectFrame(reply));
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (message.message.case === "interactionQuery") {
|
|
1461
|
+
// The server-side agent BLOCKS until this query is answered with the matching id; leaving it
|
|
1462
|
+
// unanswered is the proven stall → watchdog → upstream-502 mechanism. Reply immediately with
|
|
1463
|
+
// the non-interactive default and emit liveness so the bridge watchdog sees progress.
|
|
1464
|
+
const query = message.message.value;
|
|
1465
|
+
const plan = planInteractionQueryReply(query);
|
|
1466
|
+
debugProviderDiagnostic("cursor", "interaction-query", { id: query.id, queryCase: query.query.case ?? "unknown", reply: plan.replyCase });
|
|
1467
|
+
this.writeConnectFrame(encodeClientMessage({ message: { case: "interactionResponse", value: plan.response } }));
|
|
1468
|
+
if (!state.terminated) {
|
|
1469
|
+
if (plan.planText) {
|
|
1470
|
+
this.sawAssistantText = true;
|
|
1471
|
+
push({ type: "text", text: plan.planText });
|
|
1472
|
+
}
|
|
1473
|
+
push({ type: "heartbeat" });
|
|
1474
|
+
}
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
// A completion may carry only callId. Capture its ownership before mapping removes the open
|
|
1478
|
+
// call, because the embedded-tool classifier cannot identify that valid compact frame.
|
|
1479
|
+
const update = message.message.case === "interactionUpdate" ? message.message.value.message : undefined;
|
|
1480
|
+
if (update?.case === "turnEnded") {
|
|
1481
|
+
// T03: the application turn is complete. Close our side of HTTP/2 after a short
|
|
1482
|
+
// grace so a held-open server response cannot pin the turn to the bridge's idle
|
|
1483
|
+
// timeout (senpi #1062). finalizeTurnEvents already emitted done via the mapper.
|
|
1484
|
+
this.closeAfterTurnEnded();
|
|
1485
|
+
}
|
|
1486
|
+
const completesOpenClientTool = update?.case === "toolCallCompleted"
|
|
1487
|
+
&& state.openToolCalls.has(update.value.callId);
|
|
1488
|
+
const awaitedNativeArgsBeforeMapping = update?.case === "toolCallCompleted"
|
|
1489
|
+
&& state.openToolCalls.get(update.value.callId)?.awaitingNativeArgs === true;
|
|
1490
|
+
const mapped = mapCursorProtobufServerMessage(message, state);
|
|
1491
|
+
if (mapped.some(event => event.type === "text")) this.sawAssistantText = true;
|
|
1492
|
+
const beganAwaitingNativeClientToolArgs = update?.case === "toolCallCompleted"
|
|
1493
|
+
&& !awaitedNativeArgsBeforeMapping
|
|
1494
|
+
&& state.openToolCalls.get(update.value.callId)?.awaitingNativeArgs === true;
|
|
1495
|
+
if (mapped.length > 0) {
|
|
1496
|
+
// A client tool call announced/committed via interactionUpdate (toolCallStarted/partialToolCall/
|
|
1497
|
+
// toolCallCompleted) changes the call set, so revoke any finalize armed by an earlier drain.
|
|
1498
|
+
// A completion can also commit and drain a late call without a following mcpArgs frame; in
|
|
1499
|
+
// that case re-arm finalization here so the Responses bridge does not wait forever.
|
|
1500
|
+
const clientToolFrame = completesOpenClientTool || isClientToolFrame(message);
|
|
1501
|
+
if (clientToolFrame) this.noteClientToolActivity();
|
|
1502
|
+
for (const event of mapped) push(event);
|
|
1503
|
+
if (
|
|
1504
|
+
clientToolFrame
|
|
1505
|
+
&& state.openToolCalls.size === 0
|
|
1506
|
+
&& mapped.some(event => event.type === "tool_call_end")
|
|
1507
|
+
) this.scheduleClientToolFinalize(state, push);
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
// The frame produced no outward Responses event (e.g. toolCallStarted / partialToolCall args
|
|
1511
|
+
// buffering, a completion waiting for native args, toolCallDelta, tokenDelta, or a checkpoint
|
|
1512
|
+
// update). Tool-call protocol events are deferred to completion for atomic, parallel-safe
|
|
1513
|
+
// emission, so a turn that silently assembles several tool calls can otherwise exceed the
|
|
1514
|
+
// bridge's stall watchdog (upstream_stall_timeout).
|
|
1515
|
+
// Emit a liveness heartbeat for these progress frames so the watchdog sees the upstream is alive.
|
|
1516
|
+
// Never after a terminal (done/truncation): a stray post-terminal frame must stay fully inert.
|
|
1517
|
+
if (!state.terminated && (isCursorProgressFrame(message) || beganAwaitingNativeClientToolArgs)) {
|
|
1518
|
+
if (isClientToolFrame(message) || beganAwaitingNativeClientToolArgs) this.noteClientToolActivity();
|
|
1519
|
+
push({ type: "heartbeat" });
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Build the best-effort partial usage for a turn that failed before a clean `done` (upstream 502,
|
|
1526
|
+
* stream error, abort). Mirrors the clean-finalize math in `finalizeTurnEvents`: the last absolute
|
|
1527
|
+
* checkpoint (`contextTokens`) is the cumulative context, the streamed delta stays in outputTokens.
|
|
1528
|
+
* Returns undefined when the stream died before ANY token signal (nothing meaningful to report).
|
|
1529
|
+
* Exported for unit testing.
|
|
1530
|
+
*/
|
|
1531
|
+
export function partialUsageFromEventState(state: ReturnType<typeof createCursorProtobufEventState>): OcxUsage | undefined {
|
|
1532
|
+
const out = state.usage.outputTokens;
|
|
1533
|
+
const hasCurrentCheckpoint = Number.isFinite(state.contextTokens) && (state.contextTokens ?? 0) > 0;
|
|
1534
|
+
const hasCurrentOutput = Number.isFinite(out) && out > 0;
|
|
1535
|
+
// A carry-forward value belongs to an earlier successful turn. It can complete current-turn
|
|
1536
|
+
// usage math after this turn emits output, but cannot by itself prove that a first-frame failure
|
|
1537
|
+
// consumed anything.
|
|
1538
|
+
if (!hasCurrentCheckpoint && !hasCurrentOutput) return undefined;
|
|
1539
|
+
// Same resolution order as a clean turn, so a failed turn does not silently drop
|
|
1540
|
+
// back to inputTokens=0 when only the request-local estimate is available (#373).
|
|
1541
|
+
return { ...resolvedTurnUsage(state), estimated: true };
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Attach partial usage to a transport failure so the adapter's error path can surface real token
|
|
1546
|
+
* consumption for 502/stall rows instead of `usageStatus: unreported` with 0 tokens.
|
|
1547
|
+
*/
|
|
1548
|
+
function attachPartialUsage(failure: Error, state: ReturnType<typeof createCursorProtobufEventState>): Error {
|
|
1549
|
+
const usage = partialUsageFromEventState(state);
|
|
1550
|
+
if (usage) (failure as Error & { partialUsage?: OcxUsage }).partialUsage = usage;
|
|
1551
|
+
return failure;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Compact frame descriptor for provider debug (`ocx debug provider on`): outer case plus the inner
|
|
1556
|
+
* interactionUpdate/exec case and tool-call union case when present. No payload content is logged.
|
|
1557
|
+
*/
|
|
1558
|
+
function describeCursorServerFrame(message: AgentServerMessage): Record<string, unknown> {
|
|
1559
|
+
const out: Record<string, unknown> = { case: message.message.case ?? "unknown" };
|
|
1560
|
+
if (message.message.case === "interactionUpdate") {
|
|
1561
|
+
const update = message.message.value.message;
|
|
1562
|
+
out.update = update.case ?? "unknown";
|
|
1563
|
+
if (update.case === "toolCallStarted" || update.case === "partialToolCall" || update.case === "toolCallCompleted") {
|
|
1564
|
+
out.toolCase = update.value.toolCall?.tool.case ?? "none";
|
|
1565
|
+
out.callId = update.value.callId;
|
|
1566
|
+
}
|
|
1567
|
+
} else if (message.message.case === "execServerMessage") {
|
|
1568
|
+
out.exec = message.message.value.message.case ?? "unknown";
|
|
1569
|
+
} else if (message.message.case === "interactionQuery") {
|
|
1570
|
+
out.query = message.message.value.query.case ?? "unknown";
|
|
1571
|
+
out.id = message.message.value.id;
|
|
1572
|
+
} else if (message.message.case === "kvServerMessage") {
|
|
1573
|
+
out.kv = message.message.value.message.case ?? "unknown";
|
|
1574
|
+
} else if (message.message.case === "conversationCheckpointUpdate") {
|
|
1575
|
+
out.usedTokens = message.message.value.tokenDetails?.usedTokens ?? 0;
|
|
1576
|
+
}
|
|
1577
|
+
return out;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* True when a server frame represents real upstream progress that produced no outward Responses
|
|
1582
|
+
* event (so the bridge's stall watchdog would otherwise see silence). Covers tool-call assembly,
|
|
1583
|
+
* token/checkpoint accounting — the frames `mapCursorProtobufServerMessage` intentionally swallows.
|
|
1584
|
+
*/
|
|
1585
|
+
function isCursorProgressFrame(message: AgentServerMessage): boolean {
|
|
1586
|
+
if (message.message.case === "conversationCheckpointUpdate") return true;
|
|
1587
|
+
if (message.message.case !== "interactionUpdate") return false;
|
|
1588
|
+
switch (message.message.value.message.case) {
|
|
1589
|
+
case "toolCallStarted":
|
|
1590
|
+
case "partialToolCall":
|
|
1591
|
+
case "toolCallDelta":
|
|
1592
|
+
case "tokenDelta":
|
|
1593
|
+
return true;
|
|
1594
|
+
default:
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* A tool-call lifecycle frame that can change the CLIENT tool call set (announce a new sibling or
|
|
1601
|
+
* commit one). Used to revoke a pending finalize so a late-announced parallel call is never dropped.
|
|
1602
|
+
* Only frames whose inner ToolCall is an ocx-bridged Responses tool (`mcpToolCall` with our provider)
|
|
1603
|
+
* count: Cursor-native tool frames (readToolCall/editToolCall/...) are display-plane and must not
|
|
1604
|
+
* revoke a pending client-tool finalize. Exported for unit testing.
|
|
1605
|
+
*/
|
|
1606
|
+
export function isClientToolFrame(message: AgentServerMessage): boolean {
|
|
1607
|
+
if (message.message.case !== "interactionUpdate") return false;
|
|
1608
|
+
const update = message.message.value.message;
|
|
1609
|
+
switch (update.case) {
|
|
1610
|
+
case "toolCallStarted":
|
|
1611
|
+
case "partialToolCall":
|
|
1612
|
+
case "toolCallCompleted":
|
|
1613
|
+
return mcpArgsFromToolCall(update.value.toolCall) !== undefined;
|
|
1614
|
+
default:
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/** Host-only label for Cursor transport diagnostics — never leaks path/query/credentials. */
|
|
1620
|
+
function cursorHostLabel(baseUrl: string): string {
|
|
1621
|
+
try {
|
|
1622
|
+
return new URL(baseUrl).host;
|
|
1623
|
+
} catch {
|
|
1624
|
+
return "cursor";
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/** Redact a Cursor error message for diagnostic output. Cursor error strings can carry raw
|
|
1629
|
+
* credential key=value pairs beyond what redactSecretString covers; safeCursorErrorMessage
|
|
1630
|
+
* already applies the full sanitizer plus the classified prefix, so reuse it verbatim. */
|
|
1631
|
+
function redactCursorForLog(message: string): string {
|
|
1632
|
+
return safeCursorErrorMessage(message).slice(0, 300);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/** Extract the Connect end-stream `error.code` from the raw trailer frame payload without
|
|
1636
|
+
* surfacing the (potentially secret-bearing) message — used for `[ocx:cursor:connect-end-stream]`
|
|
1637
|
+
* diagnostics. Returns undefined when the payload is not the expected Connect error shape. */
|
|
1638
|
+
function cursorConnectErrorCode(payload: Uint8Array): string | undefined {
|
|
1639
|
+
try {
|
|
1640
|
+
const parsed = JSON.parse(new TextDecoder().decode(payload)) as { error?: { code?: string } };
|
|
1641
|
+
return parsed?.error?.code;
|
|
1642
|
+
} catch {
|
|
1643
|
+
return undefined;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
export function createLiveCursorTransport(input: CursorTransportFactoryInput): CursorTransport {
|
|
1648
|
+
return new LiveCursorTransport(input);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
export function capturedCursorCheckpointBytes(transport: CursorTransport): Uint8Array | undefined {
|
|
1652
|
+
return transport.capturedConversationCheckpoint?.();
|
|
1653
|
+
}
|