@bitkyc08/opencodex 2.44.0 → 2.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-CCfD72yq.js +115 -0
- package/gui/dist/assets/index-J96sug5C.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/openai-responses.ts +14 -0
- package/src/chat/outbound.ts +286 -35
- package/src/claude/compatibility.ts +192 -0
- package/src/claude/model-info.ts +14 -2
- package/src/cli/account-extended.ts +24 -1
- package/src/cli/init.ts +70 -13
- package/src/codex/catalog/provider-fetch.ts +38 -9
- package/src/codex/catalog/sync.ts +34 -2
- package/src/codex/catalog.ts +1 -1
- package/src/config/initialize.ts +132 -0
- package/src/config/rebase-provenance.ts +26 -0
- package/src/config.ts +50 -1
- package/src/generated/compatibility-version.json +41 -29
- package/src/lib/windows-secret-acl.ts +8 -4
- package/src/providers/quota.ts +26 -15
- package/src/responses/state.ts +48 -3
- package/src/server/chat-completions.ts +32 -36
- package/src/server/chat-native-sse.ts +23 -3
- package/src/server/chat-native.ts +15 -8
- package/src/server/claude-messages.ts +27 -0
- package/src/server/index.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +101 -14
- package/src/server/management/logs-usage-routes.ts +9 -2
- package/src/server/request-log-cursor.ts +84 -0
- package/src/server/request-log.ts +46 -3
- package/src/server/responses/agent-task-recovery.ts +50 -14
- package/src/server/responses/codex-ws-exchange.ts +79 -0
- package/src/server/responses/compact.ts +39 -33
- package/src/server/responses/core.ts +43 -27
- package/src/storage/cleanup.ts +49 -35
- package/src/types/config.ts +4 -0
- package/src/usage/log.ts +22 -0
- package/gui/dist/assets/index-B7_K1Hsj.js +0 -115
- package/gui/dist/assets/index-ltx3L-WS.css +0 -1
|
@@ -321,8 +321,9 @@ import {
|
|
|
321
321
|
import {
|
|
322
322
|
agentTaskRecoveryConfig,
|
|
323
323
|
discardEncryptedAgentTaskRecovery,
|
|
324
|
-
|
|
324
|
+
recoverEncryptedAgentTaskWithResult,
|
|
325
325
|
restoreCachedEncryptedAgentTasks,
|
|
326
|
+
type AgentTaskRecoveryFailureReason,
|
|
326
327
|
} from "./agent-task-recovery";
|
|
327
328
|
import { relaySseEagerBounded } from "../relay-eager";
|
|
328
329
|
import {
|
|
@@ -1534,7 +1535,9 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1534
1535
|
): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
|
|
1535
1536
|
if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
|
|
1536
1537
|
return (status, httpStatusOverride) => {
|
|
1537
|
-
|
|
1538
|
+
const quotaStatus = [httpStatusOverride, logCtx?.terminalHttpStatus]
|
|
1539
|
+
.find(value => value === 429 || value === 402);
|
|
1540
|
+
if (status === "incomplete" && quotaStatus === undefined) {
|
|
1538
1541
|
// Normal limit/content-filter/stall terminal — the account served the
|
|
1539
1542
|
// request. Don't penalize account health; record success to clear any
|
|
1540
1543
|
// prior soft-avoid so a healthy account isn't stuck avoided.
|
|
@@ -1559,7 +1562,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
1559
1562
|
// the parent's terminalHttpStatus so the semantic status is not lost.
|
|
1560
1563
|
const outcome = status === "completed"
|
|
1561
1564
|
? 200
|
|
1562
|
-
: (httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
|
|
1565
|
+
: (quotaStatus ?? httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
|
|
1563
1566
|
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
1564
1567
|
threadId: authCtx.affinityKey,
|
|
1565
1568
|
fixedAccount: authCtx.fixedAccount,
|
|
@@ -1931,13 +1934,14 @@ export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
|
|
|
1931
1934
|
firstByteTimeoutMs: UPSTREAM_JSON_BODY_TOTAL_TIMEOUT_MS,
|
|
1932
1935
|
};
|
|
1933
1936
|
|
|
1934
|
-
function unreadableEncryptedAgentTaskResponse(): Response {
|
|
1937
|
+
function unreadableEncryptedAgentTaskResponse(reason?: AgentTaskRecoveryFailureReason): Response {
|
|
1935
1938
|
return new Response(
|
|
1936
1939
|
JSON.stringify({
|
|
1937
1940
|
error: {
|
|
1938
1941
|
message: UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE,
|
|
1939
1942
|
type: "invalid_request_error",
|
|
1940
1943
|
code: "unreadable_encrypted_agent_task",
|
|
1944
|
+
...(reason === undefined ? {} : { recovery_reason: reason }),
|
|
1941
1945
|
},
|
|
1942
1946
|
}),
|
|
1943
1947
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
@@ -2530,6 +2534,7 @@ export async function handleComboResponses(
|
|
|
2530
2534
|
const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
2531
2535
|
comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
2532
2536
|
let encryptedTaskRecoveryAttempted = false;
|
|
2537
|
+
let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
|
|
2533
2538
|
let storedPool401ReplayDispatched = false;
|
|
2534
2539
|
const recoverUnreadableEncryptedTask = async (): Promise<boolean> => {
|
|
2535
2540
|
if (encryptedTaskRecoveryAttempted) return false;
|
|
@@ -2551,15 +2556,18 @@ export async function handleComboResponses(
|
|
|
2551
2556
|
}
|
|
2552
2557
|
let recovered = false;
|
|
2553
2558
|
try {
|
|
2554
|
-
|
|
2559
|
+
const result = await recoverEncryptedAgentTaskWithResult(
|
|
2555
2560
|
req,
|
|
2556
2561
|
(body as { input?: unknown } | undefined)?.input,
|
|
2557
2562
|
recovery,
|
|
2558
2563
|
config,
|
|
2559
2564
|
{ parentThreadId: inboundClientThreadId, abortSignal: options.abortSignal },
|
|
2560
2565
|
);
|
|
2566
|
+
recovered = result.recovered;
|
|
2567
|
+
recoveryFailureReason = result.recovered ? undefined : result.reason;
|
|
2561
2568
|
} catch {
|
|
2562
2569
|
recovered = false;
|
|
2570
|
+
recoveryFailureReason = undefined;
|
|
2563
2571
|
}
|
|
2564
2572
|
// Recovery has the same in-place input mutation contract as the direct routed path.
|
|
2565
2573
|
if (
|
|
@@ -2609,7 +2617,7 @@ export async function handleComboResponses(
|
|
|
2609
2617
|
if (!(await recoverUnreadableEncryptedTask())) {
|
|
2610
2618
|
return options.abortSignal?.aborted
|
|
2611
2619
|
? clientCancelledResponse()
|
|
2612
|
-
: unreadableEncryptedAgentTaskResponse();
|
|
2620
|
+
: unreadableEncryptedAgentTaskResponse(recoveryFailureReason);
|
|
2613
2621
|
}
|
|
2614
2622
|
}
|
|
2615
2623
|
|
|
@@ -2668,7 +2676,20 @@ export async function handleComboResponses(
|
|
|
2668
2676
|
attemptRetained = true;
|
|
2669
2677
|
};
|
|
2670
2678
|
let consumedChildFailure: ConsumedComboFailure | undefined;
|
|
2671
|
-
const callbackGate = createChildPassthroughCallbackGate(
|
|
2679
|
+
const callbackGate = createChildPassthroughCallbackGate({
|
|
2680
|
+
...options,
|
|
2681
|
+
onNativePassthroughTerminal: status => {
|
|
2682
|
+
// A committed stream can acquire terminal metadata after preflight copied
|
|
2683
|
+
// the child log. Publish it before the outer logger finalizes, but only
|
|
2684
|
+
// through the gate: discarded attempts must never affect the parent.
|
|
2685
|
+
// Undefined child fields must preserve metadata already inspected by WS.
|
|
2686
|
+
if (childLog.terminalHttpStatus !== undefined) logCtx.terminalHttpStatus = childLog.terminalHttpStatus;
|
|
2687
|
+
if (childLog.terminalIncompleteReason !== undefined) logCtx.terminalIncompleteReason = childLog.terminalIncompleteReason;
|
|
2688
|
+
if (childLog.terminalErrorCode !== undefined) logCtx.terminalErrorCode = childLog.terminalErrorCode;
|
|
2689
|
+
if (childLog.upstreamError !== undefined) logCtx.upstreamError = childLog.upstreamError;
|
|
2690
|
+
options.onNativePassthroughTerminal?.(status);
|
|
2691
|
+
},
|
|
2692
|
+
});
|
|
2672
2693
|
let response: Response;
|
|
2673
2694
|
try {
|
|
2674
2695
|
const currentTargetProvider = pick.target.provider;
|
|
@@ -3400,6 +3421,7 @@ async function handleResponsesInner(
|
|
|
3400
3421
|
previewSelectionAdmission?.release();
|
|
3401
3422
|
}
|
|
3402
3423
|
|
|
3424
|
+
let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
|
|
3403
3425
|
// Native fallback and explicitly trusted direct Responses routes can consume ciphertext,
|
|
3404
3426
|
// so recover only after final route selection.
|
|
3405
3427
|
if (
|
|
@@ -3418,15 +3440,18 @@ async function handleResponsesInner(
|
|
|
3418
3440
|
(body as { input?: unknown } | undefined)?.input,
|
|
3419
3441
|
);
|
|
3420
3442
|
if (unreadableEncryptedAgentTask) try {
|
|
3421
|
-
|
|
3443
|
+
const result = await recoverEncryptedAgentTaskWithResult(
|
|
3422
3444
|
req,
|
|
3423
3445
|
(body as { input?: unknown } | undefined)?.input,
|
|
3424
3446
|
agentTaskRecovery,
|
|
3425
3447
|
config,
|
|
3426
3448
|
{ parentThreadId, abortSignal: options.abortSignal },
|
|
3427
3449
|
);
|
|
3450
|
+
recovered = result.recovered;
|
|
3451
|
+
recoveryFailureReason = result.recovered ? undefined : result.reason;
|
|
3428
3452
|
} catch {
|
|
3429
3453
|
recovered = false;
|
|
3454
|
+
recoveryFailureReason = undefined;
|
|
3430
3455
|
}
|
|
3431
3456
|
if (recovered) {
|
|
3432
3457
|
unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
@@ -3550,7 +3575,7 @@ async function handleResponsesInner(
|
|
|
3550
3575
|
&& !finalRouteCanPassThroughEncryptedTask
|
|
3551
3576
|
&& unreadableEncryptedAgentTask
|
|
3552
3577
|
) {
|
|
3553
|
-
return unreadableEncryptedAgentTaskResponse();
|
|
3578
|
+
return unreadableEncryptedAgentTaskResponse(recoveryFailureReason);
|
|
3554
3579
|
}
|
|
3555
3580
|
|
|
3556
3581
|
// The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no
|
|
@@ -5359,12 +5384,9 @@ async function handleResponsesInner(
|
|
|
5359
5384
|
if (terminalBodyWillRecord) {
|
|
5360
5385
|
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
5361
5386
|
terminalRecorder(status, httpStatusOverride);
|
|
5362
|
-
if (status === "failed") {
|
|
5363
|
-
const quotaFailureMessage = httpStatusOverride
|
|
5364
|
-
||
|
|
5365
|
-
|| logCtx.terminalHttpStatus === 402
|
|
5366
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
5367
|
-
: undefined;
|
|
5387
|
+
if (status === "failed" || status === "incomplete") {
|
|
5388
|
+
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
|
|
5389
|
+
.find(value => value === 429 || value === 402);
|
|
5368
5390
|
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
5369
5391
|
recordSubagentQuotaFailureForThreadSpawn(
|
|
5370
5392
|
req.headers,
|
|
@@ -5570,12 +5592,9 @@ async function handleResponsesInner(
|
|
|
5570
5592
|
const reportNativeTerminal = recordTerminalOutcomes
|
|
5571
5593
|
? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
5572
5594
|
terminalRecorder?.(status, httpStatusOverride);
|
|
5573
|
-
if (status === "failed") {
|
|
5574
|
-
const quotaFailureMessage = httpStatusOverride
|
|
5575
|
-
||
|
|
5576
|
-
|| logCtx.terminalHttpStatus === 402
|
|
5577
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
5578
|
-
: undefined;
|
|
5595
|
+
if (status === "failed" || status === "incomplete") {
|
|
5596
|
+
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
|
|
5597
|
+
.find(value => value === 429 || value === 402);
|
|
5579
5598
|
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
5580
5599
|
recordSubagentQuotaFailureForThreadSpawn(
|
|
5581
5600
|
req.headers,
|
|
@@ -5663,12 +5682,9 @@ async function handleResponsesInner(
|
|
|
5663
5682
|
// client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
|
|
5664
5683
|
const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
5665
5684
|
terminalRecorder?.(status, httpStatusOverride);
|
|
5666
|
-
if (status === "failed") {
|
|
5667
|
-
const quotaFailureMessage = httpStatusOverride
|
|
5668
|
-
||
|
|
5669
|
-
|| logCtx.terminalHttpStatus === 402
|
|
5670
|
-
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
5671
|
-
: undefined;
|
|
5685
|
+
if (status === "failed" || status === "incomplete") {
|
|
5686
|
+
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
|
|
5687
|
+
.find(value => value === 429 || value === 402);
|
|
5672
5688
|
if (!isFixedCodexAccount(authCtx) && quotaFailureMessage !== undefined) {
|
|
5673
5689
|
recordSubagentQuotaFailureForThreadSpawn(
|
|
5674
5690
|
req.headers,
|
package/src/storage/cleanup.ts
CHANGED
|
@@ -30,11 +30,11 @@ import {
|
|
|
30
30
|
writeSync,
|
|
31
31
|
chmodSync,
|
|
32
32
|
} from "node:fs";
|
|
33
|
-
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
33
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
34
34
|
import { Database } from "bun:sqlite";
|
|
35
35
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
36
36
|
import { readThreadFieldsFromRollout } from "../codex/history-provider";
|
|
37
|
-
import { renameAtomicFile } from "../
|
|
37
|
+
import { renameAtomicFile } from "../lib/windows-atomic-replace";
|
|
38
38
|
|
|
39
39
|
export const ARCHIVED_SESSIONS_DIR = "archived_sessions";
|
|
40
40
|
export const TRASH_DIR = ".trash";
|
|
@@ -115,9 +115,35 @@ function chmodPrivatePath(path: string, mode: number): void {
|
|
|
115
115
|
try { chmodSync(path, mode); } catch { /* best-effort (e.g. Windows ACLs) */ }
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
/** Publish complete stage metadata without truncating the last recovery record. */
|
|
119
|
+
function writePrivateFile(
|
|
120
|
+
path: string,
|
|
121
|
+
content: string,
|
|
122
|
+
beforeRename?: (temporaryPath: string, targetPath: string) => void,
|
|
123
|
+
): void {
|
|
124
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
125
|
+
let descriptor: number | undefined;
|
|
126
|
+
let created = false;
|
|
127
|
+
try {
|
|
128
|
+
descriptor = openSync(temporaryPath, "wx", 0o600);
|
|
129
|
+
created = true;
|
|
130
|
+
writeFileSync(descriptor, content, "utf8");
|
|
131
|
+
fsyncSync(descriptor);
|
|
132
|
+
closeSync(descriptor);
|
|
133
|
+
descriptor = undefined;
|
|
134
|
+
chmodPrivatePath(temporaryPath, 0o600);
|
|
135
|
+
beforeRename?.(temporaryPath, path);
|
|
136
|
+
renameAtomicFile(temporaryPath, path, undefined, "storage-cleanup");
|
|
137
|
+
chmodPrivatePath(path, 0o600);
|
|
138
|
+
fsyncDirectoryBestEffort(dirname(path));
|
|
139
|
+
} finally {
|
|
140
|
+
if (descriptor !== undefined) {
|
|
141
|
+
try { closeSync(descriptor); } catch { /* preserve publication failure */ }
|
|
142
|
+
}
|
|
143
|
+
if (created) {
|
|
144
|
+
try { unlinkSync(temporaryPath); } catch { /* renamed or cleanup unavailable */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
121
147
|
}
|
|
122
148
|
|
|
123
149
|
function chunkIds(ids: string[], chunkSize: number): string[][] {
|
|
@@ -812,7 +838,6 @@ interface ReconcileTestHooks {
|
|
|
812
838
|
const SATELLITE_BACKUP_FILE = "satellite-backup.json";
|
|
813
839
|
/** Marks an incomplete restore so retries can accept dest files and resume metadata. */
|
|
814
840
|
const RESTORE_PENDING_FILE = "restore-pending.json";
|
|
815
|
-
let _satelliteBackupSeq = 0;
|
|
816
841
|
|
|
817
842
|
type StagedFile = { from: string; to: string; relPath: string };
|
|
818
843
|
|
|
@@ -1070,34 +1095,11 @@ function writeSatelliteBackup(
|
|
|
1070
1095
|
if (options?.failWrite) throw new Error("test_fail_satellite_backup_write");
|
|
1071
1096
|
const dest = join(stageDir, SATELLITE_BACKUP_FILE);
|
|
1072
1097
|
const replacing = existsSync(dest);
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
try {
|
|
1077
|
-
let offset = 0;
|
|
1078
|
-
while (offset < payload.length) {
|
|
1079
|
-
offset += writeSync(fd, payload, offset, payload.length - offset, null);
|
|
1098
|
+
writePrivateFile(dest, JSON.stringify(backup), () => {
|
|
1099
|
+
if (options?.failReplaceBeforeRename && replacing) {
|
|
1100
|
+
throw new Error("test_fail_satellite_backup_replace");
|
|
1080
1101
|
}
|
|
1081
|
-
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
try { closeSync(fd); } catch { /* */ }
|
|
1084
|
-
try { unlinkSync(tmp); } catch { /* */ }
|
|
1085
|
-
throw error;
|
|
1086
|
-
}
|
|
1087
|
-
closeSync(fd);
|
|
1088
|
-
chmodPrivatePath(tmp, 0o600);
|
|
1089
|
-
if (options?.failReplaceBeforeRename && replacing) {
|
|
1090
|
-
try { unlinkSync(tmp); } catch { /* */ }
|
|
1091
|
-
throw new Error("test_fail_satellite_backup_replace");
|
|
1092
|
-
}
|
|
1093
|
-
try {
|
|
1094
|
-
renameAtomicFile(tmp, dest, undefined, "storage-cleanup");
|
|
1095
|
-
} catch (error) {
|
|
1096
|
-
try { unlinkSync(tmp); } catch { /* */ }
|
|
1097
|
-
throw error;
|
|
1098
|
-
}
|
|
1099
|
-
chmodPrivatePath(dest, 0o600);
|
|
1100
|
-
fsyncDirectoryBestEffort(stageDir);
|
|
1102
|
+
});
|
|
1101
1103
|
}
|
|
1102
1104
|
|
|
1103
1105
|
function clearSatelliteBackup(stageDir: string): void {
|
|
@@ -1734,6 +1736,12 @@ export interface ExecuteCleanupOptions {
|
|
|
1734
1736
|
/** Test-only failure injection for atomicity regressions. */
|
|
1735
1737
|
_test?: {
|
|
1736
1738
|
failManifestWrite?: boolean;
|
|
1739
|
+
/** Observe the complete temp and prior destination before publication. Never serialized. */
|
|
1740
|
+
beforeManifestReplace?: (
|
|
1741
|
+
temporaryPath: string,
|
|
1742
|
+
targetPath: string,
|
|
1743
|
+
phase: "staging" | "pre-commit" | "purge-incomplete",
|
|
1744
|
+
) => void;
|
|
1737
1745
|
failPurgeBasenames?: string[];
|
|
1738
1746
|
failRollbackBasenames?: string[];
|
|
1739
1747
|
blockStageDestBasenames?: string[];
|
|
@@ -1752,14 +1760,14 @@ export interface ExecuteCleanupOptions {
|
|
|
1752
1760
|
/** Serializable cleanup test hooks allowed on the management API wire. */
|
|
1753
1761
|
export type CleanupWireTestHooks = Omit<
|
|
1754
1762
|
NonNullable<ExecuteCleanupOptions["_test"]>,
|
|
1755
|
-
"afterSatelliteMutations" | "beforeReconcileLock"
|
|
1763
|
+
"afterSatelliteMutations" | "beforeReconcileLock" | "beforeManifestReplace"
|
|
1756
1764
|
>;
|
|
1757
1765
|
|
|
1758
1766
|
function isStringArray(v: unknown): v is string[] {
|
|
1759
1767
|
return Array.isArray(v) && v.every(e => typeof e === "string");
|
|
1760
1768
|
}
|
|
1761
1769
|
|
|
1762
|
-
/** Pick only allowlisted serializable hooks; drops function hooks
|
|
1770
|
+
/** Pick only allowlisted serializable hooks; drops all function hooks and unknown keys. */
|
|
1763
1771
|
export function pickWireCleanupTestHooks(raw: unknown): CleanupWireTestHooks | undefined {
|
|
1764
1772
|
if (!raw || typeof raw !== "object") return undefined;
|
|
1765
1773
|
const o = raw as Record<string, unknown>;
|
|
@@ -1911,6 +1919,9 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
|
|
|
1911
1919
|
entries: manifestEntries,
|
|
1912
1920
|
...extra,
|
|
1913
1921
|
}, null, 2),
|
|
1922
|
+
(temporaryPath, targetPath) => options._test?.beforeManifestReplace?.(
|
|
1923
|
+
temporaryPath, targetPath, extra.staging ? "staging" : "pre-commit",
|
|
1924
|
+
),
|
|
1914
1925
|
);
|
|
1915
1926
|
};
|
|
1916
1927
|
|
|
@@ -1999,6 +2010,9 @@ export function executeArchivedCleanup(options: ExecuteCleanupOptions): CleanupR
|
|
|
1999
2010
|
}))
|
|
2000
2011
|
.filter(entry => entry.physicalRelPaths.length > 0),
|
|
2001
2012
|
}, null, 2),
|
|
2013
|
+
(temporaryPath, targetPath) => options._test?.beforeManifestReplace?.(
|
|
2014
|
+
temporaryPath, targetPath, "purge-incomplete",
|
|
2015
|
+
),
|
|
2002
2016
|
);
|
|
2003
2017
|
} catch { /* best-effort: the pre-commit manifest is still on disk */ }
|
|
2004
2018
|
return {
|
package/src/types/config.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { CodexAccount } from "./accounts";
|
|
|
6
6
|
* /v1/messages surface, the `ocx claude` launcher, and the GUI Claude page.
|
|
7
7
|
*/
|
|
8
8
|
export interface OcxClaudeCodeConfig {
|
|
9
|
+
/** Opt-in translated Messages admission; unset keeps legacy behavior. Native passthrough is exempt. */
|
|
10
|
+
compatibility?: "shadow" | "enforce";
|
|
9
11
|
/** Kill switch for the /v1/messages inbound (GUI "Claude ON" toggle). Default: enabled. */
|
|
10
12
|
enabled?: boolean;
|
|
11
13
|
/**
|
|
@@ -437,6 +439,8 @@ export interface OcxConfig {
|
|
|
437
439
|
* Unset or empty leaves catalog priorities unchanged.
|
|
438
440
|
*/
|
|
439
441
|
modelPickerOrder?: string[];
|
|
442
|
+
/** Saved preset provenance; snapshots are not recomputed during catalog discovery. */
|
|
443
|
+
modelPickerOrderMode?: "alphabetical" | "provider" | "most-used";
|
|
440
444
|
/**
|
|
441
445
|
* Priority-ordered fallback models for spawned sub-agents. When the requested
|
|
442
446
|
* model is quota-exhausted or recently failed, opencodex rewrites the child
|
package/src/usage/log.ts
CHANGED
|
@@ -9,6 +9,24 @@ import { usageDisplayTotalTokens } from "./totals";
|
|
|
9
9
|
import type { AttemptTierOutcome, OcxUsage } from "../types";
|
|
10
10
|
import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
|
|
11
11
|
import { ACCOUNT_LOG_LABEL_RE, CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
12
|
+
import { claudeCompatibilityReason, normalizeClaudeFeatureCodes, type ClaudeFeatureCode } from "../claude/compatibility";
|
|
13
|
+
|
|
14
|
+
export interface PersistedClaudeCompatibilityLog {
|
|
15
|
+
decision: "shadow";
|
|
16
|
+
featureCodes: ClaudeFeatureCode[];
|
|
17
|
+
reason?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Disk and in-memory callers share a closed-code projection; free-form reasons are discarded. */
|
|
21
|
+
export function normalizeClaudeCompatibilityUsageLog(value: unknown): PersistedClaudeCompatibilityLog | undefined {
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
23
|
+
const row = value as Record<string, unknown>;
|
|
24
|
+
if (row.decision !== "shadow") return undefined;
|
|
25
|
+
const featureCodes = normalizeClaudeFeatureCodes(row.featureCodes);
|
|
26
|
+
const reason = claudeCompatibilityReason(featureCodes, true);
|
|
27
|
+
if (!reason) return undefined;
|
|
28
|
+
return { decision: "shadow", featureCodes, reason };
|
|
29
|
+
}
|
|
12
30
|
|
|
13
31
|
export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
|
|
14
32
|
/**
|
|
@@ -160,6 +178,8 @@ export interface PersistedUsageEntry {
|
|
|
160
178
|
* contains prompts, credentials, or hidden reasoning.
|
|
161
179
|
*/
|
|
162
180
|
routeDecision?: RouteDecisionTraceV1;
|
|
181
|
+
/** Closed Claude protocol codes only; absent on older rows. */
|
|
182
|
+
claudeCompatibility?: PersistedClaudeCompatibilityLog;
|
|
163
183
|
}
|
|
164
184
|
|
|
165
185
|
const KNOWN_USAGE_SURFACES = new Set<NonNullable<PersistedUsageEntry["surface"]>>([
|
|
@@ -490,6 +510,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
490
510
|
const callerServiceTier = sanitizeLogMetadataString(entry.callerServiceTier);
|
|
491
511
|
const responseServiceTier = sanitizeLogMetadataString(entry.responseServiceTier);
|
|
492
512
|
const shadowCallRewrittenFrom = sanitizeLogMetadataString(entry.shadowCallRewrittenFrom);
|
|
513
|
+
const claudeCompatibility = normalizeClaudeCompatibilityUsageLog(entry.claudeCompatibility);
|
|
493
514
|
const routeDecision = entry.routeDecision
|
|
494
515
|
? normalizeRouteDecisionTrace(entry.routeDecision)
|
|
495
516
|
: undefined;
|
|
@@ -563,6 +584,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
563
584
|
...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
|
|
564
585
|
...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
|
|
565
586
|
...(routeDecision ? { routeDecision } : {}),
|
|
587
|
+
...(claudeCompatibility ? { claudeCompatibility } : {}),
|
|
566
588
|
};
|
|
567
589
|
}
|
|
568
590
|
|