@bitkyc08/opencodex 2.78.0 → 2.80.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/App-CSJvmjNs.css +1 -0
- package/gui/dist/assets/App-MbDVfUeC.js +54 -0
- package/gui/dist/assets/{Tray-DiJpSub0.js → Tray-CzkxK82A.js} +1 -1
- package/gui/dist/assets/{index-DmQB8D_R.css → index-Brk6F3fn.css} +1 -1
- package/gui/dist/assets/index-UJt6npXk.js +94 -0
- package/gui/dist/assets/{quota-summary-B_a96nXj.js → quota-summary-DU3sD6Z-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin.ts +37 -2
- package/src/adapters/openai-chat/messages.ts +3 -1
- package/src/claude/desktop-picker.ts +11 -9
- package/src/claude/inbound-content-options.ts +29 -2
- package/src/claude/inbound.ts +9 -0
- package/src/claude/intercept/local-ca-files.ts +265 -0
- package/src/claude/intercept/local-ca-windows.ts +73 -0
- package/src/claude/intercept/local-ca.ts +12 -24
- package/src/claude/intercept/picker-ca-persistence.ts +106 -0
- package/src/claude/intercept/picker-ca-startup.ts +16 -0
- package/src/claude/intercept/picker-ca-store.ts +70 -0
- package/src/claude/intercept/picker-ca.ts +43 -9
- package/src/claude/intercept/picker-runtime.ts +14 -4
- package/src/claude/intercept/runtime.ts +32 -23
- package/src/cli/ensure-readiness.ts +48 -0
- package/src/cli/gui-pair-client.ts +15 -7
- package/src/cli/gui.ts +4 -1
- package/src/cli/hub.ts +1 -0
- package/src/cli/index.ts +10 -11
- package/src/cli/status.ts +2 -2
- package/src/cli/update-restart-child.ts +4 -1
- package/src/codex/account-credit-use.ts +8 -2
- package/src/codex/auth-api/account-pause-group.ts +113 -0
- package/src/codex/auth-api/routes.ts +18 -12
- package/src/codex/auth-context.ts +45 -3
- package/src/codex/autostart-health.ts +13 -3
- package/src/codex/journal.ts +36 -5
- package/src/codex/pool-credit-policy.ts +67 -0
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev.ts +3 -1
- package/src/generated/compatibility-version.json +135 -63
- package/src/lib/gui-pair-capability.ts +1 -1
- package/src/lib/gui-pair-intent.ts +170 -0
- package/src/lib/windows-owner-acl.ts +143 -0
- package/src/providers/openai-sidecar-credit.ts +26 -0
- package/src/providers/openai-sidecar.ts +35 -0
- package/src/server/claude-messages.ts +1 -0
- package/src/server/gui-pair-delivery.ts +23 -0
- package/src/server/index/serve-options.ts +5 -8
- package/src/server/index.ts +12 -14
- package/src/server/live-native-calls.ts +94 -0
- package/src/server/live.ts +28 -5
- package/src/server/local-management-read-client.ts +15 -11
- package/src/server/management/oauth-account-routes.ts +15 -0
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/shared.ts +9 -3
- package/src/server/messages-native-scope.ts +60 -0
- package/src/server/messages-native.ts +9 -0
- package/src/server/responses/adapter-dispatch.ts +31 -2
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/compact.ts +17 -2
- package/src/server/responses/core-auth.ts +18 -1
- package/src/server/responses/core-codex-account.ts +18 -8
- package/src/server/responses/core-combo.ts +5 -7
- package/src/server/responses/non-replayable-error.ts +48 -0
- package/src/server/responses/passthrough-delivery.ts +5 -1
- package/src/server/responses/passthrough-dispatch.ts +21 -19
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/sidecar-execution.ts +1 -0
- package/src/server/search.ts +4 -0
- package/src/server/startup-health-cache.ts +2 -6
- package/src/service/desktop-startup.ts +100 -3
- package/src/vision/describe.ts +17 -10
- package/src/vision/index.ts +1 -0
- package/src/web-search/executor.ts +17 -10
- package/src/web-search/format-result.ts +304 -20
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/loop.ts +7 -6
- package/src/web-search/passthrough-bridge.ts +1 -0
- package/src/web-search/run-turn-loop.ts +1 -1
- package/gui/dist/assets/App-BtfG5id-.js +0 -54
- package/gui/dist/assets/App-lCIslZ1L.css +0 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +0 -94
|
@@ -24,6 +24,7 @@ import { inspectResetCredits, consumeResetCredits } from "./reset-credit-service
|
|
|
24
24
|
import { getRuntimeConfig, saveRuntimeConfig, configuredPoolAccount } from "./runtime-config";
|
|
25
25
|
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
26
26
|
import { getEffectiveCodexAutoSwitchThreshold, isCodexAccountAutoSwitchThresholdKey, parseCodexAutoSwitchThreshold, setCodexAccountAutoSwitchThresholdOverride } from "../account-auto-switch";
|
|
27
|
+
import { withCodexAccountPauseGroup } from "./account-pause-group";
|
|
27
28
|
|
|
28
29
|
export async function handleCodexAuthAPI(
|
|
29
30
|
req: Request,
|
|
@@ -100,18 +101,23 @@ export async function handleCodexAuthAPI(
|
|
|
100
101
|
|| (runtimeConfig.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
|
|
101
102
|
if (!exists) return jsonResponse({ error: "Account not found" }, 404);
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
const paused = body.paused;
|
|
105
|
+
return withCodexAccountPauseGroup(runtimeConfig, id, accountIds => {
|
|
106
|
+
// Publish every exclusion before reconciling: a duplicate must never become the fallback.
|
|
107
|
+
for (const accountId of accountIds) setCodexAccountPaused(runtimeConfig, accountId, paused);
|
|
108
|
+
if (paused) {
|
|
109
|
+
for (const accountId of accountIds) clearThreadAccountMapForAccount(accountId);
|
|
110
|
+
for (const accountId of accountIds) selectFallbackAfterPause(runtimeConfig, accountId);
|
|
111
|
+
}
|
|
112
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
113
|
+
return jsonResponse({
|
|
114
|
+
ok: true,
|
|
115
|
+
id,
|
|
116
|
+
paused,
|
|
117
|
+
affectedAccountIds: accountIds,
|
|
118
|
+
activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
|
|
119
|
+
appliesImmediately: true,
|
|
120
|
+
});
|
|
115
121
|
});
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bindPoolCreditPolicy, poolCreditHoldResetAt, poolContextCreditHoldResetAt } from "./pool-credit-policy";
|
|
1
2
|
import { hasSpendableCodexCredits } from "./quota-types";
|
|
2
3
|
import { noteMainAccountActivity } from "./main-account-external-usage";
|
|
3
4
|
import { codexAccountPriorityFailbackEnabled } from "./account-priority";
|
|
@@ -525,6 +526,23 @@ export class CodexMainAccountHardLockError extends CodexAccountCooldownError {
|
|
|
525
526
|
}
|
|
526
527
|
}
|
|
527
528
|
|
|
529
|
+
/** A stored pool account's spending policy is independent of account selection. */
|
|
530
|
+
export class CodexPoolAccountCreditsOffError extends CodexAccountCooldownError {
|
|
531
|
+
readonly resetAt: number;
|
|
532
|
+
|
|
533
|
+
constructor(accountId: string, resetAt: number) {
|
|
534
|
+
super(accountId, resetAt);
|
|
535
|
+
this.name = "CodexPoolAccountCreditsOffError";
|
|
536
|
+
this.resetAt = resetAt;
|
|
537
|
+
this.message = "The selected Codex account reached its usage limit, and spending credits is off or no fresh spendable balance is available."
|
|
538
|
+
+ " Wait for the limit to reset, choose another account, or explicitly allow credits for this account in Codex Auth.";
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function assertPoolAccountCredits(accountId: string, resetAt: number | undefined): void {
|
|
543
|
+
if (resetAt !== undefined) throw new CodexPoolAccountCreditsOffError(accountId, resetAt);
|
|
544
|
+
}
|
|
545
|
+
|
|
528
546
|
/** The main login may not spend credits and one of its usage windows is full (#6334). */
|
|
529
547
|
export class CodexMainAccountCreditsOffError extends CodexAccountCooldownError {
|
|
530
548
|
readonly resetAt?: number;
|
|
@@ -597,7 +615,7 @@ export class CodexRecoveryWithheldError extends CodexAccountCooldownError {
|
|
|
597
615
|
|
|
598
616
|
export type CodexAuthPolicyConfig = Readonly<Pick<OcxConfig,
|
|
599
617
|
"codexMainAccountHardLock" | "codexMainAccountHardLockThresholds" | "codexDesktopAuthless" | "runtimeRole" | "pausedCodexAccountIds"
|
|
600
|
-
| "creditCodexAccountIds"
|
|
618
|
+
| "creditCodexAccountIds" | "codexAccounts"
|
|
601
619
|
>>;
|
|
602
620
|
|
|
603
621
|
interface CodexAuthMaterializationOptions {
|
|
@@ -725,6 +743,22 @@ export function unwrapUpstreamRetryEvidenceError(error: unknown): unknown {
|
|
|
725
743
|
* Same evidence the hard lock reads, and no plan lookup: the plan can live in the physical auth
|
|
726
744
|
* file, which several callers are forbidden to open, so every long window counts instead.
|
|
727
745
|
*/
|
|
746
|
+
/** Recheck live account policy at the physical send, after pacing or retry backoff. */
|
|
747
|
+
export function createCodexAuthDispatchGuard(
|
|
748
|
+
ctx: CodexAuthContext,
|
|
749
|
+
config: CodexAuthPolicyConfig,
|
|
750
|
+
modelId: string,
|
|
751
|
+
admission?: Pick<DataPlaneAdmission, "source">,
|
|
752
|
+
terminalHelper = false,
|
|
753
|
+
): ((headers: Headers) => void) | undefined {
|
|
754
|
+
const reserveGuard = createCodexReserveDispatchGuard(ctx, config, modelId, admission, terminalHelper);
|
|
755
|
+
if (ctx.kind !== "pool") return reserveGuard;
|
|
756
|
+
return headers => {
|
|
757
|
+
assertPoolAccountCredits(ctx.accountId, poolContextCreditHoldResetAt(ctx, config));
|
|
758
|
+
reserveGuard?.(headers);
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
728
762
|
function mainCreditsHoldResetAt(config: Pick<OcxConfig, "creditCodexAccountIds">): number | undefined {
|
|
729
763
|
if (codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
|
|
730
764
|
&& hasSpendableCodexCredits(getMainPolicyQuota())) return undefined;
|
|
@@ -868,6 +902,7 @@ export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSele
|
|
|
868
902
|
// A credits-off refusal is a configuration policy, not a cooldown: clearing a cooldown
|
|
869
903
|
// cannot lift it, so its own wording (wait for the reset or allow credits) is the remedy.
|
|
870
904
|
|| err instanceof CodexMainAccountCreditsOffError
|
|
905
|
+
|| err instanceof CodexPoolAccountCreditsOffError
|
|
871
906
|
|| err instanceof CodexReserveUnavailableError
|
|
872
907
|
// A transient-hold refusal is not a quota cooldown. Its own wording is the only accurate
|
|
873
908
|
// one, and the quota recovery advice below would send the operator after a cooldown that
|
|
@@ -917,6 +952,7 @@ export class CodexThreadAffinityExpiredError extends Error {
|
|
|
917
952
|
export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown): boolean {
|
|
918
953
|
return !(cause instanceof CodexMainAccountHardLockError)
|
|
919
954
|
&& !(cause instanceof CodexMainAccountCreditsOffError)
|
|
955
|
+
&& !(cause instanceof CodexPoolAccountCreditsOffError)
|
|
920
956
|
&& !(cause instanceof CodexAccountValidationPendingError)
|
|
921
957
|
&& !(cause instanceof CodexReserveUnavailableError)
|
|
922
958
|
&& !(cause instanceof CodexCredentialGenerationConflictError)
|
|
@@ -1408,6 +1444,7 @@ export async function resolveCodexAuthContext(
|
|
|
1408
1444
|
// deferred credential must never become request auth through that fallback.
|
|
1409
1445
|
try {
|
|
1410
1446
|
assertCodexAccountValidationReady(accountId);
|
|
1447
|
+
assertPoolAccountCredits(accountId, poolCreditHoldResetAt(policy, accountId, config));
|
|
1411
1448
|
} catch (cause) {
|
|
1412
1449
|
// Nothing will reach upstream, so give the trial back instead of leaving the held account
|
|
1413
1450
|
// unprobeable until the lease deadline lapses (#4701).
|
|
@@ -1533,7 +1570,8 @@ export async function resolveCodexAuthContext(
|
|
|
1533
1570
|
try {
|
|
1534
1571
|
const token = await getValidCodexToken(accountId, { signal: options.signal });
|
|
1535
1572
|
assertCodexAccountValidationReady(accountId);
|
|
1536
|
-
|
|
1573
|
+
assertPoolAccountCredits(accountId, poolCreditHoldResetAt(policy, accountId, config));
|
|
1574
|
+
return bindPoolCreditPolicy({
|
|
1537
1575
|
kind: "pool",
|
|
1538
1576
|
accountId,
|
|
1539
1577
|
writerGeneration,
|
|
@@ -1548,11 +1586,12 @@ export async function resolveCodexAuthContext(
|
|
|
1548
1586
|
...(probeQuotaScope ? { probeQuotaScope } : {}),
|
|
1549
1587
|
...(affinityDecision ? { affinityDecision } : {}),
|
|
1550
1588
|
...(transientProbe ? { transientProbe } : {}),
|
|
1551
|
-
};
|
|
1589
|
+
}, config, policy);
|
|
1552
1590
|
} catch (cause) {
|
|
1553
1591
|
releaseTransientProbeGrant();
|
|
1554
1592
|
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
1555
1593
|
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
1594
|
+
if (cause instanceof CodexPoolAccountCreditsOffError) throw cause;
|
|
1556
1595
|
if (!options.signal?.aborted && shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
1557
1596
|
markAccountNeedsReauth(accountId, writerGeneration);
|
|
1558
1597
|
}
|
|
@@ -1562,6 +1601,7 @@ export async function resolveCodexAuthContext(
|
|
|
1562
1601
|
|
|
1563
1602
|
export function assertCodexAuthContextNotCooled(ctx: CodexAuthContext | undefined): void {
|
|
1564
1603
|
if (ctx?.kind !== "pool" && ctx?.kind !== "main-pool") return;
|
|
1604
|
+
if (ctx.kind === "pool") assertPoolAccountCredits(ctx.accountId, poolContextCreditHoldResetAt(ctx));
|
|
1565
1605
|
// A context holding the probe lease was deliberately admitted through the cooldown.
|
|
1566
1606
|
if (ctx.probeLeaseId) return;
|
|
1567
1607
|
const cooldown = getCodexQuotaHealthSnapshot(ctx.accountId, ctx.quotaScope);
|
|
@@ -1577,6 +1617,7 @@ export function applyCodexAuthContextToProvider(
|
|
|
1577
1617
|
): OcxRuntimeProviderConfig {
|
|
1578
1618
|
if (mode !== "pool" || (ctx.kind !== "pool" && ctx.kind !== "main-pool") || provider.authMode !== "forward") return provider;
|
|
1579
1619
|
assertCodexAccountValidationReady(ctx.accountId);
|
|
1620
|
+
if (ctx.kind === "pool") assertPoolAccountCredits(ctx.accountId, poolContextCreditHoldResetAt(ctx));
|
|
1580
1621
|
return {
|
|
1581
1622
|
...provider,
|
|
1582
1623
|
_codexAccountOverride: {
|
|
@@ -1620,6 +1661,7 @@ export function materializeCodexUpstreamAuth(
|
|
|
1620
1661
|
}
|
|
1621
1662
|
if (ctx.kind === "pool" || ctx.kind === "main-pool") {
|
|
1622
1663
|
assertCodexAccountValidationReady(ctx.accountId);
|
|
1664
|
+
if (ctx.kind === "pool") assertPoolAccountCredits(ctx.accountId, poolContextCreditHoldResetAt(ctx, options.config));
|
|
1623
1665
|
selected.set("authorization", `Bearer ${ctx.accessToken}`);
|
|
1624
1666
|
selected.set("chatgpt-account-id", ctx.chatgptAccountId);
|
|
1625
1667
|
if (ctx.kind === "main-pool") {
|
|
@@ -4,12 +4,22 @@ import type { OcxConfig } from "../types";
|
|
|
4
4
|
import { getCodexRoutingKind, type CodexRoutingKind } from "./inject";
|
|
5
5
|
import { collectRoutingAdoption, type RoutingAdoptionEvidence } from "./routing-adoption";
|
|
6
6
|
import { diagnoseCodexShim, type CodexShimDiagnostic } from "./shim";
|
|
7
|
-
import {
|
|
7
|
+
import { diagnoseDesktopStartup, type DesktopStartupDiagnostic } from "../service/desktop-startup";
|
|
8
8
|
|
|
9
9
|
export type StartupProtection = "service" | "desktop" | "shim" | "none";
|
|
10
10
|
export type StartupHealthStatus = "native" | "protected" | "at-risk";
|
|
11
11
|
export type ShimCoverage = "full" | "cli-only" | "none";
|
|
12
12
|
|
|
13
|
+
/** Bound the isolated service-manager probe and let its reader outlive that probe. */
|
|
14
|
+
export function startupHealthProbeBudgetMs(platform: NodeJS.Platform = process.platform): number {
|
|
15
|
+
return platform === "win32" ? 15_000 : 5_000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function startupHealthReadBudgetMs(platform: NodeJS.Platform = process.platform): number {
|
|
19
|
+
// The endpoint waits an extra 500ms for child settlement; reserve another second for HTTP.
|
|
20
|
+
return startupHealthProbeBudgetMs(platform) + 1_500;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
export interface StartupHealthInputs {
|
|
14
24
|
desktop?: DesktopStartupDiagnostic;
|
|
15
25
|
routingKind: CodexRoutingKind;
|
|
@@ -80,7 +90,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
|
|
|
80
90
|
// We can only credit an opencodex service/shim for routing that opencodex owns.
|
|
81
91
|
// An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
|
|
82
92
|
const ownsLocalRouting = inputs.routingKind === "opencodex-local";
|
|
83
|
-
const desktopEffective = inputs.platform === "darwin" && !inputs.diagnosticStale
|
|
93
|
+
const desktopEffective = (inputs.platform === "darwin" || inputs.platform === "linux") && !inputs.diagnosticStale
|
|
84
94
|
&& inputs.desktop?.owned === true && inputs.desktop.loginEnabled
|
|
85
95
|
&& inputs.desktop.running && inputs.desktop.viable;
|
|
86
96
|
const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
|
|
@@ -137,7 +147,7 @@ export function collectStartupHealth(
|
|
|
137
147
|
const shim = diagnostics.shim ?? diagnoseCodexShim();
|
|
138
148
|
const service = diagnostics.service ?? diagnoseService();
|
|
139
149
|
const routingKind = diagnostics.routingKind ?? getCodexRoutingKind();
|
|
140
|
-
const desktop = diagnostics.desktop ??
|
|
150
|
+
const desktop = diagnostics.desktop ?? diagnoseDesktopStartup();
|
|
141
151
|
const routingAdoption = diagnostics.routingAdoption
|
|
142
152
|
?? (routingKind === "opencodex-local" ? collectRoutingAdoption({ routingKind }) : undefined);
|
|
143
153
|
return deriveStartupHealth({
|
package/src/codex/journal.ts
CHANGED
|
@@ -85,6 +85,9 @@ export interface RestoreJournalResult {
|
|
|
85
85
|
ownershipRefusal?: CodexHomeOwnerRefusalReason;
|
|
86
86
|
configRestored: boolean;
|
|
87
87
|
profileRestored: boolean;
|
|
88
|
+
/** True only when restore rewrote or removed a file (not when state was already original). */
|
|
89
|
+
configRewritten: boolean;
|
|
90
|
+
profileRewritten: boolean;
|
|
88
91
|
configChanged: boolean;
|
|
89
92
|
profileChanged: boolean;
|
|
90
93
|
complete: boolean;
|
|
@@ -299,12 +302,13 @@ export function restoreJournalState(): RestoreJournalResult {
|
|
|
299
302
|
try { assertCodexHomeOwner(CODEX_HOME); }
|
|
300
303
|
catch (error) {
|
|
301
304
|
if (!(error instanceof CodexHomeOwnerRefusal)) throw error;
|
|
302
|
-
return { configRestored: false, profileRestored: false,
|
|
303
|
-
complete: false, unverified: true, ownershipRefusal: error.reason };
|
|
305
|
+
return { configRestored: false, profileRestored: false, configRewritten: false, profileRewritten: false,
|
|
306
|
+
configChanged: false, profileChanged: false, complete: false, unverified: true, ownershipRefusal: error.reason };
|
|
304
307
|
}
|
|
305
308
|
const journal = readJournal();
|
|
306
309
|
if (!journal) {
|
|
307
|
-
return { configRestored: false, profileRestored: false,
|
|
310
|
+
return { configRestored: false, profileRestored: false, configRewritten: false, profileRewritten: false,
|
|
311
|
+
configChanged: false, profileChanged: false, complete: false, unverified: false };
|
|
308
312
|
}
|
|
309
313
|
const currentConfig = existsSync(CODEX_CONFIG_PATH) ? readFileSync(CODEX_CONFIG_PATH, "utf-8") : null;
|
|
310
314
|
const currentProfile = existsSync(CODEX_PROFILE_PATH) ? readFileSync(CODEX_PROFILE_PATH, "utf-8") : null;
|
|
@@ -316,6 +320,8 @@ export function restoreJournalState(): RestoreJournalResult {
|
|
|
316
320
|
return {
|
|
317
321
|
configRestored: comparison.configAlreadyOriginal,
|
|
318
322
|
profileRestored: comparison.profileAlreadyOriginal,
|
|
323
|
+
configRewritten: false,
|
|
324
|
+
profileRewritten: false,
|
|
319
325
|
configChanged: !configUnchanged,
|
|
320
326
|
profileChanged: !profileUnchanged,
|
|
321
327
|
complete: false,
|
|
@@ -326,14 +332,18 @@ export function restoreJournalState(): RestoreJournalResult {
|
|
|
326
332
|
assertCodexHomeOwner(CODEX_HOME);
|
|
327
333
|
let configRestored = comparison.configAlreadyOriginal;
|
|
328
334
|
let profileRestored = comparison.profileAlreadyOriginal;
|
|
335
|
+
let configRewritten = false;
|
|
336
|
+
let profileRewritten = false;
|
|
329
337
|
if (configUnchanged && !configRestored) {
|
|
330
338
|
atomicWriteFile(CODEX_CONFIG_PATH, comparison.originalConfig);
|
|
331
339
|
configRestored = true;
|
|
340
|
+
configRewritten = true;
|
|
332
341
|
}
|
|
333
342
|
if (profileUnchanged && !profileRestored) {
|
|
334
343
|
if (comparison.originalProfile !== null) {
|
|
335
344
|
atomicWriteFile(CODEX_PROFILE_PATH, comparison.originalProfile);
|
|
336
345
|
profileRestored = true;
|
|
346
|
+
profileRewritten = true;
|
|
337
347
|
} else if (existsSync(CODEX_PROFILE_PATH)) {
|
|
338
348
|
// "There was no profile before, so remove the one we generated." Claiming success
|
|
339
349
|
// without checking is how a caller ends up deleting the journal, reporting a clean
|
|
@@ -343,6 +353,7 @@ export function restoreJournalState(): RestoreJournalResult {
|
|
|
343
353
|
try {
|
|
344
354
|
unlinkSync(CODEX_PROFILE_PATH);
|
|
345
355
|
profileRestored = true;
|
|
356
|
+
profileRewritten = true;
|
|
346
357
|
} catch (error) {
|
|
347
358
|
profileRestored = (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
348
359
|
}
|
|
@@ -355,6 +366,8 @@ export function restoreJournalState(): RestoreJournalResult {
|
|
|
355
366
|
return {
|
|
356
367
|
configRestored,
|
|
357
368
|
profileRestored,
|
|
369
|
+
configRewritten,
|
|
370
|
+
profileRewritten,
|
|
358
371
|
configChanged: !configUnchanged,
|
|
359
372
|
profileChanged: !profileUnchanged,
|
|
360
373
|
complete,
|
|
@@ -367,6 +380,11 @@ export function restoreJournal(): boolean {
|
|
|
367
380
|
return restoreJournalState().complete;
|
|
368
381
|
}
|
|
369
382
|
|
|
383
|
+
/** A no-rewrite recovery is quiet only when the stale journal is actually gone. */
|
|
384
|
+
function warnIfJournalRetained(): void {
|
|
385
|
+
if (existsSync(JOURNAL_PATH)) console.error("⚠️ Codex journal recovery found nothing to restore, but the journal could not be removed; it will be retried on the next start.");
|
|
386
|
+
}
|
|
387
|
+
|
|
370
388
|
export interface ReconcileJournalOptions {
|
|
371
389
|
activeClientApiKeyId?: string;
|
|
372
390
|
}
|
|
@@ -388,7 +406,13 @@ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean
|
|
|
388
406
|
console.error("⚠️ Codex journal recovery was not verified; current configuration files and the journal were preserved.");
|
|
389
407
|
return false;
|
|
390
408
|
}
|
|
391
|
-
if (!restored.
|
|
409
|
+
if (!restored.complete) {
|
|
410
|
+
console.error("⚠️ Codex journal recovery was incomplete; the journal was preserved — check the Codex files.");
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
// Warn only when restore actually rewrote state; an already-original
|
|
414
|
+
// snapshot still cleans itself up but has nothing to report.
|
|
415
|
+
if (!restored.configRewritten && !restored.profileRewritten) { warnIfJournalRetained(); return false; }
|
|
392
416
|
console.error(`⚠️ Uncommitted or mismatched client routing (${owner.apiKeyId}) was restored from the Codex journal.`);
|
|
393
417
|
return true;
|
|
394
418
|
}
|
|
@@ -406,7 +430,14 @@ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean
|
|
|
406
430
|
console.error("⚠️ Codex journal recovery was not verified; current configuration files and the journal were preserved.");
|
|
407
431
|
return false;
|
|
408
432
|
}
|
|
409
|
-
if (!restored.
|
|
433
|
+
if (!restored.complete) {
|
|
434
|
+
console.error("⚠️ Codex journal recovery was incomplete; the journal was preserved — check the Codex files.");
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
// Warn only when restore actually rewrote state. A stale journal whose
|
|
438
|
+
// artifacts are already original still cleans itself up (complete removes
|
|
439
|
+
// it) but has nothing to report — claiming a restore then cries wolf.
|
|
440
|
+
if (!restored.configRewritten && !restored.profileRewritten) { warnIfJournalRetained(); return false; }
|
|
410
441
|
console.error(`⚠️ Previous session (PID ${pid}) did not shut down cleanly. Codex state restored from journal.`);
|
|
411
442
|
return true;
|
|
412
443
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { codexUsageLimitResetAt, isCodexAccountHeldForCredits } from "./account-credit-use";
|
|
3
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
4
|
+
import { isSelectableCodexPoolAccount } from "./account-id";
|
|
5
|
+
import { getAccountQuota } from "./quota";
|
|
6
|
+
|
|
7
|
+
export type PoolCreditPolicyConfig = Readonly<Pick<OcxConfig, "creditCodexAccountIds" | "codexAccounts">>;
|
|
8
|
+
type PoolContext = { kind: "pool"; accountId: string };
|
|
9
|
+
|
|
10
|
+
// Keep live policy references private: they must not enter DTOs or serialized auth context.
|
|
11
|
+
const policies = new WeakMap<object, {
|
|
12
|
+
config: PoolCreditPolicyConfig;
|
|
13
|
+
policy: PoolCreditPolicyConfig;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
/** Read cached usage only. This helper never opens or refreshes a credential. */
|
|
17
|
+
export function poolCreditHoldResetAt(
|
|
18
|
+
policy: PoolCreditPolicyConfig,
|
|
19
|
+
accountId: string,
|
|
20
|
+
catalog: PoolCreditPolicyConfig = policy,
|
|
21
|
+
now = Date.now(),
|
|
22
|
+
): number | undefined {
|
|
23
|
+
// Native main has an independent identity-bound quota policy. Never read its physical plan.
|
|
24
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return undefined;
|
|
25
|
+
const quota = getAccountQuota(accountId);
|
|
26
|
+
const plan = catalog.codexAccounts
|
|
27
|
+
?.find(account => isSelectableCodexPoolAccount(account) && account.id === accountId)?.plan;
|
|
28
|
+
if (!isCodexAccountHeldForCredits(policy, accountId, quota, plan, now)) return undefined;
|
|
29
|
+
return codexUsageLimitResetAt(quota, plan, now);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Remember policy by reference so later credential materialization rechecks current values. */
|
|
33
|
+
export function bindPoolCreditPolicy<T extends PoolContext>(
|
|
34
|
+
context: T,
|
|
35
|
+
config: PoolCreditPolicyConfig,
|
|
36
|
+
policy: PoolCreditPolicyConfig = config,
|
|
37
|
+
): T {
|
|
38
|
+
policies.set(context, { config, policy });
|
|
39
|
+
return context;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Carry the resolver's live policy onto a context rebuilt by spread copy. The WeakMap keys on
|
|
44
|
+
* object identity, so `{ ...ctx }` alone loses the binding and every caller that omits an
|
|
45
|
+
* explicit override would skip credit-hold enforcement on the copy. A source with no binding
|
|
46
|
+
* leaves the target unbound rather than inventing a policy.
|
|
47
|
+
*/
|
|
48
|
+
export function rebindPoolCreditPolicy<S extends PoolContext, T extends object>(source: S, target: T): T {
|
|
49
|
+
const bound = policies.get(source);
|
|
50
|
+
if (bound) policies.set(target, bound);
|
|
51
|
+
return target;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Explicit policy wins; omitted materialization options retain the resolver's policy. */
|
|
55
|
+
export function poolContextCreditHoldResetAt(
|
|
56
|
+
context: PoolContext,
|
|
57
|
+
override?: PoolCreditPolicyConfig,
|
|
58
|
+
now = Date.now(),
|
|
59
|
+
): number | undefined {
|
|
60
|
+
const bound = policies.get(context);
|
|
61
|
+
const policy = override ?? bound?.policy;
|
|
62
|
+
// Unbound contexts skip credit-hold enforcement entirely: callers that require
|
|
63
|
+
// enforcement must supply `override` (or resolve through bindPoolCreditPolicy).
|
|
64
|
+
if (!policy) return undefined;
|
|
65
|
+
const catalog = override?.codexAccounts ? override : bound?.config ?? policy;
|
|
66
|
+
return poolCreditHoldResetAt(policy, context.accountId, catalog, now);
|
|
67
|
+
}
|
|
@@ -48,11 +48,19 @@ function lexical(path: string): Stats | null {
|
|
|
48
48
|
try { return lstatSync(path); }
|
|
49
49
|
catch (error) { if (fileErrorCode(error) === "ENOENT") return null; throw error; }
|
|
50
50
|
}
|
|
51
|
+
/** Name the path and the failed check; a group/world-writable owned file gets the chmod that fixes it. */
|
|
52
|
+
function untrustedArtifact(path: string, stat: Stats): Error {
|
|
53
|
+
const prefix = `Private Codex artifact is not an owned regular file: ${path}`;
|
|
54
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return new Error(`${prefix} is not a regular file`);
|
|
55
|
+
if (stat.uid !== process.getuid?.()) return new Error(`${prefix} is owned by uid ${stat.uid}`);
|
|
56
|
+
const mode = (stat.mode & 0o777).toString(8).padStart(4, "0");
|
|
57
|
+
return new Error(`${prefix} is group- or world-writable (mode ${mode}); run chmod 600 ${shQuote(path)} and retry`);
|
|
58
|
+
}
|
|
51
59
|
function snapshot(path: string): Snapshot | null {
|
|
52
60
|
const before = lexical(path);
|
|
53
61
|
if (!before) return null;
|
|
54
62
|
if (!before.isFile() || before.isSymbolicLink() || before.uid !== process.getuid?.() || (before.mode & 0o022)) {
|
|
55
|
-
throw
|
|
63
|
+
throw untrustedArtifact(path, before);
|
|
56
64
|
}
|
|
57
65
|
const bounded = readBoundedRegularFile(path, PRIVATE_FILE_MAX_BYTES);
|
|
58
66
|
if (!bounded || "warning" in bounded) throw new Error("Private Codex artifact is unreadable or changed");
|
package/src/combos/jev.ts
CHANGED
|
@@ -47,6 +47,7 @@ const TOOL_NAME_CHARS = 160;
|
|
|
47
47
|
const VISIBLE_TEXT_CHUNK_CHARS = 16_384;
|
|
48
48
|
|
|
49
49
|
const ENVELOPE_TAGS = [
|
|
50
|
+
"system-reminder",
|
|
50
51
|
"codex_internal_context",
|
|
51
52
|
"recommended_plugins",
|
|
52
53
|
"environment_context",
|
|
@@ -302,7 +303,8 @@ function taskWithoutProtectedEnvelopes(text: string): string {
|
|
|
302
303
|
}
|
|
303
304
|
} else {
|
|
304
305
|
if (stack.length === 0) appendTrimmedRange(visible, "\n", 0, 1);
|
|
305
|
-
|
|
306
|
+
// A Codex goal quoted inside a Claude reminder is context, never a fallback task.
|
|
307
|
+
if (!completedGoal && !goal && tag === "codex_internal_context" && !stack.includes("system-reminder")) {
|
|
306
308
|
goal = { sample: emptyTextSample(), pendingWhitespace: emptyTextSample() };
|
|
307
309
|
goalDepth = stack.length;
|
|
308
310
|
}
|