@bitkyc08/opencodex 2.47.0 → 2.48.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-B5r7LNHN.js +115 -0
- package/gui/dist/assets/index-D5SiRo8X.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/openai-responses.ts +16 -2
- package/src/adapters/opencode-go-additional-tools.ts +35 -0
- package/src/adapters/routed-agent-messages.ts +46 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/codex/auth-api.ts +211 -67
- package/src/codex/features.ts +3 -1
- package/src/codex/multi-agent-mode-policy.ts +24 -0
- package/src/codex/reset-credit-auto-redeem.ts +54 -13
- package/src/codex/routing.ts +85 -4
- package/src/codex/shim.ts +10 -1
- package/src/config/atomic-write.ts +2 -3
- package/src/config/initialize.ts +11 -6
- package/src/generated/compatibility-version.json +35 -23
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/providers/default-aliases.ts +4 -1
- package/src/providers/registry.ts +6 -1
- package/src/router.ts +5 -2
- package/src/server/claude-messages.ts +21 -4
- package/src/server/live.ts +8 -19
- package/src/server/management/agent-settings-routes.ts +3 -0
- package/src/server/responses/collaboration.ts +23 -12
- package/src/server/responses/combo-session-recall.ts +89 -0
- package/src/server/responses/compact.ts +19 -1
- package/src/server/responses/core.ts +189 -51
- package/src/types/config.ts +3 -3
- package/gui/dist/assets/index-B8ZZ0ivI.css +0 -1
- package/gui/dist/assets/index-BoXZzNaV.js +0 -115
- package/src/adapters/opencode-go.ts +0 -35
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent_message` is Codex's private multi-agent input item: it exists only in the ChatGPT
|
|
3
|
+
* Codex backend's schema. Codex replays every sub-agent reply in the history it sends, so
|
|
4
|
+
* once a thread has used sub-agents, a routed Responses destination answers the whole body
|
|
5
|
+
* with `422 unknown item type "agent_message"` and every later turn of that thread fails the
|
|
6
|
+
* same way. Rewrite the item as the public user message it already is.
|
|
7
|
+
*
|
|
8
|
+
* Genuine ciphertext and unknown part types keep their existing fail-closed path: the
|
|
9
|
+
* encrypted v2 task surface owns those, through `unreadable_encrypted_agent_task` and the
|
|
10
|
+
* opt-in recovery route. Providers using `authMode: "forward"` never reach this function.
|
|
11
|
+
*/
|
|
12
|
+
export function normalizeRoutedAgentMessages(
|
|
13
|
+
body: unknown,
|
|
14
|
+
{ allowStringContent = false }: { allowStringContent?: boolean } = {},
|
|
15
|
+
): unknown {
|
|
16
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
17
|
+
const record = body as Record<string, unknown>;
|
|
18
|
+
if (!Array.isArray(record.input)) return body;
|
|
19
|
+
let changed = false;
|
|
20
|
+
const input = record.input.map((item: unknown) => {
|
|
21
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
22
|
+
const message = item as Record<string, unknown>;
|
|
23
|
+
if (message.type !== "agent_message") return item;
|
|
24
|
+
// xAI rejects the private item even when a complete child result is a plain string.
|
|
25
|
+
// Trimming decides emptiness only; the original result bytes remain caller-owned.
|
|
26
|
+
const content = allowStringContent && typeof message.content === "string" && message.content.trim().length > 0
|
|
27
|
+
? [{ type: "input_text", text: message.content }]
|
|
28
|
+
: message.content;
|
|
29
|
+
if (!Array.isArray(content) || content.length === 0) return item;
|
|
30
|
+
// Genuine ciphertext and unknown part types must retain their existing fail-closed path.
|
|
31
|
+
if (!content.every(part => part && typeof part === "object"
|
|
32
|
+
&& ["input_text", "input_image", "input_file"].includes(part.type))) return item;
|
|
33
|
+
const identities = Object.fromEntries(["author", "recipient"]
|
|
34
|
+
.filter(key => typeof message[key] === "string")
|
|
35
|
+
.map(key => [key, message[key]]));
|
|
36
|
+
changed = true;
|
|
37
|
+
return {
|
|
38
|
+
type: "message", role: "user",
|
|
39
|
+
content: [
|
|
40
|
+
...(Object.keys(identities).length ? [{ type: "input_text", text: `Agent message ${JSON.stringify(identities)}` }] : []),
|
|
41
|
+
...content,
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return changed ? { ...record, input } : body;
|
|
46
|
+
}
|
|
@@ -29,6 +29,12 @@ export function toolsToResponses(tools: unknown): Rec[] | undefined {
|
|
|
29
29
|
name: raw.name,
|
|
30
30
|
...(typeof raw.description === "string" ? { description: raw.description } : {}),
|
|
31
31
|
parameters: raw.input_schema as Record<string, unknown>,
|
|
32
|
+
// Anthropic opts into strict tool use explicitly, while Responses reads an
|
|
33
|
+
// omitted strict as permission to normalize the schema into strict mode. That
|
|
34
|
+
// turns an optional input_schema parameter into a required one and breaks the
|
|
35
|
+
// call, so carry the source intent instead of the destination default. A
|
|
36
|
+
// non-boolean value is not a valid Anthropic opt-in and must not become one.
|
|
37
|
+
strict: typeof raw.strict === "boolean" ? raw.strict : false,
|
|
32
38
|
});
|
|
33
39
|
continue;
|
|
34
40
|
}
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -40,6 +40,10 @@ import {
|
|
|
40
40
|
} from "./account-priority";
|
|
41
41
|
import {
|
|
42
42
|
claimDueCodexQuotaRecoveryProbes,
|
|
43
|
+
claimManualResetCooldowns,
|
|
44
|
+
settleManualResetCooldown,
|
|
45
|
+
type ManualResetCooldownClaim,
|
|
46
|
+
type ManualResetRefreshLineage,
|
|
43
47
|
clearCodexAccountCooldown,
|
|
44
48
|
clearThreadAccountMapForAccount,
|
|
45
49
|
getEffectiveActiveCodexAccountId,
|
|
@@ -98,6 +102,8 @@ import {
|
|
|
98
102
|
getMainAccountInfoCache,
|
|
99
103
|
getMainQuotaCredentialGeneration,
|
|
100
104
|
isMainAccountIdentityGenerationLive,
|
|
105
|
+
isMainQuotaWriterLive,
|
|
106
|
+
type MainQuotaWriter,
|
|
101
107
|
matchesMainQuotaCredential,
|
|
102
108
|
observeMainQuotaCredential,
|
|
103
109
|
setMainAccountCredentialPresence,
|
|
@@ -387,6 +393,8 @@ interface ResetCreditAuth {
|
|
|
387
393
|
chatgptAccountId: string;
|
|
388
394
|
nativeMainLease?: AdmissionLease;
|
|
389
395
|
nativeMainSharedClaimHeld?: true;
|
|
396
|
+
poolGeneration?: number;
|
|
397
|
+
mainProof?: MainResetQuotaProof;
|
|
390
398
|
}
|
|
391
399
|
|
|
392
400
|
async function withResetCreditAuth<T>(
|
|
@@ -407,10 +415,15 @@ async function withResetCreditAuth<T>(
|
|
|
407
415
|
if (!tokens) {
|
|
408
416
|
return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
|
|
409
417
|
}
|
|
418
|
+
reconcileMainCodexAccountRuntimeState();
|
|
419
|
+
const physicalId = extractAccountId(tokens.id_token, tokens.access_token) ?? tokens.account_id;
|
|
420
|
+
const writer = physicalId === tokens.account_id
|
|
421
|
+
? observeMainQuotaCredential(tokens.access_token, tokens.account_id) : undefined;
|
|
410
422
|
return {
|
|
411
423
|
ok: true,
|
|
412
424
|
value: await operation({
|
|
413
425
|
isMain: true,
|
|
426
|
+
...(writer ? { mainProof: { writer, credentialGeneration: getMainQuotaCredentialGeneration() } } : {}),
|
|
414
427
|
accessToken: tokens.access_token,
|
|
415
428
|
chatgptAccountId: tokens.account_id,
|
|
416
429
|
nativeMainLease,
|
|
@@ -439,6 +452,7 @@ async function withResetCreditAuth<T>(
|
|
|
439
452
|
ok: true,
|
|
440
453
|
value: await operation({
|
|
441
454
|
isMain: false,
|
|
455
|
+
poolGeneration: cred.generation,
|
|
442
456
|
accessToken: cred.accessToken,
|
|
443
457
|
chatgptAccountId: cred.chatgptAccountId,
|
|
444
458
|
}),
|
|
@@ -776,8 +790,14 @@ async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
|
|
|
776
790
|
}
|
|
777
791
|
}
|
|
778
792
|
|
|
793
|
+
interface MainResetQuotaProof {
|
|
794
|
+
writer: MainQuotaWriter;
|
|
795
|
+
credentialGeneration: number;
|
|
796
|
+
}
|
|
797
|
+
|
|
779
798
|
interface MainAccountInfoFetchResult {
|
|
780
799
|
info: MainAccountInfo;
|
|
800
|
+
resetRecoveryProof?: MainResetQuotaProof & { dispatchSequence: number };
|
|
781
801
|
/** Ephemeral result of this attempt, omitted when no WHAM request was made. */
|
|
782
802
|
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
783
803
|
/** Internal dispatch fence for diagnostics only; never copied into a public DTO or cache. */
|
|
@@ -914,6 +934,7 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
914
934
|
let quotaPhase: "request" | "body" | "decode" | "publish" = "request";
|
|
915
935
|
let quotaRefreshGeneration = captureMainAccountIdentityGeneration();
|
|
916
936
|
try {
|
|
937
|
+
const dispatchSequence = ++quotaDispatchSequence;
|
|
917
938
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
918
939
|
headers: { Authorization: `Bearer ${tokens.access_token}`, "ChatGPT-Account-Id": tokens.account_id },
|
|
919
940
|
signal: quotaSignal,
|
|
@@ -923,6 +944,10 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
923
944
|
const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
924
945
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
925
946
|
if (retried) return retried;
|
|
947
|
+
if (dispatchSequence < mainQuotaPublishedSequence) {
|
|
948
|
+
return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
|
|
949
|
+
credentialChecked: true, hasCredential: true };
|
|
950
|
+
}
|
|
926
951
|
if (terminalAuthFailure) {
|
|
927
952
|
// Account for this attempt's own synchronous invalidation, never prior external drift.
|
|
928
953
|
const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
|
|
@@ -945,6 +970,12 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
945
970
|
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
946
971
|
throw new Error("Invalid WHAM usage object");
|
|
947
972
|
}
|
|
973
|
+
// Check after body/retry awaits and before any cache, credits, policy or
|
|
974
|
+
// Reserve publication. Returning cached state supplies no fresh recovery proof.
|
|
975
|
+
if (dispatchSequence < mainQuotaPublishedSequence) {
|
|
976
|
+
return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
|
|
977
|
+
credentialChecked: true, hasCredential: true };
|
|
978
|
+
}
|
|
948
979
|
quotaPhase = "publish";
|
|
949
980
|
// A delayed response from a replaced bearer cannot revoke a newer Reserve grant,
|
|
950
981
|
// even in the same workspace or after an A→B→A credential transition.
|
|
@@ -985,6 +1016,7 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
985
1016
|
if (result.quota) {
|
|
986
1017
|
setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota);
|
|
987
1018
|
}
|
|
1019
|
+
mainQuotaPublishedSequence = dispatchSequence;
|
|
988
1020
|
return {
|
|
989
1021
|
info: result,
|
|
990
1022
|
quotaRefresh: { status: quota ? "ok" : "not_reported" },
|
|
@@ -992,6 +1024,11 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
992
1024
|
credentialChecked: true,
|
|
993
1025
|
hasCredential: true,
|
|
994
1026
|
...(quota ? { freshQuota: quota } : {}),
|
|
1027
|
+
...(quota && mainQuotaWriter && isMainQuotaWriterLive(mainQuotaWriter)
|
|
1028
|
+
&& mainQuotaCredentialGeneration === getMainQuotaCredentialGeneration()
|
|
1029
|
+
&& matchesMainQuotaCredential(tokens.access_token, tokens.account_id)
|
|
1030
|
+
? { resetRecoveryProof: { writer: mainQuotaWriter, credentialGeneration: mainQuotaCredentialGeneration, dispatchSequence } }
|
|
1031
|
+
: {}),
|
|
995
1032
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
996
1033
|
};
|
|
997
1034
|
} catch (error) {
|
|
@@ -1011,6 +1048,8 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
1011
1048
|
}
|
|
1012
1049
|
|
|
1013
1050
|
interface PoolQuotaResult {
|
|
1051
|
+
/** Actual refresh result attached only to the successful usage replay. */
|
|
1052
|
+
resetRefreshLineage?: ManualResetRefreshLineage;
|
|
1014
1053
|
quota: StoredAccountQuota | null;
|
|
1015
1054
|
needsReauth: boolean;
|
|
1016
1055
|
/** Credential generation whose cache or network result this DTO state belongs to. */
|
|
@@ -1025,15 +1064,25 @@ interface PoolQuotaResult {
|
|
|
1025
1064
|
freshResetCredits?: number;
|
|
1026
1065
|
quotaProbeSkipped?: true;
|
|
1027
1066
|
/** Positive evidence captured immediately before an upstream WHAM dispatch. */
|
|
1028
|
-
quotaProbeAttempted?: { at: number; credentialGeneration: number };
|
|
1067
|
+
quotaProbeAttempted?: { at: number; credentialGeneration: number; dispatchSequence: number };
|
|
1029
1068
|
}
|
|
1030
1069
|
|
|
1070
|
+
// Process-local ordering, never a timestamp or a serialized account identifier.
|
|
1071
|
+
let quotaDispatchSequence = 0;
|
|
1072
|
+
// Shared native-main ownership permits concurrent usage readers. Only a later
|
|
1073
|
+
// successfully published response advances this fence; failed reads do not win.
|
|
1074
|
+
let mainQuotaPublishedSequence = 0;
|
|
1075
|
+
|
|
1031
1076
|
interface PoolQuotaProbeEvidence {
|
|
1077
|
+
onDispatch?: (sequence: number) => void;
|
|
1078
|
+
mayPublish?: () => boolean;
|
|
1032
1079
|
attempted?: NonNullable<PoolQuotaResult["quotaProbeAttempted"]>;
|
|
1033
1080
|
}
|
|
1034
1081
|
|
|
1035
1082
|
function markQuotaProbeAttempted(evidence: PoolQuotaProbeEvidence, credentialGeneration: number): void {
|
|
1036
|
-
|
|
1083
|
+
const dispatchSequence = ++quotaDispatchSequence;
|
|
1084
|
+
evidence.attempted = { at: Date.now(), credentialGeneration, dispatchSequence };
|
|
1085
|
+
evidence.onDispatch?.(dispatchSequence);
|
|
1037
1086
|
}
|
|
1038
1087
|
|
|
1039
1088
|
function withQuotaProbeEvidence(
|
|
@@ -1045,6 +1094,8 @@ function withQuotaProbeEvidence(
|
|
|
1045
1094
|
|
|
1046
1095
|
interface PoolQuotaRefreshFlight {
|
|
1047
1096
|
state: {
|
|
1097
|
+
dispatchSequence?: number;
|
|
1098
|
+
superseded?: boolean;
|
|
1048
1099
|
startCredentialGeneration?: number;
|
|
1049
1100
|
resolvedCredentialGeneration?: number;
|
|
1050
1101
|
};
|
|
@@ -1280,9 +1331,18 @@ async function recoverPoolQuotaFrom401(ctx: {
|
|
|
1280
1331
|
}
|
|
1281
1332
|
return { quota: existing ?? null, needsReauth: false, credentialGeneration: refreshed.generation };
|
|
1282
1333
|
}
|
|
1283
|
-
|
|
1334
|
+
const result = await commitPoolQuotaResponse(replay, {
|
|
1284
1335
|
accountId, existing, configuredPlan, generation: refreshed.generation, writerGeneration,
|
|
1336
|
+
mayPublish: ctx.quotaProbeEvidence.mayPublish,
|
|
1285
1337
|
});
|
|
1338
|
+
return result.freshCredentialGeneration === refreshed.generation ? {
|
|
1339
|
+
...result,
|
|
1340
|
+
resetRefreshLineage: {
|
|
1341
|
+
fromGeneration: rejectedGeneration,
|
|
1342
|
+
toGeneration: refreshed.generation,
|
|
1343
|
+
provenance: refreshed.provenance,
|
|
1344
|
+
},
|
|
1345
|
+
} : result;
|
|
1286
1346
|
}
|
|
1287
1347
|
|
|
1288
1348
|
/** Backoff after a refresh failure that proved nothing about the credential. */
|
|
@@ -1314,10 +1374,14 @@ async function commitPoolQuotaResponse(
|
|
|
1314
1374
|
configuredPlan: string | undefined;
|
|
1315
1375
|
generation: number;
|
|
1316
1376
|
writerGeneration: number;
|
|
1377
|
+
mayPublish?: () => boolean;
|
|
1317
1378
|
},
|
|
1318
1379
|
): Promise<PoolQuotaResult> {
|
|
1319
1380
|
const { accountId, existing, configuredPlan, generation, writerGeneration } = ctx;
|
|
1320
1381
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
1382
|
+
if (ctx.mayPublish?.() === false) {
|
|
1383
|
+
return { quota: getAccountQuota(accountId), needsReauth: false, credentialGeneration: generation };
|
|
1384
|
+
}
|
|
1321
1385
|
const freshPlan = nonEmptyPlan(data.plan_type) ?? undefined;
|
|
1322
1386
|
const quota = parseUsageQuota({ ...data, plan_type: freshPlan ?? configuredPlan });
|
|
1323
1387
|
const freshResetCredits = quota?.resetCredits;
|
|
@@ -1350,10 +1414,10 @@ async function fetchFreshPoolAccountQuota(
|
|
|
1350
1414
|
configuredPlan?: string,
|
|
1351
1415
|
onCredentialGeneration?: (generation: number) => void,
|
|
1352
1416
|
getValidToken: typeof getValidCodexToken = getValidCodexToken,
|
|
1417
|
+
quotaProbeEvidence: PoolQuotaProbeEvidence = {},
|
|
1353
1418
|
): Promise<PoolQuotaResult> {
|
|
1354
1419
|
const writerGeneration = captureConfigGeneration();
|
|
1355
1420
|
let requestCredentialGeneration = readCodexAccountRecord(accountId)?.generation;
|
|
1356
|
-
const quotaProbeEvidence: PoolQuotaProbeEvidence = {};
|
|
1357
1421
|
try {
|
|
1358
1422
|
const { accessToken, chatgptAccountId, generation } = await getValidToken(accountId);
|
|
1359
1423
|
requestCredentialGeneration = generation;
|
|
@@ -1387,6 +1451,7 @@ async function fetchFreshPoolAccountQuota(
|
|
|
1387
1451
|
}
|
|
1388
1452
|
const committed = await commitPoolQuotaResponse(resp, {
|
|
1389
1453
|
accountId, existing, configuredPlan, generation, writerGeneration,
|
|
1454
|
+
mayPublish: quotaProbeEvidence.mayPublish,
|
|
1390
1455
|
});
|
|
1391
1456
|
return withQuotaProbeEvidence(committed, quotaProbeEvidence);
|
|
1392
1457
|
} catch (e) {
|
|
@@ -1417,9 +1482,10 @@ async function fetchPoolAccountQuota(
|
|
|
1417
1482
|
forceRefresh = false,
|
|
1418
1483
|
configuredPlan?: string,
|
|
1419
1484
|
getValidToken: typeof getValidCodexToken = getValidCodexToken,
|
|
1485
|
+
afterDispatchSequence?: number,
|
|
1420
1486
|
): Promise<PoolQuotaResult> {
|
|
1421
1487
|
const existing = getAccountQuota(accountId);
|
|
1422
|
-
if (!forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) {
|
|
1488
|
+
if (afterDispatchSequence === undefined && !forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) {
|
|
1423
1489
|
return {
|
|
1424
1490
|
quota: existing,
|
|
1425
1491
|
needsReauth: false,
|
|
@@ -1434,11 +1500,18 @@ async function fetchPoolAccountQuota(
|
|
|
1434
1500
|
const current = flights && [...flights].find(flight => {
|
|
1435
1501
|
const generation = flight.state.resolvedCredentialGeneration
|
|
1436
1502
|
?? flight.state.startCredentialGeneration;
|
|
1437
|
-
return
|
|
1503
|
+
return !flight.state.superseded
|
|
1504
|
+
&& (afterDispatchSequence === undefined || (flight.state.dispatchSequence ?? 0) > afterDispatchSequence)
|
|
1505
|
+
&& generation !== undefined && isCodexAccountGenerationLive(accountId, generation);
|
|
1438
1506
|
});
|
|
1439
1507
|
if (current) return current.promise;
|
|
1440
1508
|
if (poolQuotaFlightCount() >= MAX_POOL_QUOTA_FLIGHTS) throw new PoolQuotaProbeBusyError();
|
|
1441
1509
|
|
|
1510
|
+
// A post-reset request must not let an older same-account response overwrite its evidence.
|
|
1511
|
+
// Flags live only as long as the bounded flights; no retained per-account sequence map.
|
|
1512
|
+
if (afterDispatchSequence !== undefined) {
|
|
1513
|
+
for (const flight of flights ?? []) flight.state.superseded = true;
|
|
1514
|
+
}
|
|
1442
1515
|
const state: PoolQuotaRefreshFlight["state"] = {
|
|
1443
1516
|
startCredentialGeneration: record?.generation,
|
|
1444
1517
|
};
|
|
@@ -1448,6 +1521,10 @@ async function fetchPoolAccountQuota(
|
|
|
1448
1521
|
configuredPlan,
|
|
1449
1522
|
generation => { state.resolvedCredentialGeneration = generation; },
|
|
1450
1523
|
getValidToken,
|
|
1524
|
+
{
|
|
1525
|
+
onDispatch: sequence => { state.dispatchSequence = sequence; },
|
|
1526
|
+
mayPublish: () => state.superseded !== true,
|
|
1527
|
+
},
|
|
1451
1528
|
);
|
|
1452
1529
|
const flight: PoolQuotaRefreshFlight = { state, promise: refresh };
|
|
1453
1530
|
const activeFlights = flights ?? new Set<PoolQuotaRefreshFlight>();
|
|
@@ -1463,6 +1540,74 @@ async function fetchPoolAccountQuota(
|
|
|
1463
1540
|
}
|
|
1464
1541
|
}
|
|
1465
1542
|
|
|
1543
|
+
function manualResetAuthStillLive(accountId: string, auth: ResetCreditAuth): boolean {
|
|
1544
|
+
if (!auth.isMain) {
|
|
1545
|
+
const record = readCodexAccountRecord(accountId);
|
|
1546
|
+
return auth.poolGeneration !== undefined
|
|
1547
|
+
&& isCodexAccountGenerationLive(accountId, auth.poolGeneration)
|
|
1548
|
+
&& record?.credential?.chatgptAccountId === auth.chatgptAccountId;
|
|
1549
|
+
}
|
|
1550
|
+
const tokens = readCodexTokens();
|
|
1551
|
+
return !!auth.mainProof && !!tokens
|
|
1552
|
+
&& tokens.access_token === auth.accessToken && tokens.account_id === auth.chatgptAccountId
|
|
1553
|
+
&& isMainQuotaWriterLive(auth.mainProof.writer)
|
|
1554
|
+
&& auth.mainProof.credentialGeneration === getMainQuotaCredentialGeneration()
|
|
1555
|
+
&& matchesMainQuotaCredential(auth.accessToken, auth.chatgptAccountId);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/** A confirmed spend remains successful even when its optional usage observation fails. */
|
|
1559
|
+
async function refreshAfterManualReset(
|
|
1560
|
+
config: OcxConfig,
|
|
1561
|
+
accountId: string,
|
|
1562
|
+
auth: ResetCreditAuth,
|
|
1563
|
+
claims: ManualResetCooldownClaim[],
|
|
1564
|
+
didReset: boolean,
|
|
1565
|
+
): Promise<number | undefined> {
|
|
1566
|
+
const afterDispatchSequence = quotaDispatchSequence;
|
|
1567
|
+
try {
|
|
1568
|
+
if (!manualResetAuthStillLive(accountId, auth)) return undefined;
|
|
1569
|
+
if (auth.isMain) {
|
|
1570
|
+
const result = await fetchMainAccountInfoAttempt(true, 1, auth.nativeMainLease,
|
|
1571
|
+
auth.nativeMainSharedClaimHeld === true, false);
|
|
1572
|
+
const proof = result.resetRecoveryProof;
|
|
1573
|
+
const recovered = didReset && manualResetAuthStillLive(accountId, auth)
|
|
1574
|
+
&& !!proof && !!auth.mainProof
|
|
1575
|
+
&& proof.dispatchSequence > afterDispatchSequence
|
|
1576
|
+
&& proof.credentialGeneration === auth.mainProof.credentialGeneration
|
|
1577
|
+
&& proof.writer.identityKey === auth.mainProof.writer.identityKey
|
|
1578
|
+
&& proof.writer.identityGeneration === auth.mainProof.writer.identityGeneration
|
|
1579
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan);
|
|
1580
|
+
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered);
|
|
1581
|
+
return manualResetAuthStillLive(accountId, auth) ? result.freshResetCredits : undefined;
|
|
1582
|
+
}
|
|
1583
|
+
const account = configuredPoolAccount(getRuntimeConfig(config), accountId);
|
|
1584
|
+
if (!account) return undefined;
|
|
1585
|
+
// Reuse the just-authenticated consume credential for the first usage request.
|
|
1586
|
+
// getValidCodexToken can silently advance a generation without exposing refresh
|
|
1587
|
+
// provenance. A 401 here instead uses the existing classified refresh/replay path.
|
|
1588
|
+
const resetToken: typeof getValidCodexToken = async () => {
|
|
1589
|
+
if (auth.poolGeneration === undefined || !manualResetAuthStillLive(accountId, auth)) {
|
|
1590
|
+
throw new CodexCredentialGenerationConflictError();
|
|
1591
|
+
}
|
|
1592
|
+
return { accessToken: auth.accessToken, chatgptAccountId: auth.chatgptAccountId, generation: auth.poolGeneration };
|
|
1593
|
+
};
|
|
1594
|
+
const result = await fetchPoolAccountQuota(accountId, true, account.plan, didReset ? resetToken : getValidCodexToken,
|
|
1595
|
+
didReset ? afterDispatchSequence : undefined);
|
|
1596
|
+
const record = readCodexAccountRecord(accountId);
|
|
1597
|
+
const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId
|
|
1598
|
+
&& (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence
|
|
1599
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
1600
|
+
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered, {
|
|
1601
|
+
credentialGeneration: result.freshCredentialGeneration,
|
|
1602
|
+
refreshLineage: result.resetRefreshLineage,
|
|
1603
|
+
});
|
|
1604
|
+
return record?.credential?.chatgptAccountId === auth.chatgptAccountId ? result.freshResetCredits : undefined;
|
|
1605
|
+
} catch {
|
|
1606
|
+
// The upstream reset already happened. A failed refresh must not invite another spend.
|
|
1607
|
+
return undefined;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1466
1611
|
let primeInFlight: Promise<void> | null = null;
|
|
1467
1612
|
/**
|
|
1468
1613
|
* Last prime attempt per pool account. A failed WHAM lookup stores no quota, so
|
|
@@ -2341,7 +2486,7 @@ export async function handleCodexAuthAPI(
|
|
|
2341
2486
|
const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
2342
2487
|
// The ledger keys manual operations by the *physical* ChatGPT account, which is
|
|
2343
2488
|
// only known after the auth wrapper resolves credentials. Open here, not earlier.
|
|
2344
|
-
|
|
2489
|
+
let identity = requestedOperationId === undefined
|
|
2345
2490
|
? undefined
|
|
2346
2491
|
: {
|
|
2347
2492
|
accountId,
|
|
@@ -2377,75 +2522,74 @@ export async function handleCodexAuthAPI(
|
|
|
2377
2522
|
return response;
|
|
2378
2523
|
}
|
|
2379
2524
|
// Canonical id, which an alias join may map to an earlier caller id.
|
|
2525
|
+
identity = { ...identity, operationId: opened.operationId };
|
|
2380
2526
|
idempotencyKey = opened.operationId;
|
|
2381
2527
|
} else {
|
|
2382
2528
|
idempotencyKey = crypto.randomUUID();
|
|
2383
2529
|
}
|
|
2384
|
-
|
|
2530
|
+
const claims = manualResetAuthStillLive(accountId, auth)
|
|
2531
|
+
? claimManualResetCooldowns(getRuntimeConfig(config), accountId, Date.now(), auth.poolGeneration) : [];
|
|
2385
2532
|
try {
|
|
2386
|
-
resp
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2533
|
+
let resp: Response;
|
|
2534
|
+
try {
|
|
2535
|
+
resp = await fetch(
|
|
2536
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
2537
|
+
{
|
|
2538
|
+
method: "POST",
|
|
2539
|
+
headers: {
|
|
2540
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
2541
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
2542
|
+
"Content-Type": "application/json",
|
|
2543
|
+
},
|
|
2544
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
2545
|
+
signal: AbortSignal.timeout(10_000),
|
|
2394
2546
|
},
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
// Mark ambiguous so a replay of this same id is never treated as new.
|
|
2402
|
-
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
2403
|
-
throw error;
|
|
2404
|
-
}
|
|
2405
|
-
if (!resp.ok) {
|
|
2406
|
-
await resp.body?.cancel().catch(() => {});
|
|
2407
|
-
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
2408
|
-
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
2409
|
-
}
|
|
2410
|
-
const result = safeResetCreditConsumeDto(await resp.json());
|
|
2411
|
-
if (identity) {
|
|
2412
|
-
// Narrow explicitly rather than casting: `safeResetCreditConsumeDto`
|
|
2413
|
-
// normalizes anything unrecognized to "unknown", and settling that
|
|
2414
|
-
// would come back as a mismatch and leave the row pending anyway.
|
|
2415
|
-
// Settlement failure never downgrades the user-visible outcome: the
|
|
2416
|
-
// spend already happened upstream, and reporting failure would invite
|
|
2417
|
-
// a manual retry -- the exact double-spend this unit removes.
|
|
2418
|
-
if (result.code === "reset" || result.code === "already_redeemed"
|
|
2419
|
-
|| result.code === "nothing_to_reset" || result.code === "no_credit") {
|
|
2420
|
-
settleManualResetCreditOperation(identity, result.code);
|
|
2421
|
-
} else {
|
|
2422
|
-
markManualResetCreditOperationAmbiguous(identity);
|
|
2547
|
+
);
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
// Dispatch outcome unknown: the credit may or may not have been spent.
|
|
2550
|
+
// Mark ambiguous so a replay of this same id is never treated as new.
|
|
2551
|
+
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
2552
|
+
throw error;
|
|
2423
2553
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
if (result.code === "reset" || result.code === "already_redeemed") {
|
|
2429
|
-
let freshResetCredits: number | undefined;
|
|
2430
|
-
if (auth.isMain) {
|
|
2431
|
-
({ freshResetCredits } = await fetchMainAccountInfoAttempt(
|
|
2432
|
-
true,
|
|
2433
|
-
1,
|
|
2434
|
-
auth.nativeMainLease,
|
|
2435
|
-
auth.nativeMainSharedClaimHeld === true,
|
|
2436
|
-
));
|
|
2437
|
-
} else {
|
|
2438
|
-
const account = configuredPoolAccount(getRuntimeConfig(config), accountId);
|
|
2439
|
-
({ freshResetCredits } = await fetchPoolAccountQuota(accountId, true, account?.plan));
|
|
2554
|
+
if (!resp.ok) {
|
|
2555
|
+
await resp.body?.cancel().catch(() => {});
|
|
2556
|
+
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
2557
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
2440
2558
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2559
|
+
const result = safeResetCreditConsumeDto(await resp.json());
|
|
2560
|
+
if (identity) {
|
|
2561
|
+
// Narrow explicitly rather than casting: `safeResetCreditConsumeDto`
|
|
2562
|
+
// normalizes anything unrecognized to "unknown", and settling that
|
|
2563
|
+
// would come back as a mismatch and leave the row pending anyway.
|
|
2564
|
+
// Settlement failure never downgrades the user-visible outcome: the
|
|
2565
|
+
// spend already happened upstream, and reporting failure would invite
|
|
2566
|
+
// a manual retry -- the exact double-spend this unit removes.
|
|
2567
|
+
if (result.code === "reset" || result.code === "already_redeemed"
|
|
2568
|
+
|| result.code === "nothing_to_reset" || result.code === "no_credit") {
|
|
2569
|
+
settleManualResetCreditOperation(identity, result.code);
|
|
2570
|
+
} else {
|
|
2571
|
+
markManualResetCreditOperationAmbiguous(identity);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
2575
|
+
// and return remaining only when that refresh freshly parsed available_count.
|
|
2576
|
+
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|
|
2577
|
+
if (result.code === "reset" || result.code === "already_redeemed") {
|
|
2578
|
+
const freshResetCredits = await refreshAfterManualReset(
|
|
2579
|
+
config, accountId, auth, claims, result.code === "reset",
|
|
2580
|
+
);
|
|
2581
|
+
return jsonResponse({
|
|
2582
|
+
code: result.code,
|
|
2583
|
+
...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits)
|
|
2584
|
+
? { remaining: freshResetCredits }
|
|
2585
|
+
: {}),
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
return jsonResponse(result);
|
|
2589
|
+
} finally {
|
|
2590
|
+
// Release only this invocation's leases, including every ambiguous/error outcome.
|
|
2591
|
+
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, false);
|
|
2447
2592
|
}
|
|
2448
|
-
return jsonResponse(result);
|
|
2449
2593
|
});
|
|
2450
2594
|
return operation.ok ? operation.value : operation.response;
|
|
2451
2595
|
} catch (e) {
|
package/src/codex/features.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWri
|
|
|
38
38
|
import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl";
|
|
39
39
|
import { CODEX_CONFIG_PATH } from "./paths";
|
|
40
40
|
import { resolveAndPersistCodexRuntime } from "./runtime";
|
|
41
|
+
import { canonicalizeOpenCodexModeHint } from "./multi-agent-mode-policy";
|
|
41
42
|
|
|
42
43
|
/** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */
|
|
43
44
|
export const DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY = "default_mode_request_user_input";
|
|
@@ -1091,7 +1092,8 @@ export function setMultiAgentModeHintText(value: string | null, configPath?: str
|
|
|
1091
1092
|
};
|
|
1092
1093
|
}
|
|
1093
1094
|
}
|
|
1094
|
-
|
|
1095
|
+
const canonicalValue = value === null ? null : canonicalizeOpenCodexModeHint(value);
|
|
1096
|
+
return setV2StringField("multi_agent_mode_hint_text", canonicalValue, configPath);
|
|
1095
1097
|
}
|
|
1096
1098
|
|
|
1097
1099
|
export const MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES = 8;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const MULTI_AGENT_MODE_HINT_RECOMMENDATION = {
|
|
2
|
+
revision: "proactive-trigger-v1",
|
|
3
|
+
text: [
|
|
4
|
+
"Proactive multi-agent delegation is active.",
|
|
5
|
+
"Only the delegation trigger changes: a separate explicit request is no longer required.",
|
|
6
|
+
"All existing user, authority, task-scope, and collaboration-tool rules continue to apply.",
|
|
7
|
+
"Delegate eligible independent work when parallel execution could materially improve speed or quality.",
|
|
8
|
+
"User requests override this hint.",
|
|
9
|
+
"This mode remains active until a later multi-agent mode developer message changes it.",
|
|
10
|
+
].join(" "),
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/** Byte-exact presets previously written by OpenCodex dashboard releases. */
|
|
14
|
+
export const LEGACY_OPENCODEX_MODE_HINTS = [
|
|
15
|
+
"Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.",
|
|
16
|
+
"Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
/** Upgrade only known OpenCodex-owned values; user-authored variants stay byte-identical. */
|
|
20
|
+
export function canonicalizeOpenCodexModeHint(text: string): string {
|
|
21
|
+
return LEGACY_OPENCODEX_MODE_HINTS.some(legacy => legacy === text)
|
|
22
|
+
? MULTI_AGENT_MODE_HINT_RECOMMENDATION.text
|
|
23
|
+
: text;
|
|
24
|
+
}
|