@bitkyc08/opencodex 2.36.0 → 2.38.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/bin/ocx.mjs +69 -10
- package/gui/dist/assets/index-C14iCj_Q.js +112 -0
- package/gui/dist/assets/index-D7PIz7_g.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/aside.svg +3 -0
- package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
- package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
- package/gui/dist/provider-icons/openclaw.svg +54 -0
- package/gui/dist/provider-icons/prime-agent.svg +21 -0
- package/gui/dist/provider-icons/zcode.svg +219 -0
- package/package.json +1 -1
- package/src/adapters/cursor/protobuf-request.ts +4 -1
- package/src/adapters/cursor/tool-definitions.ts +36 -4
- package/src/adapters/kiro-constants.ts +36 -2
- package/src/adapters/kiro.ts +13 -2
- package/src/cli/capabilities.ts +14 -0
- package/src/cli/claude.ts +12 -0
- package/src/cli/codex-cli-update.ts +96 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/export-command.ts +18 -17
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +3 -2
- package/src/cli/launcher-context.ts +53 -2
- package/src/cli/opencode.ts +126 -33
- package/src/cli/registry.ts +16 -10
- package/src/cli/system-command.ts +6 -1
- package/src/clients/config-export.ts +293 -28
- package/src/codex/account-store.ts +10 -4
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/provider-fetch.ts +20 -1
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/cli-install-provenance.ts +795 -0
- package/src/codex/convergence.ts +4 -3
- package/src/codex/credential-mutation-epoch.ts +11 -0
- package/src/codex/main-account.ts +2 -0
- package/src/codex/model-entitlements.ts +489 -30
- package/src/codex/native-profile-manager.ts +4 -0
- package/src/codex/reset-credit-operation-ledger.ts +1411 -0
- package/src/codex/reset-credit-recovery.ts +20 -2
- package/src/codex/shim.ts +204 -18
- package/src/codex/user-identity.ts +2 -1
- package/src/config/paths.ts +18 -3
- package/src/config.ts +23 -0
- package/src/generated/compatibility-version.json +84 -48
- package/src/integrations/registry.ts +112 -0
- package/src/integrations/state.ts +67 -5
- package/src/integrations/writer.ts +25 -9
- package/src/lib/bounded-subprocess.ts +36 -0
- package/src/lib/strict-semver.ts +47 -0
- package/src/lib/windows-elevation.ts +32 -1
- package/src/lib/windows-secret-acl.ts +47 -25
- package/src/lib/windows-service-mutation-lock.ts +133 -0
- package/src/lib/windows-user-principal.ts +15 -17
- package/src/responses/spill-store.ts +334 -29
- package/src/responses/state.ts +488 -7
- package/src/server/index.ts +4 -3
- package/src/server/lifecycle.ts +5 -1
- package/src/server/management/model-rows.ts +11 -2
- package/src/server/management/provider-routes.ts +4 -0
- package/src/server/management/system-restart.ts +5 -5
- package/src/server/management-api.ts +7 -2
- package/src/server/startup-action-control.ts +3 -2
- package/src/service.ts +594 -33
- package/src/sidecar/candidates.ts +1 -1
- package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/update/index.ts +3 -2
- package/src/update/job.ts +10 -11
- package/gui/dist/assets/index-Cy7Z_pl0.css +0 -1
- package/gui/dist/assets/index-DO8liQVL.js +0 -112
package/src/responses/state.ts
CHANGED
|
@@ -3,16 +3,23 @@ import { uptime } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { atomicWriteFileAsync, getConfigDir, resolveWriteTarget } from "../config";
|
|
5
5
|
import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
|
|
6
|
+
import { windowsSecretAclApplies } from "../lib/windows-secret-acl";
|
|
6
7
|
import type { OcxProviderContinuationState } from "../types";
|
|
7
8
|
import {
|
|
9
|
+
cleanupSupersededResponseSpillPublication,
|
|
10
|
+
createResponseSpillPublicationControl,
|
|
8
11
|
deleteResponseSpill,
|
|
12
|
+
MAX_RESPONSE_SPILL_PAYLOAD_BYTES,
|
|
9
13
|
noteStubSwapForTest,
|
|
10
14
|
readResponseSpill,
|
|
11
15
|
recoverOrphanedResponseSpills,
|
|
12
16
|
responseSpillDirectory,
|
|
13
17
|
responseSpillPayloadCap,
|
|
18
|
+
markResponseSpillPublicationSuperseded,
|
|
19
|
+
type ResponseSpillPublicationControl,
|
|
14
20
|
type ResponseSpillRef,
|
|
15
21
|
writeResponseSpillDurably,
|
|
22
|
+
writeResponseSpillDurablyAsync,
|
|
16
23
|
} from "./spill-store";
|
|
17
24
|
|
|
18
25
|
const MAX_STORED_RESPONSES = 1_000;
|
|
@@ -51,6 +58,10 @@ const PERIODIC_TEMP_MAX_CLEANUPS = 64;
|
|
|
51
58
|
const PERIODIC_TEMP_SCAN_DEADLINE_MS = 25;
|
|
52
59
|
const RESPONSE_STATE_TEMP_NAME = /^responses-state\.json\.ocx\.(\d+)\.(\d+)\.tmp$/;
|
|
53
60
|
const MAX_SNAPSHOT_REWRITE_ATTEMPTS = 4;
|
|
61
|
+
const RESPONSE_SPILL_SHUTDOWN_BUDGET_MS = 5_000;
|
|
62
|
+
const RESPONSE_SPILL_SHUTDOWN_FALLBACK_RESERVE_MS = 4_000;
|
|
63
|
+
const RESPONSE_SPILL_ASYNC_ACL_ATTEMPT_BUDGET_MS = 30_000;
|
|
64
|
+
const RESPONSE_SPILL_SHUTDOWN_TERMINALIZATION_MAX_PASSES = MAX_STORED_RESPONSES + 1;
|
|
54
65
|
|
|
55
66
|
interface ResidentResponseState {
|
|
56
67
|
kind: "resident";
|
|
@@ -160,6 +171,424 @@ const pendingSpillUnlinks: ResponseSpillRef[] = [];
|
|
|
160
171
|
// structured 400 — bounded-loss, never silent corruption or unbounded disk.
|
|
161
172
|
const PENDING_SPILL_UNLINKS_MAX = 128;
|
|
162
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Windows keeps the candidate replayable while required ACL hardening runs off the event loop.
|
|
176
|
+
* Pending bytes are pinned, not evictable; cap them below the process-owned 512 MiB ceiling so an
|
|
177
|
+
* icacls outage cannot turn the serialized queue into an unbounded resident backlog.
|
|
178
|
+
*/
|
|
179
|
+
const MAX_PENDING_RESPONSE_SPILL_BYTES = MAX_RESPONSE_SPILL_PAYLOAD_BYTES;
|
|
180
|
+
|
|
181
|
+
interface PendingResponseSpill {
|
|
182
|
+
id: string;
|
|
183
|
+
candidate: ResidentResponseState | null;
|
|
184
|
+
supersededSpill?: ResponseSpillRef;
|
|
185
|
+
directAdmission: boolean;
|
|
186
|
+
running: boolean;
|
|
187
|
+
cancelled: boolean;
|
|
188
|
+
released: boolean;
|
|
189
|
+
sizeBytes: number;
|
|
190
|
+
publicationControl: ResponseSpillPublicationControl;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const pendingResponseSpills = new Set<PendingResponseSpill>();
|
|
194
|
+
const pendingResponseSpillById = new Map<string, PendingResponseSpill>();
|
|
195
|
+
let pendingResponseSpillBytes = 0;
|
|
196
|
+
let responseSpillPublicationTail: Promise<void> = Promise.resolve();
|
|
197
|
+
let responseSpillShutdownBudgetOverride: { totalMs: number; fallbackReserveMs: number } | null = null;
|
|
198
|
+
let responseSpillShutdownTerminalizationPassLimitOverride: number | null = null;
|
|
199
|
+
let responseSpillAsyncAclAttemptBudgetOverride: number | null = null;
|
|
200
|
+
|
|
201
|
+
function deferSupersededSpill(ref: ResponseSpillRef | undefined): void {
|
|
202
|
+
if (!ref) return;
|
|
203
|
+
pendingSpillUnlinks.push(ref);
|
|
204
|
+
while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
|
|
205
|
+
deleteResponseSpill(pendingSpillUnlinks.shift()!);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function releasePendingResponseSpill(job: PendingResponseSpill): void {
|
|
210
|
+
if (job.released) return;
|
|
211
|
+
job.released = true;
|
|
212
|
+
pendingResponseSpillBytes = Math.max(0, pendingResponseSpillBytes - job.sizeBytes);
|
|
213
|
+
pendingResponseSpills.delete(job);
|
|
214
|
+
if (pendingResponseSpillById.get(job.id) === job) pendingResponseSpillById.delete(job.id);
|
|
215
|
+
job.candidate = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function cancelPendingResponseSpill(id: string): ResponseSpillRef | undefined {
|
|
219
|
+
const job = pendingResponseSpillById.get(id);
|
|
220
|
+
if (!job) return undefined;
|
|
221
|
+
pendingResponseSpillById.delete(id);
|
|
222
|
+
job.cancelled = true;
|
|
223
|
+
markResponseSpillPublicationSuperseded(job.publicationControl);
|
|
224
|
+
const superseded = job.supersededSpill;
|
|
225
|
+
// A queued job has not captured the candidate in an async frame yet, so release it now.
|
|
226
|
+
// A running job retains its accounting until settlement and will discard its stale file.
|
|
227
|
+
if (!job.running) releasePendingResponseSpill(job);
|
|
228
|
+
return superseded;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isAclTimeout(error: unknown): boolean {
|
|
232
|
+
return !!error && typeof error === "object" && "code" in error
|
|
233
|
+
&& String((error as { code?: unknown }).code) === "ETIMEDOUT";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function spillPayloadForResident(candidate: ResidentResponseState): Parameters<typeof writeResponseSpillDurably>[1] {
|
|
237
|
+
return {
|
|
238
|
+
createdAt: candidate.createdAt,
|
|
239
|
+
...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
|
|
240
|
+
items: candidate.items,
|
|
241
|
+
...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
|
|
242
|
+
...(candidate.providers ? { providers: candidate.providers } : {}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void> {
|
|
247
|
+
if (job.cancelled || !job.candidate) return;
|
|
248
|
+
job.running = true;
|
|
249
|
+
const candidate = job.candidate;
|
|
250
|
+
let ref: ResponseSpillRef | null = null;
|
|
251
|
+
try {
|
|
252
|
+
const state = spillPayloadForResident(candidate);
|
|
253
|
+
try {
|
|
254
|
+
ref = await writeResponseSpillDurablyAsync(job.id, state, {
|
|
255
|
+
aclBudgetMs: responseSpillAsyncAclAttemptBudgetMs(),
|
|
256
|
+
publicationControl: job.publicationControl,
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!isAclTimeout(error)) throw error;
|
|
260
|
+
// The ACL helper permits exactly one caller-owned recovery budget. The resident generation
|
|
261
|
+
// remains replayable during both attempts, so a transient timeout never becomes a tombstone.
|
|
262
|
+
ref = await writeResponseSpillDurablyAsync(job.id, state, {
|
|
263
|
+
aclBudgetMs: responseSpillAsyncAclAttemptBudgetMs(),
|
|
264
|
+
retryTimedOutOnce: true,
|
|
265
|
+
publicationControl: job.publicationControl,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (ref.payloadBytes > responseSpillPayloadCap()) {
|
|
269
|
+
deleteResponseSpill(ref);
|
|
270
|
+
ref = null;
|
|
271
|
+
if (job.directAdmission) admissionCounters.oversizedDrops += 1;
|
|
272
|
+
throw Object.assign(new Error("Response spill payload exceeds replay ceiling"), { code: "EFBIG" });
|
|
273
|
+
}
|
|
274
|
+
if (states.get(job.id) !== candidate || job.cancelled) {
|
|
275
|
+
deleteResponseSpill(ref);
|
|
276
|
+
ref = null;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (swapResidentForSpill(job.id, candidate, ref)) {
|
|
280
|
+
ref = null;
|
|
281
|
+
spillCounters.writes += 1;
|
|
282
|
+
if (job.directAdmission) admissionCounters.directSpills += 1;
|
|
283
|
+
deferSupersededSpill(job.supersededSpill);
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
if (ref) deleteResponseSpill(ref);
|
|
287
|
+
if (states.get(job.id) === candidate && !job.cancelled) {
|
|
288
|
+
spillCounters.writeFailures += 1;
|
|
289
|
+
replaceWithSpillFailure(job.id, candidate);
|
|
290
|
+
deferSupersededSpill(job.supersededSpill);
|
|
291
|
+
}
|
|
292
|
+
} finally {
|
|
293
|
+
const cancelled = job.cancelled;
|
|
294
|
+
releasePendingResponseSpill(job);
|
|
295
|
+
recomputeOldestResident();
|
|
296
|
+
if (!cancelled) {
|
|
297
|
+
schedulePersist();
|
|
298
|
+
pruneResponses();
|
|
299
|
+
enforceAppOwnedMemoryBudget();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function queuePendingResponseSpill(
|
|
305
|
+
id: string,
|
|
306
|
+
candidate: ResidentResponseState,
|
|
307
|
+
options: { supersededSpill?: ResponseSpillRef; directAdmission?: boolean } = {},
|
|
308
|
+
): void {
|
|
309
|
+
const inheritedSpill = cancelPendingResponseSpill(id) ?? options.supersededSpill;
|
|
310
|
+
if (pendingResponseSpillBytes + candidate.sizeBytes > MAX_PENDING_RESPONSE_SPILL_BYTES) {
|
|
311
|
+
spillCounters.writeFailures += 1;
|
|
312
|
+
replaceWithSpillFailure(id, candidate);
|
|
313
|
+
deferSupersededSpill(inheritedSpill);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const job: PendingResponseSpill = {
|
|
317
|
+
id,
|
|
318
|
+
candidate,
|
|
319
|
+
...(inheritedSpill ? { supersededSpill: inheritedSpill } : {}),
|
|
320
|
+
directAdmission: options.directAdmission === true,
|
|
321
|
+
running: false,
|
|
322
|
+
cancelled: false,
|
|
323
|
+
released: false,
|
|
324
|
+
sizeBytes: candidate.sizeBytes,
|
|
325
|
+
publicationControl: createResponseSpillPublicationControl(),
|
|
326
|
+
};
|
|
327
|
+
pendingResponseSpills.add(job);
|
|
328
|
+
pendingResponseSpillById.set(id, job);
|
|
329
|
+
pendingResponseSpillBytes += job.sizeBytes;
|
|
330
|
+
recomputeOldestResident();
|
|
331
|
+
responseSpillPublicationTail = responseSpillPublicationTail
|
|
332
|
+
.then(() => runPendingResponseSpill(job), () => runPendingResponseSpill(job));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function replaceWithPendingResponseSpill(
|
|
336
|
+
id: string,
|
|
337
|
+
candidate: ResidentResponseState,
|
|
338
|
+
expected: StoredResponseState | undefined,
|
|
339
|
+
options: { directAdmission?: boolean } = {},
|
|
340
|
+
): boolean {
|
|
341
|
+
const inheritedSpill = pendingResponseSpillById.get(id)?.supersededSpill
|
|
342
|
+
?? (expected?.kind === "spill" ? expected.spill : undefined);
|
|
343
|
+
if (!replaceMapEntry(id, candidate, expected)) return false;
|
|
344
|
+
queuePendingResponseSpill(id, candidate, {
|
|
345
|
+
...(inheritedSpill ? { supersededSpill: inheritedSpill } : {}),
|
|
346
|
+
directAdmission: options.directAdmission === true,
|
|
347
|
+
});
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Test-only: settle every serialized Windows spill publication. */
|
|
352
|
+
export async function flushPendingResponseSpillsForTests(): Promise<void> {
|
|
353
|
+
await drainResponseSpillPublications();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Test-only: observe ordinary queue settlement without invoking shutdown fallback. */
|
|
357
|
+
export async function awaitResponseSpillPublicationTailForTests(): Promise<void> {
|
|
358
|
+
await responseSpillPublicationTail;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Test-only: observe the bounded queue without exposing payloads. */
|
|
362
|
+
export function pendingResponseSpillMetricsForTests(): { count: number; bytes: number } {
|
|
363
|
+
return { count: pendingResponseSpills.size, bytes: pendingResponseSpillBytes };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Test-only: shorten the shutdown drain/fallback budget (null restores production values). */
|
|
367
|
+
export function setResponseSpillShutdownBudgetForTests(
|
|
368
|
+
budget: { totalMs: number; fallbackReserveMs: number } | null,
|
|
369
|
+
): void {
|
|
370
|
+
responseSpillShutdownBudgetOverride = budget;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Test-only: shorten the ordinary async whole-attempt ACL budget. */
|
|
374
|
+
export function setResponseSpillAsyncAclAttemptBudgetForTests(budgetMs: number | null): void {
|
|
375
|
+
responseSpillAsyncAclAttemptBudgetOverride = budgetMs;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function responseSpillAsyncAclAttemptBudgetMs(): number {
|
|
379
|
+
return responseSpillAsyncAclAttemptBudgetOverride ?? RESPONSE_SPILL_ASYNC_ACL_ATTEMPT_BUDGET_MS;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Test-only: lower the hard terminalization pass guard (null restores production). */
|
|
383
|
+
export function setResponseSpillShutdownTerminalizationPassLimitForTests(limit: number | null): void {
|
|
384
|
+
responseSpillShutdownTerminalizationPassLimitOverride = limit;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function responseSpillShutdownTerminalizationPassLimit(): number {
|
|
388
|
+
return responseSpillShutdownTerminalizationPassLimitOverride
|
|
389
|
+
?? RESPONSE_SPILL_SHUTDOWN_TERMINALIZATION_MAX_PASSES;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function responseSpillShutdownBudget(): { totalMs: number; fallbackReserveMs: number } {
|
|
393
|
+
return responseSpillShutdownBudgetOverride ?? {
|
|
394
|
+
totalMs: RESPONSE_SPILL_SHUTDOWN_BUDGET_MS,
|
|
395
|
+
fallbackReserveMs: RESPONSE_SPILL_SHUTDOWN_FALLBACK_RESERVE_MS,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function awaitResponseSpillTailUntil(observed: Promise<void>, deadline: number): Promise<boolean> {
|
|
400
|
+
const remaining = deadline - Date.now();
|
|
401
|
+
if (remaining <= 0) return Promise.resolve(false);
|
|
402
|
+
return new Promise(resolve => {
|
|
403
|
+
let finished = false;
|
|
404
|
+
const finish = (settled: boolean): void => {
|
|
405
|
+
if (finished) return;
|
|
406
|
+
finished = true;
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
resolve(settled);
|
|
409
|
+
};
|
|
410
|
+
const timer = setTimeout(() => finish(false), remaining);
|
|
411
|
+
observed.then(() => finish(true), () => finish(true));
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function installShutdownFallbackSpill(
|
|
416
|
+
job: PendingResponseSpill,
|
|
417
|
+
candidate: ResidentResponseState,
|
|
418
|
+
aclBudgetMs: number,
|
|
419
|
+
): void {
|
|
420
|
+
let ref: ResponseSpillRef | null = null;
|
|
421
|
+
try {
|
|
422
|
+
ref = writeResponseSpillDurably(job.id, spillPayloadForResident(candidate), { aclBudgetMs });
|
|
423
|
+
if (ref.payloadBytes > responseSpillPayloadCap()) {
|
|
424
|
+
deleteResponseSpill(ref);
|
|
425
|
+
ref = null;
|
|
426
|
+
if (job.directAdmission) admissionCounters.oversizedDrops += 1;
|
|
427
|
+
throw Object.assign(new Error("Response spill payload exceeds replay ceiling"), { code: "EFBIG" });
|
|
428
|
+
}
|
|
429
|
+
if (states.get(job.id) !== candidate) {
|
|
430
|
+
deleteResponseSpill(ref);
|
|
431
|
+
ref = null;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (swapResidentForSpill(job.id, candidate, ref)) {
|
|
435
|
+
ref = null;
|
|
436
|
+
spillCounters.writes += 1;
|
|
437
|
+
if (job.directAdmission) admissionCounters.directSpills += 1;
|
|
438
|
+
deferSupersededSpill(job.supersededSpill);
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (ref) deleteResponseSpill(ref);
|
|
442
|
+
if (states.get(job.id) === candidate) {
|
|
443
|
+
spillCounters.writeFailures += 1;
|
|
444
|
+
replaceWithSpillFailure(job.id, candidate);
|
|
445
|
+
deferSupersededSpill(job.supersededSpill);
|
|
446
|
+
}
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function terminalizeShutdownFallbackCandidate(
|
|
452
|
+
job: PendingResponseSpill,
|
|
453
|
+
candidate: ResidentResponseState,
|
|
454
|
+
): void {
|
|
455
|
+
if (states.get(job.id) !== candidate) return;
|
|
456
|
+
spillCounters.writeFailures += 1;
|
|
457
|
+
replaceWithSpillFailure(job.id, candidate);
|
|
458
|
+
deferSupersededSpill(job.supersededSpill);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function pendingShutdownFallbackCandidates(): Array<{
|
|
462
|
+
job: PendingResponseSpill;
|
|
463
|
+
candidate: ResidentResponseState;
|
|
464
|
+
}> {
|
|
465
|
+
return [...pendingResponseSpills]
|
|
466
|
+
.map(job => ({ job, candidate: job.candidate }))
|
|
467
|
+
.filter((entry): entry is { job: PendingResponseSpill; candidate: ResidentResponseState } => !!entry.candidate);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function supersedeShutdownFallbackBatch(
|
|
471
|
+
pending: Array<{ job: PendingResponseSpill; candidate: ResidentResponseState }>,
|
|
472
|
+
failures: Error[],
|
|
473
|
+
): void {
|
|
474
|
+
for (const { job } of pending) {
|
|
475
|
+
job.cancelled = true;
|
|
476
|
+
markResponseSpillPublicationSuperseded(job.publicationControl);
|
|
477
|
+
}
|
|
478
|
+
for (const { job } of pending) {
|
|
479
|
+
const cleanupFailure = cleanupSupersededResponseSpillPublication(job.publicationControl);
|
|
480
|
+
if (cleanupFailure) failures.push(cleanupFailure);
|
|
481
|
+
releasePendingResponseSpill(job);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function stopAtShutdownTerminalizationPassLimit(
|
|
486
|
+
pending: Array<{ job: PendingResponseSpill; candidate: ResidentResponseState }>,
|
|
487
|
+
failures: Error[],
|
|
488
|
+
): void {
|
|
489
|
+
failures.push(Object.assign(new Error("Response spill shutdown terminalization pass limit exceeded"), { code: "ELOOP" }));
|
|
490
|
+
supersedeShutdownFallbackBatch(pending, failures);
|
|
491
|
+
for (const { job, candidate } of pending) {
|
|
492
|
+
terminalizeShutdownFallbackCandidate(job, candidate);
|
|
493
|
+
}
|
|
494
|
+
for (const [id, state] of [...states]) {
|
|
495
|
+
if (state.kind !== "resident") continue;
|
|
496
|
+
spillCounters.writeFailures += 1;
|
|
497
|
+
replaceWithSpillFailure(id, state);
|
|
498
|
+
}
|
|
499
|
+
recomputeOldestResident();
|
|
500
|
+
pruneResponses();
|
|
501
|
+
enforceAppOwnedMemoryBudget();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function terminalizeExhaustedShutdownFallback(
|
|
505
|
+
initial: Array<{ job: PendingResponseSpill; candidate: ResidentResponseState }>,
|
|
506
|
+
failures: Error[],
|
|
507
|
+
): void {
|
|
508
|
+
let pending = initial;
|
|
509
|
+
let passes = 0;
|
|
510
|
+
const passLimit = responseSpillShutdownTerminalizationPassLimit();
|
|
511
|
+
// Every pass replaces each captured resident with a tombstone. Pruning may expose
|
|
512
|
+
// another finite batch, but resident count strictly decreases until none can requeue.
|
|
513
|
+
while (pending.length > 0) {
|
|
514
|
+
if (passes >= passLimit) {
|
|
515
|
+
stopAtShutdownTerminalizationPassLimit(pending, failures);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
passes += 1;
|
|
519
|
+
supersedeShutdownFallbackBatch(pending, failures);
|
|
520
|
+
for (const { job, candidate } of pending) {
|
|
521
|
+
failures.push(Object.assign(new Error("Response spill shutdown fallback budget exhausted"), { code: "ETIMEDOUT" }));
|
|
522
|
+
terminalizeShutdownFallbackCandidate(job, candidate);
|
|
523
|
+
}
|
|
524
|
+
recomputeOldestResident();
|
|
525
|
+
pruneResponses();
|
|
526
|
+
enforceAppOwnedMemoryBudget();
|
|
527
|
+
pending = pendingShutdownFallbackCandidates();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function fallbackPendingResponseSpills(reserveMs: number): Error[] {
|
|
532
|
+
const deadline = Date.now() + reserveMs;
|
|
533
|
+
const failures: Error[] = [];
|
|
534
|
+
for (;;) {
|
|
535
|
+
const pending = pendingShutdownFallbackCandidates();
|
|
536
|
+
if (pending.length === 0) return failures;
|
|
537
|
+
if (Date.now() >= deadline) {
|
|
538
|
+
terminalizeExhaustedShutdownFallback(pending, failures);
|
|
539
|
+
return failures;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
supersedeShutdownFallbackBatch(pending, failures);
|
|
543
|
+
let reserveExhausted = false;
|
|
544
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
545
|
+
const { job, candidate } = pending[index]!;
|
|
546
|
+
if (states.get(job.id) !== candidate) continue;
|
|
547
|
+
const remaining = deadline - Date.now();
|
|
548
|
+
if (remaining <= 0) {
|
|
549
|
+
reserveExhausted = true;
|
|
550
|
+
for (const exhausted of pending.slice(index)) {
|
|
551
|
+
failures.push(Object.assign(new Error("Response spill shutdown fallback budget exhausted"), { code: "ETIMEDOUT" }));
|
|
552
|
+
terminalizeShutdownFallbackCandidate(exhausted.job, exhausted.candidate);
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
installShutdownFallbackSpill(job, candidate, remaining);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
failures.push(error instanceof Error ? error : new Error("Response spill shutdown fallback failed"));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
recomputeOldestResident();
|
|
563
|
+
pruneResponses();
|
|
564
|
+
enforceAppOwnedMemoryBudget();
|
|
565
|
+
if (reserveExhausted || Date.now() >= deadline) {
|
|
566
|
+
terminalizeExhaustedShutdownFallback(pendingShutdownFallbackCandidates(), failures);
|
|
567
|
+
return failures;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function drainResponseSpillPublications(): Promise<void> {
|
|
573
|
+
const budget = responseSpillShutdownBudget();
|
|
574
|
+
const fallbackReserveMs = Math.min(budget.totalMs, Math.max(1, budget.fallbackReserveMs));
|
|
575
|
+
const drainDeadline = Date.now() + Math.max(0, budget.totalMs - fallbackReserveMs);
|
|
576
|
+
|
|
577
|
+
for (;;) {
|
|
578
|
+
if (pendingResponseSpills.size === 0) return;
|
|
579
|
+
const observed = responseSpillPublicationTail;
|
|
580
|
+
const settled = await awaitResponseSpillTailUntil(observed, drainDeadline);
|
|
581
|
+
if (!settled) {
|
|
582
|
+
const failures = fallbackPendingResponseSpills(fallbackReserveMs);
|
|
583
|
+
if (failures.length > 0) {
|
|
584
|
+
throw new AggregateError(failures, "Response spill shutdown fallback incomplete");
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (observed === responseSpillPublicationTail) return;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
163
592
|
function byteCap(): number {
|
|
164
593
|
return byteCapOverride ?? MAX_STORED_RESPONSE_BYTES;
|
|
165
594
|
}
|
|
@@ -200,6 +629,7 @@ function recomputeOldestResident(): void {
|
|
|
200
629
|
oldestResidentAt = null;
|
|
201
630
|
for (const [id, state] of states) {
|
|
202
631
|
if (state.kind !== "resident") continue;
|
|
632
|
+
if (pendingResponseSpillById.get(id)?.candidate === state) continue;
|
|
203
633
|
if (oldestResidentAt !== null && state.createdAt >= oldestResidentAt) continue;
|
|
204
634
|
oldestResidentId = id;
|
|
205
635
|
oldestResidentAt = state.createdAt;
|
|
@@ -248,6 +678,7 @@ function deleteOwnedSpills(entry: StoredResponseState): void {
|
|
|
248
678
|
function deleteEntry(id: string, options: { deleteSpill?: boolean } = {}): void {
|
|
249
679
|
const existing = states.get(id);
|
|
250
680
|
if (!existing) return;
|
|
681
|
+
const supersededSpill = cancelPendingResponseSpill(id);
|
|
251
682
|
storedResponseBytes -= existing.sizeBytes;
|
|
252
683
|
if (existing.kind === "resident") {
|
|
253
684
|
residentResponseBytes -= existing.sizeBytes;
|
|
@@ -258,6 +689,7 @@ function deleteEntry(id: string, options: { deleteSpill?: boolean } = {}): void
|
|
|
258
689
|
if (oldestResidentId === id) recomputeOldestResident();
|
|
259
690
|
stateRevision += 1;
|
|
260
691
|
if (options.deleteSpill !== false) deleteOwnedSpills(existing);
|
|
692
|
+
if (options.deleteSpill !== false && supersededSpill) deleteResponseSpill(supersededSpill);
|
|
261
693
|
}
|
|
262
694
|
|
|
263
695
|
function replaceWithSpillFailure(
|
|
@@ -362,11 +794,18 @@ function setResidentEntry(id: string, entry: ResidentInput): void {
|
|
|
362
794
|
pruneResponses();
|
|
363
795
|
return;
|
|
364
796
|
}
|
|
797
|
+
const pending = pendingResponseSpillById.get(id);
|
|
798
|
+
if (windowsSecretAclApplies() && (expected?.kind === "spill" || pending?.supersededSpill)) {
|
|
799
|
+
replaceWithPendingResponseSpill(id, candidate, expected);
|
|
800
|
+
pruneResponses();
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
365
803
|
if (expected?.kind === "spill") {
|
|
366
804
|
replaceSpillEntryAtomically(id, expected, candidate);
|
|
367
805
|
pruneResponses();
|
|
368
806
|
return;
|
|
369
807
|
}
|
|
808
|
+
if (windowsSecretAclApplies()) cancelPendingResponseSpill(id);
|
|
370
809
|
if (!replaceMapEntry(id, candidate, expected)) return;
|
|
371
810
|
pruneResponses();
|
|
372
811
|
}
|
|
@@ -389,6 +828,10 @@ function admitOversizedCandidate(
|
|
|
389
828
|
replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
|
|
390
829
|
return;
|
|
391
830
|
}
|
|
831
|
+
if (windowsSecretAclApplies()) {
|
|
832
|
+
replaceWithPendingResponseSpill(id, candidate, expected, { directAdmission: true });
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
392
835
|
try {
|
|
393
836
|
const ref = writeResponseSpillDurably(id, {
|
|
394
837
|
createdAt: candidate.createdAt,
|
|
@@ -927,8 +1370,7 @@ function schedulePersist(): void {
|
|
|
927
1370
|
schedulePersistAt(snapshotPath());
|
|
928
1371
|
}
|
|
929
1372
|
|
|
930
|
-
|
|
931
|
-
export async function flushResponseState(): Promise<void> {
|
|
1373
|
+
async function flushResponseSnapshot(): Promise<void> {
|
|
932
1374
|
if (persistTimer) {
|
|
933
1375
|
await persistNow(pendingPersistPath ?? snapshotPath(), true);
|
|
934
1376
|
return;
|
|
@@ -941,6 +1383,23 @@ export async function flushResponseState(): Promise<void> {
|
|
|
941
1383
|
if (persistTimer) await persistNow(pendingPersistPath ?? snapshotPath(), true);
|
|
942
1384
|
}
|
|
943
1385
|
|
|
1386
|
+
/** Flush publications and snapshot state; report drain failure only after persistence completes. */
|
|
1387
|
+
export async function flushResponseState(): Promise<void> {
|
|
1388
|
+
const failures: unknown[] = [];
|
|
1389
|
+
try {
|
|
1390
|
+
await drainResponseSpillPublications();
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
failures.push(error);
|
|
1393
|
+
}
|
|
1394
|
+
try {
|
|
1395
|
+
await flushResponseSnapshot();
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
failures.push(error);
|
|
1398
|
+
}
|
|
1399
|
+
if (failures.length === 1) throw failures[0];
|
|
1400
|
+
if (failures.length > 1) throw new AggregateError(failures, "Response state shutdown flush incomplete");
|
|
1401
|
+
}
|
|
1402
|
+
|
|
944
1403
|
function inputItems(input: unknown): unknown[] {
|
|
945
1404
|
if (input === undefined) return [];
|
|
946
1405
|
if (Array.isArray(input)) return input;
|
|
@@ -1048,7 +1507,11 @@ function pruneResponses(at = now()): void {
|
|
|
1048
1507
|
// Unconditional RAM cap. Resident payloads demote durably; stubs/tombstones are
|
|
1049
1508
|
// deleted only when even their bounded metadata cannot fit the override.
|
|
1050
1509
|
while (storedResponseBytes > byteCap() && states.size > 0) {
|
|
1051
|
-
const oldestResident = [...states].find(([, entry]) => entry.kind === "resident"
|
|
1510
|
+
const oldestResident = [...states].find(([id, entry]) => entry.kind === "resident"
|
|
1511
|
+
&& pendingResponseSpillById.get(id)?.candidate !== entry);
|
|
1512
|
+
const hasPendingResident = !oldestResident && [...states].some(([id, entry]) => entry.kind === "resident"
|
|
1513
|
+
&& pendingResponseSpillById.get(id)?.candidate === entry);
|
|
1514
|
+
if (hasPendingResident) break;
|
|
1052
1515
|
const oldestId = oldestResident?.[0] ?? states.keys().next().value as string | undefined;
|
|
1053
1516
|
if (!oldestId) break;
|
|
1054
1517
|
const entry = states.get(oldestId)!;
|
|
@@ -1056,6 +1519,10 @@ function pruneResponses(at = now()): void {
|
|
|
1056
1519
|
deleteEntry(oldestId);
|
|
1057
1520
|
continue;
|
|
1058
1521
|
}
|
|
1522
|
+
if (windowsSecretAclApplies()) {
|
|
1523
|
+
queuePendingResponseSpill(oldestId, entry);
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1059
1526
|
try {
|
|
1060
1527
|
const ref = writeResponseSpillDurably(oldestId, {
|
|
1061
1528
|
createdAt: entry.createdAt,
|
|
@@ -1143,11 +1610,18 @@ export function sweepAbandonedResponseStateTemps(): number {
|
|
|
1143
1610
|
}
|
|
1144
1611
|
|
|
1145
1612
|
export function responseContinuationRetainedStoreSnapshot(): RetainedStoreSnapshot {
|
|
1613
|
+
let currentPendingBytes = 0;
|
|
1614
|
+
for (const job of pendingResponseSpills) {
|
|
1615
|
+
if (job.candidate && states.get(job.id) === job.candidate) currentPendingBytes += job.sizeBytes;
|
|
1616
|
+
}
|
|
1617
|
+
const detachedPendingBytes = Math.max(0, pendingResponseSpillBytes - currentPendingBytes);
|
|
1618
|
+
const bytes = storedResponseBytes + detachedPendingBytes;
|
|
1619
|
+
const evictableBytes = Math.max(0, residentResponseBytes - currentPendingBytes);
|
|
1146
1620
|
return {
|
|
1147
1621
|
count: states.size,
|
|
1148
|
-
bytes
|
|
1149
|
-
evictableBytes
|
|
1150
|
-
pinnedBytes: Math.max(0,
|
|
1622
|
+
bytes,
|
|
1623
|
+
evictableBytes,
|
|
1624
|
+
pinnedBytes: Math.max(0, bytes - evictableBytes),
|
|
1151
1625
|
oldestAt: oldestResidentAt,
|
|
1152
1626
|
};
|
|
1153
1627
|
}
|
|
@@ -1157,6 +1631,11 @@ export function evictOldestResponseContinuationForBudget(): number {
|
|
|
1157
1631
|
const id = oldestResidentId;
|
|
1158
1632
|
const entry = states.get(id);
|
|
1159
1633
|
if (!entry || entry.kind !== "resident") return 0;
|
|
1634
|
+
if (windowsSecretAclApplies()) {
|
|
1635
|
+
queuePendingResponseSpill(id, entry);
|
|
1636
|
+
schedulePersist();
|
|
1637
|
+
return 0;
|
|
1638
|
+
}
|
|
1160
1639
|
try {
|
|
1161
1640
|
const ref = writeResponseSpillDurably(id, {
|
|
1162
1641
|
createdAt: entry.createdAt,
|
|
@@ -1380,7 +1859,7 @@ export function responseStateMetrics(): ResponseStateMetrics {
|
|
|
1380
1859
|
residentCount,
|
|
1381
1860
|
spillStubCount,
|
|
1382
1861
|
tombstoneCount,
|
|
1383
|
-
totalBytes:
|
|
1862
|
+
totalBytes: responseContinuationRetainedStoreSnapshot().bytes,
|
|
1384
1863
|
spillPayloadBytes,
|
|
1385
1864
|
largestBytes,
|
|
1386
1865
|
oldestAgeMs: states.size > 0 ? at - oldestCreatedAt : 0,
|
|
@@ -1492,6 +1971,8 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
1492
1971
|
persistTimer = null;
|
|
1493
1972
|
}
|
|
1494
1973
|
pendingPersistPath = null;
|
|
1974
|
+
for (const id of [...pendingResponseSpillById.keys()]) cancelPendingResponseSpill(id);
|
|
1975
|
+
pendingResponseSpillById.clear();
|
|
1495
1976
|
states.clear();
|
|
1496
1977
|
storedResponseBytes = 0;
|
|
1497
1978
|
residentResponseBytes = 0;
|
package/src/server/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../codex
|
|
|
66
66
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
67
67
|
import {
|
|
68
68
|
availableAccountGatedNativeModels,
|
|
69
|
+
codexModelEntitlementStateForAccount,
|
|
69
70
|
resolveCodexModelEntitlements,
|
|
70
71
|
} from "../codex/model-entitlements";
|
|
71
72
|
export {
|
|
@@ -1211,10 +1212,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1211
1212
|
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config)].map(([selector, slugs]) => {
|
|
1212
1213
|
const target = accountTargets.get(selector);
|
|
1213
1214
|
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
1214
|
-
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
1215
|
-
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
1216
1215
|
return [selector, slugs.filter(slug => (
|
|
1217
|
-
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)
|
|
1216
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)
|
|
1217
|
+
|| (accountId !== undefined
|
|
1218
|
+
&& codexModelEntitlementStateForAccount(modelEntitlements, accountId, slug) === "granted")
|
|
1218
1219
|
))] as const;
|
|
1219
1220
|
}))
|
|
1220
1221
|
: new Map<string, readonly string[]>();
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -458,8 +458,9 @@ export function trackStreamLifetime(
|
|
|
458
458
|
export async function drainAndShutdown(
|
|
459
459
|
server: ReturnType<typeof Bun.serve> | undefined,
|
|
460
460
|
timeoutMs: number,
|
|
461
|
-
): Promise<
|
|
461
|
+
): Promise<boolean> {
|
|
462
462
|
const s = server ?? _serverRef;
|
|
463
|
+
let shutdownSucceeded = true;
|
|
463
464
|
// One absolute budget covers both a pre-existing scoped profile drain and
|
|
464
465
|
// ordinary in-flight turns. A stuck scoped owner must not pin shutdown forever.
|
|
465
466
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
@@ -491,9 +492,11 @@ export async function drainAndShutdown(
|
|
|
491
492
|
// shutdown is usually part of.
|
|
492
493
|
const stateFlush = await Promise.allSettled([flushResponseState(), flushAntigravityReplay()]);
|
|
493
494
|
if (stateFlush[0]?.status === "rejected") {
|
|
495
|
+
shutdownSucceeded = false;
|
|
494
496
|
console.warn("[responses] state flush during shutdown failed");
|
|
495
497
|
}
|
|
496
498
|
if (stateFlush[1]?.status === "rejected") {
|
|
499
|
+
shutdownSucceeded = false;
|
|
497
500
|
console.warn("[antigravity] replay flush during shutdown failed");
|
|
498
501
|
}
|
|
499
502
|
|
|
@@ -546,4 +549,5 @@ export async function drainAndShutdown(
|
|
|
546
549
|
// never resume admission merely because shutdown cleanup returned.
|
|
547
550
|
}
|
|
548
551
|
}
|
|
552
|
+
return shutdownSucceeded;
|
|
549
553
|
}
|
|
@@ -25,6 +25,7 @@ import { providerContextCap } from "../../providers/context-cap";
|
|
|
25
25
|
import { isVisionReasoningEffort } from "../../reasoning-effort";
|
|
26
26
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
27
27
|
import type { OcxConfig } from "../../types";
|
|
28
|
+
import { ensureCodexEntitlementFreshness } from "../../codex/model-entitlements";
|
|
28
29
|
import { fetchAllModels } from "./shared";
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -47,8 +48,16 @@ export type ManagementModelRow = Partial<CatalogModel> & {
|
|
|
47
48
|
* models the GUI's Models tab shows — including this function's `disabled` computation,
|
|
48
49
|
* which the export core (src/clients/config-export.ts) deliberately does not perform.
|
|
49
50
|
*/
|
|
50
|
-
export async function listManagementModelRows(
|
|
51
|
-
|
|
51
|
+
export async function listManagementModelRows(
|
|
52
|
+
config: OcxConfig,
|
|
53
|
+
options: { entitlementWaitMs?: number } = {},
|
|
54
|
+
): Promise<ManagementModelRow[]> {
|
|
55
|
+
const [models] = await Promise.all([
|
|
56
|
+
fetchAllModels(config),
|
|
57
|
+
ensureCodexEntitlementFreshness(config, {
|
|
58
|
+
waitMs: options.entitlementWaitMs ?? 3_000,
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
52
61
|
const disabled = new Set(config.disabledModels ?? []);
|
|
53
62
|
// Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
|
|
54
63
|
// from the static supported set so a disabled model stays listed and re-enableable.
|