@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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
export type AppOwnedRetainedCategory = "logs" | "caches" | "blobs" | "continuation";
|
|
2
|
+
export type AppOwnedObservedCategory = "translator" | "serialized_tails";
|
|
3
|
+
|
|
4
|
+
export interface RetainedStoreSnapshot {
|
|
5
|
+
count: number;
|
|
6
|
+
bytes: number;
|
|
7
|
+
evictableBytes: number;
|
|
8
|
+
pinnedBytes: number;
|
|
9
|
+
oldestAt: number | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RetainedStoreRegistration {
|
|
13
|
+
id: string;
|
|
14
|
+
category: AppOwnedRetainedCategory;
|
|
15
|
+
snapshot(): RetainedStoreSnapshot;
|
|
16
|
+
evictOldest(): number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ObservedBufferRegistration {
|
|
20
|
+
id: string;
|
|
21
|
+
category: AppOwnedObservedCategory;
|
|
22
|
+
snapshot(): { currentBytes: number; highWaterBytes: number; active: number };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AppOwnedBytesSnapshot {
|
|
26
|
+
budgetBytes: number;
|
|
27
|
+
retainedBytes: number;
|
|
28
|
+
evictableBytes: number;
|
|
29
|
+
pinnedBytes: number;
|
|
30
|
+
overBudgetBytes: number;
|
|
31
|
+
stores: Record<string, RetainedStoreSnapshot>;
|
|
32
|
+
observedInFlight: Record<string, { currentBytes: number; highWaterBytes: number; active: number }>;
|
|
33
|
+
enforcement: {
|
|
34
|
+
runs: number;
|
|
35
|
+
entriesDemoted: number;
|
|
36
|
+
bytesReleased: number;
|
|
37
|
+
noEvictableCandidate: number;
|
|
38
|
+
snapshotFailures: number;
|
|
39
|
+
oldestAtContractViolations: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES = 256 * 1024 * 1024;
|
|
44
|
+
/**
|
|
45
|
+
* The budget above is an eviction target, not a hard resident limit: active/pinned
|
|
46
|
+
* state may temporarily keep retained bytes above it. Every pin-capable owner has
|
|
47
|
+
* its own finite admission cap; their documented aggregate must remain below this
|
|
48
|
+
* process-owned hard ceiling.
|
|
49
|
+
*/
|
|
50
|
+
export const APP_OWNED_WORST_CASE_PINNED_BYTES = 512 * 1024 * 1024;
|
|
51
|
+
export const MIN_APP_OWNED_MEMORY_BUDGET_MB = 64;
|
|
52
|
+
export const MAX_APP_OWNED_MEMORY_BUDGET_MB = 4_096;
|
|
53
|
+
|
|
54
|
+
const CATEGORY_ORDER: readonly AppOwnedRetainedCategory[] = [
|
|
55
|
+
"logs",
|
|
56
|
+
"caches",
|
|
57
|
+
"blobs",
|
|
58
|
+
"continuation",
|
|
59
|
+
];
|
|
60
|
+
const WARN_INTERVAL_MS = 60_000;
|
|
61
|
+
const ZERO_RETAINED_SNAPSHOT: RetainedStoreSnapshot = {
|
|
62
|
+
count: 0,
|
|
63
|
+
bytes: 0,
|
|
64
|
+
evictableBytes: 0,
|
|
65
|
+
pinnedBytes: 0,
|
|
66
|
+
oldestAt: null,
|
|
67
|
+
};
|
|
68
|
+
const ZERO_OBSERVED_SNAPSHOT = { currentBytes: 0, highWaterBytes: 0, active: 0 };
|
|
69
|
+
|
|
70
|
+
const retainedStores = new Map<string, RetainedStoreRegistration>();
|
|
71
|
+
const observedBuffers = new Map<string, ObservedBufferRegistration>();
|
|
72
|
+
let budgetBytes = DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES;
|
|
73
|
+
let lastSaturationWarningAt = Number.NEGATIVE_INFINITY;
|
|
74
|
+
const enforcementCounters = {
|
|
75
|
+
runs: 0,
|
|
76
|
+
entriesDemoted: 0,
|
|
77
|
+
bytesReleased: 0,
|
|
78
|
+
noEvictableCandidate: 0,
|
|
79
|
+
snapshotFailures: 0,
|
|
80
|
+
oldestAtContractViolations: 0,
|
|
81
|
+
};
|
|
82
|
+
let isEnforcing = false;
|
|
83
|
+
|
|
84
|
+
export function resolveAppOwnedMemoryBudgetBytes(value: unknown): number {
|
|
85
|
+
return typeof value === "number"
|
|
86
|
+
&& Number.isInteger(value)
|
|
87
|
+
&& value >= MIN_APP_OWNED_MEMORY_BUDGET_MB
|
|
88
|
+
&& value <= MAX_APP_OWNED_MEMORY_BUDGET_MB
|
|
89
|
+
? value * 1024 * 1024
|
|
90
|
+
: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function registerRetainedStore(registration: RetainedStoreRegistration): () => void {
|
|
94
|
+
retainedStores.set(registration.id, registration);
|
|
95
|
+
return () => {
|
|
96
|
+
if (retainedStores.get(registration.id) === registration) retainedStores.delete(registration.id);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function registerObservedBuffer(registration: ObservedBufferRegistration): () => void {
|
|
101
|
+
observedBuffers.set(registration.id, registration);
|
|
102
|
+
return () => {
|
|
103
|
+
if (observedBuffers.get(registration.id) === registration) observedBuffers.delete(registration.id);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function configureAppOwnedMemoryBudget(bytes: number): void {
|
|
108
|
+
budgetBytes = bytes;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function retainedSnapshot(registration: RetainedStoreRegistration): RetainedStoreSnapshot {
|
|
112
|
+
try {
|
|
113
|
+
return registration.snapshot();
|
|
114
|
+
} catch {
|
|
115
|
+
enforcementCounters.snapshotFailures += 1;
|
|
116
|
+
return { ...ZERO_RETAINED_SNAPSHOT };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function observedSnapshot(
|
|
121
|
+
registration: ObservedBufferRegistration,
|
|
122
|
+
): { currentBytes: number; highWaterBytes: number; active: number } {
|
|
123
|
+
try {
|
|
124
|
+
return registration.snapshot();
|
|
125
|
+
} catch {
|
|
126
|
+
enforcementCounters.snapshotFailures += 1;
|
|
127
|
+
return { ...ZERO_OBSERVED_SNAPSHOT };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function appOwnedBytesSnapshot(): AppOwnedBytesSnapshot {
|
|
132
|
+
return appOwnedBytesSnapshotFrom(retainedSnapshots().stores);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function appOwnedBytesSnapshotFrom(
|
|
136
|
+
retained: ReadonlyMap<string, RetainedStoreSnapshot>,
|
|
137
|
+
): AppOwnedBytesSnapshot {
|
|
138
|
+
const stores: Record<string, RetainedStoreSnapshot> = {};
|
|
139
|
+
let retainedBytes = 0;
|
|
140
|
+
let evictableBytes = 0;
|
|
141
|
+
let pinnedBytes = 0;
|
|
142
|
+
for (const registration of retainedStores.values()) {
|
|
143
|
+
const snapshot = retained.get(registration.id) ?? ZERO_RETAINED_SNAPSHOT;
|
|
144
|
+
stores[registration.id] = snapshot;
|
|
145
|
+
retainedBytes += snapshot.bytes;
|
|
146
|
+
evictableBytes += snapshot.evictableBytes;
|
|
147
|
+
pinnedBytes += snapshot.pinnedBytes;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const observedInFlight: AppOwnedBytesSnapshot["observedInFlight"] = {};
|
|
151
|
+
for (const registration of observedBuffers.values()) {
|
|
152
|
+
observedInFlight[registration.id] = observedSnapshot(registration);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
budgetBytes,
|
|
157
|
+
retainedBytes,
|
|
158
|
+
evictableBytes,
|
|
159
|
+
pinnedBytes,
|
|
160
|
+
overBudgetBytes: Math.max(0, retainedBytes - budgetBytes),
|
|
161
|
+
stores,
|
|
162
|
+
observedInFlight,
|
|
163
|
+
enforcement: { ...enforcementCounters },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function nextCandidate(
|
|
168
|
+
snapshots: ReadonlyMap<string, RetainedStoreSnapshot>,
|
|
169
|
+
ineligible: Set<string>,
|
|
170
|
+
): RetainedStoreRegistration | undefined {
|
|
171
|
+
for (const category of CATEGORY_ORDER) {
|
|
172
|
+
let candidate: RetainedStoreRegistration | undefined;
|
|
173
|
+
let candidateAt = Number.POSITIVE_INFINITY;
|
|
174
|
+
for (const registration of retainedStores.values()) {
|
|
175
|
+
if (registration.category !== category || ineligible.has(registration.id)) continue;
|
|
176
|
+
const snapshot = snapshots.get(registration.id);
|
|
177
|
+
if (!snapshot || snapshot.evictableBytes <= 0) continue;
|
|
178
|
+
const oldestAt = snapshot.oldestAt;
|
|
179
|
+
if (oldestAt === null || !Number.isFinite(oldestAt)) {
|
|
180
|
+
enforcementCounters.oldestAtContractViolations += 1;
|
|
181
|
+
ineligible.add(registration.id);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!candidate || oldestAt < candidateAt) {
|
|
185
|
+
candidate = registration;
|
|
186
|
+
candidateAt = oldestAt;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (candidate) return candidate;
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function retainedSnapshots(): { total: number; stores: Map<string, RetainedStoreSnapshot> } {
|
|
195
|
+
const stores = new Map<string, RetainedStoreSnapshot>();
|
|
196
|
+
let total = 0;
|
|
197
|
+
for (const registration of retainedStores.values()) {
|
|
198
|
+
const snapshot = retainedSnapshot(registration);
|
|
199
|
+
stores.set(registration.id, snapshot);
|
|
200
|
+
total += snapshot.bytes;
|
|
201
|
+
}
|
|
202
|
+
return { total, stores };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function warnPinnedSaturation(): void {
|
|
206
|
+
const at = Date.now();
|
|
207
|
+
if (at - lastSaturationWarningAt < WARN_INTERVAL_MS) return;
|
|
208
|
+
lastSaturationWarningAt = at;
|
|
209
|
+
console.warn("[app-owned-memory] retained state remains over budget with no evictable candidate");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function enforceAppOwnedMemoryBudget(): AppOwnedBytesSnapshot {
|
|
213
|
+
if (isEnforcing) return appOwnedBytesSnapshot();
|
|
214
|
+
isEnforcing = true;
|
|
215
|
+
enforcementCounters.runs += 1;
|
|
216
|
+
try {
|
|
217
|
+
const ineligible = new Set<string>();
|
|
218
|
+
const current = retainedSnapshots();
|
|
219
|
+
while (current.total > budgetBytes) {
|
|
220
|
+
const candidate = nextCandidate(current.stores, ineligible);
|
|
221
|
+
if (!candidate) {
|
|
222
|
+
enforcementCounters.noEvictableCandidate += 1;
|
|
223
|
+
warnPinnedSaturation();
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let reportedReleased = 0;
|
|
228
|
+
try {
|
|
229
|
+
reportedReleased = candidate.evictOldest();
|
|
230
|
+
} catch {
|
|
231
|
+
ineligible.add(candidate.id);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const previousSnapshot = current.stores.get(candidate.id) ?? ZERO_RETAINED_SNAPSHOT;
|
|
235
|
+
const nextSnapshot = retainedSnapshot(candidate);
|
|
236
|
+
current.stores.set(candidate.id, nextSnapshot);
|
|
237
|
+
const nextTotal = Math.max(0, current.total - previousSnapshot.bytes + nextSnapshot.bytes);
|
|
238
|
+
const actualReleased = Math.max(0, current.total - nextTotal);
|
|
239
|
+
if (reportedReleased <= 0 || actualReleased <= 0) {
|
|
240
|
+
ineligible.add(candidate.id);
|
|
241
|
+
} else {
|
|
242
|
+
enforcementCounters.entriesDemoted += 1;
|
|
243
|
+
enforcementCounters.bytesReleased += actualReleased;
|
|
244
|
+
}
|
|
245
|
+
current.total = nextTotal;
|
|
246
|
+
}
|
|
247
|
+
return appOwnedBytesSnapshotFrom(current.stores);
|
|
248
|
+
} finally {
|
|
249
|
+
isEnforcing = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function resetAppOwnedMemoryForTests(): void {
|
|
254
|
+
retainedStores.clear();
|
|
255
|
+
observedBuffers.clear();
|
|
256
|
+
budgetBytes = DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES;
|
|
257
|
+
lastSaturationWarningAt = Number.NEGATIVE_INFINITY;
|
|
258
|
+
enforcementCounters.runs = 0;
|
|
259
|
+
enforcementCounters.entriesDemoted = 0;
|
|
260
|
+
enforcementCounters.bytesReleased = 0;
|
|
261
|
+
enforcementCounters.noEvictableCandidate = 0;
|
|
262
|
+
enforcementCounters.snapshotFailures = 0;
|
|
263
|
+
enforcementCounters.oldestAtContractViolations = 0;
|
|
264
|
+
isEnforcing = false;
|
|
265
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bun runtime stream-capability gate for
|
|
2
|
+
* Bun runtime stream-capability gate for eager SSE passthrough (#314).
|
|
3
3
|
*
|
|
4
4
|
* The eager bounded relay (src/server/relay-eager.ts) uses a JS async producer
|
|
5
5
|
* loop — the exact shape of the Bun#32111 use-after-free (fixed upstream by Bun
|
|
6
6
|
* PR #32120, merged 2026-06-21). No RELEASED Bun version is proven to carry
|
|
7
7
|
* that fix yet, so `MIN_FIXED_BUN_VERSION` is null: every runtime is
|
|
8
|
-
* "known-bad" until a bundle-bump commit sets it.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* "known-bad" until a bundle-bump commit sets it. Windows no-rewrite traffic
|
|
9
|
+
* follows this runtime/config decision. Darwin no-rewrite traffic stays on tee
|
|
10
|
+
* for `auto` regardless of runtime capability and reaches eager relay only via
|
|
11
|
+
* explicit `streamMode: "eager-relay"` opt-in (see
|
|
12
|
+
* devlog/_plan/260731_macos_rss_retention/100_darwin_eager_optin.md).
|
|
11
13
|
*
|
|
12
14
|
* Prerelease conservatism: a version carrying a prerelease suffix (e.g.
|
|
13
15
|
* `1.4.0-canary.3`) is NEVER treated as fixed even when its numeric triple
|
|
@@ -71,9 +73,8 @@ export type EagerRelayDecision = {
|
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
/**
|
|
74
|
-
* Decide the
|
|
75
|
-
* for tests
|
|
76
|
-
* unchanged); the caller owns the platform check.
|
|
76
|
+
* Decide the runtime/config SSE client-path capability. `version`/`minFixed`
|
|
77
|
+
* are injectable for tests; `selectEagerPath` applies platform policy.
|
|
77
78
|
*/
|
|
78
79
|
export function decideEagerRelay(
|
|
79
80
|
mode: StreamMode,
|
|
@@ -86,3 +87,26 @@ export function decideEagerRelay(
|
|
|
86
87
|
? { useEagerRelay: true, reason: "auto-fixed-runtime" }
|
|
87
88
|
: { useEagerRelay: false, reason: "auto-known-bad" };
|
|
88
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Apply the two-platform eager-relay policy to the runtime/config capability.
|
|
93
|
+
* Windows preserves the decision for no-rewrite traffic. Darwin permits only
|
|
94
|
+
* explicit config opt-in; `auto` remains tee even on a future fixed runtime.
|
|
95
|
+
* Returns the normalized effective decision, or null when platform policy,
|
|
96
|
+
* rewrite needs, or a Darwin non-config-eager mode selects tee.
|
|
97
|
+
*/
|
|
98
|
+
export function selectEagerPath(
|
|
99
|
+
platform: NodeJS.Platform,
|
|
100
|
+
needsClientRewrite: boolean,
|
|
101
|
+
mode: StreamMode,
|
|
102
|
+
version: string = Bun.version,
|
|
103
|
+
minFixed: string | null = MIN_FIXED_BUN_VERSION,
|
|
104
|
+
): EagerRelayDecision | null {
|
|
105
|
+
if (needsClientRewrite || (platform !== "win32" && platform !== "darwin")) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const decision = decideEagerRelay(mode, version, minFixed);
|
|
110
|
+
if (platform === "win32") return decision;
|
|
111
|
+
return decision.reason === "config-eager" ? decision : null;
|
|
112
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
|
+
import type { GenerationContext } from "./state-store-sweeper";
|
|
15
16
|
|
|
16
17
|
export const CONFIG_OWNER_FILE = ".opencodex-owner.json";
|
|
17
18
|
export const CONFIG_UNINSTALL_MANIFEST = ".opencodex-uninstall.json";
|
|
@@ -47,6 +48,10 @@ const INITIAL_OWNED_PATHS = [
|
|
|
47
48
|
"codex-shim.autorestore.lock",
|
|
48
49
|
"codex-shim.json",
|
|
49
50
|
"config.json",
|
|
51
|
+
"config-mutation.sqlite",
|
|
52
|
+
"config-mutation.sqlite-journal",
|
|
53
|
+
"config-mutation.sqlite-shm",
|
|
54
|
+
"config-mutation.sqlite-wal",
|
|
50
55
|
"crash.log",
|
|
51
56
|
"kimi-device-id",
|
|
52
57
|
"mimo-client-id",
|
|
@@ -76,6 +81,34 @@ const ownershipCache = new Map<string, {
|
|
|
76
81
|
owner: ConfigOwner;
|
|
77
82
|
manifest: ConfigUninstallManifest;
|
|
78
83
|
} | null>();
|
|
84
|
+
let lastReconciledGeneration = 0;
|
|
85
|
+
|
|
86
|
+
export function listLiveConfigOwnershipRoots(currentConfigDir: string): ReadonlySet<string> {
|
|
87
|
+
const currentRoot = ownershipCacheKey(currentConfigDir);
|
|
88
|
+
const roots = new Set<string>([currentRoot]);
|
|
89
|
+
for (const [root, ownership] of ownershipCache) {
|
|
90
|
+
if (root === currentRoot) continue;
|
|
91
|
+
if (ownership?.manifest.paths.some(rel => existsSync(join(root, ...rel.split("/"))))) {
|
|
92
|
+
roots.add(root);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return roots;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function reconcileConfigOwnershipRoots(context: GenerationContext): number {
|
|
99
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
100
|
+
let removed = 0;
|
|
101
|
+
for (const [root, ownership] of ownershipCache) {
|
|
102
|
+
if (context.configRoots.has(root)) continue;
|
|
103
|
+
const hasLiveOwnedPath = ownership?.manifest.paths.some(rel =>
|
|
104
|
+
existsSync(join(root, ...rel.split("/"))));
|
|
105
|
+
if (hasLiveOwnedPath) continue;
|
|
106
|
+
ownershipCache.delete(root);
|
|
107
|
+
removed += 1;
|
|
108
|
+
}
|
|
109
|
+
lastReconciledGeneration = context.generation;
|
|
110
|
+
return removed;
|
|
111
|
+
}
|
|
79
112
|
|
|
80
113
|
function ownershipCacheKey(configDir: string): string {
|
|
81
114
|
const key = resolve(configDir);
|
package/src/lib/crash-guard.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { getConfigDir } from "../config";
|
|
|
4
4
|
import { recordOwnedConfigPath } from "./config-ownership";
|
|
5
5
|
import { redactSecretString, redactUrlForLog } from "./redact";
|
|
6
6
|
import { sidecarBreadcrumb, activityBreadcrumb } from "./sidecar-tracker";
|
|
7
|
+
import { retainedUtf8Bytes, truncateRetainedUtf8 } from "./admission";
|
|
8
|
+
import { enforceAppOwnedMemoryBudget } from "./app-owned-memory";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Process-level safety net for the long-running proxy daemon.
|
|
@@ -205,8 +207,42 @@ function record(kind: string, err: unknown, promise?: unknown): void {
|
|
|
205
207
|
|
|
206
208
|
interface FetchTrace { url: string; at: number; origin: string; settled: boolean; rejected?: string }
|
|
207
209
|
const FETCH_RING_MAX = 12;
|
|
210
|
+
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
208
211
|
const fetchRing: FetchTrace[] = [];
|
|
209
212
|
let fetchInstrumented = false;
|
|
213
|
+
let fetchRingBytes = 0;
|
|
214
|
+
|
|
215
|
+
function fetchTraceBytes(trace: FetchTrace): number {
|
|
216
|
+
return retainedUtf8Bytes(trace.url) + retainedUtf8Bytes(trace.origin) + retainedUtf8Bytes(trace.rejected ?? "");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function removeOldestFetchTrace(): number {
|
|
220
|
+
const trace = fetchRing.shift();
|
|
221
|
+
if (!trace) return 0;
|
|
222
|
+
const bytes = fetchTraceBytes(trace);
|
|
223
|
+
fetchRingBytes -= bytes;
|
|
224
|
+
return bytes;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setFetchTraceRejected(trace: FetchTrace, value: string): void {
|
|
228
|
+
const retained = fetchRing.includes(trace);
|
|
229
|
+
if (retained) fetchRingBytes -= retainedUtf8Bytes(trace.rejected ?? "");
|
|
230
|
+
trace.rejected = truncateRetainedUtf8(value, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
231
|
+
if (retained) fetchRingBytes += retainedUtf8Bytes(trace.rejected);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function appendFetchTrace(url: string, origin: string): FetchTrace {
|
|
235
|
+
const trace: FetchTrace = {
|
|
236
|
+
url: truncateRetainedUtf8(redactUrlForLog(url), MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
237
|
+
at: Date.now(),
|
|
238
|
+
origin: truncateRetainedUtf8(origin, MAX_DIAGNOSTIC_VALUE_BYTES),
|
|
239
|
+
settled: false,
|
|
240
|
+
};
|
|
241
|
+
fetchRing.push(trace);
|
|
242
|
+
fetchRingBytes += fetchTraceBytes(trace);
|
|
243
|
+
while (fetchRing.length > FETCH_RING_MAX) removeOldestFetchTrace();
|
|
244
|
+
return trace;
|
|
245
|
+
}
|
|
210
246
|
|
|
211
247
|
/**
|
|
212
248
|
* The recurring native-only rejection carries no source location, and every JS `await fetch(...)`
|
|
@@ -228,24 +264,48 @@ function instrumentFetch(): void {
|
|
|
228
264
|
url = typeof input === "string" ? input : input instanceof URL ? input.href : (input as Request)?.url ?? "";
|
|
229
265
|
} catch { /* best-effort */ }
|
|
230
266
|
const origin = (new Error().stack ?? "").split("\n").slice(2, 5).map(l => l.trim()).join(" <- ");
|
|
231
|
-
const trace
|
|
232
|
-
|
|
233
|
-
if (fetchRing.length > FETCH_RING_MAX) fetchRing.shift();
|
|
267
|
+
const trace = appendFetchTrace(url, origin);
|
|
268
|
+
enforceAppOwnedMemoryBudget();
|
|
234
269
|
let p: ReturnType<typeof fetch>;
|
|
235
270
|
try {
|
|
236
271
|
p = original.apply(this, args);
|
|
237
272
|
} catch (e) {
|
|
238
273
|
trace.settled = true;
|
|
239
|
-
trace
|
|
274
|
+
setFetchTraceRejected(trace, redactDiagnosticText(e instanceof Error ? `${e.name}: ${e.message}` : String(e)));
|
|
240
275
|
throw e;
|
|
241
276
|
}
|
|
242
277
|
return p.then(
|
|
243
278
|
r => { trace.settled = true; return r; },
|
|
244
|
-
e => { trace.settled = true; trace
|
|
279
|
+
e => { trace.settled = true; setFetchTraceRejected(trace, redactDiagnosticText(e instanceof Error ? `${e.name}: ${e.message}` : String(e))); throw e; },
|
|
245
280
|
);
|
|
246
281
|
} as typeof fetch;
|
|
247
282
|
}
|
|
248
283
|
|
|
284
|
+
export function crashRingMetrics(): { entries: number; bytes: number; oldestAt: number | null } {
|
|
285
|
+
return { entries: fetchRing.length, bytes: fetchRingBytes, oldestAt: fetchRing[0]?.at ?? null };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function evictOldestCrashTraceForBudget(): number {
|
|
289
|
+
return removeOldestFetchTrace();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function appendCrashTraceForTests(url: string, origin: string, rejected?: string): void {
|
|
293
|
+
const trace = appendFetchTrace(url, origin);
|
|
294
|
+
if (rejected !== undefined) {
|
|
295
|
+
trace.settled = true;
|
|
296
|
+
setFetchTraceRejected(trace, rejected);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function crashRingEntriesForTests(): readonly Readonly<FetchTrace>[] {
|
|
301
|
+
return fetchRing.map(trace => ({ ...trace }));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function resetCrashRingForTests(): void {
|
|
305
|
+
fetchRing.length = 0;
|
|
306
|
+
fetchRingBytes = 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
249
309
|
/** Render the recent fetch ring (pending first) for the crash breadcrumb. */
|
|
250
310
|
function recentFetches(): string {
|
|
251
311
|
try {
|
|
@@ -8,15 +8,31 @@ export interface DebugLogEntry {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const MAX_LINES = 2_000;
|
|
11
|
+
const MAX_DEBUG_SUBSCRIBERS = 64;
|
|
12
|
+
const MAX_DEBUG_LINE_BYTES = 16 * 1024;
|
|
11
13
|
const buffer: DebugLogEntry[] = [];
|
|
12
|
-
|
|
14
|
+
interface DebugSubscriberRegistration { lease: AdmissionLease }
|
|
15
|
+
const listeners = new Map<(entry: DebugLogEntry) => void, DebugSubscriberRegistration>();
|
|
16
|
+
const subscriberGate = createAdmissionGate("debug_subscribers", MAX_DEBUG_SUBSCRIBERS);
|
|
13
17
|
let nextSeq = 1;
|
|
18
|
+
let bufferBytes = 0;
|
|
19
|
+
|
|
20
|
+
function removeOldestEntry(): number {
|
|
21
|
+
const entry = buffer.shift();
|
|
22
|
+
if (!entry) return 0;
|
|
23
|
+
const bytes = retainedUtf8Bytes(entry.line);
|
|
24
|
+
bufferBytes -= bytes;
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
14
27
|
|
|
15
28
|
export function appendDebugLogLine(line: string): void {
|
|
16
|
-
const
|
|
29
|
+
const retainedLine = truncateRetainedUtf8(line, MAX_DEBUG_LINE_BYTES);
|
|
30
|
+
const entry: DebugLogEntry = { seq: nextSeq++, at: Date.now(), line: retainedLine };
|
|
17
31
|
buffer.push(entry);
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
bufferBytes += retainedUtf8Bytes(retainedLine);
|
|
33
|
+
while (buffer.length > MAX_LINES) removeOldestEntry();
|
|
34
|
+
enforceAppOwnedMemoryBudget();
|
|
35
|
+
for (const listener of listeners.keys()) {
|
|
20
36
|
try { listener(entry); } catch { /* listeners must not break logging */ }
|
|
21
37
|
}
|
|
22
38
|
}
|
|
@@ -30,13 +46,38 @@ export function getDebugLogEntries(options?: { after?: number; limit?: number })
|
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
export function subscribeDebugLogEntries(listener: (entry: DebugLogEntry) => void): () => void {
|
|
33
|
-
listeners.
|
|
34
|
-
|
|
49
|
+
let registration = listeners.get(listener);
|
|
50
|
+
if (!registration) {
|
|
51
|
+
const lease = subscriberGate.tryAcquire();
|
|
52
|
+
if (!lease) throw new ResourceAdmissionError("debug_subscribers", MAX_DEBUG_SUBSCRIBERS);
|
|
53
|
+
registration = { lease };
|
|
54
|
+
listeners.set(listener, registration);
|
|
55
|
+
}
|
|
56
|
+
let disposed = false;
|
|
57
|
+
return () => {
|
|
58
|
+
if (disposed) return;
|
|
59
|
+
disposed = true;
|
|
60
|
+
if (listeners.get(listener) !== registration) return;
|
|
61
|
+
listeners.delete(listener);
|
|
62
|
+
registration.lease.release();
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function debugBufferMetrics(): { entries: number; bytes: number; subscribers: AdmissionMetrics; oldestAt: number | null } {
|
|
67
|
+
return { entries: buffer.length, bytes: bufferBytes, subscribers: subscriberGate.metrics(), oldestAt: buffer[0]?.at ?? null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function evictOldestDebugEntryForBudget(): number {
|
|
71
|
+
return removeOldestEntry();
|
|
35
72
|
}
|
|
36
73
|
|
|
37
74
|
/** Test isolation. */
|
|
38
75
|
export function resetDebugLogBufferForTests(): void {
|
|
39
76
|
buffer.length = 0;
|
|
77
|
+
bufferBytes = 0;
|
|
78
|
+
for (const registration of listeners.values()) registration.lease.release();
|
|
40
79
|
listeners.clear();
|
|
41
80
|
nextSeq = 1;
|
|
42
81
|
}
|
|
82
|
+
import { createAdmissionGate, ResourceAdmissionError, retainedUtf8Bytes, truncateRetainedUtf8, type AdmissionLease, type AdmissionMetrics } from "./admission";
|
|
83
|
+
import { enforceAppOwnedMemoryBudget } from "./app-owned-memory";
|
|
@@ -130,7 +130,18 @@ function registryAllowsPrivateNetwork(name: string): boolean {
|
|
|
130
130
|
return getProviderRegistryEntry(name)?.allowPrivateNetworkByDefault === true;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Whether a provider may reach loopback/private addresses.
|
|
135
|
+
*
|
|
136
|
+
* Two sources, and both have to be consulted at every boundary: the operator's explicit
|
|
137
|
+
* `allowPrivateNetwork`, and the registry's `allowPrivateNetworkByDefault` for entries that are
|
|
138
|
+
* local BY DEFINITION (Ollama, vLLM, LM Studio, LiteLLM). Config validation already read both;
|
|
139
|
+
* outbound discovery read only the first, so a stock Ollama entry passed validation and was then
|
|
140
|
+
* refused at the fetch (#758).
|
|
141
|
+
*
|
|
142
|
+
* This grants nothing new. Metadata, link-local and unspecified destinations are rejected before
|
|
143
|
+
* this is consulted, and a provider without either source still cannot reach a private address.
|
|
144
|
+
*/
|
|
134
145
|
export function providerAllowsPrivateNetwork(
|
|
135
146
|
name: string,
|
|
136
147
|
provider: Pick<OcxProviderConfig, "allowPrivateNetwork">,
|
package/src/lib/errors.ts
CHANGED
|
@@ -104,6 +104,9 @@ export function isClientClosedMessage(text: string): boolean {
|
|
|
104
104
|
|
|
105
105
|
export function classifyError(status: number, type: string, message: string): OcxErrorPayload {
|
|
106
106
|
const text = message.toLowerCase();
|
|
107
|
+
if (type === "previous_response_not_found") {
|
|
108
|
+
return { message, type: "invalid_request_error", code: "previous_response_not_found" };
|
|
109
|
+
}
|
|
107
110
|
// Preserve explicit cancel types used by compact/combo JSON errors; unify message-inferred
|
|
108
111
|
// client closes (web-search abort text) onto client_closed_request for /api/logs.
|
|
109
112
|
if (type === "client_cancelled") {
|
package/src/lib/gcp-adc.ts
CHANGED
|
@@ -18,6 +18,11 @@ import { Buffer } from "node:buffer";
|
|
|
18
18
|
import * as os from "node:os";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
21
|
+
import {
|
|
22
|
+
captureConfigGeneration,
|
|
23
|
+
sweepExpiredOnWrite,
|
|
24
|
+
type GenerationContext,
|
|
25
|
+
} from "./state-store-sweeper";
|
|
21
26
|
|
|
22
27
|
/** Injectable fetch (tests pass a mock); defaults to the global fetch. */
|
|
23
28
|
export type FetchImpl = typeof fetch;
|
|
@@ -60,6 +65,33 @@ interface TokenResponse {
|
|
|
60
65
|
|
|
61
66
|
const tokenCache = new Map<string, CachedToken>();
|
|
62
67
|
const inflight = new Map<string, Promise<string>>();
|
|
68
|
+
let lastReconciledGeneration = 0;
|
|
69
|
+
let liveSources = new Set<string>();
|
|
70
|
+
|
|
71
|
+
export function sweepExpiredGcpAdcTokens(now = Date.now()): number {
|
|
72
|
+
const skew = getRefreshSkewMs();
|
|
73
|
+
let removed = 0;
|
|
74
|
+
for (const [source, cached] of tokenCache) {
|
|
75
|
+
if (cached.expiresAtMs - skew > now) continue;
|
|
76
|
+
tokenCache.delete(source);
|
|
77
|
+
removed += 1;
|
|
78
|
+
}
|
|
79
|
+
return removed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function reconcileGcpAdcTokens(context: GenerationContext): number {
|
|
83
|
+
if (context.generation <= lastReconciledGeneration) return 0;
|
|
84
|
+
const currentSource = currentAdcSourceKey();
|
|
85
|
+
let removed = 0;
|
|
86
|
+
for (const source of tokenCache.keys()) {
|
|
87
|
+
if (source === currentSource) continue;
|
|
88
|
+
tokenCache.delete(source);
|
|
89
|
+
removed += 1;
|
|
90
|
+
}
|
|
91
|
+
liveSources = new Set([currentSource]);
|
|
92
|
+
lastReconciledGeneration = context.generation;
|
|
93
|
+
return removed;
|
|
94
|
+
}
|
|
63
95
|
|
|
64
96
|
function getRefreshSkewMs(): number {
|
|
65
97
|
const raw = Number(process.env.GOOGLE_VERTEX_REFRESH_SKEW_MS);
|
|
@@ -271,6 +303,7 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
|
|
|
271
303
|
// Only serve a cached token that matches the source the next resolve would actually use; prune
|
|
272
304
|
// expired or now-stale (different-source) entries so a credential-source change is honored.
|
|
273
305
|
const expectedSource = currentAdcSourceKey();
|
|
306
|
+
const writerGeneration = captureConfigGeneration();
|
|
274
307
|
for (const [source, cached] of tokenCache) {
|
|
275
308
|
if (source === expectedSource && cached.expiresAtMs - skew > now) return cached.token;
|
|
276
309
|
if (cached.expiresAtMs - skew <= now) tokenCache.delete(source);
|
|
@@ -286,7 +319,10 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
|
|
|
286
319
|
try {
|
|
287
320
|
const { source, token } = await resolveAccessTokenUncached(options?.signal, fetchImpl);
|
|
288
321
|
const expiresAtMs = Date.now() + Math.max(0, token.expires_in * 1000);
|
|
289
|
-
|
|
322
|
+
if (writerGeneration >= lastReconciledGeneration || liveSources.has(source)) {
|
|
323
|
+
tokenCache.set(source, { token: token.access_token, expiresAtMs });
|
|
324
|
+
sweepExpiredOnWrite(Date.now());
|
|
325
|
+
}
|
|
290
326
|
return token.access_token;
|
|
291
327
|
} finally {
|
|
292
328
|
inflight.delete(cacheKey);
|
|
@@ -296,8 +332,10 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
|
|
|
296
332
|
return promise;
|
|
297
333
|
}
|
|
298
334
|
|
|
299
|
-
/** Test seam:
|
|
335
|
+
/** Test seam: restores complete process-local token and reconciliation state. */
|
|
300
336
|
export function __resetVertexTokenCache(): void {
|
|
301
337
|
tokenCache.clear();
|
|
302
338
|
inflight.clear();
|
|
339
|
+
lastReconciledGeneration = 0;
|
|
340
|
+
liveSources = new Set();
|
|
303
341
|
}
|