@bitkyc08/opencodex 2.58.0 → 2.60.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/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -87,7 +87,7 @@ import {
|
|
|
87
87
|
type BackgroundShellTerminationReport,
|
|
88
88
|
} from "./native-exec-shell";
|
|
89
89
|
import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
|
|
90
|
-
import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
90
|
+
import type { CursorStreamHealthClock, CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
91
91
|
import { CursorHttp1BidiConnection } from "./http1-bidi";
|
|
92
92
|
import { isPinnedHttp1 } from "../../lib/upstream-http-version";
|
|
93
93
|
|
|
@@ -107,6 +107,16 @@ const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
|
|
|
107
107
|
* Reset on every decoded frame that is not liveness-only.
|
|
108
108
|
*/
|
|
109
109
|
const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000;
|
|
110
|
+
/**
|
|
111
|
+
* The production T04 clock. The watchdog reads time and schedules its one timer through this
|
|
112
|
+
* object so a test can supply a clock it advances by hand; nothing else about the transport's
|
|
113
|
+
* timers moves with it.
|
|
114
|
+
*/
|
|
115
|
+
const REAL_STREAM_HEALTH_CLOCK: CursorStreamHealthClock = {
|
|
116
|
+
now: () => Date.now(),
|
|
117
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
118
|
+
clearTimeout: timer => clearTimeout(timer),
|
|
119
|
+
};
|
|
110
120
|
/**
|
|
111
121
|
* After `turnEnded` is decoded, the application turn is complete. A server that keeps
|
|
112
122
|
* HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
|
|
@@ -494,6 +504,8 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
494
504
|
private lastInboundFrameAt = 0;
|
|
495
505
|
private lastMeaningfulFrameAt = 0;
|
|
496
506
|
private streamHealthFail?: (error: Error) => void;
|
|
507
|
+
/** Time source for the T04 watchdog only; the globals unless a test injects one. */
|
|
508
|
+
private readonly streamHealthClock: CursorStreamHealthClock;
|
|
497
509
|
private committed = false;
|
|
498
510
|
private expectedClose = false;
|
|
499
511
|
/**
|
|
@@ -542,6 +554,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
542
554
|
// so the transport-level race test can drive it deterministically.
|
|
543
555
|
this.clientToolFinalizeGraceMs = input.clientToolFinalizeGraceMs ?? CLIENT_TOOL_FINALIZE_GRACE_MS;
|
|
544
556
|
this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
|
|
557
|
+
this.streamHealthClock = input.streamHealthClock ?? REAL_STREAM_HEALTH_CLOCK;
|
|
545
558
|
// Desktop (computer-use / record-screen) executors are available even with no MCP servers.
|
|
546
559
|
this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
|
|
547
560
|
this.execContext = {
|
|
@@ -847,7 +860,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
847
860
|
|
|
848
861
|
private clearStreamHealthTimer(): void {
|
|
849
862
|
if (this.streamHealthTimer) {
|
|
850
|
-
clearTimeout(this.streamHealthTimer);
|
|
863
|
+
this.streamHealthClock.clearTimeout(this.streamHealthTimer);
|
|
851
864
|
this.streamHealthTimer = undefined;
|
|
852
865
|
}
|
|
853
866
|
this.streamHealthFail = undefined;
|
|
@@ -858,24 +871,28 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
858
871
|
* failAndClear; the timer owns nothing else. Never armed before the first decoded
|
|
859
872
|
* frame (the first-frame timer covers dial + first response), and disarmed by
|
|
860
873
|
* every settle / expected-close path alongside the other timers.
|
|
874
|
+
*
|
|
875
|
+
* Both clocks and the timer go through `streamHealthClock`, which is the globals in
|
|
876
|
+
* production. The `elapsedMs` diagnostic below deliberately stays on the wall clock,
|
|
877
|
+
* because `turnStartedAt` is stamped there and the pair has to subtract coherently.
|
|
861
878
|
*/
|
|
862
879
|
private armStreamHealthTimer(fail: (error: Error) => void): void {
|
|
863
|
-
if (this.streamHealthTimer) clearTimeout(this.streamHealthTimer);
|
|
880
|
+
if (this.streamHealthTimer) this.streamHealthClock.clearTimeout(this.streamHealthTimer);
|
|
864
881
|
if (this.expectedClose) return;
|
|
865
882
|
this.streamHealthFail = fail;
|
|
866
883
|
const silenceMs = this.input.streamSilenceFailMs ?? CURSOR_STREAM_SILENCE_FAIL_MS;
|
|
867
884
|
const heartbeatOnlyMs = this.input.streamHeartbeatOnlyFailMs ?? CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS;
|
|
868
|
-
const now =
|
|
885
|
+
const now = this.streamHealthClock.now();
|
|
869
886
|
const deadline = Math.min(
|
|
870
887
|
this.lastInboundFrameAt + silenceMs,
|
|
871
888
|
this.lastMeaningfulFrameAt + heartbeatOnlyMs,
|
|
872
889
|
);
|
|
873
|
-
this.streamHealthTimer = setTimeout(() => {
|
|
890
|
+
this.streamHealthTimer = this.streamHealthClock.setTimeout(() => {
|
|
874
891
|
this.streamHealthTimer = undefined;
|
|
875
892
|
const failFn = this.streamHealthFail;
|
|
876
893
|
if (!failFn || this.expectedClose) return;
|
|
877
|
-
const stalledFor =
|
|
878
|
-
const meaningfulStalledFor =
|
|
894
|
+
const stalledFor = this.streamHealthClock.now() - this.lastInboundFrameAt;
|
|
895
|
+
const meaningfulStalledFor = this.streamHealthClock.now() - this.lastMeaningfulFrameAt;
|
|
879
896
|
if (stalledFor < silenceMs && meaningfulStalledFor < heartbeatOnlyMs) {
|
|
880
897
|
// A frame landed between arming and firing — re-arm for the fresh deadline.
|
|
881
898
|
this.armStreamHealthTimer(failFn);
|
|
@@ -906,7 +923,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
906
923
|
* heartbeat-only threshold.
|
|
907
924
|
*/
|
|
908
925
|
private noteInboundFrame(livenessOnly: boolean): void {
|
|
909
|
-
const now =
|
|
926
|
+
const now = this.streamHealthClock.now();
|
|
910
927
|
this.lastInboundFrameAt = now;
|
|
911
928
|
if (!livenessOnly) this.lastMeaningfulFrameAt = now;
|
|
912
929
|
if (this.streamHealthFail) this.armStreamHealthTimer(this.streamHealthFail);
|
|
@@ -1288,7 +1305,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1288
1305
|
const decodedUpdate = decoded.message.case === "interactionUpdate" ? decoded.message.value.message?.case : undefined;
|
|
1289
1306
|
const livenessOnly = decodedUpdate === "heartbeat" || decoded.message.case === "conversationCheckpointUpdate";
|
|
1290
1307
|
if (!this.streamHealthFail) {
|
|
1291
|
-
const now =
|
|
1308
|
+
const now = this.streamHealthClock.now();
|
|
1292
1309
|
this.lastInboundFrameAt = now;
|
|
1293
1310
|
this.lastMeaningfulFrameAt = now;
|
|
1294
1311
|
this.streamHealthFail = failAndClear;
|
|
@@ -46,7 +46,8 @@ export function mapCursorServerMessage(
|
|
|
46
46
|
state.writeClient(cursorExecResult(message.requestId, message.execCase));
|
|
47
47
|
return [];
|
|
48
48
|
case "local_side_effect":
|
|
49
|
-
// Internal retry-safety signal only; keep the bridge alive without producing protocol output
|
|
50
|
-
|
|
49
|
+
// Internal retry-safety signal only; keep the bridge alive without producing protocol output,
|
|
50
|
+
// while preventing server-level failover from replaying the completed local operation.
|
|
51
|
+
return [{ type: "heartbeat", replayUnsafe: true }];
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -6,6 +6,7 @@ import { namespacedToolName } from "../../types";
|
|
|
6
6
|
import type { CursorRunRequest } from "./types";
|
|
7
7
|
import { decodeCursorCallId } from "./call-id";
|
|
8
8
|
import { cursorNeedsExternalToolContinuation, isCursorExternalWireModel } from "./discovery";
|
|
9
|
+
import { stripAssistantEchoedToolEnvelope } from "./envelope-echo";
|
|
9
10
|
import { normalizeCursorToolResultText } from "./tool-result-normalize";
|
|
10
11
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
11
12
|
import {
|
|
@@ -208,11 +209,13 @@ function assistantRootText(
|
|
|
208
209
|
message: Extract<OcxMessage, { role: "assistant" }>,
|
|
209
210
|
includeThinking: boolean,
|
|
210
211
|
): string {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
const raw = typeof message.content === "string"
|
|
213
|
+
? message.content
|
|
214
|
+
: message.content
|
|
215
|
+
.map(part => (part.type === "text" ? part.text : includeThinking && part.type === "thinking" ? part.thinking : undefined))
|
|
216
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
217
|
+
.join("\n");
|
|
218
|
+
return stripAssistantEchoedToolEnvelope(raw);
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
// Cursor builds the actual model prompt from rootPromptMessagesJson (turns[] is UI/display metadata),
|
|
@@ -340,7 +340,13 @@ export function cursorConversationIdFromClientThread(threadId: string, identityS
|
|
|
340
340
|
|
|
341
341
|
/**
|
|
342
342
|
* Resolve the Cursor conversation id for this turn.
|
|
343
|
-
* Priority: force-fresh → isolate helper →
|
|
343
|
+
* Priority: force-fresh → isolate helper → thread remint override → stored conversation id
|
|
344
|
+
* → client thread hash → random.
|
|
345
|
+
*
|
|
346
|
+
* The remint override must beat a stored `_cursorConversationId`. Only the remint path writes
|
|
347
|
+
* the thread store (cursor.ts), so a stored id that disagrees with it is the pre-remint value,
|
|
348
|
+
* and preferring it let a second Responses chain in the same Codex thread keep resuming the
|
|
349
|
+
* conversation the previous turn just rotated away from.
|
|
344
350
|
* Never use OpenAI Responses `previous_response_id` (resp_*) or shared `prompt_cache_key`
|
|
345
351
|
* (cache-cohort fingerprint, not conversation ownership).
|
|
346
352
|
*/
|
|
@@ -351,11 +357,16 @@ export function resolveCursorConversationId(
|
|
|
351
357
|
): string {
|
|
352
358
|
if (options.forceFreshConversation === true) return generatedCursorConversationId();
|
|
353
359
|
if (parsed._cursorIsolateConversation === true) return generatedCursorConversationId();
|
|
354
|
-
if (parsed._cursorConversationId) return parsed._cursorConversationId;
|
|
355
360
|
const threadId = cursorClientThreadOwner(parsed);
|
|
356
|
-
|
|
361
|
+
// A compaction turn carries its own conversation id and must not be pulled onto the parent's
|
|
362
|
+
// thread override. It is isolated in effect without ever setting the isolate flag, which is why
|
|
363
|
+
// the override check has to exclude it explicitly rather than rely on that flag.
|
|
364
|
+
if (threadId && parsed._compactionRequest !== true) {
|
|
357
365
|
const recovered = lookupCursorThreadConversation(threadId, parsed._cursorIdentityScope);
|
|
358
366
|
if (recovered) return recovered;
|
|
367
|
+
}
|
|
368
|
+
if (parsed._cursorConversationId) return parsed._cursorConversationId;
|
|
369
|
+
if (threadId) {
|
|
359
370
|
return cursorConversationIdFromClientThread(`thread:${threadId}`, parsed._cursorIdentityScope);
|
|
360
371
|
}
|
|
361
372
|
return generatedCursorConversationId();
|
|
@@ -465,7 +476,13 @@ function resolveCursorCheckpoint(
|
|
|
465
476
|
if (cursorCheckpointModelAffinityId(snapshot.modelId) !== cursorCheckpointModelAffinityId(request.modelId)) {
|
|
466
477
|
return { reason: "model_changed" };
|
|
467
478
|
}
|
|
468
|
-
|
|
479
|
+
// The continuation state only exists on the ref path; a ref-less prefix hit carries
|
|
480
|
+
// the suspension on the snapshot itself, or a non-toolResult request could resume
|
|
481
|
+
// bytes upstream serialized mid-tool-call.
|
|
482
|
+
if (
|
|
483
|
+
parsed.context.messages.at(-1)?.role !== "toolResult"
|
|
484
|
+
&& (snapshot.toolSuspended === true || cursorState?.checkpointUsable === false)
|
|
485
|
+
) {
|
|
469
486
|
return { reason: "trailing_tool_result" };
|
|
470
487
|
}
|
|
471
488
|
const lineage = lineageMismatch(parsed, snapshot);
|
|
@@ -169,21 +169,65 @@ type IncompleteToolRemintState = {
|
|
|
169
169
|
updatedAt: number;
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
/**
|
|
173
|
+
* One bounded next-turn remint allowance, keyed by retained thread scope.
|
|
174
|
+
*
|
|
175
|
+
* Each recovery reason owns its own instance. Sharing one budget would let a cheap, frequent
|
|
176
|
+
* failure spend the allowance that a rarer, more expensive recovery depends on.
|
|
177
|
+
*/
|
|
178
|
+
function createCursorRemintBudget(max: number, ttlMs: number, maxEntries: number) {
|
|
179
|
+
const byScope = new Map<string, IncompleteToolRemintState>();
|
|
173
180
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
incompleteToolRemintByScope.delete(scopeKey);
|
|
181
|
+
const prune = (at: number): void => {
|
|
182
|
+
for (const [scopeKey, entry] of byScope) {
|
|
183
|
+
if (at - entry.updatedAt > ttlMs) byScope.delete(scopeKey);
|
|
178
184
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
+
while (byScope.size > maxEntries) {
|
|
186
|
+
const oldest = byScope.keys().next().value;
|
|
187
|
+
if (oldest === undefined) break;
|
|
188
|
+
byScope.delete(oldest);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
/** Record one remint; returns false when this budget is exhausted. */
|
|
194
|
+
record(scopeKey: string): boolean {
|
|
195
|
+
const at = now();
|
|
196
|
+
prune(at);
|
|
197
|
+
const existing = byScope.get(scopeKey);
|
|
198
|
+
if (existing && existing.remintCount >= max) {
|
|
199
|
+
existing.updatedAt = at;
|
|
200
|
+
byScope.delete(scopeKey);
|
|
201
|
+
byScope.set(scopeKey, existing);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const entry = existing ?? { remintCount: 0, updatedAt: at };
|
|
205
|
+
entry.remintCount += 1;
|
|
206
|
+
entry.updatedAt = at;
|
|
207
|
+
byScope.delete(scopeKey);
|
|
208
|
+
byScope.set(scopeKey, entry);
|
|
209
|
+
prune(at);
|
|
210
|
+
return true;
|
|
211
|
+
},
|
|
212
|
+
clear(scopeKey: string): void {
|
|
213
|
+
byScope.delete(scopeKey);
|
|
214
|
+
},
|
|
215
|
+
clearForTests(): void {
|
|
216
|
+
byScope.clear();
|
|
217
|
+
},
|
|
218
|
+
countForTests(): number {
|
|
219
|
+
prune(now());
|
|
220
|
+
return byScope.size;
|
|
221
|
+
},
|
|
222
|
+
};
|
|
185
223
|
}
|
|
186
224
|
|
|
225
|
+
const incompleteToolRemintBudget = createCursorRemintBudget(
|
|
226
|
+
CURSOR_INCOMPLETE_TOOL_REMINT_MAX,
|
|
227
|
+
CURSOR_INCOMPLETE_TOOL_REMINT_TTL_MS,
|
|
228
|
+
CURSOR_INCOMPLETE_TOOL_REMINT_MAX_ENTRIES,
|
|
229
|
+
);
|
|
230
|
+
|
|
187
231
|
/** Incomplete-tool and overflow recovery share ownership scope, but keep independent budgets. */
|
|
188
232
|
export function cursorIncompleteToolRemintScopeKey(
|
|
189
233
|
threadOwner: string | undefined,
|
|
@@ -194,34 +238,64 @@ export function cursorIncompleteToolRemintScopeKey(
|
|
|
194
238
|
|
|
195
239
|
/** Record one incomplete-tool remint; returns false when the independent cap is exhausted. */
|
|
196
240
|
export function recordCursorIncompleteToolRemint(scopeKey: string): boolean {
|
|
197
|
-
|
|
198
|
-
pruneIncompleteToolRemints(at);
|
|
199
|
-
const existing = incompleteToolRemintByScope.get(scopeKey);
|
|
200
|
-
if (existing && existing.remintCount >= CURSOR_INCOMPLETE_TOOL_REMINT_MAX) {
|
|
201
|
-
existing.updatedAt = at;
|
|
202
|
-
incompleteToolRemintByScope.delete(scopeKey);
|
|
203
|
-
incompleteToolRemintByScope.set(scopeKey, existing);
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
const entry = existing ?? { remintCount: 0, updatedAt: at };
|
|
207
|
-
entry.remintCount += 1;
|
|
208
|
-
entry.updatedAt = at;
|
|
209
|
-
incompleteToolRemintByScope.delete(scopeKey);
|
|
210
|
-
incompleteToolRemintByScope.set(scopeKey, entry);
|
|
211
|
-
pruneIncompleteToolRemints(at);
|
|
212
|
-
return true;
|
|
241
|
+
return incompleteToolRemintBudget.record(scopeKey);
|
|
213
242
|
}
|
|
214
243
|
|
|
215
244
|
/** A clean turn replenishes this recovery without changing the overflow retry budget. */
|
|
216
245
|
export function clearCursorIncompleteToolRemint(scopeKey: string): void {
|
|
217
|
-
|
|
246
|
+
incompleteToolRemintBudget.clear(scopeKey);
|
|
218
247
|
}
|
|
219
248
|
|
|
220
249
|
export function clearCursorIncompleteToolRemintForTests(): void {
|
|
221
|
-
|
|
250
|
+
incompleteToolRemintBudget.clearForTests();
|
|
222
251
|
}
|
|
223
252
|
|
|
224
253
|
export function cursorIncompleteToolRemintCountForTests(): number {
|
|
225
|
-
|
|
226
|
-
|
|
254
|
+
return incompleteToolRemintBudget.countForTests();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Max next-turn rotations after a MID-STREAM envelope echo, per retained scope.
|
|
259
|
+
*
|
|
260
|
+
* Deliberately a separate budget from the incomplete-tool allowance. A mid-stream echo is a
|
|
261
|
+
* cheap, repeatable formatting failure, while an incomplete client-tool stream is a rarer
|
|
262
|
+
* structural one; on a shared counter a model that echoes every turn would spend the budget
|
|
263
|
+
* that incomplete-tool recovery depends on. Bounding it at all is the point: the echo has
|
|
264
|
+
* already reached the client and cannot be quarantined, so without a cap a persistently
|
|
265
|
+
* echoing model would remint the conversation on every single turn, forever.
|
|
266
|
+
*/
|
|
267
|
+
export const CURSOR_ENVELOPE_ECHO_REMINT_MAX = 3;
|
|
268
|
+
export const CURSOR_ENVELOPE_ECHO_REMINT_TTL_MS = CURSOR_OVERFLOW_REMINT_TTL_MS;
|
|
269
|
+
export const CURSOR_ENVELOPE_ECHO_REMINT_MAX_ENTRIES = CURSOR_OVERFLOW_REMINT_MAX_ENTRIES;
|
|
270
|
+
|
|
271
|
+
const envelopeEchoRemintBudget = createCursorRemintBudget(
|
|
272
|
+
CURSOR_ENVELOPE_ECHO_REMINT_MAX,
|
|
273
|
+
CURSOR_ENVELOPE_ECHO_REMINT_TTL_MS,
|
|
274
|
+
CURSOR_ENVELOPE_ECHO_REMINT_MAX_ENTRIES,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
/** Echo recovery shares ownership scope with overflow and incomplete-tool, budget apart. */
|
|
278
|
+
export function cursorEnvelopeEchoRemintScopeKey(
|
|
279
|
+
threadOwner: string | undefined,
|
|
280
|
+
identityScope?: string,
|
|
281
|
+
): string | null {
|
|
282
|
+
return cursorOverflowRemintScopeKey(threadOwner, identityScope);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Record one envelope-echo remint; returns false when the independent cap is exhausted. */
|
|
286
|
+
export function recordCursorEnvelopeEchoRemint(scopeKey: string): boolean {
|
|
287
|
+
return envelopeEchoRemintBudget.record(scopeKey);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** A turn that completed without an echo replenishes only this budget. */
|
|
291
|
+
export function clearCursorEnvelopeEchoRemint(scopeKey: string): void {
|
|
292
|
+
envelopeEchoRemintBudget.clear(scopeKey);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function clearCursorEnvelopeEchoRemintForTests(): void {
|
|
296
|
+
envelopeEchoRemintBudget.clearForTests();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function cursorEnvelopeEchoRemintCountForTests(): number {
|
|
300
|
+
return envelopeEchoRemintBudget.countForTests();
|
|
227
301
|
}
|
|
@@ -4,13 +4,14 @@ import { CODEX_SHELL_BRIDGE_TOOL_NAMES, CODEX_TOOL_SEARCH_TOOL, CODEX_UNIFIED_EX
|
|
|
4
4
|
|
|
5
5
|
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE =
|
|
6
6
|
'Shell commands use the Codex shell bridge tool shown in this turn\'s catalog (`shell_command` or `exec_command`) with JSON arguments like {"cmd":"..."}. The long `mcp_opencodex-responses_*` display name is the same tool. Prefer it over Cursor-native Shell.';
|
|
7
|
-
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
|
|
7
|
+
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS", "Write"] as const;
|
|
8
8
|
const NEIGHBOR_AGENT_TOOL_ALIASES: Record<(typeof NEIGHBOR_AGENT_TOOL_NAMES)[number], readonly string[]> = {
|
|
9
9
|
Read: ["read", "read_file"],
|
|
10
10
|
Grep: ["grep"],
|
|
11
11
|
Glob: ["glob", "find"],
|
|
12
12
|
Bash: ["bash", "shell"],
|
|
13
13
|
LS: ["ls"],
|
|
14
|
+
Write: ["write", "write_file"],
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
@@ -22,7 +23,7 @@ export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
|
22
23
|
"The Cursor bridge may suspend after the first returned bridge tool call, so emit sibling calls together before any result is needed.",
|
|
23
24
|
"If parallel emission is unavailable, continue with separate shell-bridge calls until the requested count has returned.",
|
|
24
25
|
"Do not use `tool_search`, external MCP, or resource discovery just to pad the count unless explicitly asked.",
|
|
25
|
-
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `
|
|
26
|
+
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, `LS`, or `Write` unless this turn's catalog lists those exact names or an equivalent listed client tool.",
|
|
26
27
|
].join(" ");
|
|
27
28
|
|
|
28
29
|
|
|
@@ -190,7 +191,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
190
191
|
? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers. " + CODE_MODE_HOST_CONTRACT_SENTENCE
|
|
191
192
|
: undefined,
|
|
192
193
|
codeMode
|
|
193
|
-
? "NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces."
|
|
194
|
+
? "NEVER attempt Cursor-native Shell, Read, Grep, List, Write, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces."
|
|
194
195
|
: undefined,
|
|
195
196
|
hasBareExec
|
|
196
197
|
? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.`
|
|
@@ -199,7 +200,7 @@ export function buildCursorToolGuidanceSystemNote(
|
|
|
199
200
|
? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
|
|
200
201
|
: undefined,
|
|
201
202
|
hasBareExec
|
|
202
|
-
? `NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.`
|
|
203
|
+
? `NEVER attempt Cursor-native Shell, Read, Grep, List, Write, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.`
|
|
203
204
|
: undefined,
|
|
204
205
|
hasBareExec
|
|
205
206
|
? "Tool-selection commentary is forbidden: for any shell, read, grep, list, or file operation, your FIRST visible action is the bridge call itself — never a sentence about which tool you will use, which tool was redirected, or switching surfaces. Words like 차단/전환/blocked/switching must not appear in your output for tool-routing reasons."
|
|
@@ -55,6 +55,25 @@ export interface CursorTransportFactoryInput {
|
|
|
55
55
|
* client thread. Distinct from the per-transport native-exec/shell owner.
|
|
56
56
|
*/
|
|
57
57
|
sessionId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Test-only clock for the T04 inbound stream-health watchdog. Production omits it and the
|
|
60
|
+
* transport uses the global timers; injecting here must not change scheduling semantics.
|
|
61
|
+
*
|
|
62
|
+
* It exists because the watchdog's contract — a meaningful frame re-arms both deadlines — can
|
|
63
|
+
* only be asserted against real timers by keeping a synthetic server delivering frames faster
|
|
64
|
+
* than the silence budget for several multiples of it. That is an assertion about how busy the
|
|
65
|
+
* machine is, and it failed in the unsharded macOS lane for exactly that reason while the
|
|
66
|
+
* watchdog was working correctly. With the clock injected, virtual time advances only where
|
|
67
|
+
* the test says it does, so no amount of runner contention can expire a deadline.
|
|
68
|
+
*/
|
|
69
|
+
streamHealthClock?: CursorStreamHealthClock;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The three time primitives the T04 watchdog uses. Production binds all three to the globals. */
|
|
73
|
+
export interface CursorStreamHealthClock {
|
|
74
|
+
now(): number;
|
|
75
|
+
setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
|
|
76
|
+
clearTimeout(timer: ReturnType<typeof setTimeout>): void;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
export type CursorTransportFactory = (input: CursorTransportFactoryInput) => CursorTransport;
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -34,9 +34,12 @@ import { estimateTokens } from "../lib/token-estimate";
|
|
|
34
34
|
import {
|
|
35
35
|
clearCursorIncompleteToolRemint,
|
|
36
36
|
cursorIncompleteToolRemintScopeKey,
|
|
37
|
+
clearCursorEnvelopeEchoRemint,
|
|
38
|
+
cursorEnvelopeEchoRemintScopeKey,
|
|
37
39
|
cursorOverflowRemintScopeKey,
|
|
38
40
|
markCursorOverflowSurfaced,
|
|
39
41
|
recordCursorIncompleteToolRemint,
|
|
42
|
+
recordCursorEnvelopeEchoRemint,
|
|
40
43
|
recordCursorOverflowRemint,
|
|
41
44
|
rememberCursorThreadConversation,
|
|
42
45
|
shouldSkipCursorOverflowRemint,
|
|
@@ -206,6 +209,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
206
209
|
let lastTransport: { captured?: Uint8Array } | undefined;
|
|
207
210
|
let emittedClientTool = false;
|
|
208
211
|
let sawIncompleteToolCall = false;
|
|
212
|
+
let sawMidstreamEnvelopeEcho = false;
|
|
209
213
|
// Ordering proof for tool-suspended checkpoints: true only when the newest captured
|
|
210
214
|
// checkpoint bytes arrived AFTER the turn emitted a client tool call, i.e. upstream
|
|
211
215
|
// serialized its suspended-on-tool-call state. Only that snapshot can safely resume
|
|
@@ -250,6 +254,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
250
254
|
coveredMessageCount,
|
|
251
255
|
prefixDigest: cursorCoveredPrefixDigest(_parsed, coveredMessageCount),
|
|
252
256
|
systemDigest: cursorInstructionDigest(_parsed),
|
|
257
|
+
toolSuspended: toolSuspendedCommit,
|
|
253
258
|
});
|
|
254
259
|
if (!checkpointRef) return;
|
|
255
260
|
if (previousRef && previousRef !== checkpointRef) invalidateCursorCheckpoint(previousRef);
|
|
@@ -259,7 +264,8 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
259
264
|
...(_parsed._providerContinuation?.cursor ?? {}),
|
|
260
265
|
conversationId: activeRequest.conversationId,
|
|
261
266
|
// A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
|
|
262
|
-
// continuation; the request-builder guard keys on checkpointUsable=false
|
|
267
|
+
// continuation; the request-builder guard keys on this checkpointUsable=false and
|
|
268
|
+
// the snapshot's persisted toolSuspended flag for that.
|
|
263
269
|
checkpointUsable: !toolSuspendedCommit,
|
|
264
270
|
checkpointRef,
|
|
265
271
|
},
|
|
@@ -390,7 +396,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
390
396
|
}
|
|
391
397
|
if (event.type !== "heartbeat") emittedOutput = true;
|
|
392
398
|
if (event.type === "done") {
|
|
393
|
-
|
|
399
|
+
const midstreamFindings = midstreamObserver?.findings() ?? [];
|
|
400
|
+
if (midstreamFindings.length > 0) sawMidstreamEnvelopeEcho = true;
|
|
401
|
+
for (const finding of midstreamFindings) {
|
|
394
402
|
debugProviderDiagnostic("cursor", "midstream-envelope-echo", {
|
|
395
403
|
wireModel: activeRequest.modelId,
|
|
396
404
|
conversationHash: activeRequest.conversationId.slice(0, 16),
|
|
@@ -564,6 +572,41 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
564
572
|
} else if (!sawIncompleteToolCall && completedNormally && incompleteToolRemintScopeKey) {
|
|
565
573
|
clearCursorIncompleteToolRemint(incompleteToolRemintScopeKey);
|
|
566
574
|
}
|
|
575
|
+
// A mid-stream envelope echo has ALREADY reached the client — the prefix sniffer only
|
|
576
|
+
// watches the first bytes of a turn, and grok-4.6 writes a real sentence before pasting
|
|
577
|
+
// the envelope. It cannot be quarantined, so the recovery is the same as the
|
|
578
|
+
// incomplete-tool case: leave this turn alone and rotate the next turn's id, otherwise
|
|
579
|
+
// the stored echo is replayed and primes the model to echo again.
|
|
580
|
+
//
|
|
581
|
+
// Its own budget, not the incomplete-tool one: echoing is cheap and repeatable while an
|
|
582
|
+
// incomplete client-tool stream is rare and structural, so a shared counter would let a
|
|
583
|
+
// persistently echoing model spend the allowance the other recovery needs. Skipped when
|
|
584
|
+
// the incomplete-tool arm already reminted this turn — one rotation is enough.
|
|
585
|
+
const envelopeEchoRemintScopeKey =
|
|
586
|
+
_parsed._cursorIsolateConversation !== true
|
|
587
|
+
&& request.contextUsageStoreCheckpoints !== false
|
|
588
|
+
? cursorEnvelopeEchoRemintScopeKey(
|
|
589
|
+
cursorClientThreadOwner(_parsed),
|
|
590
|
+
_parsed._cursorIdentityScope,
|
|
591
|
+
)
|
|
592
|
+
: null;
|
|
593
|
+
if (sawMidstreamEnvelopeEcho && !sawIncompleteToolCall && envelopeEchoRemintScopeKey) {
|
|
594
|
+
if (recordCursorEnvelopeEchoRemint(envelopeEchoRemintScopeKey)) {
|
|
595
|
+
if (inheritedCheckpointRef) invalidateCursorCheckpoint(inheritedCheckpointRef);
|
|
596
|
+
debugProviderDiagnostic("cursor", "midstream-envelope-echo-remint", {
|
|
597
|
+
wireModel: request.modelId,
|
|
598
|
+
conversationHash: request.conversationId.slice(0, 16),
|
|
599
|
+
});
|
|
600
|
+
remintConversationId(request.conversationId);
|
|
601
|
+
} else {
|
|
602
|
+
debugProviderDiagnostic("cursor", "midstream-envelope-echo-remint-exhausted", {
|
|
603
|
+
wireModel: request.modelId,
|
|
604
|
+
conversationHash: request.conversationId.slice(0, 16),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
} else if (!sawMidstreamEnvelopeEcho && completedNormally && envelopeEchoRemintScopeKey) {
|
|
608
|
+
clearCursorEnvelopeEchoRemint(envelopeEchoRemintScopeKey);
|
|
609
|
+
}
|
|
567
610
|
if (
|
|
568
611
|
request.checkpointInvalidationReason
|
|
569
612
|
&& request.checkpointInvalidationReason !== "missing_ref"
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
} from './wire.js';
|
|
36
36
|
import { buildMetadata } from './metadata.js';
|
|
37
37
|
import { getCachedUserJwt } from './auth.js';
|
|
38
|
-
import { getCachedCatalog, ModelNotAvailableError } from './catalog.js';
|
|
38
|
+
import { getCachedCatalog, ModelNotAvailableError, type CacheEntry } from './catalog.js';
|
|
39
39
|
import { anySignal, cancelBodyOnAbort } from '../../../lib/abort.js';
|
|
40
40
|
import { resolveDevinApiBaseUrl } from '../../../oauth/devin/api-base.js';
|
|
41
41
|
|
|
@@ -1046,8 +1046,17 @@ export interface CloudChatRequest {
|
|
|
1046
1046
|
completionOpts?: BuildArgs['completionOpts'];
|
|
1047
1047
|
/** Override request_type (default = 5, CASCADE). */
|
|
1048
1048
|
requestType?: number;
|
|
1049
|
+
/**
|
|
1050
|
+
* Catalog the caller already resolved this turn. An explicit `null`
|
|
1051
|
+
* records a failed lookup: the pre-flight below then skips its own fetch
|
|
1052
|
+
* instead of paying a second catalog timeout on the same turn. Omit the
|
|
1053
|
+
* field to let the pre-flight perform its own cached lookup.
|
|
1054
|
+
*/
|
|
1055
|
+
catalog?: CacheEntry | null;
|
|
1049
1056
|
/** Abort signal — closes the fetch stream. */
|
|
1050
1057
|
signal?: AbortSignal;
|
|
1058
|
+
/** Executor for the inference POST only; catalog and JWT RPCs retain their own transport. */
|
|
1059
|
+
executor?: typeof globalThis.fetch;
|
|
1051
1060
|
}
|
|
1052
1061
|
|
|
1053
1062
|
export class CloudChatError extends Error {
|
|
@@ -1140,7 +1149,9 @@ export async function* streamChatEvents(req: CloudChatRequest): AsyncGenerator<C
|
|
|
1140
1149
|
// error and the trailer-error path below enriches the message in-place.
|
|
1141
1150
|
// Treat an empty catalog (schema drift / unexpected response) as "no catalog"
|
|
1142
1151
|
// so chat passes through instead of failing every request.
|
|
1143
|
-
const catalog =
|
|
1152
|
+
const catalog = req.catalog !== undefined
|
|
1153
|
+
? req.catalog
|
|
1154
|
+
: await getCachedCatalog(req.apiKey, host, req.signal).catch(() => null);
|
|
1144
1155
|
if (catalog && catalog.byUid.size > 0) {
|
|
1145
1156
|
const entry = catalog.byUid.get(req.modelUid);
|
|
1146
1157
|
if (!entry) {
|
|
@@ -1207,7 +1218,7 @@ export async function* streamChatEvents(req: CloudChatRequest): AsyncGenerator<C
|
|
|
1207
1218
|
|
|
1208
1219
|
let resp: Response;
|
|
1209
1220
|
try {
|
|
1210
|
-
resp = await fetch(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
|
|
1221
|
+
resp = await (req.executor ?? globalThis.fetch)(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
|
|
1211
1222
|
method: 'POST',
|
|
1212
1223
|
headers: {
|
|
1213
1224
|
'Content-Type': 'application/connect+proto',
|
|
@@ -49,6 +49,13 @@ export {
|
|
|
49
49
|
type ToolDef,
|
|
50
50
|
} from './chat.js';
|
|
51
51
|
|
|
52
|
+
export {
|
|
53
|
+
streamChatEventsWithResetRetry,
|
|
54
|
+
STATED_RESET_MAX_REPLAYS,
|
|
55
|
+
STATED_RESET_MAX_WAIT_MS,
|
|
56
|
+
type StatedResetRetryOptions,
|
|
57
|
+
} from './stated-reset-retry.js';
|
|
58
|
+
|
|
52
59
|
export {
|
|
53
60
|
mintUserJwt,
|
|
54
61
|
getCachedUserJwt,
|