codex-multi-auth 2.17.0 → 2.17.2
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/.codex-plugin/plugin.json +9 -9
- package/LICENSE +21 -21
- package/README.md +487 -484
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +31 -31
- package/config/codex-legacy.json +1221 -1221
- package/config/codex-modern.json +423 -423
- package/config/minimal-codex.json +15 -15
- package/dist/lib/accounts.d.ts +1 -7
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +3 -14
- package/dist/lib/codex-manager/commands/forecast.d.ts +1 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.js +2 -2
- package/dist/lib/codex-manager/health-check.js +1 -1
- package/dist/lib/codex-manager/login-menu-data.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-data.js +7 -3
- package/dist/lib/codex-manager/quota-cache-helpers.d.ts +7 -1
- package/dist/lib/codex-manager/quota-cache-helpers.d.ts.map +1 -1
- package/dist/lib/codex-manager/quota-cache-helpers.js +34 -19
- package/dist/lib/codex-manager/repair-commands.js +1 -1
- package/dist/lib/context-budget-response.js +8 -8
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.js +79 -79
- package/dist/lib/quota-cache.d.ts +7 -3
- package/dist/lib/quota-cache.d.ts.map +1 -1
- package/dist/lib/quota-cache.js +91 -59
- package/dist/lib/quota-readiness.d.ts +7 -2
- package/dist/lib/quota-readiness.d.ts.map +1 -1
- package/dist/lib/quota-readiness.js +25 -4
- package/dist/lib/runtime/app-bind.d.ts.map +1 -1
- package/dist/lib/runtime/app-bind.js +10 -2
- package/dist/lib/runtime/automatic-account-checks.d.ts +5 -1
- package/dist/lib/runtime/automatic-account-checks.d.ts.map +1 -1
- package/dist/lib/runtime/automatic-account-checks.js +29 -2
- package/dist/lib/runtime/automatic-subscription-checks.d.ts +1 -1
- package/dist/lib/runtime/automatic-subscription-checks.d.ts.map +1 -1
- package/dist/lib/runtime/automatic-subscription-checks.js +13 -16
- package/dist/lib/runtime-rotation-proxy.js +3 -3
- package/dist/lib/storage/pending-auth.d.ts +1 -0
- package/dist/lib/storage/pending-auth.d.ts.map +1 -1
- package/dist/lib/storage/pending-auth.js +4 -2
- package/dist/lib/storage/record-identity.d.ts +16 -0
- package/dist/lib/storage/record-identity.d.ts.map +1 -0
- package/dist/lib/storage/record-identity.js +16 -0
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
- package/node_modules/@codex-ai/plugin/package.json +9 -9
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
- package/node_modules/@codex-ai/sdk/package.json +8 -8
- package/package.json +200 -200
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +373 -373
- package/scripts/codex-bin-resolver.js +313 -313
- package/scripts/codex-multi-auth.js +59 -59
- package/scripts/codex-routing.js +62 -62
- package/scripts/codex.js +6642 -6642
- package/scripts/install-codex-auth-utils.js +213 -213
- package/scripts/mcodex.js +367 -367
- package/scripts/postinstall.js +114 -114
- package/scripts/preuninstall.js +265 -265
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
- package/vendor/codex-ai-plugin/dist/index.js +2 -2
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
- package/vendor/codex-ai-plugin/dist/tool.js +29 -29
- package/vendor/codex-ai-plugin/package.json +9 -9
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
- package/vendor/codex-ai-sdk/dist/index.js +2 -2
- package/vendor/codex-ai-sdk/package.json +8 -8
|
@@ -381,83 +381,83 @@ export function prewarmCodexInstructions(models = []) {
|
|
|
381
381
|
/**
|
|
382
382
|
* Tool remapping instructions for host runtime tools
|
|
383
383
|
*/
|
|
384
|
-
export const TOOL_REMAP_MESSAGE = `<user_instructions priority="0">
|
|
385
|
-
<environment_override priority="0">
|
|
386
|
-
YOU ARE IN A DIFFERENT ENVIRONMENT. These instructions override ALL previous tool references.
|
|
387
|
-
</environment_override>
|
|
388
|
-
|
|
389
|
-
<tool_replacements priority="0">
|
|
390
|
-
<critical_rule priority="0">
|
|
391
|
-
apply_patch/applyPatch are Codex names, but host tool names vary by version.
|
|
392
|
-
- Inspect the actual tool list before editing.
|
|
393
|
-
- If edit exists: use edit for precise in-place string replacements and hashline edits.
|
|
394
|
-
- If edit is absent and apply_patch exists: use apply_patch for those precise/hashline edits in this plugin.
|
|
395
|
-
- For diff-style or multi-line structural edits: use patch if available, otherwise use apply_patch.
|
|
396
|
-
- In this plugin, edit/apply_patch support hashline refs (lineRef, endLineRef, operation, content)
|
|
397
|
-
</critical_rule>
|
|
398
|
-
|
|
399
|
-
<critical_rule priority="0">
|
|
400
|
-
UPDATE_PLAN DOES NOT EXIST -> USE "todowrite" INSTEAD
|
|
401
|
-
- NEVER use: update_plan, updatePlan
|
|
402
|
-
- ALWAYS use: todowrite for ALL task/plan operations
|
|
403
|
-
- Use todoread to read current plan
|
|
404
|
-
- Before plan operations: Verify you're using "todowrite", NOT "update_plan"
|
|
405
|
-
</critical_rule>
|
|
406
|
-
</tool_replacements>
|
|
407
|
-
|
|
408
|
-
<available_tools priority="0">
|
|
409
|
-
File Operations:
|
|
410
|
-
- write - Create new files (if available)
|
|
411
|
-
- edit - Modify existing files with string replacement (version-dependent)
|
|
412
|
-
- oldString must be literal text from the current file; never pass unresolved placeholders like \${TARGET_SNIPPET}
|
|
413
|
-
- apply_patch - May be the edit/patch tool name in newer host builds (version-dependent)
|
|
414
|
-
- hashline_read - Read lines with hashline refs (L<line>#<hash>) for deterministic edits
|
|
415
|
-
- patch - Apply diff patches (version-dependent)
|
|
416
|
-
- read - Read file contents
|
|
417
|
-
|
|
418
|
-
Search/Discovery:
|
|
419
|
-
- grep - Search file contents
|
|
420
|
-
- glob - Find files by pattern
|
|
421
|
-
- list - List directories (if available)
|
|
422
|
-
|
|
423
|
-
Execution:
|
|
424
|
-
- bash - Run shell commands
|
|
425
|
-
|
|
426
|
-
Network:
|
|
427
|
-
- webfetch - Fetch web content
|
|
428
|
-
|
|
429
|
-
Task Management:
|
|
430
|
-
- todowrite - Manage tasks/plans (REPLACES update_plan)
|
|
431
|
-
- todoread - Read current plan
|
|
432
|
-
</available_tools>
|
|
433
|
-
|
|
434
|
-
<substitution_rules priority="0">
|
|
435
|
-
Base instruction says: You MUST use instead:
|
|
436
|
-
apply_patch -> patch (preferred if available), otherwise edit/apply_patch based on actual tool list
|
|
437
|
-
update_plan -> todowrite
|
|
438
|
-
read_plan -> todoread
|
|
439
|
-
absolute paths -> relative paths
|
|
440
|
-
</substitution_rules>
|
|
441
|
-
|
|
442
|
-
<verification_checklist priority="0">
|
|
443
|
-
Before file/plan modifications:
|
|
444
|
-
1. Am I using the actual available edit tool name (edit, patch, or apply_patch)?
|
|
445
|
-
2. Am I using "todowrite" NOT "update_plan"?
|
|
446
|
-
3. Is this tool in the approved list above?
|
|
447
|
-
4. Am I using relative paths?
|
|
448
|
-
|
|
449
|
-
If ANY answer is NO -> STOP and correct before proceeding.
|
|
450
|
-
</verification_checklist>
|
|
451
|
-
|
|
452
|
-
<hashline_workflow priority="0">
|
|
453
|
-
When hashline_read is available:
|
|
454
|
-
1. call hashline_read on the file
|
|
455
|
-
2. edit/apply_patch using lineRef/endLineRef + operation + content
|
|
456
|
-
3. use oldString/newString only as fallback, and only with literal file text (no template placeholders)
|
|
457
|
-
</hashline_workflow>
|
|
458
|
-
|
|
459
|
-
<safety_rules priority="0">
|
|
460
|
-
- Never run destructive git commands (\`git reset --hard\`, \`git checkout --\`) unless explicitly requested by the user.
|
|
461
|
-
- Never call \`request_user_input\` unless collaboration mode is explicitly Plan mode.
|
|
462
|
-
</safety_rules>
|
|
384
|
+
export const TOOL_REMAP_MESSAGE = `<user_instructions priority="0">
|
|
385
|
+
<environment_override priority="0">
|
|
386
|
+
YOU ARE IN A DIFFERENT ENVIRONMENT. These instructions override ALL previous tool references.
|
|
387
|
+
</environment_override>
|
|
388
|
+
|
|
389
|
+
<tool_replacements priority="0">
|
|
390
|
+
<critical_rule priority="0">
|
|
391
|
+
apply_patch/applyPatch are Codex names, but host tool names vary by version.
|
|
392
|
+
- Inspect the actual tool list before editing.
|
|
393
|
+
- If edit exists: use edit for precise in-place string replacements and hashline edits.
|
|
394
|
+
- If edit is absent and apply_patch exists: use apply_patch for those precise/hashline edits in this plugin.
|
|
395
|
+
- For diff-style or multi-line structural edits: use patch if available, otherwise use apply_patch.
|
|
396
|
+
- In this plugin, edit/apply_patch support hashline refs (lineRef, endLineRef, operation, content)
|
|
397
|
+
</critical_rule>
|
|
398
|
+
|
|
399
|
+
<critical_rule priority="0">
|
|
400
|
+
UPDATE_PLAN DOES NOT EXIST -> USE "todowrite" INSTEAD
|
|
401
|
+
- NEVER use: update_plan, updatePlan
|
|
402
|
+
- ALWAYS use: todowrite for ALL task/plan operations
|
|
403
|
+
- Use todoread to read current plan
|
|
404
|
+
- Before plan operations: Verify you're using "todowrite", NOT "update_plan"
|
|
405
|
+
</critical_rule>
|
|
406
|
+
</tool_replacements>
|
|
407
|
+
|
|
408
|
+
<available_tools priority="0">
|
|
409
|
+
File Operations:
|
|
410
|
+
- write - Create new files (if available)
|
|
411
|
+
- edit - Modify existing files with string replacement (version-dependent)
|
|
412
|
+
- oldString must be literal text from the current file; never pass unresolved placeholders like \${TARGET_SNIPPET}
|
|
413
|
+
- apply_patch - May be the edit/patch tool name in newer host builds (version-dependent)
|
|
414
|
+
- hashline_read - Read lines with hashline refs (L<line>#<hash>) for deterministic edits
|
|
415
|
+
- patch - Apply diff patches (version-dependent)
|
|
416
|
+
- read - Read file contents
|
|
417
|
+
|
|
418
|
+
Search/Discovery:
|
|
419
|
+
- grep - Search file contents
|
|
420
|
+
- glob - Find files by pattern
|
|
421
|
+
- list - List directories (if available)
|
|
422
|
+
|
|
423
|
+
Execution:
|
|
424
|
+
- bash - Run shell commands
|
|
425
|
+
|
|
426
|
+
Network:
|
|
427
|
+
- webfetch - Fetch web content
|
|
428
|
+
|
|
429
|
+
Task Management:
|
|
430
|
+
- todowrite - Manage tasks/plans (REPLACES update_plan)
|
|
431
|
+
- todoread - Read current plan
|
|
432
|
+
</available_tools>
|
|
433
|
+
|
|
434
|
+
<substitution_rules priority="0">
|
|
435
|
+
Base instruction says: You MUST use instead:
|
|
436
|
+
apply_patch -> patch (preferred if available), otherwise edit/apply_patch based on actual tool list
|
|
437
|
+
update_plan -> todowrite
|
|
438
|
+
read_plan -> todoread
|
|
439
|
+
absolute paths -> relative paths
|
|
440
|
+
</substitution_rules>
|
|
441
|
+
|
|
442
|
+
<verification_checklist priority="0">
|
|
443
|
+
Before file/plan modifications:
|
|
444
|
+
1. Am I using the actual available edit tool name (edit, patch, or apply_patch)?
|
|
445
|
+
2. Am I using "todowrite" NOT "update_plan"?
|
|
446
|
+
3. Is this tool in the approved list above?
|
|
447
|
+
4. Am I using relative paths?
|
|
448
|
+
|
|
449
|
+
If ANY answer is NO -> STOP and correct before proceeding.
|
|
450
|
+
</verification_checklist>
|
|
451
|
+
|
|
452
|
+
<hashline_workflow priority="0">
|
|
453
|
+
When hashline_read is available:
|
|
454
|
+
1. call hashline_read on the file
|
|
455
|
+
2. edit/apply_patch using lineRef/endLineRef + operation + content
|
|
456
|
+
3. use oldString/newString only as fallback, and only with literal file text (no template placeholders)
|
|
457
|
+
</hashline_workflow>
|
|
458
|
+
|
|
459
|
+
<safety_rules priority="0">
|
|
460
|
+
- Never run destructive git commands (\`git reset --hard\`, \`git checkout --\`) unless explicitly requested by the user.
|
|
461
|
+
- Never call \`request_user_input\` unless collaboration mode is explicitly Plan mode.
|
|
462
|
+
</safety_rules>
|
|
463
463
|
</user_instructions>`;
|
|
@@ -14,6 +14,8 @@ export interface QuotaCacheEntry {
|
|
|
14
14
|
export interface QuotaCacheData {
|
|
15
15
|
byAccountId: Record<string, QuotaCacheEntry>;
|
|
16
16
|
byEmail: Record<string, QuotaCacheEntry>;
|
|
17
|
+
/** Quota scoped to a saved credential record and exact workspace. */
|
|
18
|
+
byWorkspace?: Record<string, QuotaCacheEntry>;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Get the absolute filesystem path to the quota-cache.json file.
|
|
@@ -52,8 +54,9 @@ export declare function loadQuotaCache(): Promise<QuotaCacheData>;
|
|
|
52
54
|
* quota cache path. Failures are logged and do not throw, so callers should handle
|
|
53
55
|
* eventual consistency or retry as needed.
|
|
54
56
|
*
|
|
55
|
-
* Concurrency:
|
|
56
|
-
*
|
|
57
|
+
* Concurrency: reload, apply changes since `baseline`, and atomically write under
|
|
58
|
+
* a cross-process lock. Pass the pre-edit snapshot for read–modify–write callers.
|
|
59
|
+
* Without a baseline, supplied entries are upserts; omitted entries are retained.
|
|
57
60
|
*
|
|
58
61
|
* Filesystem notes: Windows path length, permissions, or antivirus locks may cause
|
|
59
62
|
* write failures; such errors are logged rather than thrown.
|
|
@@ -63,6 +66,7 @@ export declare function loadQuotaCache(): Promise<QuotaCacheData>;
|
|
|
63
66
|
*
|
|
64
67
|
* @param data - The quota cache data (byAccountId and byEmail maps) to persist; callers
|
|
65
68
|
* should pass normalized QuotaCacheData.
|
|
69
|
+
* @param baseline - The unmodified pre-edit snapshot; only changes since it are applied.
|
|
66
70
|
*/
|
|
67
|
-
export declare function saveQuotaCache(data: QuotaCacheData): Promise<void>;
|
|
71
|
+
export declare function saveQuotaCache(data: QuotaCacheData, baseline?: QuotaCacheData): Promise<void>;
|
|
68
72
|
//# sourceMappingURL=quota-cache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quota-cache.d.ts","sourceRoot":"","sources":["../../lib/quota-cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"quota-cache.d.ts","sourceRoot":"","sources":["../../lib/quota-cache.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,gBAAgB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC9C;AAwGD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,cAAc,CAAC,CAO9D;AA2CD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,GAAE,cAAiD,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DrI"}
|
package/dist/lib/quota-cache.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { withRetry } from "./fs-retry.js";
|
|
4
4
|
import { logWarn } from "./logger.js";
|
|
5
5
|
import { getCodexMultiAuthDir } from "./runtime-paths.js";
|
|
6
6
|
import { tempPathFor } from "./temp-path.js";
|
|
7
|
+
import { withFileTransactionLock } from "./storage/file-lock.js";
|
|
7
8
|
import { isRecord } from "./utils.js";
|
|
8
9
|
const QUOTA_CACHE_PATH = join(getCodexMultiAuthDir(), "quota-cache.json");
|
|
9
10
|
const QUOTA_CACHE_LABEL = basename(QUOTA_CACHE_PATH);
|
|
@@ -127,29 +128,57 @@ export function getQuotaCachePath() {
|
|
|
127
128
|
* will be empty if the on-disk file is absent, malformed, or could not be read.
|
|
128
129
|
*/
|
|
129
130
|
export async function loadQuotaCache() {
|
|
130
|
-
if (!existsSync(QUOTA_CACHE_PATH)) {
|
|
131
|
-
return { byAccountId: {}, byEmail: {} };
|
|
132
|
-
}
|
|
133
131
|
try {
|
|
134
|
-
|
|
135
|
-
const parsed = JSON.parse(content);
|
|
136
|
-
if (!isRecord(parsed)) {
|
|
137
|
-
return { byAccountId: {}, byEmail: {} };
|
|
138
|
-
}
|
|
139
|
-
if (parsed.version !== 1) {
|
|
140
|
-
logWarn(`Quota cache rejected due to version mismatch: ${String(parsed.version)}`);
|
|
141
|
-
return { byAccountId: {}, byEmail: {} };
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
byAccountId: normalizeEntryMap(parsed.byAccountId),
|
|
145
|
-
byEmail: normalizeEntryMap(parsed.byEmail),
|
|
146
|
-
};
|
|
132
|
+
return await readQuotaCache();
|
|
147
133
|
}
|
|
148
134
|
catch (error) {
|
|
149
135
|
logWarn(`Failed to load quota cache from ${QUOTA_CACHE_LABEL}: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
136
|
return { byAccountId: {}, byEmail: {} };
|
|
151
137
|
}
|
|
152
138
|
}
|
|
139
|
+
/** Only a missing file is empty for writers; read failures must never erase concurrent data. */
|
|
140
|
+
async function readQuotaCache() {
|
|
141
|
+
let content;
|
|
142
|
+
try {
|
|
143
|
+
content = await readCacheFileWithRetry(QUOTA_CACHE_PATH);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error.code === "ENOENT")
|
|
147
|
+
return { byAccountId: {}, byEmail: {} };
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
const parsed = JSON.parse(content);
|
|
151
|
+
if (!isRecord(parsed) || parsed.version !== 1) {
|
|
152
|
+
throw Error("Quota cache rejected due to version mismatch or invalid payload");
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
byAccountId: normalizeEntryMap(parsed.byAccountId),
|
|
156
|
+
byEmail: normalizeEntryMap(parsed.byEmail),
|
|
157
|
+
...(parsed.byWorkspace ? { byWorkspace: normalizeEntryMap(parsed.byWorkspace) } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** Reapply this run's changed entries, preserving unrelated or newer concurrent observations. */
|
|
161
|
+
function mergeQuotaChanges(current, proposed, baseline) {
|
|
162
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
163
|
+
for (const namespace of ["byAccountId", "byEmail", "byWorkspace"]) {
|
|
164
|
+
const before = baseline[namespace] ?? {}, next = proposed[namespace] ?? {};
|
|
165
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(next)])) {
|
|
166
|
+
if (same(before[key], next[key]))
|
|
167
|
+
continue;
|
|
168
|
+
const latest = current[namespace]?.[key], update = next[key];
|
|
169
|
+
if (update) {
|
|
170
|
+
if (!latest || update.updatedAt >= latest.updatedAt) {
|
|
171
|
+
current[namespace] ??= {};
|
|
172
|
+
current[namespace][key] = update;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (same(latest, before[key])) {
|
|
176
|
+
delete current[namespace]?.[key];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return current;
|
|
181
|
+
}
|
|
153
182
|
/**
|
|
154
183
|
* Persist the quota cache to the on-disk JSON file used by the multi-auth runtime.
|
|
155
184
|
*
|
|
@@ -157,8 +186,9 @@ export async function loadQuotaCache() {
|
|
|
157
186
|
* quota cache path. Failures are logged and do not throw, so callers should handle
|
|
158
187
|
* eventual consistency or retry as needed.
|
|
159
188
|
*
|
|
160
|
-
* Concurrency:
|
|
161
|
-
*
|
|
189
|
+
* Concurrency: reload, apply changes since `baseline`, and atomically write under
|
|
190
|
+
* a cross-process lock. Pass the pre-edit snapshot for read–modify–write callers.
|
|
191
|
+
* Without a baseline, supplied entries are upserts; omitted entries are retained.
|
|
162
192
|
*
|
|
163
193
|
* Filesystem notes: Windows path length, permissions, or antivirus locks may cause
|
|
164
194
|
* write failures; such errors are logged rather than thrown.
|
|
@@ -168,54 +198,56 @@ export async function loadQuotaCache() {
|
|
|
168
198
|
*
|
|
169
199
|
* @param data - The quota cache data (byAccountId and byEmail maps) to persist; callers
|
|
170
200
|
* should pass normalized QuotaCacheData.
|
|
201
|
+
* @param baseline - The unmodified pre-edit snapshot; only changes since it are applied.
|
|
171
202
|
*/
|
|
172
|
-
export async function saveQuotaCache(data) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
byAccountId: data.byAccountId,
|
|
176
|
-
byEmail: data.byEmail,
|
|
177
|
-
};
|
|
203
|
+
export async function saveQuotaCache(data, baseline = { byAccountId: {}, byEmail: {} }) {
|
|
204
|
+
// Snapshot at invocation, not after waiting behind other writers.
|
|
205
|
+
const proposed = structuredClone(data), before = structuredClone(baseline);
|
|
178
206
|
const writeTask = async () => {
|
|
179
207
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
catch {
|
|
193
|
-
// Best-effort hardening only.
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const tempPath = tempPathFor(QUOTA_CACHE_PATH);
|
|
197
|
-
await fs.writeFile(tempPath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
198
|
-
encoding: "utf8",
|
|
199
|
-
mode: 0o600,
|
|
200
|
-
});
|
|
201
|
-
let renamed = false;
|
|
202
|
-
try {
|
|
203
|
-
await withRetry(() => fs.rename(tempPath, QUOTA_CACHE_PATH), {
|
|
204
|
-
maxAttempts: 5,
|
|
205
|
-
backoffMs: (attempt) => 10 * 2 ** (attempt - 1),
|
|
206
|
-
});
|
|
207
|
-
renamed = true;
|
|
208
|
-
}
|
|
209
|
-
finally {
|
|
210
|
-
if (!renamed) {
|
|
208
|
+
await withFileTransactionLock(QUOTA_CACHE_PATH, async () => {
|
|
209
|
+
const merged = mergeQuotaChanges(await readQuotaCache(), proposed, before);
|
|
210
|
+
const payload = { version: 1, ...merged };
|
|
211
|
+
const cacheDir = getCodexMultiAuthDir();
|
|
212
|
+
// The quota cache lives alongside other at-rest secrets, so keep the
|
|
213
|
+
// directory owner-only on POSIX (mode is a no-op on win32 / ACL-based).
|
|
214
|
+
await fs.mkdir(cacheDir, { recursive: true, mode: 0o700 });
|
|
215
|
+
// mkdir's mode only applies to a freshly-created dir; an upgrade with a
|
|
216
|
+
// pre-existing multi-auth dir keeps its old (possibly world-listable)
|
|
217
|
+
// perms, so re-assert 0o700 on POSIX. Best-effort: a chmod failure must
|
|
218
|
+
// not break the cache write (the 0o600 file below still protects data).
|
|
219
|
+
if (process.platform !== "win32") {
|
|
211
220
|
try {
|
|
212
|
-
await fs.
|
|
221
|
+
await fs.chmod(cacheDir, 0o700);
|
|
213
222
|
}
|
|
214
223
|
catch {
|
|
215
|
-
// Best
|
|
224
|
+
// Best-effort hardening only.
|
|
216
225
|
}
|
|
217
226
|
}
|
|
218
|
-
|
|
227
|
+
const tempPath = tempPathFor(QUOTA_CACHE_PATH);
|
|
228
|
+
await fs.writeFile(tempPath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
mode: 0o600,
|
|
231
|
+
});
|
|
232
|
+
let renamed = false;
|
|
233
|
+
try {
|
|
234
|
+
await withRetry(() => fs.rename(tempPath, QUOTA_CACHE_PATH), {
|
|
235
|
+
maxAttempts: 5,
|
|
236
|
+
backoffMs: (attempt) => 10 * 2 ** (attempt - 1),
|
|
237
|
+
});
|
|
238
|
+
renamed = true;
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
if (!renamed) {
|
|
242
|
+
try {
|
|
243
|
+
await fs.unlink(tempPath);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Best effort temp cleanup.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
219
251
|
}
|
|
220
252
|
catch (error) {
|
|
221
253
|
logWarn(`Failed to save quota cache to ${QUOTA_CACHE_LABEL}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuotaCacheData, QuotaCacheEntry } from "./quota-cache.js";
|
|
2
2
|
import type { AccountMetadataV3 } from "./storage.js";
|
|
3
|
-
export type QuotaCacheAccountRef = Pick<AccountMetadataV3, "accountId" | "email"
|
|
3
|
+
export type QuotaCacheAccountRef = Pick<AccountMetadataV3, "accountId" | "email" | "recordId" | "workspaces" | "currentWorkspaceIndex"> & Partial<Pick<AccountMetadataV3, "refreshToken" | "addedAt">>;
|
|
4
4
|
export declare function normalizeQuotaAccountId(value: string | undefined): string | null;
|
|
5
5
|
export declare function normalizeQuotaEmail(value: string | undefined): string | null;
|
|
6
6
|
export declare function hasUniqueQuotaAccountId(accounts: readonly QuotaCacheAccountRef[], account: QuotaCacheAccountRef): boolean;
|
|
@@ -15,5 +15,10 @@ export declare function quotaUsedPercentIsExhausted(usedPercent: number | undefi
|
|
|
15
15
|
export declare function isQuotaCacheEntryExhausted(entry: Pick<QuotaCacheEntry, "primary" | "secondary"> & {
|
|
16
16
|
updatedAt?: number;
|
|
17
17
|
} | null | undefined, now?: number): boolean;
|
|
18
|
-
|
|
18
|
+
/** Cache identity includes the credential record, so shared organization IDs cannot collide. */
|
|
19
|
+
export declare function quotaWorkspaceKey(account: QuotaCacheAccountRef, workspaceId: string): string | undefined;
|
|
20
|
+
/** Display the selected workspace; callers routing another scope must pass that exact workspace. */
|
|
21
|
+
export declare function quotaWorkspaceId(account: QuotaCacheAccountRef): string | undefined;
|
|
22
|
+
/** Read exact-workspace quota, using legacy binding entries only for that binding. */
|
|
23
|
+
export declare function findQuotaCacheEntryForAccount(cache: QuotaCacheData | null | undefined, account: QuotaCacheAccountRef, accounts: readonly QuotaCacheAccountRef[], emailFallbackState?: ReadonlyMap<string, QuotaEmailFallbackState>, workspaceId?: string | undefined): QuotaCacheEntry | null;
|
|
19
24
|
//# sourceMappingURL=quota-readiness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quota-readiness.d.ts","sourceRoot":"","sources":["../../lib/quota-readiness.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"quota-readiness.d.ts","sourceRoot":"","sources":["../../lib/quota-readiness.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAoB,MAAM,kBAAkB,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,uBAAuB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,cAAc,GAAG,SAAS,CAAC,CAAC,CAAC;AAIvM,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAGhF;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAG5E;AAED,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EACzC,OAAO,EAAE,oBAAoB,GAC3B,OAAO,CAQT;AAED,MAAM,MAAM,uBAAuB,GAAG;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAChC,CAAC;AAEF,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,SAAS,oBAAoB,EAAE,GACvC,WAAW,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAiB9C;AAED,wBAAgB,yBAAyB,CACxC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAChE,OAAO,EAAE,oBAAoB,GAC3B,OAAO,CAMT;AAED,wBAAgB,wBAAwB,CACvC,WAAW,EAAE,MAAM,GAAG,SAAS,GAC7B,MAAM,GAAG,SAAS,CAKpB;AAQD,wBAAgB,2BAA2B,CAC1C,WAAW,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAMT;AA2BD,wBAAgB,0BAA0B,CACzC,KAAK,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,WAAW,CAAC,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACjG,GAAG,SAAa,GACd,OAAO,CAQT;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIxG;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAIlF;AAED,sFAAsF;AACtF,wBAAgB,6BAA6B,CAC5C,KAAK,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EACxC,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EACzC,kBAAkB,+CAAyC,EAC3D,WAAW,qBAA4B,GACrC,eAAe,GAAG,IAAI,CAgBxB"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { resolveAccountRecordId } from "./storage/record-identity.js";
|
|
1
3
|
export function normalizeQuotaAccountId(value) {
|
|
2
4
|
const trimmed = value?.trim();
|
|
3
5
|
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
@@ -89,16 +91,35 @@ export function isQuotaCacheEntryExhausted(entry, now = Date.now()) {
|
|
|
89
91
|
return (quotaWindowIsExhausted(entry?.primary, now, updatedAt) ||
|
|
90
92
|
quotaWindowIsExhausted(entry?.secondary, now, updatedAt));
|
|
91
93
|
}
|
|
92
|
-
|
|
94
|
+
/** Cache identity includes the credential record, so shared organization IDs cannot collide. */
|
|
95
|
+
export function quotaWorkspaceKey(account, workspaceId) {
|
|
96
|
+
const recordId = account.recordId?.trim() || (account.refreshToken !== undefined && account.addedAt !== undefined
|
|
97
|
+
? resolveAccountRecordId({ ...account, refreshToken: account.refreshToken, addedAt: account.addedAt }) : undefined);
|
|
98
|
+
return recordId ? createHash("sha256").update(JSON.stringify(["oauth", recordId, workspaceId.trim()])).digest("hex") : undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Display the selected workspace; callers routing another scope must pass that exact workspace. */
|
|
101
|
+
export function quotaWorkspaceId(account) {
|
|
102
|
+
return account.workspaces?.length
|
|
103
|
+
? account.workspaces[account.currentWorkspaceIndex ?? 0]?.id.trim()
|
|
104
|
+
: account.accountId?.trim();
|
|
105
|
+
}
|
|
106
|
+
/** Read exact-workspace quota, using legacy binding entries only for that binding. */
|
|
107
|
+
export function findQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts), workspaceId = quotaWorkspaceId(account)) {
|
|
93
108
|
if (!cache)
|
|
94
109
|
return null;
|
|
110
|
+
const key = workspaceId && quotaWorkspaceKey(account, workspaceId);
|
|
111
|
+
const scoped = key ? cache.byWorkspace?.[key] : undefined;
|
|
112
|
+
// Legacy entries describe only the stored binding, never a sibling workspace.
|
|
113
|
+
if (normalizeQuotaAccountId(workspaceId) !== normalizeQuotaAccountId(account.accountId))
|
|
114
|
+
return scoped ?? null;
|
|
95
115
|
const accountId = normalizeQuotaAccountId(account.accountId);
|
|
116
|
+
const newer = (legacy) => scoped && (!legacy || scoped.updatedAt >= legacy.updatedAt) ? scoped : legacy ?? null;
|
|
96
117
|
if (accountId && hasUniqueQuotaAccountId(accounts, account) && cache.byAccountId[accountId]) {
|
|
97
|
-
return cache.byAccountId[accountId]
|
|
118
|
+
return newer(cache.byAccountId[accountId]);
|
|
98
119
|
}
|
|
99
120
|
const email = normalizeQuotaEmail(account.email);
|
|
100
121
|
if (email && hasSafeQuotaEmailFallback(emailFallbackState, account) && cache.byEmail[email]) {
|
|
101
|
-
return cache.byEmail[email]
|
|
122
|
+
return newer(cache.byEmail[email]);
|
|
102
123
|
}
|
|
103
|
-
return null;
|
|
124
|
+
return scoped ?? null;
|
|
104
125
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-bind.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-bind.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"app-bind.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-bind.ts"],"names":[],"mappings":"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"}
|
|
@@ -570,6 +570,13 @@ async function runWindowsTaskkill(pid) {
|
|
|
570
570
|
}
|
|
571
571
|
});
|
|
572
572
|
}
|
|
573
|
+
// `ps -o lstart=` is locale-sensitive: under a non-English LC_TIME it prints
|
|
574
|
+
// localized day/month names that parsePosixProcessStartTime cannot read, so
|
|
575
|
+
// every POSIX identity probe would fail and unbind would leak live routers.
|
|
576
|
+
const POSIX_IDENTITY_PROBE_ENV = {
|
|
577
|
+
...process.env,
|
|
578
|
+
LC_ALL: "C",
|
|
579
|
+
};
|
|
573
580
|
async function runProcessIdentityProbe(command, args, options = {}) {
|
|
574
581
|
return new Promise((resolve) => {
|
|
575
582
|
let output = "";
|
|
@@ -601,6 +608,7 @@ async function runProcessIdentityProbe(command, args, options = {}) {
|
|
|
601
608
|
child = spawn(command, args, {
|
|
602
609
|
stdio: ["ignore", "pipe", "ignore"],
|
|
603
610
|
windowsHide: true,
|
|
611
|
+
...(options.env ? { env: options.env } : {}),
|
|
604
612
|
});
|
|
605
613
|
child.stdout?.setEncoding("utf8");
|
|
606
614
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -649,8 +657,8 @@ async function readProcessIdentity(pid, platform, log) {
|
|
|
649
657
|
};
|
|
650
658
|
}
|
|
651
659
|
const [startedAtRaw, commandLineRaw] = await Promise.all([
|
|
652
|
-
runProcessIdentityProbe("ps", ["-p", String(pid), "-o", "lstart="], { log }),
|
|
653
|
-
runProcessIdentityProbe("ps", ["-p", String(pid), "-o", "command="], { log }),
|
|
660
|
+
runProcessIdentityProbe("ps", ["-p", String(pid), "-o", "lstart="], { env: POSIX_IDENTITY_PROBE_ENV, log }),
|
|
661
|
+
runProcessIdentityProbe("ps", ["-p", String(pid), "-o", "command="], { env: POSIX_IDENTITY_PROBE_ENV, log }),
|
|
654
662
|
]);
|
|
655
663
|
if (!startedAtRaw || !commandLineRaw) {
|
|
656
664
|
log?.(`Process identity probe returned no POSIX record for PID ${pid}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AccountPolicyStore } from "../account-policy.js";
|
|
2
|
-
import type { AccountStorageV3 } from "../storage.js";
|
|
2
|
+
import type { AccountMetadataV3, AccountStorageV3 } from "../storage.js";
|
|
3
3
|
export declare const AUTOMATIC_CHECK_INTERVAL_MS: number;
|
|
4
4
|
/** First tick after router start, so a short CLI session still gets its check. */
|
|
5
5
|
export declare const AUTOMATIC_CHECK_INITIAL_DELAY_MS = 5000;
|
|
@@ -11,6 +11,10 @@ export interface AutomaticAccountCheckOptions {
|
|
|
11
11
|
now?: () => number;
|
|
12
12
|
signal?: AbortSignal;
|
|
13
13
|
}
|
|
14
|
+
/** Honor the saved selection; a disabled/invalid selection must not fall back to a sibling. */
|
|
15
|
+
export declare function automaticCheckWorkspaceId(account: AccountMetadataV3): string | undefined;
|
|
16
|
+
/** Policy keys can be shared by organization members; attempt limits must never be. */
|
|
17
|
+
export declare function automaticAccountCheckKey(account: AccountMetadataV3): string;
|
|
14
18
|
/** A durable attempt precedes network I/O, so failures and competing routers cannot double-prime. */
|
|
15
19
|
export declare function runAutomaticAccountChecks(options: AutomaticAccountCheckOptions): Promise<void>;
|
|
16
20
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automatic-account-checks.d.ts","sourceRoot":"","sources":["../../../lib/runtime/automatic-account-checks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"automatic-account-checks.d.ts","sourceRoot":"","sources":["../../../lib/runtime/automatic-account-checks.ts"],"names":[],"mappings":"AAKA,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAKzE,eAAO,MAAM,2BAA2B,QAAa,CAAC;AACtD,kFAAkF;AAClF,eAAO,MAAM,gCAAgC,OAAO,CAAC;AAKrD,MAAM,WAAW,4BAA4B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACrD,YAAY,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAChD,KAAK,EAAE,CAAC,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AACD,+FAA+F;AAC/F,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAMxF;AAED,uFAAuF;AACvF,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAE3E;AAkCD,qGAAqG;AACrG,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCpG;AACD;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC;;EAwBtF"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { promises as fs } from "node:fs";
|
|
2
3
|
import { z } from "zod";
|
|
4
|
+
import { resolveAccountRecordId } from "../accounts.js";
|
|
5
|
+
import { extractAccountId } from "../auth/token-utils.js";
|
|
3
6
|
import { getAccountPolicyKey } from "../account-policy.js";
|
|
4
7
|
import { withFileTransactionLock } from "../storage/file-lock.js";
|
|
5
8
|
import { withRetry } from "../fs-retry.js";
|
|
@@ -12,6 +15,25 @@ export const AUTOMATIC_CHECK_INITIAL_DELAY_MS = 5000;
|
|
|
12
15
|
const FUTURE_SKEW_MS = 5 * 60000;
|
|
13
16
|
const schema = z.record(z.string().regex(/^sha256:[a-f0-9]{64}$/), z.number().finite().nonnegative());
|
|
14
17
|
const retry = { maxAttempts: 6, backoffMs: 25 };
|
|
18
|
+
/** Honor the saved selection; a disabled/invalid selection must not fall back to a sibling. */
|
|
19
|
+
export function automaticCheckWorkspaceId(account) {
|
|
20
|
+
if (account.workspaces?.length) {
|
|
21
|
+
const selected = account.workspaces[account.currentWorkspaceIndex ?? 0];
|
|
22
|
+
return selected?.enabled === false ? undefined : selected?.id.trim() || undefined;
|
|
23
|
+
}
|
|
24
|
+
return account.accountId?.trim() || extractAccountId(account.accessToken) || undefined;
|
|
25
|
+
}
|
|
26
|
+
/** Policy keys can be shared by organization members; attempt limits must never be. */
|
|
27
|
+
export function automaticAccountCheckKey(account) {
|
|
28
|
+
return workspaceAttemptKey(account, automaticCheckWorkspaceId(account) ?? "");
|
|
29
|
+
}
|
|
30
|
+
/** Build the same key for inactive workspaces while pruning the attempt journal. */
|
|
31
|
+
function workspaceAttemptKey(account, workspaceId) {
|
|
32
|
+
return `sha256:${createHash("sha256").update(JSON.stringify([
|
|
33
|
+
"automatic-check-v2", resolveAccountRecordId(account), workspaceId,
|
|
34
|
+
])).digest("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
/** Reject unreadable or oversized history rather than silently allowing duplicate probes. */
|
|
15
37
|
async function readAttempts(path) {
|
|
16
38
|
try {
|
|
17
39
|
const raw = await withRetry(() => fs.readFile(path, "utf8"), retry);
|
|
@@ -25,6 +47,7 @@ async function readAttempts(path) {
|
|
|
25
47
|
throw error;
|
|
26
48
|
}
|
|
27
49
|
}
|
|
50
|
+
/** Atomically persist attempt timestamps before any billable network request. */
|
|
28
51
|
async function saveAttempts(path, attempts) {
|
|
29
52
|
const temp = tempPathFor(path);
|
|
30
53
|
try {
|
|
@@ -46,7 +69,11 @@ export async function runAutomaticAccountChecks(options) {
|
|
|
46
69
|
if (!storage || signal.aborted)
|
|
47
70
|
return;
|
|
48
71
|
const attempts = await readAttempts(options.path);
|
|
49
|
-
|
|
72
|
+
// Keep every still-saved workspace's throttle, including temporarily disabled selections.
|
|
73
|
+
const keys = new Set(storage.accounts.flatMap(account => [
|
|
74
|
+
automaticAccountCheckKey(account),
|
|
75
|
+
...(account.workspaces ?? []).map(workspace => workspaceAttemptKey(account, workspace.id.trim())),
|
|
76
|
+
]));
|
|
50
77
|
for (const key of Object.keys(attempts))
|
|
51
78
|
if (!keys.has(key))
|
|
52
79
|
delete attempts[key];
|
|
@@ -56,7 +83,7 @@ export async function runAutomaticAccountChecks(options) {
|
|
|
56
83
|
const account = storage.accounts[index];
|
|
57
84
|
if (!account)
|
|
58
85
|
continue;
|
|
59
|
-
const key =
|
|
86
|
+
const key = automaticAccountCheckKey(account), policy = policies.accounts[getAccountPolicyKey(account)], now = options.now?.() ?? Date.now();
|
|
60
87
|
if (!policy?.autoPrime || policy.paused || policy.drained || account.enabled === false || account.authInvalidatedAt || (account.coolingDownUntil ?? 0) > now)
|
|
61
88
|
continue;
|
|
62
89
|
const lastAttempt = attempts[key];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AccountMetadataV3 } from "../storage.js";
|
|
2
2
|
import { type CodexQuotaSnapshot } from "../quota-probe.js";
|
|
3
|
-
type Observer = (account: AccountMetadataV3, snapshot: CodexQuotaSnapshot, accounts: AccountMetadataV3[]) => Promise<void> | void;
|
|
3
|
+
type Observer = (account: AccountMetadataV3, snapshot: CodexQuotaSnapshot, accounts: AccountMetadataV3[], workspaceId: string) => Promise<void> | void;
|
|
4
4
|
/** Captures this router's pool; reads only verified primary credentials, never API/ZDR entries. */
|
|
5
5
|
export declare function createAutomaticSubscriptionCheck(observe?: Observer): (signal: AbortSignal) => Promise<void>;
|
|
6
6
|
export {};
|