@bitkyc08/opencodex 2.8.0 → 2.9.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 +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { enforceAppOwnedMemoryBudget } from "../../lib/app-owned-memory";
|
|
2
3
|
import { create } from "@bufbuild/protobuf";
|
|
3
4
|
import {
|
|
4
5
|
DiagnosticsErrorSchema,
|
|
5
6
|
DiagnosticsResultSchema,
|
|
7
|
+
ErrorSchema,
|
|
6
8
|
GetBlobResultSchema,
|
|
7
9
|
KvClientMessageSchema,
|
|
8
10
|
McpErrorSchema,
|
|
@@ -60,6 +62,8 @@ export type CursorNativeExecDeps = CursorNativeNetworkDeps & CursorNativeToolDep
|
|
|
60
62
|
* never told any MCP tools exist, so it never sends `mcpArgs`.
|
|
61
63
|
*/
|
|
62
64
|
export interface CursorNativeExecContext extends CursorNativeExecDeps {
|
|
65
|
+
/** Stable owner for background shells created by this transport session. */
|
|
66
|
+
sessionId?: string;
|
|
63
67
|
mcpToolDefs?: McpToolDefinition[];
|
|
64
68
|
clientToolDefs?: McpToolDefinition[];
|
|
65
69
|
/** Unsafe opt-in escape hatch for Cursor server-driven local fs/shell/fetch execution. */
|
|
@@ -80,42 +84,245 @@ export function cursorUnsafeNativeLocalExecEnabled(input: Pick<CursorNativeExecC
|
|
|
80
84
|
* their blobs on every turn (`rootPromptMessages` → `storeCursorBlob`), so TTL + cap eviction is
|
|
81
85
|
* safe for live sessions: only genuinely abandoned entries age out.
|
|
82
86
|
*/
|
|
83
|
-
const BLOB_TTL_MS = 15 *
|
|
84
|
-
const BLOB_MAX_ENTRIES =
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
const BLOB_TTL_MS = 15 * 60_000;
|
|
88
|
+
const BLOB_MAX_ENTRIES = 4_096;
|
|
89
|
+
const BLOB_MAX_ENTRY_BYTES = 16 * 1024 * 1024;
|
|
90
|
+
export const CURSOR_BLOB_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
91
|
+
|
|
92
|
+
type CursorBlobProvenance = "local-regenerated" | "remote-setBlobArgs";
|
|
93
|
+
export type CursorBlobRequestScopeToken = symbol;
|
|
94
|
+
|
|
95
|
+
interface CursorBlobEntry {
|
|
96
|
+
data: Uint8Array;
|
|
97
|
+
storedAt: number;
|
|
98
|
+
sizeBytes: number;
|
|
99
|
+
provenance: CursorBlobProvenance;
|
|
100
|
+
requestPins: Set<CursorBlobRequestScopeToken>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type CursorBlobRejectionReason = "entry_too_large" | "pinned_saturation" | "request_pinned_conflict";
|
|
104
|
+
type CursorBlobAdmission =
|
|
105
|
+
| { admitted: true; replaced: boolean }
|
|
106
|
+
| { admitted: false; reason: CursorBlobRejectionReason };
|
|
107
|
+
|
|
108
|
+
interface CursorBlobLimits {
|
|
109
|
+
ttlMs: number;
|
|
110
|
+
maxEntries: number;
|
|
111
|
+
maxEntryBytes: number;
|
|
112
|
+
maxTotalBytes: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface CursorBlobRequestScopeState {
|
|
116
|
+
keys: Set<string>;
|
|
117
|
+
sealed: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const DEFAULT_BLOB_LIMITS: CursorBlobLimits = {
|
|
121
|
+
ttlMs: BLOB_TTL_MS,
|
|
122
|
+
maxEntries: BLOB_MAX_ENTRIES,
|
|
123
|
+
maxEntryBytes: BLOB_MAX_ENTRY_BYTES,
|
|
124
|
+
maxTotalBytes: CURSOR_BLOB_MAX_TOTAL_BYTES,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const blobs = new Map<string, CursorBlobEntry>();
|
|
128
|
+
const blobRequestScopes = new Map<CursorBlobRequestScopeToken, CursorBlobRequestScopeState>();
|
|
129
|
+
let blobLimits = { ...DEFAULT_BLOB_LIMITS };
|
|
130
|
+
let blobBytes = 0;
|
|
131
|
+
let blobLocalBytes = 0;
|
|
132
|
+
let blobPinnedBytes = 0;
|
|
133
|
+
let blobEvictableBytes = 0;
|
|
134
|
+
let blobOldestEvictableAt: number | null = null;
|
|
135
|
+
let rejectedEntryTooLarge = 0;
|
|
136
|
+
let rejectedPinnedSaturation = 0;
|
|
137
|
+
let blobExpiryAccountingTimer: ReturnType<typeof setTimeout> | undefined;
|
|
138
|
+
|
|
139
|
+
function isExpired(entry: CursorBlobEntry, now: number): boolean {
|
|
140
|
+
return now - entry.storedAt >= blobLimits.ttlMs;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
144
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
145
|
+
for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function recomputeBlobClassAccounting(): void {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
let localBytes = 0;
|
|
152
|
+
let pinnedBytes = 0;
|
|
153
|
+
let evictableBytes = 0;
|
|
154
|
+
let oldestAt: number | null = null;
|
|
155
|
+
for (const entry of blobs.values()) {
|
|
156
|
+
const requestPinned = entry.requestPins.size > 0;
|
|
157
|
+
const provenancePinned = entry.provenance === "remote-setBlobArgs" && !isExpired(entry, now);
|
|
158
|
+
if (entry.provenance === "local-regenerated") localBytes += entry.sizeBytes;
|
|
159
|
+
if (requestPinned || provenancePinned) pinnedBytes += entry.sizeBytes;
|
|
160
|
+
if (!requestPinned && (entry.provenance === "local-regenerated" || isExpired(entry, now))) {
|
|
161
|
+
evictableBytes += entry.sizeBytes;
|
|
162
|
+
oldestAt = oldestAt === null ? entry.storedAt : Math.min(oldestAt, entry.storedAt);
|
|
93
163
|
}
|
|
94
|
-
return;
|
|
95
164
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
165
|
+
blobLocalBytes = localBytes;
|
|
166
|
+
blobPinnedBytes = pinnedBytes;
|
|
167
|
+
blobEvictableBytes = evictableBytes;
|
|
168
|
+
blobOldestEvictableAt = oldestAt;
|
|
169
|
+
scheduleBlobExpiryAccounting(now);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function reconcileBlobClassAccountingAndEnforce(): void {
|
|
173
|
+
recomputeBlobClassAccounting();
|
|
174
|
+
enforceAppOwnedMemoryBudget();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scheduleBlobExpiryAccounting(now: number): void {
|
|
178
|
+
if (blobExpiryAccountingTimer) clearTimeout(blobExpiryAccountingTimer);
|
|
179
|
+
blobExpiryAccountingTimer = undefined;
|
|
180
|
+
let nextExpiry = Number.POSITIVE_INFINITY;
|
|
181
|
+
for (const entry of blobs.values()) {
|
|
182
|
+
if (entry.provenance !== "remote-setBlobArgs" || entry.requestPins.size > 0) continue;
|
|
183
|
+
const expiresAt = entry.storedAt + blobLimits.ttlMs;
|
|
184
|
+
if (expiresAt > now) nextExpiry = Math.min(nextExpiry, expiresAt);
|
|
185
|
+
}
|
|
186
|
+
if (!Number.isFinite(nextExpiry)) return;
|
|
187
|
+
blobExpiryAccountingTimer = setTimeout(() => {
|
|
188
|
+
blobExpiryAccountingTimer = undefined;
|
|
189
|
+
reconcileBlobClassAccountingAndEnforce();
|
|
190
|
+
}, Math.max(0, nextExpiry - now));
|
|
191
|
+
blobExpiryAccountingTimer.unref?.();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function deleteBlob(k: string, recompute = true): number {
|
|
195
|
+
const entry = blobs.get(k);
|
|
196
|
+
if (!entry) return 0;
|
|
197
|
+
blobs.delete(k);
|
|
198
|
+
blobBytes -= entry.sizeBytes;
|
|
199
|
+
for (const scope of entry.requestPins) blobRequestScopes.get(scope)?.keys.delete(k);
|
|
200
|
+
if (recompute) recomputeBlobClassAccounting();
|
|
201
|
+
return entry.sizeBytes;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function releaseHydratedBlob(k: string, requestScope?: CursorBlobRequestScopeToken): void {
|
|
205
|
+
const entry = blobs.get(k);
|
|
206
|
+
if (!entry) return;
|
|
207
|
+
const scopes = requestScope ? [requestScope] : [...entry.requestPins];
|
|
208
|
+
let changed = false;
|
|
209
|
+
for (const scope of scopes) {
|
|
210
|
+
if (!entry.requestPins.delete(scope)) continue;
|
|
211
|
+
changed = true;
|
|
212
|
+
const state = blobRequestScopes.get(scope);
|
|
213
|
+
state?.keys.delete(k);
|
|
214
|
+
if (state?.sealed && state.keys.size === 0) blobRequestScopes.delete(scope);
|
|
104
215
|
}
|
|
216
|
+
if (changed) reconcileBlobClassAccountingAndEnforce();
|
|
105
217
|
}
|
|
106
218
|
|
|
107
|
-
function setBlob(
|
|
219
|
+
function setBlob(
|
|
220
|
+
k: string,
|
|
221
|
+
data: Uint8Array,
|
|
222
|
+
provenance: CursorBlobProvenance,
|
|
223
|
+
requestScope?: CursorBlobRequestScopeToken,
|
|
224
|
+
): CursorBlobAdmission {
|
|
225
|
+
if (data.byteLength > blobLimits.maxEntryBytes) {
|
|
226
|
+
rejectedEntryTooLarge++;
|
|
227
|
+
return { admitted: false, reason: "entry_too_large" };
|
|
228
|
+
}
|
|
229
|
+
|
|
108
230
|
const now = Date.now();
|
|
109
|
-
blobs.
|
|
110
|
-
|
|
111
|
-
|
|
231
|
+
const existing = blobs.get(k);
|
|
232
|
+
const sameData = existing ? bytesEqual(existing.data, data) : false;
|
|
233
|
+
if (existing && !sameData && existing.requestPins.size > 0) {
|
|
234
|
+
return { admitted: false, reason: "request_pinned_conflict" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const removals = new Set<string>();
|
|
238
|
+
for (const [candidateKey, entry] of blobs) {
|
|
239
|
+
if (candidateKey === k && sameData) continue;
|
|
240
|
+
if (entry.requestPins.size === 0 && isExpired(entry, now)) removals.add(candidateKey);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const existingRemovedByTtl = existing !== undefined && removals.has(k);
|
|
244
|
+
const reuseExisting = existing !== undefined && sameData && !existingRemovedByTtl;
|
|
245
|
+
const mergedProvenance: CursorBlobProvenance = reuseExisting && existing.provenance === "remote-setBlobArgs"
|
|
246
|
+
? "remote-setBlobArgs"
|
|
247
|
+
: provenance;
|
|
248
|
+
const storedAt = reuseExisting && existing.provenance === "remote-setBlobArgs" && provenance === "local-regenerated"
|
|
249
|
+
? existing.storedAt
|
|
250
|
+
: now;
|
|
251
|
+
|
|
252
|
+
let projectedBytes = blobBytes;
|
|
253
|
+
let projectedCount = blobs.size;
|
|
254
|
+
for (const candidateKey of removals) {
|
|
255
|
+
const entry = blobs.get(candidateKey);
|
|
256
|
+
if (!entry) continue;
|
|
257
|
+
projectedBytes -= entry.sizeBytes;
|
|
258
|
+
projectedCount--;
|
|
259
|
+
}
|
|
260
|
+
if (existing && !existingRemovedByTtl) {
|
|
261
|
+
projectedBytes -= existing.sizeBytes;
|
|
262
|
+
projectedCount--;
|
|
263
|
+
}
|
|
264
|
+
projectedBytes += data.byteLength;
|
|
265
|
+
projectedCount++;
|
|
266
|
+
|
|
267
|
+
if (projectedBytes > blobLimits.maxTotalBytes || projectedCount > blobLimits.maxEntries) {
|
|
268
|
+
const localVictims = [...blobs.entries()].sort((a, b) => a[1].storedAt - b[1].storedAt);
|
|
269
|
+
for (const [candidateKey, entry] of localVictims) {
|
|
270
|
+
if (
|
|
271
|
+
candidateKey === k
|
|
272
|
+
|| removals.has(candidateKey)
|
|
273
|
+
|| entry.requestPins.size > 0
|
|
274
|
+
|| entry.provenance !== "local-regenerated"
|
|
275
|
+
) continue;
|
|
276
|
+
removals.add(candidateKey);
|
|
277
|
+
projectedBytes -= entry.sizeBytes;
|
|
278
|
+
projectedCount--;
|
|
279
|
+
if (projectedBytes <= blobLimits.maxTotalBytes && projectedCount <= blobLimits.maxEntries) break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (projectedBytes > blobLimits.maxTotalBytes || projectedCount > blobLimits.maxEntries) {
|
|
284
|
+
rejectedPinnedSaturation++;
|
|
285
|
+
return { admitted: false, reason: "pinned_saturation" };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Carry forward only pins whose scope still has live state; a dead scope's
|
|
289
|
+
// token can never be released again (review C1-1).
|
|
290
|
+
const requestPins = new Set<CursorBlobRequestScopeToken>();
|
|
291
|
+
if (reuseExisting) {
|
|
292
|
+
for (const scope of existing.requestPins) {
|
|
293
|
+
if (blobRequestScopes.has(scope)) requestPins.add(scope);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// A pin is only attached when the scope still has live UNSEALED state:
|
|
297
|
+
// sealing fixes the advertised key set (locked contract), so a remote
|
|
298
|
+
// setBlobArgs arriving after seal must not extend the pin set — and a
|
|
299
|
+
// sealed scope whose advertised keys have all hydrated is already deleted,
|
|
300
|
+
// so attaching its stale token would create a permanent untracked pin the
|
|
301
|
+
// terminal release could never clear (reviews C1-1/C2-1).
|
|
302
|
+
const scopeState = requestScope ? blobRequestScopes.get(requestScope) : undefined;
|
|
303
|
+
if (requestScope && scopeState && !scopeState.sealed) requestPins.add(requestScope);
|
|
304
|
+
const entry: CursorBlobEntry = {
|
|
305
|
+
data: reuseExisting ? existing.data : data.slice(),
|
|
306
|
+
storedAt,
|
|
307
|
+
sizeBytes: data.byteLength,
|
|
308
|
+
provenance: mergedProvenance,
|
|
309
|
+
requestPins,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
for (const candidateKey of removals) deleteBlob(candidateKey, false);
|
|
313
|
+
if (blobs.has(k)) deleteBlob(k, false);
|
|
314
|
+
blobs.set(k, entry);
|
|
315
|
+
blobBytes += entry.sizeBytes;
|
|
316
|
+
for (const scope of entry.requestPins) blobRequestScopes.get(scope)?.keys.add(k);
|
|
317
|
+
reconcileBlobClassAccountingAndEnforce();
|
|
318
|
+
return { admitted: true, replaced: existing !== undefined };
|
|
112
319
|
}
|
|
113
320
|
|
|
114
321
|
function getBlob(k: string): Uint8Array | undefined {
|
|
115
322
|
const entry = blobs.get(k);
|
|
116
323
|
if (!entry) return undefined;
|
|
117
|
-
if (
|
|
118
|
-
|
|
324
|
+
if (entry.requestPins.size === 0 && isExpired(entry, Date.now())) {
|
|
325
|
+
deleteBlob(k);
|
|
119
326
|
return undefined;
|
|
120
327
|
}
|
|
121
328
|
return entry.data;
|
|
@@ -130,12 +337,136 @@ function key(bytes: Uint8Array): string {
|
|
|
130
337
|
* blob id. Cursor's `rootPromptMessagesJson`/turn entries are blob IDS, not inline content — the
|
|
131
338
|
* server fetches the bytes back via `getBlobArgs`. Mirrors jawcode `createBlobId`/`storeCursorBlob`.
|
|
132
339
|
*/
|
|
133
|
-
export
|
|
340
|
+
export class CursorBlobAdmissionError extends Error {
|
|
341
|
+
readonly code = "cursor_blob_capacity";
|
|
342
|
+
|
|
343
|
+
constructor(readonly reason: CursorBlobRejectionReason) {
|
|
344
|
+
super("Cursor blob capacity is exhausted; retry with a smaller request.");
|
|
345
|
+
this.name = "CursorBlobAdmissionError";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let blobScopeSequence = 0;
|
|
350
|
+
|
|
351
|
+
export function createCursorBlobRequestScope(): CursorBlobRequestScopeToken {
|
|
352
|
+
// Unique description per token so debug snapshots expose exact pin
|
|
353
|
+
// IDENTITY, not just pin counts (review C2-2: identical descriptions made
|
|
354
|
+
// scope-swap bugs invisible to deep comparison).
|
|
355
|
+
const scope = Symbol(`cursor-blob-request-${++blobScopeSequence}`);
|
|
356
|
+
blobRequestScopes.set(scope, { keys: new Set(), sealed: false });
|
|
357
|
+
return scope;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function sealCursorBlobRequestScope(scope: CursorBlobRequestScopeToken): void {
|
|
361
|
+
const state = blobRequestScopes.get(scope);
|
|
362
|
+
if (!state) return;
|
|
363
|
+
state.sealed = true;
|
|
364
|
+
if (state.keys.size === 0) blobRequestScopes.delete(scope);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function releaseCursorBlobRequestScope(scope: CursorBlobRequestScopeToken): void {
|
|
368
|
+
const state = blobRequestScopes.get(scope);
|
|
369
|
+
if (!state) return;
|
|
370
|
+
for (const k of state.keys) blobs.get(k)?.requestPins.delete(scope);
|
|
371
|
+
blobRequestScopes.delete(scope);
|
|
372
|
+
reconcileBlobClassAccountingAndEnforce();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobRequestScopeToken): Uint8Array {
|
|
134
376
|
const blobId = new Uint8Array(createHash("sha256").update(data).digest());
|
|
135
|
-
setBlob(key(blobId), data);
|
|
377
|
+
const admission = setBlob(key(blobId), data, "local-regenerated", requestScope);
|
|
378
|
+
if (!admission.admitted) throw new CursorBlobAdmissionError(admission.reason);
|
|
136
379
|
return blobId;
|
|
137
380
|
}
|
|
138
381
|
|
|
382
|
+
export interface CursorBlobMetrics {
|
|
383
|
+
count: number;
|
|
384
|
+
totalBytes: number;
|
|
385
|
+
localBytes: number;
|
|
386
|
+
pinnedBytes: number;
|
|
387
|
+
rejectedEntryTooLarge: number;
|
|
388
|
+
rejectedPinnedSaturation: number;
|
|
389
|
+
oldestAt: number | null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function cursorBlobMetrics(): CursorBlobMetrics {
|
|
393
|
+
return {
|
|
394
|
+
count: blobs.size,
|
|
395
|
+
totalBytes: blobBytes,
|
|
396
|
+
localBytes: blobLocalBytes,
|
|
397
|
+
pinnedBytes: blobPinnedBytes,
|
|
398
|
+
rejectedEntryTooLarge,
|
|
399
|
+
rejectedPinnedSaturation,
|
|
400
|
+
oldestAt: blobOldestEvictableAt,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function cursorBlobRetainedStoreSnapshot(): {
|
|
405
|
+
count: number;
|
|
406
|
+
bytes: number;
|
|
407
|
+
evictableBytes: number;
|
|
408
|
+
pinnedBytes: number;
|
|
409
|
+
oldestAt: number | null;
|
|
410
|
+
} {
|
|
411
|
+
return {
|
|
412
|
+
count: blobs.size,
|
|
413
|
+
bytes: blobBytes,
|
|
414
|
+
evictableBytes: blobEvictableBytes,
|
|
415
|
+
pinnedBytes: blobPinnedBytes,
|
|
416
|
+
oldestAt: blobOldestEvictableAt,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function evictOldestCursorBlobForBudget(): number {
|
|
421
|
+
const now = Date.now();
|
|
422
|
+
let oldest: [string, CursorBlobEntry] | undefined;
|
|
423
|
+
for (const candidate of blobs) {
|
|
424
|
+
const entry = candidate[1];
|
|
425
|
+
if (entry.requestPins.size > 0) continue;
|
|
426
|
+
if (entry.provenance !== "local-regenerated" && !isExpired(entry, now)) continue;
|
|
427
|
+
if (!oldest || entry.storedAt < oldest[1].storedAt) oldest = candidate;
|
|
428
|
+
}
|
|
429
|
+
return oldest ? deleteBlob(oldest[0]) : 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function setCursorBlobLimitsForTests(limits?: Partial<CursorBlobLimits>): void {
|
|
433
|
+
resetCursorBlobStateForTests();
|
|
434
|
+
blobLimits = limits ? { ...DEFAULT_BLOB_LIMITS, ...limits } : { ...DEFAULT_BLOB_LIMITS };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function resetCursorBlobStateForTests(): void {
|
|
438
|
+
if (blobExpiryAccountingTimer) clearTimeout(blobExpiryAccountingTimer);
|
|
439
|
+
blobExpiryAccountingTimer = undefined;
|
|
440
|
+
blobs.clear();
|
|
441
|
+
blobRequestScopes.clear();
|
|
442
|
+
blobBytes = 0;
|
|
443
|
+
rejectedEntryTooLarge = 0;
|
|
444
|
+
rejectedPinnedSaturation = 0;
|
|
445
|
+
recomputeBlobClassAccounting();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function cursorBlobStoreDebugSnapshotForTests(): Array<{
|
|
449
|
+
key: string;
|
|
450
|
+
sizeBytes: number;
|
|
451
|
+
storedAt: number;
|
|
452
|
+
provenance: CursorBlobProvenance;
|
|
453
|
+
requestPins: number;
|
|
454
|
+
dataDigest: string;
|
|
455
|
+
pinTokens: string[];
|
|
456
|
+
}> {
|
|
457
|
+
return [...blobs].map(([blobKey, entry]) => ({
|
|
458
|
+
key: blobKey,
|
|
459
|
+
sizeBytes: entry.sizeBytes,
|
|
460
|
+
storedAt: entry.storedAt,
|
|
461
|
+
provenance: entry.provenance,
|
|
462
|
+
requestPins: entry.requestPins.size,
|
|
463
|
+
// Byte-for-byte content identity + exact pin-set membership so rollback
|
|
464
|
+
// tests can deep-compare complete state (review C1-2).
|
|
465
|
+
dataDigest: createHash("sha256").update(entry.data).digest("hex"),
|
|
466
|
+
pinTokens: [...entry.requestPins].map(scope => String(scope.description ?? "scope")).sort(),
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
|
|
139
470
|
export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: CursorNativeExecContext = {}): Promise<Uint8Array[]> {
|
|
140
471
|
const execCase = execMsg.message.case;
|
|
141
472
|
if (execCase === "requestContextArgs") {
|
|
@@ -163,8 +494,8 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
|
|
|
163
494
|
if (execCase === "grepArgs") return [grepExec(execMsg)];
|
|
164
495
|
if (execCase === "shellArgs") return [shellExec(execMsg)];
|
|
165
496
|
if (execCase === "shellStreamArgs") return shellStreamExec(execMsg);
|
|
166
|
-
if (execCase === "backgroundShellSpawnArgs") return [backgroundShellSpawnExec(execMsg)];
|
|
167
|
-
if (execCase === "writeShellStdinArgs") return [writeShellStdinExec(execMsg)];
|
|
497
|
+
if (execCase === "backgroundShellSpawnArgs") return [backgroundShellSpawnExec(execMsg, deps.sessionId ?? "")];
|
|
498
|
+
if (execCase === "writeShellStdinArgs") return [writeShellStdinExec(execMsg, deps.sessionId ?? "")];
|
|
168
499
|
if (execCase === "fetchArgs") return [await fetchExec(execMsg, deps)];
|
|
169
500
|
if (execCase === "mcpArgs" && execMsg.message.value.providerIdentifier === OCX_RESPONSES_TOOL_PROVIDER) {
|
|
170
501
|
return [execBytes(execMsg, "mcpResult", create(McpResultSchema, {
|
|
@@ -199,9 +530,14 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
|
|
|
199
530
|
}
|
|
200
531
|
|
|
201
532
|
|
|
202
|
-
export function handleCursorNativeKv(
|
|
533
|
+
export function handleCursorNativeKv(
|
|
534
|
+
kvMsg: KvServerMessage,
|
|
535
|
+
requestScope?: CursorBlobRequestScopeToken,
|
|
536
|
+
): Uint8Array {
|
|
203
537
|
if (kvMsg.message.case === "getBlobArgs") {
|
|
204
|
-
const
|
|
538
|
+
const blobKey = key(kvMsg.message.value.blobId);
|
|
539
|
+
const blobData = getBlob(blobKey);
|
|
540
|
+
if (blobData) releaseHydratedBlob(blobKey, requestScope);
|
|
205
541
|
return clientBytes({
|
|
206
542
|
message: {
|
|
207
543
|
case: "kvClientMessage",
|
|
@@ -213,13 +549,25 @@ export function handleCursorNativeKv(kvMsg: KvServerMessage): Uint8Array {
|
|
|
213
549
|
});
|
|
214
550
|
}
|
|
215
551
|
if (kvMsg.message.case === "setBlobArgs") {
|
|
216
|
-
setBlob(
|
|
552
|
+
const admission = setBlob(
|
|
553
|
+
key(kvMsg.message.value.blobId),
|
|
554
|
+
kvMsg.message.value.blobData,
|
|
555
|
+
"remote-setBlobArgs",
|
|
556
|
+
requestScope,
|
|
557
|
+
);
|
|
217
558
|
return clientBytes({
|
|
218
559
|
message: {
|
|
219
560
|
case: "kvClientMessage",
|
|
220
561
|
value: create(KvClientMessageSchema, {
|
|
221
562
|
id: kvMsg.id,
|
|
222
|
-
message: {
|
|
563
|
+
message: {
|
|
564
|
+
case: "setBlobResult",
|
|
565
|
+
value: create(SetBlobResultSchema, admission.admitted ? {} : {
|
|
566
|
+
error: create(ErrorSchema, {
|
|
567
|
+
message: "Cursor blob capacity is exhausted; the blob was not stored.",
|
|
568
|
+
}),
|
|
569
|
+
}),
|
|
570
|
+
},
|
|
223
571
|
}),
|
|
224
572
|
},
|
|
225
573
|
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
responsesToolNameFromCursorWire,
|
|
14
14
|
} from "./tool-definitions";
|
|
15
15
|
import type { CursorServerMessage } from "./types";
|
|
16
|
+
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
16
17
|
|
|
17
18
|
const DEFAULT_CONTEXT_USAGE_MAX_ENTRIES = 200;
|
|
18
19
|
const DEFAULT_CONTEXT_USAGE_TTL_MS = 60 * 60 * 1_000;
|
|
@@ -160,6 +161,7 @@ export interface CursorProtobufEventState {
|
|
|
160
161
|
toolSchemas?: Map<string, unknown>;
|
|
161
162
|
/** Cursor wire-name → original Responses/Codex tool name for this request. */
|
|
162
163
|
cursorToolNameMap?: Map<string, string>;
|
|
164
|
+
translatorBudget?: TranslatorBudget;
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
export function createCursorProtobufEventState(options: {
|
|
@@ -174,6 +176,7 @@ export function createCursorProtobufEventState(options: {
|
|
|
174
176
|
* the checkpoint tracker (#373).
|
|
175
177
|
*/
|
|
176
178
|
estimatedInputTokens?: number;
|
|
179
|
+
translatorBudget?: TranslatorBudget;
|
|
177
180
|
} = {}): CursorProtobufEventState {
|
|
178
181
|
return {
|
|
179
182
|
// Cursor provides no authoritative usage frame; token counts are heuristic estimates from
|
|
@@ -186,6 +189,7 @@ export function createCursorProtobufEventState(options: {
|
|
|
186
189
|
startedClientToolCalls: 0,
|
|
187
190
|
...(options.toolSchemas ? { toolSchemas: options.toolSchemas } : {}),
|
|
188
191
|
...(options.cursorToolNameMap ? { cursorToolNameMap: options.cursorToolNameMap } : {}),
|
|
192
|
+
...(options.translatorBudget ? { translatorBudget: options.translatorBudget } : {}),
|
|
189
193
|
...(options.contextUsage?.carryForwardTokens !== undefined ? { contextCarryForwardTokens: options.contextUsage.carryForwardTokens } : {}),
|
|
190
194
|
...(options.contextUsage?.recordContextTokens ? { recordContextTokens: options.contextUsage.recordContextTokens } : {}),
|
|
191
195
|
...(typeof options.estimatedInputTokens === "number"
|
|
@@ -363,6 +367,7 @@ function recordToolCall(state: CursorProtobufEventState, callId: string, cursorW
|
|
|
363
367
|
// land on the tool Codex actually exposed this turn (#399).
|
|
364
368
|
const mapKey = advertisedName ?? normalizeCursorWireName(cursorWireName);
|
|
365
369
|
state.openToolCalls.set(callId, { name: responsesToolNameFromCursorWire(mapKey, state.cursorToolNameMap), args: "" });
|
|
370
|
+
state.translatorBudget?.openCall(callId);
|
|
366
371
|
state.startedClientToolCalls++;
|
|
367
372
|
return [];
|
|
368
373
|
}
|
|
@@ -375,6 +380,7 @@ function recordToolCall(state: CursorProtobufEventState, callId: string, cursorW
|
|
|
375
380
|
*/
|
|
376
381
|
function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
|
|
377
382
|
state.openToolCalls.delete(callId);
|
|
383
|
+
state.translatorBudget?.closeCall(callId);
|
|
378
384
|
state.completedToolCalls.add(callId);
|
|
379
385
|
return [{ type: "error", message: cursorShellBridgeDropError(toolName) }];
|
|
380
386
|
}
|
|
@@ -386,6 +392,16 @@ function commitToolCall(state: CursorProtobufEventState, callId: string, finalAr
|
|
|
386
392
|
if (!cursorShellBridgeArgsValid(finalArgs, open.name, schema)) {
|
|
387
393
|
if (isCodexShellBridgeToolName(open.name)) return dropShellBridgeCall(state, callId, open.name);
|
|
388
394
|
}
|
|
395
|
+
if (finalArgs !== open.args) {
|
|
396
|
+
const previousBytes = Buffer.byteLength(open.args);
|
|
397
|
+
const reservation = state.translatorBudget?.reserveTransient(
|
|
398
|
+
Buffer.byteLength(finalArgs),
|
|
399
|
+
{ kind: "tool_args", callId },
|
|
400
|
+
);
|
|
401
|
+
open.args = finalArgs;
|
|
402
|
+
reservation?.commitRetained();
|
|
403
|
+
state.translatorBudget?.releaseRetained(previousBytes, { kind: "tool_args", callId });
|
|
404
|
+
}
|
|
389
405
|
const out: CursorServerMessage[] = [{ type: "tool_call_start", id: callId, name: open.name }];
|
|
390
406
|
if (finalArgs.length > 0) out.push({ type: "tool_call_delta", arguments: finalArgs });
|
|
391
407
|
out.push(...endToolCall(state, callId));
|
|
@@ -401,12 +417,23 @@ function commitToolCall(state: CursorProtobufEventState, callId: string, finalAr
|
|
|
401
417
|
function bufferToolArgs(state: CursorProtobufEventState, callId: string, cumulative: string): void {
|
|
402
418
|
const open = state.openToolCalls.get(callId);
|
|
403
419
|
if (!open) return;
|
|
404
|
-
if (cumulative.length >= open.args.length)
|
|
420
|
+
if (cumulative.length >= open.args.length) {
|
|
421
|
+
const encoder = new TextEncoder();
|
|
422
|
+
const previousBytes = encoder.encode(open.args).byteLength;
|
|
423
|
+
const reservation = state.translatorBudget?.reserveTransient(
|
|
424
|
+
encoder.encode(cumulative).byteLength,
|
|
425
|
+
{ kind: "tool_args", callId },
|
|
426
|
+
);
|
|
427
|
+
open.args = cumulative;
|
|
428
|
+
reservation?.commitRetained();
|
|
429
|
+
state.translatorBudget?.releaseRetained(previousBytes, { kind: "tool_args", callId });
|
|
430
|
+
}
|
|
405
431
|
}
|
|
406
432
|
|
|
407
433
|
function endToolCall(state: CursorProtobufEventState, callId: string): CursorServerMessage[] {
|
|
408
434
|
if (!state.openToolCalls.has(callId)) return [];
|
|
409
435
|
state.openToolCalls.delete(callId);
|
|
436
|
+
state.translatorBudget?.closeCall(callId);
|
|
410
437
|
state.completedToolCalls.add(callId);
|
|
411
438
|
return [{ type: "tool_call_end", id: callId }];
|
|
412
439
|
}
|