@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.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/gui/dist/assets/index-CWXut3rG.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
|
2
|
+
<path fill="#F4F4F4" d="M512.00 100.50 C591.13 100.50 697.66 102.37 749.39 106.27 C801.11 110.17 802.35 115.84 822.35 123.90 C842.34 131.96 856.40 141.68 869.36 154.64 C882.32 167.60 892.04 181.66 900.10 201.65 C908.16 221.65 913.83 222.89 917.73 274.61 C921.63 326.34 923.50 432.87 923.50 512.00 C923.50 591.13 921.63 697.66 917.73 749.39 C913.83 801.11 908.16 802.35 900.10 822.35 C892.04 842.34 882.32 856.40 869.36 869.36 C856.40 882.32 842.34 892.04 822.35 900.10 C802.35 908.16 801.11 913.83 749.39 917.73 C697.66 921.63 591.13 923.50 512.00 923.50 C432.87 923.50 326.34 921.63 274.61 917.73 C222.89 913.83 221.65 908.16 201.65 900.10 C181.66 892.04 167.60 882.32 154.64 869.36 C141.68 856.40 131.96 842.34 123.90 822.35 C115.84 802.35 110.17 801.11 106.27 749.39 C102.37 697.66 100.50 591.13 100.50 512.00 C100.50 432.87 102.37 326.34 106.27 274.61 C110.17 222.89 115.84 221.65 123.90 201.65 C131.96 181.66 141.68 167.60 154.64 154.64 C167.60 141.68 181.66 131.96 201.65 123.90 C221.65 115.84 222.89 110.17 274.61 106.27 C326.34 102.37 432.87 100.50 512.00 100.50Z"/>
|
|
3
|
+
<g transform="scale(0.5)">
|
|
4
|
+
<g transform="translate(0.000000,2048.000000) scale(0.050000,-0.050000)">
|
|
5
|
+
<path fill="#041617" fill-rule="evenodd" d="M9270 32167 c-309 -64 -456 -152 -621 -373 -562 -748 -776 -2689
|
|
6
|
+
-547 -4984 118 -1187 325 -2600 570 -3890 143 -753 152 -653 -128 -1380 -403
|
|
7
|
+
-1045 -643 -1873 -812 -2800 -853 -4689 1979 -7986 7858 -9148 1140 -225 2514
|
|
8
|
+
-397 3202 -400 l361 -2 79 55 c264 182 210 642 -111 942 -387 362 -950 532
|
|
9
|
+
-2801 845 -3004 507 -4249 965 -5520 2030 -1358 1138 -2012 2878 -1809 4810
|
|
10
|
+
545 5195 6617 8726 13529 7866 5506 -684 9480 -4255 9480 -8518 0 -2607 -1462
|
|
11
|
+
-4522 -4157 -5448 -927 -318 -1534 -459 -3203 -740 -1851 -313 -2414 -483
|
|
12
|
+
-2801 -845 -321 -300 -375 -760 -111 -942 l79 -55 361 2 c688 3 2062 175 3202
|
|
13
|
+
400 5879 1162 8711 4459 7858 9148 -169 927 -409 1755 -812 2800 -280 727
|
|
14
|
+
-271 627 -128 1380 245 1290 452 2703 570 3890 271 2720 -77 4861 -855 5259
|
|
15
|
+
-758 388 -2310 -238 -5021 -2024 -868 -571 -764 -557 -1489 -211 -3271 1558
|
|
16
|
+
-6755 1558 -10026 0 -725 -346 -621 -360 -1489 211 -2371 1562 -3959 2278
|
|
17
|
+
-4708 2122z m4132 -3345 c175 -175 124 -268 -409 -739 -1028 -910 -1995 -2053
|
|
18
|
+
-2713 -3209 -97 -156 -200 -318 -228 -359 -221 -321 -646 -76 -469 270 153
|
|
19
|
+
301 883 1334 1388 1965 578 723 1466 1614 2029 2035 191 142 288 151 402 37z
|
|
20
|
+
m14507 -1 c496 -341 1482 -1323 2080 -2071 505 -631 1235 -1664 1388 -1965
|
|
21
|
+
177 -346 -248 -591 -469 -270 -28 41 -131 203 -228 359 -718 1156 -1685 2299
|
|
22
|
+
-2713 3209 -535 473 -585 564 -407 742 100 100 199 99 349 -4z M19910 25308 c-4039 -205 -7212 -1666 -9086 -4184 -1840 -2473 -1753
|
|
23
|
+
-5794 196 -7540 1161 -1040 2447 -1518 5420 -2015 1374 -229 1998 -380 2456
|
|
24
|
+
-591 754 -348 1163 -1000 1033 -1650 -10 -49 -1 -72 47 -124 l60 -64 444 0
|
|
25
|
+
444 0 60 64 c48 52 57 75 47 124 -130 650 279 1302 1033 1650 458 211 1082
|
|
26
|
+
362 2456 591 2973 497 4259 975 5420 2015 2084 1866 2026 5374 -131 7956
|
|
27
|
+
-2063 2469 -5995 3966 -9899 3768z m-4644 -4374 c1356 -360 2272 -1909 1998
|
|
28
|
+
-3378 -426 -2278 -2856 -3144 -4424 -1576 -1497 1496 -924 4218 1031 4899 393
|
|
29
|
+
137 996 161 1395 55z m11480 35 c1491 -261 2504 -1921 2154 -3532 -487 -2247
|
|
30
|
+
-3108 -3024 -4525 -1341 -1719 2042 -129 5310 2371 4873z m-6948 -2980 c172
|
|
31
|
+
-40 341 -133 495 -273 95 -86 143 -116 187 -116 44 0 92 30 187 116 718 650
|
|
32
|
+
1716 188 1854 -860 35 -267 18 -968 -25 -1048 -119 -219 -350 -251 -491 -68
|
|
33
|
+
-68 89 -73 124 -85 670 -13 623 -25 675 -200 838 -212 198 -516 199 -735 2
|
|
34
|
+
-180 -161 -192 -212 -205 -840 -12 -546 -17 -581 -85 -670 -122 -159 -308
|
|
35
|
+
-159 -430 0 -68 89 -73 124 -85 670 -13 628 -25 679 -205 840 -219 197 -523
|
|
36
|
+
196 -735 -2 -175 -163 -187 -215 -200 -838 -12 -546 -17 -581 -85 -670 -141
|
|
37
|
+
-183 -372 -151 -491 68 -74 136 -54 1039 30 1332 170 594 758 978 1304 849z M14340 20290 c-1858 -372 -2230 -3228 -546 -4191 1280 -732 2786 338
|
|
38
|
+
2786 1981 0 1391 -1068 2445 -2240 2210z M25990 20291 c-1733 -343 -2208 -2981 -732 -4069 1287 -949 2981 108
|
|
39
|
+
2982 1860 0 1392 -1070 2443 -2250 2209z"/>
|
|
40
|
+
</g>
|
|
41
|
+
</g>
|
|
42
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.52.0
|
|
3
|
+
"version": "2.52.0",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"privacy:scan": "bun scripts/privacy-scan.ts",
|
|
49
49
|
"skill:surface": "bun scripts/generate-ocx-skill-surface.ts",
|
|
50
50
|
"skill:surface:check": "bun scripts/generate-ocx-skill-surface.ts --check",
|
|
51
|
+
"structure:index": "bun scripts/structure-ssot.ts --fix",
|
|
52
|
+
"structure:check": "bun scripts/structure-ssot.ts",
|
|
51
53
|
"generate:model-metadata": "bun scripts/generate-model-metadata.ts",
|
|
52
54
|
"build:gui": "cd gui && bun install --frozen-lockfile && bun run build && cd .. && bun run prepare:package",
|
|
53
55
|
"prepare:package": "bun scripts/prepare-package.ts",
|
package/src/AGENTS.md
CHANGED
|
@@ -8,7 +8,7 @@ This file applies to `src/` and inherits the repository-wide rules in `/AGENTS.m
|
|
|
8
8
|
- Do not assume a separate server compilation step.
|
|
9
9
|
- Prefer Bun and Web-platform APIs. Introduce a Node-only runtime dependency only when the task explicitly requires compatibility code and the owning module already has that role.
|
|
10
10
|
- Preserve existing public exports and configuration compatibility unless the task explicitly changes them.
|
|
11
|
-
- Read the applicable documents in `structure/` before changing shared routing, adapters, transports, sidecars, authentication, configuration, or server architecture.
|
|
11
|
+
- Read the applicable documents in `structure/` before changing shared routing, adapters, transports, sidecars, authentication, configuration, or server architecture. [`structure/INDEX.md`](../structure/INDEX.md) maps each source area to the docs that describe it — usually more than one, because those docs are organised by topic while `src/` is organised by module — and every doc listed for an area is updated in the same change that changes the area.
|
|
12
12
|
|
|
13
13
|
## Implementation rules
|
|
14
14
|
|
|
@@ -495,7 +495,6 @@ function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string
|
|
|
495
495
|
let wire = requested;
|
|
496
496
|
const lower = canonicalId.toLowerCase();
|
|
497
497
|
const needsAlias =
|
|
498
|
-
lower === "deepseek/deepseek-v4-pro" ||
|
|
499
498
|
lower === "deepseek/deepseek-v4-flash" ||
|
|
500
499
|
lower === "zai-org/glm-5.2";
|
|
501
500
|
if (requested === "xhigh" && !supported.includes("xhigh") && supported.includes("max")) {
|
|
@@ -184,6 +184,43 @@ export function cursorCheckpointRefHash(ref: string): string {
|
|
|
184
184
|
return createHash("sha256").update("ocx:cursor:ckpt-ref:").update(ref).digest("hex").slice(0, 16);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Counts only — never content. Diagnostics about a checkpoint have so far reported its size in
|
|
189
|
+
* bytes, which says nothing about what is in it, and that gap is exactly what left #4245's native
|
|
190
|
+
* half undecidable: `capturedAfterClientTool` proves a snapshot ARRIVED after the tool call, and
|
|
191
|
+
* only `pendingToolCalls` says whether the snapshot actually knows about one.
|
|
192
|
+
*
|
|
193
|
+
* `pendingToolCalls` is documented upstream as raw JSON tool-call parts awaiting execution, so a
|
|
194
|
+
* non-zero count on a suspended turn is the coverage evidence. The strings themselves are request
|
|
195
|
+
* content and are never read here.
|
|
196
|
+
*
|
|
197
|
+
* If you extend this, keep it counts-only. `ConversationStateStructure` also carries
|
|
198
|
+
* `readPaths`, `previousWorkspaceUris`, and the `fileStates`/`fileStatesV2` keys — all of which
|
|
199
|
+
* are user paths or workspace identity, and all of which would turn a diagnostic into a privacy
|
|
200
|
+
* leak the moment someone returns them as values instead of lengths.
|
|
201
|
+
*/
|
|
202
|
+
export function cursorCheckpointShape(checkpointBytes: Uint8Array | undefined): {
|
|
203
|
+
turns: number;
|
|
204
|
+
turnsOld: number;
|
|
205
|
+
rootPromptMessages: number;
|
|
206
|
+
todos: number;
|
|
207
|
+
pendingToolCalls: number;
|
|
208
|
+
} | undefined {
|
|
209
|
+
if (!checkpointBytes || checkpointBytes.byteLength === 0) return undefined;
|
|
210
|
+
try {
|
|
211
|
+
const state = fromBinary(ConversationStateStructureSchema, checkpointBytes);
|
|
212
|
+
return {
|
|
213
|
+
turns: state.turns.length,
|
|
214
|
+
turnsOld: state.turnsOld.length,
|
|
215
|
+
rootPromptMessages: state.rootPromptMessagesJson.length,
|
|
216
|
+
todos: state.todos.length,
|
|
217
|
+
pendingToolCalls: state.pendingToolCalls.length,
|
|
218
|
+
};
|
|
219
|
+
} catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
187
224
|
export function commitCursorCheckpoint(input: {
|
|
188
225
|
conversationId: string;
|
|
189
226
|
identityScope?: string;
|
|
@@ -115,6 +115,15 @@ const CLIENT_TOOL_FINALIZE_GRACE_MS = 50;
|
|
|
115
115
|
const GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS = 750;
|
|
116
116
|
const GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS = 1_800;
|
|
117
117
|
const GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS = 125;
|
|
118
|
+
/**
|
|
119
|
+
* A client-tool turn suspends before `turnEnded`, and upstream sends that turn's conversation
|
|
120
|
+
* checkpoint right after `toolCallStarted` — after the 50 ms drain grace, so it used to be
|
|
121
|
+
* cancelled away and every such turn full-replayed with `cached_tokens: 0`. Measured on a live
|
|
122
|
+
* account with the tool catalog held constant: 50 ms captures nothing, 1500 ms captures 3036
|
|
123
|
+
* bytes (#4245, devlog 260911_cursor_checkpoint_capture). This window is paid only by a turn
|
|
124
|
+
* that never sends one; `handleServerMessage` finalizes early as soon as the frame lands.
|
|
125
|
+
*/
|
|
126
|
+
const CHECKPOINT_CAPTURE_GRACE_MS = 1_500;
|
|
118
127
|
const cursorContextUsageTracker = createCursorContextUsageTracker();
|
|
119
128
|
|
|
120
129
|
/**
|
|
@@ -413,6 +422,28 @@ export function finalizeAfterDrain(state: ReturnType<typeof createCursorProtobuf
|
|
|
413
422
|
return finalizeTurnEvents(state);
|
|
414
423
|
}
|
|
415
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Whether a drained client-tool turn should wait one more bounded window for the conversation
|
|
427
|
+
* checkpoint instead of cancelling now.
|
|
428
|
+
*
|
|
429
|
+
* This mirrors `finalizeAfterDrain`'s two guards rather than calling it, and that is load-bearing:
|
|
430
|
+
* `finalizeAfterDrain` reaches `finalizeTurnEvents`, which sets `state.terminated`, and then returns
|
|
431
|
+
* `[]` for a terminated state. Draining first and re-arming would make the retry return early at the
|
|
432
|
+
* length check and leave the stream uncancelled. Pure for unit testing.
|
|
433
|
+
*/
|
|
434
|
+
export function shouldExtendForCheckpointCapture(input: {
|
|
435
|
+
/** Optional on the event state, so accept its real shape rather than forcing a cast at the call site. */
|
|
436
|
+
terminated: boolean | undefined;
|
|
437
|
+
openToolCallCount: number;
|
|
438
|
+
wantsCheckpointCapture: boolean;
|
|
439
|
+
hasCapturedCheckpoint: boolean;
|
|
440
|
+
alreadyExtended: boolean;
|
|
441
|
+
}): boolean {
|
|
442
|
+
if (input.terminated || input.openToolCallCount > 0) return false;
|
|
443
|
+
if (!input.wantsCheckpointCapture || input.hasCapturedCheckpoint) return false;
|
|
444
|
+
return !input.alreadyExtended;
|
|
445
|
+
}
|
|
446
|
+
|
|
416
447
|
export function clientToolFinalizeGraceMsForRequest(request: CursorRunRequest, baseGraceMs = CLIENT_TOOL_FINALIZE_GRACE_MS): number {
|
|
417
448
|
if (request.rawMessages?.at(-1)?.role === "toolResult") return baseGraceMs;
|
|
418
449
|
const text = activePromptText(request);
|
|
@@ -468,6 +499,10 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
468
499
|
*/
|
|
469
500
|
private emittedTerminal = false;
|
|
470
501
|
private pendingFinalize?: ReturnType<typeof setTimeout>;
|
|
502
|
+
/** The armed finalize body, so the checkpoint frame can run it early instead of waiting out the window. */
|
|
503
|
+
private pendingFinalizeRun?: () => void;
|
|
504
|
+
private checkpointGraceExtended = false;
|
|
505
|
+
private wantsCheckpointCapture = false;
|
|
471
506
|
private readonly clientToolFinalizeGraceMs: number;
|
|
472
507
|
private activeClientToolFinalizeGraceMs: number;
|
|
473
508
|
private readonly token: string;
|
|
@@ -641,6 +676,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
641
676
|
};
|
|
642
677
|
const activeText = activePromptText(activeRequest);
|
|
643
678
|
this.activeClientToolFinalizeGraceMs = clientToolFinalizeGraceMsForRequest(activeRequest, this.clientToolFinalizeGraceMs);
|
|
679
|
+
this.wantsCheckpointCapture = activeRequest.contextUsageStoreCheckpoints !== false;
|
|
644
680
|
const cursorVisibleTools = cursorToolsForActivePrompt(activeRequest.tools, activeText, activeRequest.toolChoice);
|
|
645
681
|
const clientToolDefs = buildCursorToolDefinitions(cursorVisibleTools, activeRequest.toolChoice);
|
|
646
682
|
// `request.tools` is the catalog already filtered and budgeted by request-builder. Derive
|
|
@@ -981,6 +1017,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
981
1017
|
clearTimeout(this.pendingFinalize);
|
|
982
1018
|
this.pendingFinalize = undefined;
|
|
983
1019
|
}
|
|
1020
|
+
this.pendingFinalizeRun = undefined;
|
|
984
1021
|
}
|
|
985
1022
|
|
|
986
1023
|
/**
|
|
@@ -1001,11 +1038,26 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1001
1038
|
private scheduleClientToolFinalize(
|
|
1002
1039
|
state: ReturnType<typeof createCursorProtobufEventState>,
|
|
1003
1040
|
push: (message: CursorServerMessage) => void,
|
|
1041
|
+
graceMsOverride?: number,
|
|
1004
1042
|
): void {
|
|
1005
1043
|
this.clearPendingFinalize();
|
|
1006
|
-
|
|
1044
|
+
const run = (): void => {
|
|
1007
1045
|
this.pendingFinalize = undefined;
|
|
1046
|
+
this.pendingFinalizeRun = undefined;
|
|
1008
1047
|
if (this.expectedClose) return;
|
|
1048
|
+
// The checkpoint for THIS turn is the one we most want and the one we used to throw
|
|
1049
|
+
// away. Extend once, bounded, before draining (#4245).
|
|
1050
|
+
if (shouldExtendForCheckpointCapture({
|
|
1051
|
+
terminated: state.terminated,
|
|
1052
|
+
openToolCallCount: state.openToolCalls.size,
|
|
1053
|
+
wantsCheckpointCapture: this.wantsCheckpointCapture,
|
|
1054
|
+
hasCapturedCheckpoint: this.capturedCheckpointBytes !== undefined,
|
|
1055
|
+
alreadyExtended: this.checkpointGraceExtended,
|
|
1056
|
+
})) {
|
|
1057
|
+
this.checkpointGraceExtended = true;
|
|
1058
|
+
this.scheduleClientToolFinalize(state, push, CHECKPOINT_CAPTURE_GRACE_MS);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1009
1061
|
const terminal = finalizeAfterDrain(state);
|
|
1010
1062
|
if (terminal.length === 0) return;
|
|
1011
1063
|
for (const event of terminal) push(event);
|
|
@@ -1013,9 +1065,13 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1013
1065
|
reason: "Responses bridge owns client tools; ending turn without fake mcpResult",
|
|
1014
1066
|
framesReceived: this.framesReceived,
|
|
1015
1067
|
elapsedMs: Date.now() - this.turnStartedAt,
|
|
1068
|
+
graceMs: graceMsOverride ?? this.activeClientToolFinalizeGraceMs,
|
|
1069
|
+
checkpointGraceExtended: this.checkpointGraceExtended,
|
|
1016
1070
|
});
|
|
1017
1071
|
this.cancelCursorRun();
|
|
1018
|
-
}
|
|
1072
|
+
};
|
|
1073
|
+
this.pendingFinalizeRun = run;
|
|
1074
|
+
this.pendingFinalize = setTimeout(run, graceMsOverride ?? this.activeClientToolFinalizeGraceMs);
|
|
1019
1075
|
}
|
|
1020
1076
|
|
|
1021
1077
|
private open(
|
|
@@ -1032,6 +1088,10 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1032
1088
|
}
|
|
1033
1089
|
this.turnStartedAt = Date.now();
|
|
1034
1090
|
this.framesReceived = 0;
|
|
1091
|
+
this.checkpointGraceExtended = false;
|
|
1092
|
+
// A turn must not inherit the previous one's snapshot: a stale capture would make
|
|
1093
|
+
// shouldExtendForCheckpointCapture skip the wait this turn actually needs.
|
|
1094
|
+
this.capturedCheckpointBytes = undefined;
|
|
1035
1095
|
this.sawAssistantText = false;
|
|
1036
1096
|
this.emittedTerminal = false;
|
|
1037
1097
|
this.firstFrameAt = undefined;
|
|
@@ -1450,6 +1510,18 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
1450
1510
|
} catch {
|
|
1451
1511
|
this.capturedCheckpointBytes = undefined;
|
|
1452
1512
|
}
|
|
1513
|
+
// We are only still open because the grace was extended waiting for exactly this frame.
|
|
1514
|
+
// Stop waiting, so a turn that does send a checkpoint pays arrival latency rather than the
|
|
1515
|
+
// whole window. Deferred one tick so this frame finishes being mapped and pushed first;
|
|
1516
|
+
// finalizing inline would emit the terminal events ahead of it.
|
|
1517
|
+
if (this.checkpointGraceExtended && this.pendingFinalizeRun && this.capturedCheckpointBytes) {
|
|
1518
|
+
const run = this.pendingFinalizeRun;
|
|
1519
|
+
this.clearPendingFinalize();
|
|
1520
|
+
this.pendingFinalize = setTimeout(run, 0);
|
|
1521
|
+
// Keep the pair in step: clearPendingFinalize() drops the callback, and every other
|
|
1522
|
+
// owner of pendingFinalize expects pendingFinalizeRun to describe it.
|
|
1523
|
+
this.pendingFinalizeRun = run;
|
|
1524
|
+
}
|
|
1453
1525
|
}
|
|
1454
1526
|
if (message.message.case === "kvServerMessage") {
|
|
1455
1527
|
this.writeConnectFrame(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
commitCursorCheckpoint,
|
|
26
26
|
cursorCheckpointRefHash,
|
|
27
|
+
cursorCheckpointShape,
|
|
27
28
|
invalidateCursorCheckpoint,
|
|
28
29
|
} from "./cursor/checkpoint-store";
|
|
29
30
|
import { debugProviderDiagnostic } from "../lib/debug";
|
|
31
|
+
import { isDebugEnabled } from "../lib/debug-settings";
|
|
30
32
|
import { createAdapterTierMetadata } from "../providers/fastwire";
|
|
31
33
|
import { estimateTokens } from "../lib/token-estimate";
|
|
32
34
|
import { rememberCursorThreadConversation } from "./cursor/thread-continuity";
|
|
@@ -208,6 +210,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
208
210
|
externalModel: isCursorExternalWireModel(activeRequest.modelId),
|
|
209
211
|
storeCheckpoints: activeRequest.contextUsageStoreCheckpoints !== false,
|
|
210
212
|
capturedBytes: lastTransport?.captured?.byteLength ?? 0,
|
|
213
|
+
// Byte length says nothing about coverage. `pendingToolCalls` does: it is what
|
|
214
|
+
// distinguishes a snapshot that knows about the suspended call from one that merely
|
|
215
|
+
// arrived after it (#4245). Counts only; the decode is skipped unless debug is on.
|
|
216
|
+
capturedShape: isDebugEnabled() ? cursorCheckpointShape(lastTransport?.captured) : undefined,
|
|
211
217
|
});
|
|
212
218
|
return;
|
|
213
219
|
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Derived from rsvedant/opencode-windsurf-auth (src/cloud-direct/), MIT licensed,
|
|
3
|
+
* Copyright (c) 2026 Vedant. The full notice is in ./index.ts.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Mint the short-lived `user_jwt` that accompanies the persistent OAuth-issued
|
|
7
|
+
* `api_key`. The catalog RPC uses it. The hosted chat path does not need it and
|
|
8
|
+
* only sends it when an operator opts in, so a mint failure here cannot take
|
|
9
|
+
* down a turn.
|
|
10
|
+
*
|
|
11
|
+
* POST https://server.codeium.com/exa.auth_pb.AuthService/GetUserJwt
|
|
12
|
+
* Content-Type: application/proto ← unary, NOT streaming
|
|
13
|
+
* Body: GetUserJwtRequest { metadata: Metadata }
|
|
14
|
+
* Response: GetUserJwtResponse { user_jwt: string } (field 1)
|
|
15
|
+
*
|
|
16
|
+
* The returned JWT has a payload like:
|
|
17
|
+
* {
|
|
18
|
+
* "api_key": "devin-synthetic-apikey$account-…$user-…",
|
|
19
|
+
* "auth_uid": "devin-auth-uid$…",
|
|
20
|
+
* "email": "user@example.com",
|
|
21
|
+
* "exp": <unix-seconds>, ← ~24 minute TTL
|
|
22
|
+
* "pro": true,
|
|
23
|
+
* "teams_tier": "TEAMS_TIER_DEVIN_PRO",
|
|
24
|
+
* ...
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* The JWT is signed HS256 by the server — can't be forged client-side. We
|
|
28
|
+
* cache it and refresh shortly before `exp`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as crypto from 'crypto';
|
|
32
|
+
import { encodeMessage, iterFields } from './wire.js';
|
|
33
|
+
import { buildMetadata } from './metadata.js';
|
|
34
|
+
import { anySignal } from '../../../lib/abort.js';
|
|
35
|
+
import { validateDevinApiBaseUrl } from '../../../oauth/devin/api-base.js';
|
|
36
|
+
|
|
37
|
+
const DEFAULT_HOST = 'https://server.codeium.com';
|
|
38
|
+
|
|
39
|
+
export interface MintedUserJwt {
|
|
40
|
+
jwt: string;
|
|
41
|
+
/** Unix epoch seconds when the JWT expires. */
|
|
42
|
+
expiresAt: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class CloudAuthError extends Error {
|
|
46
|
+
constructor(message: string, public readonly status?: number) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'CloudAuthError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default mint timeout — 30s is generous (the endpoint responds in ~200ms
|
|
54
|
+
* in steady state) but enough headroom for slow networks. Callers can pass
|
|
55
|
+
* a tighter `signal` to override.
|
|
56
|
+
*/
|
|
57
|
+
const MINT_TIMEOUT_MS = 30_000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mint a fresh user_jwt by calling exa.auth_pb.AuthService/GetUserJwt.
|
|
61
|
+
* `host` defaults to https://server.codeium.com — pass your tenant URL if your
|
|
62
|
+
* RegisterUser response gave a different host.
|
|
63
|
+
*
|
|
64
|
+
* Always applies an internal 30s timeout so a network stall here can't
|
|
65
|
+
* deadlock every concurrent chat request. If the caller passes a `signal`,
|
|
66
|
+
* we honor whichever fires first via AbortSignal.any.
|
|
67
|
+
*/
|
|
68
|
+
export async function mintUserJwt(
|
|
69
|
+
apiKey: string,
|
|
70
|
+
host: string = DEFAULT_HOST,
|
|
71
|
+
signal?: AbortSignal,
|
|
72
|
+
): Promise<MintedUserJwt> {
|
|
73
|
+
const metadata = buildMetadata({
|
|
74
|
+
apiKey,
|
|
75
|
+
sessionId: crypto.randomUUID(),
|
|
76
|
+
requestId: BigInt(Date.now()),
|
|
77
|
+
triggerId: crypto.randomUUID(),
|
|
78
|
+
});
|
|
79
|
+
// GetUserJwtRequest { metadata: Metadata } — Metadata is field 1
|
|
80
|
+
const req = encodeMessage(1, metadata);
|
|
81
|
+
|
|
82
|
+
// Compose caller signal with our internal timeout via `anySignal` — a
|
|
83
|
+
// small polyfill of `AbortSignal.any` for runtimes (Node 18 / older
|
|
84
|
+
// Bun) that lack the built-in. The previous fallback silently dropped
|
|
85
|
+
// the CALLER's signal on those runtimes, so a chat-cancel during a
|
|
86
|
+
// GetUserJwt mint would keep the network request alive for up to the
|
|
87
|
+
// full 30s timeout.
|
|
88
|
+
const timeoutSignal = AbortSignal.timeout(MINT_TIMEOUT_MS);
|
|
89
|
+
const composed = signal ? anySignal([signal, timeoutSignal]) : undefined;
|
|
90
|
+
const combinedSignal: AbortSignal = composed?.signal ?? timeoutSignal;
|
|
91
|
+
|
|
92
|
+
// The host arrives from RegisterUser via the credential store. It is checked
|
|
93
|
+
// again here because this request carries the long-lived api_key inside the
|
|
94
|
+
// protobuf body, and a host that slipped past persistence would exfiltrate it.
|
|
95
|
+
const base = validateDevinApiBaseUrl(host);
|
|
96
|
+
if (!base) {
|
|
97
|
+
throw new CloudAuthError(`Refusing to mint a user_jwt against a non-Cognition host.`);
|
|
98
|
+
}
|
|
99
|
+
let resp: Response;
|
|
100
|
+
try {
|
|
101
|
+
resp = await fetch(`${base}/exa.auth_pb.AuthService/GetUserJwt`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/proto',
|
|
105
|
+
'Connect-Protocol-Version': '1',
|
|
106
|
+
},
|
|
107
|
+
body: new Uint8Array(req),
|
|
108
|
+
// A redirect would replay this POST - whose body holds the api_key - at
|
|
109
|
+
// whatever host Location names.
|
|
110
|
+
redirect: 'error',
|
|
111
|
+
signal: combinedSignal,
|
|
112
|
+
});
|
|
113
|
+
} finally {
|
|
114
|
+
// The caller's signal belongs to a whole turn; do not keep a listener on it.
|
|
115
|
+
composed?.cleanup();
|
|
116
|
+
}
|
|
117
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
118
|
+
|
|
119
|
+
if (!resp.ok) {
|
|
120
|
+
// The body is not echoed. A Connect error here can quote the request, and
|
|
121
|
+
// the request contains the api_key; this message reaches CLI output, the
|
|
122
|
+
// adapter's error event, and /api/logs.
|
|
123
|
+
throw new CloudAuthError(`GetUserJwt failed (HTTP ${resp.status})`, resp.status);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Response is GetUserJwtResponse { user_jwt: string } where user_jwt is
|
|
127
|
+
// field 1, length-delimited. Decode the field properly instead of
|
|
128
|
+
// regex-scanning the whole buffer — the previous regex would pick up
|
|
129
|
+
// any JWT-shaped substring in the response (trace IDs, signature
|
|
130
|
+
// headers, any cached token inadvertently logged) and could even land
|
|
131
|
+
// on a non-user_jwt if Cognition ever embeds another JWT in a sibling
|
|
132
|
+
// field.
|
|
133
|
+
let jwt: string | null = null;
|
|
134
|
+
for (const f of iterFields(buf)) {
|
|
135
|
+
if (f.num === 1 && f.wire === 2 && Buffer.isBuffer(f.value)) {
|
|
136
|
+
const s = (f.value as Buffer).toString('utf8');
|
|
137
|
+
// Sanity-check the shape — defensive: if the cloud ever moves user_jwt
|
|
138
|
+
// out from field 1 we want a clean error, not silently wrong creds.
|
|
139
|
+
// base64url with OPTIONAL `=` padding on each segment. Most modern
|
|
140
|
+
// JWTs omit the `=`, but the spec allows it and a future server-side
|
|
141
|
+
// change could re-introduce it; either way it's still a valid token.
|
|
142
|
+
if (/^eyJ[A-Za-z0-9_-]{10,}={0,2}\.[A-Za-z0-9_-]+={0,2}\.[A-Za-z0-9_-]+={0,2}$/.test(s)) {
|
|
143
|
+
jwt = s;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!jwt) {
|
|
149
|
+
throw new CloudAuthError(
|
|
150
|
+
// Same reason: a 200 whose field-1 value failed the shape check may still
|
|
151
|
+
// be a live token, so only the size is reported.
|
|
152
|
+
`GetUserJwt returned 200 without a usable field-1 JWT (${buf.length} bytes)`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Decode the payload to get the expiry.
|
|
157
|
+
let expiresAt = Math.floor(Date.now() / 1000) + 600; // fallback: 10 min
|
|
158
|
+
try {
|
|
159
|
+
const parts = jwt.split('.');
|
|
160
|
+
const pad = (s: string) => s + '='.repeat((4 - (s.length % 4)) % 4);
|
|
161
|
+
const payload = JSON.parse(
|
|
162
|
+
Buffer.from(pad(parts[1]).replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'),
|
|
163
|
+
);
|
|
164
|
+
if (typeof payload.exp === 'number') expiresAt = payload.exp;
|
|
165
|
+
} catch { /* fall back to default */ }
|
|
166
|
+
|
|
167
|
+
return { jwt, expiresAt };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ----------------------------------------------------------------------------
|
|
171
|
+
// In-memory cache — refresh ~60s before expiry
|
|
172
|
+
// ----------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
interface CacheEntry {
|
|
175
|
+
jwt: string;
|
|
176
|
+
expiresAt: number;
|
|
177
|
+
apiKey: string;
|
|
178
|
+
host: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Cache is keyed by (apiKey, host). A single shared `cache` slot only holds
|
|
183
|
+
* the MOST RECENTLY USED entry — common case is one account at a time, so
|
|
184
|
+
* a single slot is enough. inFlight is a per-key map so a JWT mint for
|
|
185
|
+
* account A doesn't get returned to a concurrent request for account B.
|
|
186
|
+
*
|
|
187
|
+
* Previously `inFlight` was a singleton — if account A's mint was in flight
|
|
188
|
+
* and a request for account B arrived, B got A's JWT. That's the M1
|
|
189
|
+
* "concurrent requests after account switch get wrong JWT" bug.
|
|
190
|
+
*/
|
|
191
|
+
let cache: CacheEntry | null = null;
|
|
192
|
+
const inFlight = new Map<string, Promise<MintedUserJwt>>();
|
|
193
|
+
/**
|
|
194
|
+
* Monotonic epoch counter. Incremented on every `clearCachedUserJwt()`
|
|
195
|
+
* call so an in-flight mint that started BEFORE the clear can't
|
|
196
|
+
* repopulate the cache after-the-fact. Without this, a logout that
|
|
197
|
+
* happened concurrently with a mint would silently get its just-
|
|
198
|
+
* invalidated JWT cached and served for the next ~24 minutes.
|
|
199
|
+
*/
|
|
200
|
+
let cacheEpoch = 0;
|
|
201
|
+
|
|
202
|
+
function flightKey(apiKey: string, host: string): string {
|
|
203
|
+
return `${host}\x1f${apiKey}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get a cached user_jwt or mint a new one. Refreshes when the cached JWT is
|
|
208
|
+
* within 60s of expiry. Multiple concurrent callers for the SAME (apiKey, host)
|
|
209
|
+
* share the same in-flight mint; concurrent callers for DIFFERENT keys each
|
|
210
|
+
* get their own mint.
|
|
211
|
+
*/
|
|
212
|
+
export async function getCachedUserJwt(apiKey: string, host: string = DEFAULT_HOST, signal?: AbortSignal): Promise<string> {
|
|
213
|
+
const now = Math.floor(Date.now() / 1000);
|
|
214
|
+
if (cache && cache.apiKey === apiKey && cache.host === host && cache.expiresAt > now + 60) {
|
|
215
|
+
return cache.jwt;
|
|
216
|
+
}
|
|
217
|
+
// Race the caller's signal against the shared promise so one caller's
|
|
218
|
+
// cancellation doesn't propagate to unrelated callers sharing the mint.
|
|
219
|
+
// mintUserJwt has its own MINT_TIMEOUT_MS guard for the shared lifetime.
|
|
220
|
+
const raceSignal = <T>(p: Promise<T>): Promise<T> =>
|
|
221
|
+
signal
|
|
222
|
+
? Promise.race([
|
|
223
|
+
p,
|
|
224
|
+
new Promise<T>((_, reject) => {
|
|
225
|
+
if (signal.aborted) reject(signal.reason);
|
|
226
|
+
else signal.addEventListener('abort', () => reject(signal.reason), { once: true });
|
|
227
|
+
}),
|
|
228
|
+
])
|
|
229
|
+
: p;
|
|
230
|
+
const key = flightKey(apiKey, host);
|
|
231
|
+
const existing = inFlight.get(key);
|
|
232
|
+
if (existing) {
|
|
233
|
+
const minted = await raceSignal(existing);
|
|
234
|
+
return minted.jwt;
|
|
235
|
+
}
|
|
236
|
+
const promise = mintUserJwt(apiKey, host);
|
|
237
|
+
inFlight.set(key, promise);
|
|
238
|
+
// Snapshot the epoch BEFORE awaiting the mint. If clearCachedUserJwt()
|
|
239
|
+
// fires while we're awaiting (logout-during-mint), the epoch changes
|
|
240
|
+
// and we won't repopulate the cache with the just-invalidated JWT.
|
|
241
|
+
const epochAtStart = cacheEpoch;
|
|
242
|
+
try {
|
|
243
|
+
const minted = await raceSignal(promise);
|
|
244
|
+
if (cacheEpoch === epochAtStart) {
|
|
245
|
+
cache = { jwt: minted.jwt, expiresAt: minted.expiresAt, apiKey, host };
|
|
246
|
+
}
|
|
247
|
+
return minted.jwt;
|
|
248
|
+
} finally {
|
|
249
|
+
inFlight.delete(key);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Drop the in-memory JWT cache. Call after credential changes (logout,
|
|
255
|
+
* account switch) so long-running opencode processes don't keep using a
|
|
256
|
+
* JWT minted from a now-invalid api_key. Also bumps the cache epoch so
|
|
257
|
+
* any in-flight mint racing with this clear can't repopulate cache
|
|
258
|
+
* with the stale JWT after-the-fact.
|
|
259
|
+
*/
|
|
260
|
+
export function clearCachedUserJwt(): void {
|
|
261
|
+
cache = null;
|
|
262
|
+
inFlight.clear();
|
|
263
|
+
cacheEpoch++;
|
|
264
|
+
}
|