@mostok/codexes 0.2.0 → 0.3.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/README.md +9 -3
- package/dist/cli.js +2072 -1385
- package/dist/cli.js.map +4 -4
- package/dist/commands/account-list/run-account-list-command.js +36 -18
- package/dist/commands/account-list/run-account-list-command.js.map +1 -1
- package/dist/commands/root/run-root-command.js +54 -6
- package/dist/commands/root/run-root-command.js.map +1 -1
- package/dist/config/wrapper-config.d.ts +2 -1
- package/dist/config/wrapper-config.js +66 -17
- package/dist/config/wrapper-config.js.map +1 -1
- package/dist/core/context.d.ts +3 -0
- package/dist/core/context.js +3 -0
- package/dist/core/context.js.map +1 -1
- package/dist/selection/format-selection-summary.d.ts +12 -0
- package/dist/selection/format-selection-summary.js +182 -0
- package/dist/selection/format-selection-summary.js.map +1 -0
- package/dist/selection/select-account.js +7 -163
- package/dist/selection/select-account.js.map +1 -1
- package/dist/selection/selection-summary.d.ts +38 -0
- package/dist/selection/selection-summary.js +405 -0
- package/dist/selection/selection-summary.js.map +1 -0
- package/dist/selection/usage-cache.js +23 -0
- package/dist/selection/usage-cache.js.map +1 -1
- package/dist/selection/usage-normalize.js +182 -19
- package/dist/selection/usage-normalize.js.map +1 -1
- package/dist/selection/usage-probe-coordinator.js +13 -0
- package/dist/selection/usage-probe-coordinator.js.map +1 -1
- package/dist/selection/usage-types.d.ts +18 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -70,14 +70,20 @@ async function resolveWrapperConfig(input) {
|
|
|
70
70
|
input.paths.codexConfigFile,
|
|
71
71
|
input.logger
|
|
72
72
|
);
|
|
73
|
+
const selectionStrategy = resolveAccountSelectionStrategy(input.env, input.logger);
|
|
73
74
|
const resolved = {
|
|
74
75
|
configFilePath: input.paths.wrapperConfigFile,
|
|
75
76
|
codexConfigFilePath: input.paths.codexConfigFile,
|
|
76
77
|
selectionCacheFilePath: input.paths.selectionCacheFile,
|
|
77
78
|
credentialStoreMode,
|
|
78
79
|
credentialStorePolicyReason: credentialStoreMode === "file" ? "file mode detected in Codex config" : "codexes currently supports only file-backed auth storage",
|
|
79
|
-
accountSelectionStrategy:
|
|
80
|
-
|
|
80
|
+
accountSelectionStrategy: selectionStrategy.strategy,
|
|
81
|
+
accountSelectionStrategySource: selectionStrategy.source,
|
|
82
|
+
experimentalSelection: resolveExperimentalSelectionConfig({
|
|
83
|
+
env: input.env,
|
|
84
|
+
logger: input.logger,
|
|
85
|
+
strategy: selectionStrategy.strategy
|
|
86
|
+
})
|
|
81
87
|
};
|
|
82
88
|
input.logger.info("wrapper_config.resolved", {
|
|
83
89
|
configFilePath: resolved.configFilePath,
|
|
@@ -85,28 +91,29 @@ async function resolveWrapperConfig(input) {
|
|
|
85
91
|
selectionCacheFilePath: resolved.selectionCacheFilePath,
|
|
86
92
|
credentialStoreMode: resolved.credentialStoreMode,
|
|
87
93
|
accountSelectionStrategy: resolved.accountSelectionStrategy,
|
|
94
|
+
accountSelectionStrategySource: resolved.accountSelectionStrategySource,
|
|
88
95
|
experimentalSelection: resolved.experimentalSelection
|
|
89
96
|
});
|
|
90
97
|
return resolved;
|
|
91
98
|
}
|
|
92
|
-
function resolveExperimentalSelectionConfig(
|
|
99
|
+
function resolveExperimentalSelectionConfig(input) {
|
|
93
100
|
const probeTimeoutMs = resolvePositiveIntegerEnv({
|
|
94
101
|
defaultValue: DEFAULT_EXPERIMENTAL_PROBE_TIMEOUT_MS,
|
|
95
|
-
env,
|
|
102
|
+
env: input.env,
|
|
96
103
|
envKey: "CODEXES_EXPERIMENTAL_SELECTION_TIMEOUT_MS",
|
|
97
|
-
logger
|
|
104
|
+
logger: input.logger
|
|
98
105
|
});
|
|
99
106
|
const cacheTtlMs = resolvePositiveIntegerEnv({
|
|
100
107
|
defaultValue: DEFAULT_EXPERIMENTAL_CACHE_TTL_MS,
|
|
101
|
-
env,
|
|
108
|
+
env: input.env,
|
|
102
109
|
envKey: "CODEXES_EXPERIMENTAL_SELECTION_CACHE_TTL_MS",
|
|
103
|
-
logger
|
|
110
|
+
logger: input.logger
|
|
104
111
|
});
|
|
105
112
|
const useAccountIdHeader = resolveBooleanEnv(
|
|
106
|
-
env.CODEXES_EXPERIMENTAL_SELECTION_USE_ACCOUNT_ID_HEADER
|
|
113
|
+
input.env.CODEXES_EXPERIMENTAL_SELECTION_USE_ACCOUNT_ID_HEADER
|
|
107
114
|
);
|
|
108
|
-
const enabled =
|
|
109
|
-
logger.debug("wrapper_config.experimental_selection_resolved", {
|
|
115
|
+
const enabled = input.strategy === "remaining-limit" || input.strategy === "remaining-limit-experimental";
|
|
116
|
+
input.logger.debug("wrapper_config.experimental_selection_resolved", {
|
|
110
117
|
enabled,
|
|
111
118
|
probeTimeoutMs,
|
|
112
119
|
cacheTtlMs,
|
|
@@ -119,18 +126,59 @@ function resolveExperimentalSelectionConfig(env, logger) {
|
|
|
119
126
|
useAccountIdHeader
|
|
120
127
|
};
|
|
121
128
|
}
|
|
122
|
-
function resolveAccountSelectionStrategy(env) {
|
|
123
|
-
|
|
129
|
+
function resolveAccountSelectionStrategy(env, logger) {
|
|
130
|
+
const rawOverride = env.CODEXES_ACCOUNT_SELECTION_STRATEGY?.trim().toLowerCase();
|
|
131
|
+
switch (rawOverride) {
|
|
124
132
|
case "single-account":
|
|
125
|
-
|
|
133
|
+
logger.info("wrapper_config.selection_strategy_override_applied", {
|
|
134
|
+
envKey: "CODEXES_ACCOUNT_SELECTION_STRATEGY",
|
|
135
|
+
requestedStrategy: rawOverride,
|
|
136
|
+
resolvedStrategy: "single-account"
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
source: "env-override",
|
|
140
|
+
strategy: "single-account"
|
|
141
|
+
};
|
|
142
|
+
case "remaining-limit":
|
|
126
143
|
case "remaining-limit-experimental":
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
logger.info("wrapper_config.selection_strategy_override_applied", {
|
|
145
|
+
envKey: "CODEXES_ACCOUNT_SELECTION_STRATEGY",
|
|
146
|
+
requestedStrategy: rawOverride,
|
|
147
|
+
resolvedStrategy: "remaining-limit"
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
source: "env-override",
|
|
151
|
+
strategy: "remaining-limit"
|
|
152
|
+
};
|
|
129
153
|
case void 0:
|
|
130
154
|
case "":
|
|
131
|
-
|
|
155
|
+
logger.info("wrapper_config.selection_strategy_default_applied", {
|
|
156
|
+
defaultStrategy: "remaining-limit"
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
source: "default",
|
|
160
|
+
strategy: "remaining-limit"
|
|
161
|
+
};
|
|
162
|
+
case "manual-default":
|
|
163
|
+
logger.info("wrapper_config.selection_strategy_override_applied", {
|
|
164
|
+
envKey: "CODEXES_ACCOUNT_SELECTION_STRATEGY",
|
|
165
|
+
requestedStrategy: rawOverride,
|
|
166
|
+
resolvedStrategy: "manual-default"
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
source: "env-override",
|
|
170
|
+
strategy: "manual-default"
|
|
171
|
+
};
|
|
132
172
|
default:
|
|
133
|
-
|
|
173
|
+
logger.warn("wrapper_config.selection_strategy_invalid_override", {
|
|
174
|
+
envKey: "CODEXES_ACCOUNT_SELECTION_STRATEGY",
|
|
175
|
+
rawValue: rawOverride,
|
|
176
|
+
fallbackStrategy: "remaining-limit"
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
source: "invalid-env-fallback",
|
|
180
|
+
strategy: "remaining-limit"
|
|
181
|
+
};
|
|
134
182
|
}
|
|
135
183
|
}
|
|
136
184
|
async function detectCredentialStoreMode(configFile, logger) {
|
|
@@ -907,6 +955,9 @@ async function buildAppContext(argv, io) {
|
|
|
907
955
|
stdout: io.stdout,
|
|
908
956
|
stderr: io.stderr
|
|
909
957
|
},
|
|
958
|
+
output: {
|
|
959
|
+
stdoutIsTTY: io.stdout.isTTY === true
|
|
960
|
+
},
|
|
910
961
|
logging: {
|
|
911
962
|
level: logLevel,
|
|
912
963
|
sink
|
|
@@ -1664,1560 +1715,2148 @@ function buildLoginFailureMessage(loginResult) {
|
|
|
1664
1715
|
`;
|
|
1665
1716
|
}
|
|
1666
1717
|
|
|
1667
|
-
// src/
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1718
|
+
// src/selection/format-selection-summary.ts
|
|
1719
|
+
function formatSelectionSummary(input) {
|
|
1720
|
+
const renderMode = input.capabilities.useColor ? "color" : "plain";
|
|
1721
|
+
input.logger.debug("selection.format_summary.start", {
|
|
1722
|
+
mode: input.summary.mode,
|
|
1723
|
+
strategy: input.summary.strategy,
|
|
1724
|
+
entryCount: input.summary.entries.length,
|
|
1725
|
+
stdoutIsTTY: input.capabilities.stdoutIsTTY,
|
|
1726
|
+
useColor: input.capabilities.useColor,
|
|
1727
|
+
renderMode,
|
|
1728
|
+
fallbackReason: input.summary.fallbackReason,
|
|
1729
|
+
selectedAccountId: input.summary.selectedAccount?.id ?? null,
|
|
1730
|
+
executionBlockedReason: input.summary.executionBlockedReason
|
|
1680
1731
|
});
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1732
|
+
const lines = [
|
|
1733
|
+
"Account selection summary:",
|
|
1734
|
+
...input.summary.entries.map(
|
|
1735
|
+
(entry) => formatSelectionEntry(entry, input.capabilities)
|
|
1736
|
+
)
|
|
1737
|
+
];
|
|
1738
|
+
if (input.summary.selectedAccount && input.summary.selectedBy) {
|
|
1739
|
+
lines.push(
|
|
1740
|
+
`Selected account: ${input.summary.selectedAccount.label} (${input.summary.selectedAccount.id}) via ${describeSelectionMode(input.summary)}.`
|
|
1687
1741
|
);
|
|
1742
|
+
} else {
|
|
1743
|
+
lines.push("Selected account: unavailable for execution.");
|
|
1744
|
+
}
|
|
1745
|
+
if (input.summary.fallbackReason) {
|
|
1746
|
+
lines.push(`Fallback: ${describeFallback(input.summary.fallbackReason)}.`);
|
|
1747
|
+
}
|
|
1748
|
+
if (input.summary.executionBlockedReason) {
|
|
1749
|
+
lines.push(`Execution note: ${input.summary.executionBlockedReason}`);
|
|
1750
|
+
}
|
|
1751
|
+
input.logger.debug("selection.format_summary.complete", {
|
|
1752
|
+
mode: input.summary.mode,
|
|
1753
|
+
strategy: input.summary.strategy,
|
|
1754
|
+
renderMode,
|
|
1755
|
+
lineCount: lines.length,
|
|
1756
|
+
fallbackIncluded: input.summary.fallbackReason !== null,
|
|
1757
|
+
selectedAccountId: input.summary.selectedAccount?.id ?? null,
|
|
1758
|
+
executionBlockedReason: input.summary.executionBlockedReason
|
|
1759
|
+
});
|
|
1760
|
+
return `${lines.join("\n")}
|
|
1761
|
+
`;
|
|
1762
|
+
}
|
|
1763
|
+
function formatSelectionEntry(entry, capabilities) {
|
|
1764
|
+
const tags = [
|
|
1765
|
+
entry.isSelected ? "selected" : null,
|
|
1766
|
+
entry.isDefault ? "default" : null,
|
|
1767
|
+
entry.rankingPosition !== null ? `rank #${entry.rankingPosition}` : null
|
|
1768
|
+
].filter((value) => value !== null).join(", ");
|
|
1769
|
+
const status = entry.snapshot?.status ?? (entry.failureCategory ? "probe-failed" : "not-probed");
|
|
1770
|
+
const detail = entry.failureMessage ?? entry.snapshot?.statusReason ?? "usage probing was not required for this strategy";
|
|
1771
|
+
return [
|
|
1772
|
+
"-",
|
|
1773
|
+
`${entry.account.label} (${entry.account.id})`,
|
|
1774
|
+
tags ? colorize(capabilities, "tag", `[${tags}]`) : null,
|
|
1775
|
+
colorize(capabilities, mapStatusTone(status), `status=${status}`),
|
|
1776
|
+
formatWindowMetric(capabilities, "5h", entry.snapshot?.dailyRemaining ?? null),
|
|
1777
|
+
formatWindowMetric(capabilities, "weekly", entry.snapshot?.weeklyRemaining ?? null),
|
|
1778
|
+
entry.snapshot?.plan ? `${colorize(capabilities, "plan", "plan")}=${colorize(capabilities, "planValue", entry.snapshot.plan)}` : null,
|
|
1779
|
+
colorize(capabilities, "source", `source=${entry.source}`),
|
|
1780
|
+
`detail=${describeDetailMarker(entry, detail)}`
|
|
1781
|
+
].filter((value) => value !== null).join(" ");
|
|
1782
|
+
}
|
|
1783
|
+
function formatPercent(value) {
|
|
1784
|
+
return value === null ? "unknown" : `${trimTrailingZeroes(value)}%`;
|
|
1785
|
+
}
|
|
1786
|
+
function formatWindowMetric(capabilities, label, value) {
|
|
1787
|
+
const renderedPercent = colorize(
|
|
1788
|
+
capabilities,
|
|
1789
|
+
mapRemainingTone(value),
|
|
1790
|
+
formatPercent(value)
|
|
1791
|
+
);
|
|
1792
|
+
return `${colorize(capabilities, "windowLabel", label)}=${renderedPercent}`;
|
|
1793
|
+
}
|
|
1794
|
+
function describeSelectionMode(summary) {
|
|
1795
|
+
if (summary.selectedBy === null) {
|
|
1796
|
+
return "no execution selection";
|
|
1688
1797
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1798
|
+
switch (summary.selectedBy) {
|
|
1799
|
+
case "experimental-ranked":
|
|
1800
|
+
return "remaining-limit";
|
|
1801
|
+
case "single-account":
|
|
1802
|
+
return "single-account";
|
|
1803
|
+
case "manual-default":
|
|
1804
|
+
return "manual-default";
|
|
1805
|
+
case "manual-default-fallback-single":
|
|
1806
|
+
return summary.fallbackReason ? "manual-default fallback because only one account was available" : "manual-default because only one account is configured";
|
|
1692
1807
|
}
|
|
1693
|
-
return match;
|
|
1694
1808
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1809
|
+
function describeFallback(reason) {
|
|
1810
|
+
switch (reason) {
|
|
1811
|
+
case "experimental-config-missing":
|
|
1812
|
+
return "remaining-limit probing was unavailable, so codexes fell back to manual-default";
|
|
1813
|
+
case "all-probes-failed":
|
|
1814
|
+
return "every account probe failed, so codexes could not establish a reliable execution winner";
|
|
1815
|
+
case "mixed-probe-outcomes":
|
|
1816
|
+
return "some account probes failed, so codexes could not establish a reliable execution winner";
|
|
1817
|
+
case "all-accounts-exhausted":
|
|
1818
|
+
return "all probed accounts were exhausted, so codexes could not establish a reliable execution winner";
|
|
1819
|
+
case "ambiguous-usage":
|
|
1820
|
+
return "the usage data was incomplete or ambiguous, so codexes could not establish a reliable execution winner";
|
|
1821
|
+
case null:
|
|
1822
|
+
return "no fallback was required";
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
function describeDetailMarker(entry, detail) {
|
|
1826
|
+
if (entry.failureCategory === "timeout") {
|
|
1827
|
+
return "probe-timeout";
|
|
1828
|
+
}
|
|
1829
|
+
if (entry.failureCategory === "http-error") {
|
|
1830
|
+
return "http-error";
|
|
1702
1831
|
}
|
|
1703
|
-
|
|
1832
|
+
if (entry.failureCategory === "auth-missing") {
|
|
1833
|
+
return "auth-missing";
|
|
1834
|
+
}
|
|
1835
|
+
if (entry.failureCategory === "invalid-response") {
|
|
1836
|
+
return "invalid-response";
|
|
1837
|
+
}
|
|
1838
|
+
switch (entry.snapshot?.status) {
|
|
1839
|
+
case "usable":
|
|
1840
|
+
return "rankable";
|
|
1841
|
+
case "limit-reached":
|
|
1842
|
+
return "exhausted";
|
|
1843
|
+
case "not-allowed":
|
|
1844
|
+
return "blocked";
|
|
1845
|
+
case "missing-usage-data":
|
|
1846
|
+
return "incomplete";
|
|
1847
|
+
default:
|
|
1848
|
+
return detail.includes("not required") ? "not-probed" : "diagnostic";
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function mapStatusTone(status) {
|
|
1852
|
+
switch (status) {
|
|
1853
|
+
case "usable":
|
|
1854
|
+
return "success";
|
|
1855
|
+
case "limit-reached":
|
|
1856
|
+
return "warning";
|
|
1857
|
+
case "probe-failed":
|
|
1858
|
+
case "not-allowed":
|
|
1859
|
+
return "error";
|
|
1860
|
+
default:
|
|
1861
|
+
return "muted";
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
function colorize(capabilities, tone, value) {
|
|
1865
|
+
if (!capabilities.useColor) {
|
|
1866
|
+
return value;
|
|
1867
|
+
}
|
|
1868
|
+
const open = ANSI_CODES[tone];
|
|
1869
|
+
return `${open}${value}${ANSI_RESET}`;
|
|
1870
|
+
}
|
|
1871
|
+
function mapRemainingTone(value) {
|
|
1872
|
+
if (value === null) {
|
|
1873
|
+
return "muted";
|
|
1874
|
+
}
|
|
1875
|
+
if (value <= 20) {
|
|
1876
|
+
return "error";
|
|
1877
|
+
}
|
|
1878
|
+
if (value <= 50) {
|
|
1879
|
+
return "warning";
|
|
1880
|
+
}
|
|
1881
|
+
return "success";
|
|
1704
1882
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1883
|
+
function trimTrailingZeroes(value) {
|
|
1884
|
+
return value.toFixed(2).replace(/\.00$/, "").replace(/(\.\d)0$/, "$1");
|
|
1885
|
+
}
|
|
1886
|
+
var ANSI_RESET = "\x1B[0m";
|
|
1887
|
+
var ANSI_CODES = {
|
|
1888
|
+
source: "\x1B[36m",
|
|
1889
|
+
success: "\x1B[32m",
|
|
1890
|
+
muted: "\x1B[90m",
|
|
1891
|
+
warning: "\x1B[33m",
|
|
1892
|
+
error: "\x1B[31m",
|
|
1893
|
+
tag: "\x1B[1m",
|
|
1894
|
+
windowLabel: "\x1B[94m",
|
|
1895
|
+
plan: "\x1B[95m",
|
|
1896
|
+
planValue: "\x1B[1;95m"
|
|
1897
|
+
};
|
|
1898
|
+
|
|
1899
|
+
// src/selection/account-auth-state.ts
|
|
1900
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1901
|
+
import path9 from "node:path";
|
|
1902
|
+
async function readAccountAuthState(input) {
|
|
1903
|
+
const filePath = path9.join(input.account.authDirectory, "state", "auth.json");
|
|
1904
|
+
input.logger.debug("selection.account_auth_state.read_start", {
|
|
1905
|
+
accountId: input.account.id,
|
|
1906
|
+
label: input.account.label,
|
|
1907
|
+
filePath
|
|
1908
|
+
});
|
|
1707
1909
|
try {
|
|
1708
|
-
const raw = await readFile6(
|
|
1910
|
+
const raw = await readFile6(filePath, "utf8");
|
|
1709
1911
|
const parsed = JSON.parse(raw);
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1912
|
+
if (!isRecord(parsed)) {
|
|
1913
|
+
input.logger.warn("selection.account_auth_state.unsupported_shape", {
|
|
1914
|
+
accountId: input.account.id,
|
|
1915
|
+
label: input.account.label,
|
|
1916
|
+
filePath,
|
|
1917
|
+
topLevelType: typeof parsed
|
|
1918
|
+
});
|
|
1919
|
+
return {
|
|
1920
|
+
ok: false,
|
|
1921
|
+
category: "unsupported-auth-shape",
|
|
1922
|
+
filePath,
|
|
1923
|
+
message: "auth.json is not a JSON object."
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
const accessToken = resolveString(
|
|
1927
|
+
parsed.access_token,
|
|
1928
|
+
getNestedString(parsed, ["tokens", "access_token"]),
|
|
1929
|
+
getNestedString(parsed, ["tokens", "accessToken"])
|
|
1930
|
+
);
|
|
1931
|
+
if (!accessToken) {
|
|
1932
|
+
input.logger.warn("selection.account_auth_state.access_token_missing", {
|
|
1933
|
+
accountId: input.account.id,
|
|
1934
|
+
label: input.account.label,
|
|
1935
|
+
filePath,
|
|
1936
|
+
hasTokensObject: isRecord(parsed.tokens)
|
|
1937
|
+
});
|
|
1938
|
+
return {
|
|
1939
|
+
ok: false,
|
|
1940
|
+
category: "missing-access-token",
|
|
1941
|
+
filePath,
|
|
1942
|
+
message: "auth.json does not contain an access_token."
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
const result = {
|
|
1946
|
+
ok: true,
|
|
1947
|
+
filePath,
|
|
1948
|
+
state: {
|
|
1949
|
+
accessToken,
|
|
1950
|
+
accountId: resolveString(
|
|
1951
|
+
parsed.account_id,
|
|
1952
|
+
parsed.accountId,
|
|
1953
|
+
getNestedString(parsed, ["tokens", "account_id"]),
|
|
1954
|
+
getNestedString(parsed, ["tokens", "accountId"])
|
|
1955
|
+
),
|
|
1956
|
+
authMode: resolveString(
|
|
1957
|
+
parsed.auth_mode,
|
|
1958
|
+
parsed.authMode,
|
|
1959
|
+
getNestedString(parsed, ["tokens", "auth_mode"]),
|
|
1960
|
+
getNestedString(parsed, ["tokens", "authMode"])
|
|
1961
|
+
),
|
|
1962
|
+
lastRefresh: resolveString(
|
|
1963
|
+
parsed.last_refresh,
|
|
1964
|
+
parsed.lastRefresh,
|
|
1965
|
+
parsed.refresh_at,
|
|
1966
|
+
parsed.refreshAt
|
|
1967
|
+
)
|
|
1968
|
+
}
|
|
1713
1969
|
};
|
|
1714
|
-
logger.debug("
|
|
1715
|
-
accountId: account.id,
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1970
|
+
input.logger.debug("selection.account_auth_state.read_complete", {
|
|
1971
|
+
accountId: input.account.id,
|
|
1972
|
+
label: input.account.label,
|
|
1973
|
+
filePath,
|
|
1974
|
+
hasAccessToken: true,
|
|
1975
|
+
authAccountId: result.state.accountId,
|
|
1976
|
+
authMode: result.state.authMode,
|
|
1977
|
+
hasRefreshMetadata: result.state.lastRefresh !== null
|
|
1719
1978
|
});
|
|
1720
|
-
return
|
|
1979
|
+
return result;
|
|
1721
1980
|
} catch (error) {
|
|
1722
|
-
if (
|
|
1723
|
-
logger.
|
|
1724
|
-
accountId: account.id,
|
|
1725
|
-
|
|
1981
|
+
if (isNodeErrorWithCode(error, "ENOENT")) {
|
|
1982
|
+
input.logger.warn("selection.account_auth_state.missing_file", {
|
|
1983
|
+
accountId: input.account.id,
|
|
1984
|
+
label: input.account.label,
|
|
1985
|
+
filePath
|
|
1986
|
+
});
|
|
1987
|
+
return {
|
|
1988
|
+
ok: false,
|
|
1989
|
+
category: "missing-file",
|
|
1990
|
+
filePath,
|
|
1991
|
+
message: "auth.json was not found for the account profile."
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
if (error instanceof SyntaxError) {
|
|
1995
|
+
input.logger.warn("selection.account_auth_state.malformed_json", {
|
|
1996
|
+
accountId: input.account.id,
|
|
1997
|
+
label: input.account.label,
|
|
1998
|
+
filePath,
|
|
1999
|
+
message: error.message
|
|
1726
2000
|
});
|
|
1727
|
-
return {
|
|
2001
|
+
return {
|
|
2002
|
+
ok: false,
|
|
2003
|
+
category: "malformed-json",
|
|
2004
|
+
filePath,
|
|
2005
|
+
message: error.message
|
|
2006
|
+
};
|
|
1728
2007
|
}
|
|
1729
|
-
logger.warn("
|
|
1730
|
-
accountId: account.id,
|
|
1731
|
-
|
|
2008
|
+
input.logger.warn("selection.account_auth_state.unsupported_shape", {
|
|
2009
|
+
accountId: input.account.id,
|
|
2010
|
+
label: input.account.label,
|
|
2011
|
+
filePath,
|
|
1732
2012
|
message: error instanceof Error ? error.message : String(error)
|
|
1733
2013
|
});
|
|
1734
|
-
return {
|
|
2014
|
+
return {
|
|
2015
|
+
ok: false,
|
|
2016
|
+
category: "unsupported-auth-shape",
|
|
2017
|
+
filePath,
|
|
2018
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
function getNestedString(value, pathParts) {
|
|
2023
|
+
let current = value;
|
|
2024
|
+
for (const part of pathParts) {
|
|
2025
|
+
if (!isRecord(current) || typeof current[part] === "undefined") {
|
|
2026
|
+
return null;
|
|
2027
|
+
}
|
|
2028
|
+
current = current[part];
|
|
2029
|
+
}
|
|
2030
|
+
return typeof current === "string" && current.trim().length > 0 ? current : null;
|
|
2031
|
+
}
|
|
2032
|
+
function resolveString(...values) {
|
|
2033
|
+
for (const value of values) {
|
|
2034
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2035
|
+
return value;
|
|
2036
|
+
}
|
|
1735
2037
|
}
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
function isRecord(value) {
|
|
2041
|
+
return typeof value === "object" && value !== null;
|
|
2042
|
+
}
|
|
2043
|
+
function isNodeErrorWithCode(error, code) {
|
|
2044
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1736
2045
|
}
|
|
1737
2046
|
|
|
1738
|
-
// src/
|
|
1739
|
-
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2047
|
+
// src/selection/usage-normalize.ts
|
|
2048
|
+
function normalizeWhamUsageResponse(input) {
|
|
2049
|
+
const payloadShape = resolvePayloadShape(input.raw);
|
|
2050
|
+
const dailyWindow = resolveUsageWindow(input.raw, "daily");
|
|
2051
|
+
const weeklyWindow = resolveUsageWindow(input.raw, "weekly");
|
|
2052
|
+
input.logger.debug("selection.usage_normalize.start", {
|
|
2053
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2054
|
+
payloadShape,
|
|
2055
|
+
hasPrimaryWindow: dailyWindow.source === "rate_limit.primary_window",
|
|
2056
|
+
hasSecondaryWindow: weeklyWindow.source === "rate_limit.secondary_window",
|
|
2057
|
+
topLevelKeys: Object.keys(input.raw).sort()
|
|
1744
2058
|
});
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
logger,
|
|
1748
|
-
|
|
2059
|
+
const daily = normalizeUsageWindow({
|
|
2060
|
+
accountIdHint: input.accountIdHint,
|
|
2061
|
+
logger: input.logger,
|
|
2062
|
+
raw: dailyWindow.raw,
|
|
2063
|
+
source: dailyWindow.source,
|
|
2064
|
+
window: "daily"
|
|
1749
2065
|
});
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
defaultAccountId: defaultAccount?.id ?? null
|
|
2066
|
+
const weekly = normalizeUsageWindow({
|
|
2067
|
+
accountIdHint: input.accountIdHint,
|
|
2068
|
+
logger: input.logger,
|
|
2069
|
+
raw: weeklyWindow.raw,
|
|
2070
|
+
source: weeklyWindow.source,
|
|
2071
|
+
window: "weekly"
|
|
1757
2072
|
});
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
logger.info("command.empty");
|
|
1766
|
-
return 0;
|
|
1767
|
-
}
|
|
1768
|
-
const presentations = await buildAccountPresentations({ accounts, logger });
|
|
1769
|
-
const lines = presentations.map(({ account, authAccountId, authMode }) => {
|
|
1770
|
-
const markers = [
|
|
1771
|
-
defaultAccount?.id === account.id ? "default" : null,
|
|
1772
|
-
authMode ? `auth=${authMode}` : null,
|
|
1773
|
-
authAccountId ? `authAccountId=${authAccountId}` : null
|
|
1774
|
-
].filter((value) => Boolean(value)).join(", ");
|
|
1775
|
-
return `${defaultAccount?.id === account.id ? "*" : " "} ${account.label} (${account.id})${markers ? ` [${markers}]` : ""}`;
|
|
1776
|
-
});
|
|
1777
|
-
context.io.stdout.write(`${lines.join("\n")}
|
|
1778
|
-
`);
|
|
1779
|
-
logger.info("command.complete", {
|
|
1780
|
-
accountIds: presentations.map(({ account }) => account.id)
|
|
1781
|
-
});
|
|
1782
|
-
return 0;
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
// src/commands/account-remove/run-account-remove-command.ts
|
|
1786
|
-
import { rm as rm3 } from "node:fs/promises";
|
|
1787
|
-
async function runAccountRemoveCommand(context, argv) {
|
|
1788
|
-
const logger = createLogger({
|
|
1789
|
-
level: context.logging.level,
|
|
1790
|
-
name: "account_remove",
|
|
1791
|
-
sink: context.logging.sink
|
|
1792
|
-
});
|
|
1793
|
-
if (argv.includes("--help")) {
|
|
1794
|
-
context.io.stdout.write(`${buildAccountRemoveHelpText()}
|
|
1795
|
-
`);
|
|
1796
|
-
logger.info("help.rendered");
|
|
1797
|
-
return 0;
|
|
1798
|
-
}
|
|
1799
|
-
const selector = argv[0]?.trim();
|
|
1800
|
-
if (!selector || argv.length > 1) {
|
|
1801
|
-
throw new Error(buildAccountRemoveHelpText());
|
|
1802
|
-
}
|
|
1803
|
-
const registry = createAccountRegistry({
|
|
1804
|
-
accountRoot: context.paths.accountRoot,
|
|
1805
|
-
logger,
|
|
1806
|
-
registryFile: context.paths.registryFile
|
|
1807
|
-
});
|
|
1808
|
-
const accounts = await registry.listAccounts();
|
|
1809
|
-
if (accounts.length === 0) {
|
|
1810
|
-
context.io.stdout.write("No accounts configured.\n");
|
|
1811
|
-
logger.info("command.empty");
|
|
1812
|
-
return 0;
|
|
1813
|
-
}
|
|
1814
|
-
const account = resolveAccountBySelector({ accounts, logger, selector });
|
|
1815
|
-
logger.info("command.start", {
|
|
1816
|
-
requestedSelector: selector,
|
|
1817
|
-
resolvedAccountId: account.id,
|
|
1818
|
-
label: account.label
|
|
1819
|
-
});
|
|
1820
|
-
await registry.removeAccount(account.id);
|
|
1821
|
-
await rm3(account.authDirectory, { force: true, recursive: true });
|
|
1822
|
-
context.io.stdout.write(`Removed account "${account.label}" (${account.id}).
|
|
1823
|
-
`);
|
|
1824
|
-
logger.info("command.complete", {
|
|
1825
|
-
requestedSelector: selector,
|
|
1826
|
-
resolvedAccountId: account.id
|
|
1827
|
-
});
|
|
1828
|
-
return 0;
|
|
1829
|
-
}
|
|
1830
|
-
function buildAccountRemoveHelpText() {
|
|
1831
|
-
return [
|
|
1832
|
-
"Usage:",
|
|
1833
|
-
" codexes account remove <account-id-or-label>"
|
|
1834
|
-
].join("\n");
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
// src/commands/account-use/run-account-use-command.ts
|
|
1838
|
-
async function runAccountUseCommand(context, argv) {
|
|
1839
|
-
const logger = createLogger({
|
|
1840
|
-
level: context.logging.level,
|
|
1841
|
-
name: "account_use",
|
|
1842
|
-
sink: context.logging.sink
|
|
1843
|
-
});
|
|
1844
|
-
if (argv.includes("--help")) {
|
|
1845
|
-
context.io.stdout.write(`${buildAccountUseHelpText()}
|
|
1846
|
-
`);
|
|
1847
|
-
logger.info("help.rendered");
|
|
1848
|
-
return 0;
|
|
1849
|
-
}
|
|
1850
|
-
const registry = createAccountRegistry({
|
|
1851
|
-
accountRoot: context.paths.accountRoot,
|
|
1852
|
-
logger,
|
|
1853
|
-
registryFile: context.paths.registryFile
|
|
1854
|
-
});
|
|
1855
|
-
const accounts = await registry.listAccounts();
|
|
1856
|
-
if (accounts.length === 0) {
|
|
1857
|
-
context.io.stdout.write(
|
|
1858
|
-
[
|
|
1859
|
-
"No accounts configured.",
|
|
1860
|
-
"Add one with: codexes account add <label>"
|
|
1861
|
-
].join("\n") + "\n"
|
|
1862
|
-
);
|
|
1863
|
-
logger.info("command.empty");
|
|
1864
|
-
return 0;
|
|
1865
|
-
}
|
|
1866
|
-
const selector = argv[0]?.trim() ?? null;
|
|
1867
|
-
if (argv.length > 1) {
|
|
1868
|
-
throw new Error(buildAccountUseHelpText());
|
|
1869
|
-
}
|
|
1870
|
-
let targetAccount = null;
|
|
1871
|
-
if (!selector) {
|
|
1872
|
-
if (accounts.length === 1) {
|
|
1873
|
-
const [singleAccount] = accounts;
|
|
1874
|
-
if (!singleAccount) {
|
|
1875
|
-
throw new Error("No accounts configured.");
|
|
1876
|
-
}
|
|
1877
|
-
targetAccount = singleAccount;
|
|
1878
|
-
logger.info("command.single_account_default", {
|
|
1879
|
-
resolvedAccountId: targetAccount.id,
|
|
1880
|
-
label: targetAccount.label
|
|
1881
|
-
});
|
|
1882
|
-
} else {
|
|
1883
|
-
throw new Error(
|
|
1884
|
-
"Multiple accounts exist. Specify which one to use: codexes account use <account-id-or-label>"
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
} else {
|
|
1888
|
-
targetAccount = resolveAccountBySelector({ accounts, logger, selector });
|
|
1889
|
-
}
|
|
1890
|
-
if (!targetAccount) {
|
|
1891
|
-
throw new Error("Could not resolve the account to use.");
|
|
1892
|
-
}
|
|
1893
|
-
logger.info("command.start", {
|
|
1894
|
-
requestedSelector: selector,
|
|
1895
|
-
resolvedAccountId: targetAccount.id,
|
|
1896
|
-
label: targetAccount.label
|
|
1897
|
-
});
|
|
1898
|
-
const selectedAccount = await registry.selectAccount(targetAccount.id);
|
|
1899
|
-
context.io.stdout.write(
|
|
1900
|
-
`Using account "${selectedAccount.label}" (${selectedAccount.id}) as the default.
|
|
1901
|
-
`
|
|
1902
|
-
);
|
|
1903
|
-
logger.info("command.complete", {
|
|
1904
|
-
requestedSelector: selector,
|
|
1905
|
-
resolvedAccountId: selectedAccount.id
|
|
1906
|
-
});
|
|
1907
|
-
return 0;
|
|
1908
|
-
}
|
|
1909
|
-
function buildAccountUseHelpText() {
|
|
1910
|
-
return [
|
|
1911
|
-
"Usage:",
|
|
1912
|
-
" codexes account use <account-id-or-label>",
|
|
1913
|
-
" codexes account use",
|
|
1914
|
-
"",
|
|
1915
|
-
"When only one account exists, `codexes account use` selects it automatically."
|
|
1916
|
-
].join("\n");
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
// src/runtime/lock/runtime-lock.ts
|
|
1920
|
-
import os3 from "node:os";
|
|
1921
|
-
import path10 from "node:path";
|
|
1922
|
-
import { mkdir as mkdir6, readFile as readFile7, rm as rm4, stat as stat5, writeFile as writeFile5 } from "node:fs/promises";
|
|
1923
|
-
var DEFAULT_WAIT_TIMEOUT_MS = 15e3;
|
|
1924
|
-
var DEFAULT_STALE_LOCK_MS = 5 * 60 * 1e3;
|
|
1925
|
-
var DEFAULT_POLL_INTERVAL_MS = 250;
|
|
1926
|
-
async function acquireRuntimeLock(input) {
|
|
1927
|
-
const lockRoot = path10.join(input.runtimeRoot, "lock");
|
|
1928
|
-
const ownerFile = path10.join(lockRoot, "owner.json");
|
|
1929
|
-
const waitTimeoutMs = input.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1930
|
-
const staleLockMs = input.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
|
|
1931
|
-
const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1932
|
-
const startedAt = Date.now();
|
|
1933
|
-
input.logger.info("runtime_lock.acquire.start", {
|
|
1934
|
-
lockRoot,
|
|
1935
|
-
waitTimeoutMs,
|
|
1936
|
-
staleLockMs,
|
|
1937
|
-
pollIntervalMs
|
|
1938
|
-
});
|
|
1939
|
-
while (true) {
|
|
1940
|
-
try {
|
|
1941
|
-
await mkdir6(lockRoot);
|
|
1942
|
-
const owner = {
|
|
1943
|
-
pid: process.pid,
|
|
1944
|
-
host: os3.hostname(),
|
|
1945
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1946
|
-
};
|
|
1947
|
-
await writeFile5(ownerFile, JSON.stringify(owner, null, 2), "utf8");
|
|
1948
|
-
input.logger.info("runtime_lock.acquire.complete", {
|
|
1949
|
-
lockRoot,
|
|
1950
|
-
waitedMs: Date.now() - startedAt,
|
|
1951
|
-
owner
|
|
1952
|
-
});
|
|
1953
|
-
return {
|
|
1954
|
-
async release() {
|
|
1955
|
-
input.logger.info("runtime_lock.release.start", { lockRoot });
|
|
1956
|
-
await rm4(lockRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
1957
|
-
input.logger.info("runtime_lock.release.complete", { lockRoot });
|
|
1958
|
-
}
|
|
1959
|
-
};
|
|
1960
|
-
} catch (error) {
|
|
1961
|
-
if (!isAlreadyExistsError(error)) {
|
|
1962
|
-
input.logger.error("runtime_lock.acquire.failed", {
|
|
1963
|
-
lockRoot,
|
|
1964
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1965
|
-
});
|
|
1966
|
-
throw error;
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
const lockAgeMs = await readLockAgeMs(lockRoot, ownerFile);
|
|
1970
|
-
if (lockAgeMs !== null && lockAgeMs > staleLockMs) {
|
|
1971
|
-
input.logger.warn("runtime_lock.stale_detected", {
|
|
1972
|
-
lockRoot,
|
|
1973
|
-
lockAgeMs
|
|
1974
|
-
});
|
|
1975
|
-
await rm4(lockRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
1976
|
-
continue;
|
|
1977
|
-
}
|
|
1978
|
-
const waitedMs = Date.now() - startedAt;
|
|
1979
|
-
input.logger.debug("runtime_lock.acquire.waiting", {
|
|
1980
|
-
lockRoot,
|
|
1981
|
-
waitedMs,
|
|
1982
|
-
lockAgeMs
|
|
1983
|
-
});
|
|
1984
|
-
if (waitedMs >= waitTimeoutMs) {
|
|
1985
|
-
input.logger.error("runtime_lock.acquire.timeout", {
|
|
1986
|
-
lockRoot,
|
|
1987
|
-
waitedMs,
|
|
1988
|
-
lockAgeMs
|
|
1989
|
-
});
|
|
1990
|
-
throw new Error(
|
|
1991
|
-
`Timed out waiting for the shared runtime lock after ${waitTimeoutMs}ms.`
|
|
1992
|
-
);
|
|
1993
|
-
}
|
|
1994
|
-
await sleep(pollIntervalMs);
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
async function readLockAgeMs(lockRoot, ownerFile) {
|
|
1998
|
-
const ownerContents = await readFile7(ownerFile, "utf8").catch(() => null);
|
|
1999
|
-
if (ownerContents) {
|
|
2000
|
-
const parsed = JSON.parse(ownerContents);
|
|
2001
|
-
if (typeof parsed.createdAt === "string") {
|
|
2002
|
-
return Date.now() - new Date(parsed.createdAt).getTime();
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
const lockStats = await stat5(lockRoot).catch(() => null);
|
|
2006
|
-
return lockStats ? Date.now() - lockStats.mtimeMs : null;
|
|
2007
|
-
}
|
|
2008
|
-
function isAlreadyExistsError(error) {
|
|
2009
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
2010
|
-
}
|
|
2011
|
-
function sleep(durationMs) {
|
|
2012
|
-
return new Promise((resolve) => {
|
|
2013
|
-
setTimeout(resolve, durationMs);
|
|
2014
|
-
});
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// src/runtime/activate-account/activate-account.ts
|
|
2018
|
-
import { copyFile as copyFile3, cp as cp3, mkdir as mkdir7, readFile as readFile8, rm as rm5, stat as stat6 } from "node:fs/promises";
|
|
2019
|
-
import path11 from "node:path";
|
|
2020
|
-
import { createHash } from "node:crypto";
|
|
2021
|
-
async function activateAccountIntoSharedRuntime(input) {
|
|
2022
|
-
const runtimePaths = resolveAccountRuntimePaths(input.runtimeContract, input.account.id);
|
|
2023
|
-
const accountStateRoot = runtimePaths.accountStateDirectory;
|
|
2024
|
-
const backupRoot = path11.join(runtimePaths.runtimeBackupDirectory, "active");
|
|
2025
|
-
input.logger.info("account_activation.start", {
|
|
2026
|
-
accountId: input.account.id,
|
|
2027
|
-
label: input.account.label,
|
|
2028
|
-
accountStateRoot,
|
|
2029
|
-
sharedCodexHome: input.sharedCodexHome,
|
|
2030
|
-
backupRoot
|
|
2031
|
-
});
|
|
2032
|
-
await rm5(backupRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
2033
|
-
await mkdir7(backupRoot, { recursive: true });
|
|
2034
|
-
const accountRules = input.runtimeContract.fileRules.filter(
|
|
2035
|
-
(rule) => rule.classification === "account"
|
|
2036
|
-
);
|
|
2037
|
-
const authSourcePath = path11.join(accountStateRoot, "auth.json");
|
|
2038
|
-
if (!await pathExists4(authSourcePath)) {
|
|
2039
|
-
input.logger.error("account_activation.missing_auth", {
|
|
2040
|
-
accountId: input.account.id,
|
|
2041
|
-
authSourcePath
|
|
2042
|
-
});
|
|
2043
|
-
throw new Error(
|
|
2044
|
-
`Account "${input.account.label}" has no stored auth.json; add the account again.`
|
|
2045
|
-
);
|
|
2046
|
-
}
|
|
2047
|
-
try {
|
|
2048
|
-
for (const rule of accountRules) {
|
|
2049
|
-
await backupRuntimeArtifact({
|
|
2050
|
-
backupRoot,
|
|
2051
|
-
logger: input.logger,
|
|
2052
|
-
rule,
|
|
2053
|
-
sharedCodexHome: input.sharedCodexHome
|
|
2054
|
-
});
|
|
2055
|
-
await replaceRuntimeArtifact({
|
|
2056
|
-
accountStateRoot,
|
|
2057
|
-
logger: input.logger,
|
|
2058
|
-
rule,
|
|
2059
|
-
sharedCodexHome: input.sharedCodexHome
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
} catch (error) {
|
|
2063
|
-
input.logger.error("account_activation.failed", {
|
|
2064
|
-
accountId: input.account.id,
|
|
2065
|
-
message: error instanceof Error ? error.message : String(error)
|
|
2066
|
-
});
|
|
2067
|
-
await restoreSharedRuntimeFromBackup({
|
|
2068
|
-
account: input.account,
|
|
2069
|
-
backupRoot,
|
|
2070
|
-
logger: input.logger,
|
|
2071
|
-
runtimeContract: input.runtimeContract,
|
|
2072
|
-
sharedCodexHome: input.sharedCodexHome
|
|
2073
|
-
});
|
|
2074
|
-
throw error;
|
|
2075
|
-
}
|
|
2076
|
-
input.logger.info("account_activation.complete", {
|
|
2077
|
-
accountId: input.account.id,
|
|
2078
|
-
sharedCodexHome: input.sharedCodexHome
|
|
2079
|
-
});
|
|
2080
|
-
return {
|
|
2081
|
-
account: input.account,
|
|
2082
|
-
backupRoot,
|
|
2083
|
-
runtimeContract: input.runtimeContract,
|
|
2084
|
-
sharedCodexHome: input.sharedCodexHome,
|
|
2085
|
-
sourceAccountStateRoot: accountStateRoot
|
|
2086
|
-
};
|
|
2087
|
-
}
|
|
2088
|
-
async function syncSharedRuntimeBackToAccount(input) {
|
|
2089
|
-
const accountRules = input.session.runtimeContract.fileRules.filter(
|
|
2090
|
-
(rule) => rule.classification === "account"
|
|
2091
|
-
);
|
|
2092
|
-
input.logger.info("account_sync.start", {
|
|
2093
|
-
accountId: input.session.account.id,
|
|
2094
|
-
sharedCodexHome: input.session.sharedCodexHome,
|
|
2095
|
-
accountStateRoot: input.session.sourceAccountStateRoot
|
|
2096
|
-
});
|
|
2097
|
-
for (const rule of accountRules) {
|
|
2098
|
-
await syncRuntimeArtifact({
|
|
2099
|
-
accountStateRoot: input.session.sourceAccountStateRoot,
|
|
2100
|
-
logger: input.logger,
|
|
2101
|
-
rule,
|
|
2102
|
-
sharedCodexHome: input.session.sharedCodexHome
|
|
2103
|
-
});
|
|
2104
|
-
}
|
|
2105
|
-
input.logger.info("account_sync.complete", {
|
|
2106
|
-
accountId: input.session.account.id
|
|
2107
|
-
});
|
|
2108
|
-
}
|
|
2109
|
-
async function restoreSharedRuntimeFromBackup(input) {
|
|
2110
|
-
const accountRules = input.runtimeContract.fileRules.filter(
|
|
2111
|
-
(rule) => rule.classification === "account"
|
|
2073
|
+
const accountId = pickString(input.raw.account_id, input.raw.accountId, input.accountIdHint);
|
|
2074
|
+
const plan = pickString(
|
|
2075
|
+
input.raw.plan,
|
|
2076
|
+
input.raw.subscription_plan,
|
|
2077
|
+
input.raw.plan_type,
|
|
2078
|
+
input.raw.rate_limit?.plan,
|
|
2079
|
+
input.raw.rate_limit?.subscription_plan
|
|
2112
2080
|
);
|
|
2113
|
-
input.
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2081
|
+
const allowed = pickBoolean(input.raw.allowed, input.raw.rate_limit?.allowed, true) ?? true;
|
|
2082
|
+
const limitReached = (pickBoolean(input.raw.limit_reached, input.raw.rate_limit?.limit_reached) ?? daily.limitReached) || weekly.limitReached;
|
|
2083
|
+
const status = classifyUsageStatus({
|
|
2084
|
+
allowed,
|
|
2085
|
+
dailyRemaining: daily.remaining,
|
|
2086
|
+
limitReached,
|
|
2087
|
+
weeklyRemaining: weekly.remaining
|
|
2117
2088
|
});
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2089
|
+
const statusEvent = `selection.usage_normalize.status_${status}`;
|
|
2090
|
+
const statusDetails = {
|
|
2091
|
+
accountId,
|
|
2092
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2093
|
+
allowed,
|
|
2094
|
+
payloadShape,
|
|
2095
|
+
dailyRemaining: daily.remaining,
|
|
2096
|
+
weeklyRemaining: weekly.remaining,
|
|
2097
|
+
dailyWindowSource: daily.source,
|
|
2098
|
+
weeklyWindowSource: weekly.source,
|
|
2099
|
+
hasRemainingPercent: daily.remaining !== null || weekly.remaining !== null
|
|
2100
|
+
};
|
|
2101
|
+
if (status === "usable") {
|
|
2102
|
+
input.logger.info(statusEvent, statusDetails);
|
|
2103
|
+
} else {
|
|
2104
|
+
input.logger.warn(statusEvent, statusDetails);
|
|
2125
2105
|
}
|
|
2126
|
-
|
|
2127
|
-
accountId
|
|
2106
|
+
const snapshot = {
|
|
2107
|
+
accountId,
|
|
2108
|
+
allowed,
|
|
2109
|
+
limitReached,
|
|
2110
|
+
plan,
|
|
2111
|
+
dailyRemaining: daily.remaining,
|
|
2112
|
+
weeklyRemaining: weekly.remaining,
|
|
2113
|
+
dailyResetsAt: daily.resetsAt,
|
|
2114
|
+
weeklyResetsAt: weekly.resetsAt,
|
|
2115
|
+
dailyPercentUsed: daily.percentUsed,
|
|
2116
|
+
weeklyPercentUsed: weekly.percentUsed,
|
|
2117
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2118
|
+
status,
|
|
2119
|
+
statusReason: describeUsageStatus(status),
|
|
2120
|
+
windows: {
|
|
2121
|
+
daily,
|
|
2122
|
+
weekly
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
input.logger.debug("selection.usage_normalize.complete", {
|
|
2126
|
+
accountId: snapshot.accountId,
|
|
2127
|
+
allowed: snapshot.allowed,
|
|
2128
|
+
payloadShape,
|
|
2129
|
+
limitReached: snapshot.limitReached,
|
|
2130
|
+
plan: snapshot.plan,
|
|
2131
|
+
dailyRemaining: snapshot.dailyRemaining,
|
|
2132
|
+
weeklyRemaining: snapshot.weeklyRemaining,
|
|
2133
|
+
dailyResetsAt: snapshot.dailyResetsAt,
|
|
2134
|
+
weeklyResetsAt: snapshot.weeklyResetsAt,
|
|
2135
|
+
status: snapshot.status,
|
|
2136
|
+
statusReason: snapshot.statusReason
|
|
2128
2137
|
});
|
|
2138
|
+
return snapshot;
|
|
2129
2139
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
sourcePath,
|
|
2137
|
-
reason: "missing"
|
|
2140
|
+
function normalizeUsageWindow(input) {
|
|
2141
|
+
if (!input.raw) {
|
|
2142
|
+
input.logger.debug("selection.usage_normalize.window_missing", {
|
|
2143
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2144
|
+
source: input.source,
|
|
2145
|
+
window: input.window
|
|
2138
2146
|
});
|
|
2139
|
-
return
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2147
|
+
return {
|
|
2148
|
+
limit: null,
|
|
2149
|
+
used: null,
|
|
2150
|
+
remaining: null,
|
|
2151
|
+
limitReached: false,
|
|
2152
|
+
resetsAt: null,
|
|
2153
|
+
percentUsed: null,
|
|
2154
|
+
source: input.source
|
|
2155
|
+
};
|
|
2146
2156
|
}
|
|
2147
|
-
input.
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2157
|
+
const limit = pickNumber(input.raw.limit);
|
|
2158
|
+
const percentResolution = resolveUsedPercent({
|
|
2159
|
+
accountIdHint: input.accountIdHint,
|
|
2160
|
+
logger: input.logger,
|
|
2161
|
+
raw: input.raw,
|
|
2162
|
+
window: input.window
|
|
2163
|
+
});
|
|
2164
|
+
const remainingFromPercent = calculateRemainingFromPercent(percentResolution.percentUsed);
|
|
2165
|
+
const used = pickNumber(
|
|
2166
|
+
input.raw.used,
|
|
2167
|
+
calculateUsed(limit, pickNumber(input.raw.remaining)),
|
|
2168
|
+
calculateUsed(limit, remainingFromPercent)
|
|
2169
|
+
);
|
|
2170
|
+
const remaining = pickNumber(
|
|
2171
|
+
input.raw.remaining,
|
|
2172
|
+
remainingFromPercent,
|
|
2173
|
+
calculateRemaining(limit, used)
|
|
2174
|
+
);
|
|
2175
|
+
const limitReached = typeof input.raw.limit_reached === "boolean" ? input.raw.limit_reached : remaining !== null ? remaining <= 0 : false;
|
|
2176
|
+
const percentUsed = percentResolution.percentUsed ?? calculatePercentUsed(limit, used, remaining);
|
|
2177
|
+
const resetsAt = normalizeTimestamp(
|
|
2178
|
+
input.raw.reset_at,
|
|
2179
|
+
input.raw.resets_at,
|
|
2180
|
+
input.raw.next_reset_at,
|
|
2181
|
+
calculateResetAtFromSeconds(input.raw.reset_after_seconds)
|
|
2182
|
+
);
|
|
2183
|
+
const source = resolveWindowSource(input.raw, input.source);
|
|
2184
|
+
input.logger.debug("selection.usage_normalize.window_complete", {
|
|
2185
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2186
|
+
window: input.window,
|
|
2187
|
+
source,
|
|
2188
|
+
limit,
|
|
2189
|
+
used,
|
|
2190
|
+
remaining,
|
|
2191
|
+
limitReached,
|
|
2192
|
+
rawUsedPercent: percentResolution.rawValue,
|
|
2193
|
+
percentResolutionSource: percentResolution.source,
|
|
2194
|
+
percentUsed,
|
|
2195
|
+
resetsAt,
|
|
2196
|
+
limitWindowSeconds: pickNumber(input.raw.limit_window_seconds)
|
|
2151
2197
|
});
|
|
2198
|
+
return {
|
|
2199
|
+
limit,
|
|
2200
|
+
used,
|
|
2201
|
+
remaining,
|
|
2202
|
+
limitReached,
|
|
2203
|
+
resetsAt,
|
|
2204
|
+
percentUsed,
|
|
2205
|
+
source
|
|
2206
|
+
};
|
|
2152
2207
|
}
|
|
2153
|
-
|
|
2154
|
-
const
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
sourcePath,
|
|
2161
|
-
reason: "missing"
|
|
2162
|
-
});
|
|
2163
|
-
return;
|
|
2208
|
+
function resolveUsageWindow(raw, window) {
|
|
2209
|
+
const rateLimitWindow = window === "daily" ? raw.rate_limit?.primary_window : raw.rate_limit?.secondary_window;
|
|
2210
|
+
if (isRecord2(rateLimitWindow)) {
|
|
2211
|
+
return {
|
|
2212
|
+
raw: rateLimitWindow,
|
|
2213
|
+
source: `rate_limit.${window === "daily" ? "primary_window" : "secondary_window"}`
|
|
2214
|
+
};
|
|
2164
2215
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2216
|
+
const candidates = [
|
|
2217
|
+
{ raw: raw[window], source: `legacy.${window}` },
|
|
2218
|
+
{ raw: raw.usage?.[window], source: `usage.${window}` },
|
|
2219
|
+
{ raw: raw.quotas?.[window], source: `quotas.${window}` }
|
|
2220
|
+
];
|
|
2221
|
+
for (const candidate of candidates) {
|
|
2222
|
+
if (isRecord2(candidate.raw)) {
|
|
2223
|
+
return {
|
|
2224
|
+
raw: candidate.raw,
|
|
2225
|
+
source: candidate.source
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2170
2228
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
});
|
|
2229
|
+
return {
|
|
2230
|
+
raw: null,
|
|
2231
|
+
source: null
|
|
2232
|
+
};
|
|
2176
2233
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
if (!await pathExists4(sourcePath)) {
|
|
2181
|
-
input.logger.debug("account_sync.skip", {
|
|
2182
|
-
pathPattern: input.rule.pathPattern,
|
|
2183
|
-
sourcePath,
|
|
2184
|
-
reason: "missing"
|
|
2185
|
-
});
|
|
2186
|
-
return;
|
|
2234
|
+
function resolveWindowSource(raw, fallbackSource) {
|
|
2235
|
+
if (typeof raw.source === "string") {
|
|
2236
|
+
return raw.source;
|
|
2187
2237
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
input.logger.debug("account_sync.no_change", {
|
|
2191
|
-
pathPattern: input.rule.pathPattern,
|
|
2192
|
-
sourcePath,
|
|
2193
|
-
targetPath
|
|
2194
|
-
});
|
|
2195
|
-
return;
|
|
2238
|
+
if (typeof raw.kind === "string") {
|
|
2239
|
+
return raw.kind;
|
|
2196
2240
|
}
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
await copyFile3(sourcePath, targetPath);
|
|
2241
|
+
return fallbackSource;
|
|
2242
|
+
}
|
|
2243
|
+
function resolvePayloadShape(raw) {
|
|
2244
|
+
if (isRecord2(raw.rate_limit) && (isRecord2(raw.rate_limit.primary_window) || isRecord2(raw.rate_limit.secondary_window))) {
|
|
2245
|
+
return "rate_limit";
|
|
2203
2246
|
}
|
|
2204
|
-
|
|
2205
|
-
pathPattern: input.rule.pathPattern,
|
|
2206
|
-
sourcePath,
|
|
2207
|
-
targetPath
|
|
2208
|
-
});
|
|
2247
|
+
return "legacy";
|
|
2209
2248
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
|
|
2214
|
-
if (!await pathExists4(backupPath)) {
|
|
2215
|
-
input.logger.debug("account_activation.restore.skip", {
|
|
2216
|
-
pathPattern: input.rule.pathPattern,
|
|
2217
|
-
backupPath,
|
|
2218
|
-
reason: "missing"
|
|
2219
|
-
});
|
|
2220
|
-
return;
|
|
2249
|
+
function classifyUsageStatus(input) {
|
|
2250
|
+
if (!input.allowed) {
|
|
2251
|
+
return "not-allowed";
|
|
2221
2252
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
await cp3(backupPath, targetPath, { recursive: true });
|
|
2225
|
-
} else {
|
|
2226
|
-
await copyFile3(backupPath, targetPath);
|
|
2253
|
+
if (input.limitReached) {
|
|
2254
|
+
return "limit-reached";
|
|
2227
2255
|
}
|
|
2228
|
-
input.
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
});
|
|
2256
|
+
if (input.dailyRemaining === null && input.weeklyRemaining === null) {
|
|
2257
|
+
return "missing-usage-data";
|
|
2258
|
+
}
|
|
2259
|
+
return "usable";
|
|
2233
2260
|
}
|
|
2234
|
-
function
|
|
2235
|
-
|
|
2261
|
+
function describeUsageStatus(status) {
|
|
2262
|
+
switch (status) {
|
|
2263
|
+
case "not-allowed":
|
|
2264
|
+
return "usage endpoint reported that the account is not allowed to launch";
|
|
2265
|
+
case "limit-reached":
|
|
2266
|
+
return "usage endpoint reported an exhausted limit window";
|
|
2267
|
+
case "missing-usage-data":
|
|
2268
|
+
return "usage endpoint did not expose enough quota fields to rank this account";
|
|
2269
|
+
case "usable":
|
|
2270
|
+
return "usage endpoint exposed enough quota fields to rank this account";
|
|
2271
|
+
}
|
|
2236
2272
|
}
|
|
2237
|
-
function
|
|
2238
|
-
|
|
2273
|
+
function normalizeTimestamp(...values) {
|
|
2274
|
+
for (const value of values) {
|
|
2275
|
+
if (typeof value === "string") {
|
|
2276
|
+
const parsed = new Date(value);
|
|
2277
|
+
if (!Number.isNaN(parsed.valueOf())) {
|
|
2278
|
+
return parsed.toISOString();
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2282
|
+
const normalizedValue = value > 1e10 ? value : value * 1e3;
|
|
2283
|
+
const parsed = new Date(normalizedValue);
|
|
2284
|
+
if (!Number.isNaN(parsed.valueOf())) {
|
|
2285
|
+
return parsed.toISOString();
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return null;
|
|
2239
2290
|
}
|
|
2240
|
-
|
|
2241
|
-
if (!
|
|
2242
|
-
return
|
|
2291
|
+
function calculateResetAtFromSeconds(value) {
|
|
2292
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
2293
|
+
return null;
|
|
2243
2294
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
return sourceHash2 !== targetHash2;
|
|
2295
|
+
return new Date(Date.now() + value * 1e3).toISOString();
|
|
2296
|
+
}
|
|
2297
|
+
function calculateRemaining(limit, used) {
|
|
2298
|
+
if (limit === null || used === null) {
|
|
2299
|
+
return null;
|
|
2250
2300
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2301
|
+
return limit - used;
|
|
2302
|
+
}
|
|
2303
|
+
function calculateUsed(limit, remaining) {
|
|
2304
|
+
if (limit === null || remaining === null) {
|
|
2305
|
+
return null;
|
|
2306
|
+
}
|
|
2307
|
+
return limit - remaining;
|
|
2256
2308
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2309
|
+
function calculatePercentUsed(limit, used, remaining) {
|
|
2310
|
+
if (limit === null || limit <= 0) {
|
|
2311
|
+
return null;
|
|
2312
|
+
}
|
|
2313
|
+
const numerator = used ?? calculateUsed(limit, remaining);
|
|
2314
|
+
if (numerator === null) {
|
|
2315
|
+
return null;
|
|
2316
|
+
}
|
|
2317
|
+
return Number((numerator / limit * 100).toFixed(2));
|
|
2260
2318
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
for (const entry of entries.sort()) {
|
|
2265
|
-
hash.update(entry.relativePath);
|
|
2266
|
-
hash.update(await readFile8(entry.absolutePath));
|
|
2319
|
+
function calculateRemainingFromPercent(percentUsed) {
|
|
2320
|
+
if (percentUsed === null) {
|
|
2321
|
+
return null;
|
|
2267
2322
|
}
|
|
2268
|
-
return
|
|
2323
|
+
return Number((100 - percentUsed).toFixed(2));
|
|
2269
2324
|
}
|
|
2270
|
-
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
|
|
2325
|
+
function resolveUsedPercent(input) {
|
|
2326
|
+
const candidates = [
|
|
2327
|
+
{ source: "used_percent", value: input.raw.used_percent },
|
|
2328
|
+
{ source: "percent_used", value: input.raw.percent_used },
|
|
2329
|
+
{ source: "percentage_used", value: input.raw.percentage_used }
|
|
2330
|
+
];
|
|
2331
|
+
for (const candidate of candidates) {
|
|
2332
|
+
if (candidate.value === null || candidate.value === void 0 || candidate.value === "") {
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
const parsed = normalizePercentValue(candidate.value);
|
|
2336
|
+
if (parsed === null) {
|
|
2337
|
+
input.logger.debug("selection.usage_normalize.percent_invalid", {
|
|
2338
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2339
|
+
window: input.window,
|
|
2340
|
+
source: candidate.source,
|
|
2341
|
+
rawValue: candidate.value,
|
|
2342
|
+
behavior: "skip"
|
|
2343
|
+
});
|
|
2344
|
+
return {
|
|
2345
|
+
percentUsed: null,
|
|
2346
|
+
rawValue: typeof candidate.value === "string" || typeof candidate.value === "number" ? candidate.value : null,
|
|
2347
|
+
source: "invalid"
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
if (parsed.wasClamped) {
|
|
2351
|
+
input.logger.debug("selection.usage_normalize.percent_clamped", {
|
|
2352
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2353
|
+
window: input.window,
|
|
2354
|
+
source: candidate.source,
|
|
2355
|
+
rawValue: candidate.value,
|
|
2356
|
+
clampedValue: parsed.value
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
input.logger.debug("selection.usage_normalize.percent_resolved", {
|
|
2360
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2361
|
+
window: input.window,
|
|
2362
|
+
source: candidate.source,
|
|
2363
|
+
rawValue: candidate.value,
|
|
2364
|
+
percentUsed: parsed.value,
|
|
2365
|
+
remainingPercent: calculateRemainingFromPercent(parsed.value)
|
|
2366
|
+
});
|
|
2367
|
+
return {
|
|
2368
|
+
percentUsed: parsed.value,
|
|
2369
|
+
rawValue: typeof candidate.value === "string" || typeof candidate.value === "number" ? candidate.value : null,
|
|
2370
|
+
source: candidate.source
|
|
2371
|
+
};
|
|
2274
2372
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2373
|
+
input.logger.debug("selection.usage_normalize.percent_missing", {
|
|
2374
|
+
accountIdHint: input.accountIdHint ?? null,
|
|
2375
|
+
window: input.window,
|
|
2376
|
+
fallback: "derive-from-limit-or-remaining-if-possible"
|
|
2377
|
+
});
|
|
2378
|
+
return {
|
|
2379
|
+
percentUsed: null,
|
|
2380
|
+
rawValue: null,
|
|
2381
|
+
source: "missing"
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
function normalizePercentValue(value) {
|
|
2385
|
+
const numericValue = typeof value === "number" ? value : value.trim().length > 0 ? Number(value) : Number.NaN;
|
|
2386
|
+
if (!Number.isFinite(numericValue)) {
|
|
2387
|
+
return null;
|
|
2277
2388
|
}
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2389
|
+
const clampedValue = Math.min(100, Math.max(0, numericValue));
|
|
2390
|
+
return {
|
|
2391
|
+
value: Number(clampedValue.toFixed(2)),
|
|
2392
|
+
wasClamped: clampedValue !== numericValue
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
function pickString(...values) {
|
|
2396
|
+
for (const value of values) {
|
|
2397
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
2398
|
+
return value;
|
|
2284
2399
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
continue;
|
|
2293
|
-
}
|
|
2294
|
-
if (entry.isFile()) {
|
|
2295
|
-
results.push({
|
|
2296
|
-
absolutePath,
|
|
2297
|
-
relativePath: path11.relative(root, absolutePath).split(path11.sep).join("/")
|
|
2298
|
-
});
|
|
2299
|
-
}
|
|
2400
|
+
}
|
|
2401
|
+
return null;
|
|
2402
|
+
}
|
|
2403
|
+
function pickNumber(...values) {
|
|
2404
|
+
for (const value of values) {
|
|
2405
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2406
|
+
return value;
|
|
2300
2407
|
}
|
|
2301
2408
|
}
|
|
2302
|
-
return
|
|
2409
|
+
return null;
|
|
2303
2410
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
} catch (error) {
|
|
2309
|
-
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
2310
|
-
return false;
|
|
2411
|
+
function pickBoolean(...values) {
|
|
2412
|
+
for (const value of values) {
|
|
2413
|
+
if (typeof value === "boolean") {
|
|
2414
|
+
return value;
|
|
2311
2415
|
}
|
|
2312
|
-
throw error;
|
|
2313
2416
|
}
|
|
2417
|
+
return null;
|
|
2314
2418
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
2318
|
-
async function spawnCodexCommand(input) {
|
|
2319
|
-
const launchSpec = await resolveCodexLaunchSpec(input.codexBinaryPath, input.argv);
|
|
2320
|
-
input.logger.info("spawn_codex.start", {
|
|
2321
|
-
codexBinaryPath: input.codexBinaryPath,
|
|
2322
|
-
resolvedCommand: launchSpec.command,
|
|
2323
|
-
codexHome: input.codexHome,
|
|
2324
|
-
argv: launchSpec.args,
|
|
2325
|
-
stdinIsTTY: process.stdin.isTTY ?? false,
|
|
2326
|
-
stdoutIsTTY: process.stdout.isTTY ?? false,
|
|
2327
|
-
stderrIsTTY: process.stderr.isTTY ?? false
|
|
2328
|
-
});
|
|
2329
|
-
return new Promise((resolve, reject) => {
|
|
2330
|
-
const child = spawn2(launchSpec.command, launchSpec.args, {
|
|
2331
|
-
env: {
|
|
2332
|
-
...process.env,
|
|
2333
|
-
CODEX_HOME: input.codexHome
|
|
2334
|
-
},
|
|
2335
|
-
shell: false,
|
|
2336
|
-
stdio: "inherit",
|
|
2337
|
-
windowsHide: false
|
|
2338
|
-
});
|
|
2339
|
-
let settled = false;
|
|
2340
|
-
const forwardSignal = (signal) => {
|
|
2341
|
-
input.logger.warn("spawn_codex.parent_signal", {
|
|
2342
|
-
signal,
|
|
2343
|
-
pid: child.pid ?? null
|
|
2344
|
-
});
|
|
2345
|
-
child.kill(signal);
|
|
2346
|
-
};
|
|
2347
|
-
const signalHandlers = {
|
|
2348
|
-
SIGINT: () => forwardSignal("SIGINT"),
|
|
2349
|
-
SIGTERM: () => forwardSignal("SIGTERM")
|
|
2350
|
-
};
|
|
2351
|
-
process.on("SIGINT", signalHandlers.SIGINT);
|
|
2352
|
-
process.on("SIGTERM", signalHandlers.SIGTERM);
|
|
2353
|
-
const cleanup = () => {
|
|
2354
|
-
process.off("SIGINT", signalHandlers.SIGINT);
|
|
2355
|
-
process.off("SIGTERM", signalHandlers.SIGTERM);
|
|
2356
|
-
};
|
|
2357
|
-
child.on("error", (error) => {
|
|
2358
|
-
if (settled) {
|
|
2359
|
-
return;
|
|
2360
|
-
}
|
|
2361
|
-
settled = true;
|
|
2362
|
-
cleanup();
|
|
2363
|
-
input.logger.error("spawn_codex.error", {
|
|
2364
|
-
codexBinaryPath: input.codexBinaryPath,
|
|
2365
|
-
message: error.message
|
|
2366
|
-
});
|
|
2367
|
-
reject(error);
|
|
2368
|
-
});
|
|
2369
|
-
child.on("exit", (exitCode2, signal) => {
|
|
2370
|
-
if (settled) {
|
|
2371
|
-
return;
|
|
2372
|
-
}
|
|
2373
|
-
settled = true;
|
|
2374
|
-
cleanup();
|
|
2375
|
-
input.logger.info("spawn_codex.complete", {
|
|
2376
|
-
codexBinaryPath: input.codexBinaryPath,
|
|
2377
|
-
exitCode: exitCode2,
|
|
2378
|
-
signal
|
|
2379
|
-
});
|
|
2380
|
-
resolve(exitCode2 ?? 1);
|
|
2381
|
-
});
|
|
2382
|
-
});
|
|
2419
|
+
function isRecord2(value) {
|
|
2420
|
+
return typeof value === "object" && value !== null;
|
|
2383
2421
|
}
|
|
2384
2422
|
|
|
2385
|
-
// src/selection/
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
input.logger.debug("selection.account_auth_state.read_start", {
|
|
2423
|
+
// src/selection/usage-client.ts
|
|
2424
|
+
var WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
2425
|
+
async function probeAccountUsage(input) {
|
|
2426
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
2427
|
+
input.logger.info("selection.usage_probe.start", {
|
|
2391
2428
|
accountId: input.account.id,
|
|
2392
2429
|
label: input.account.label,
|
|
2393
|
-
|
|
2430
|
+
timeoutMs: input.probeConfig.probeTimeoutMs,
|
|
2431
|
+
useAccountIdHeader: input.probeConfig.useAccountIdHeader
|
|
2394
2432
|
});
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
const accessToken = resolveString(
|
|
2413
|
-
parsed.access_token,
|
|
2414
|
-
getNestedString(parsed, ["tokens", "access_token"]),
|
|
2415
|
-
getNestedString(parsed, ["tokens", "accessToken"])
|
|
2416
|
-
);
|
|
2417
|
-
if (!accessToken) {
|
|
2418
|
-
input.logger.warn("selection.account_auth_state.access_token_missing", {
|
|
2419
|
-
accountId: input.account.id,
|
|
2420
|
-
label: input.account.label,
|
|
2421
|
-
filePath,
|
|
2422
|
-
hasTokensObject: isRecord(parsed.tokens)
|
|
2423
|
-
});
|
|
2424
|
-
return {
|
|
2425
|
-
ok: false,
|
|
2426
|
-
category: "missing-access-token",
|
|
2427
|
-
filePath,
|
|
2428
|
-
message: "auth.json does not contain an access_token."
|
|
2429
|
-
};
|
|
2430
|
-
}
|
|
2431
|
-
const result = {
|
|
2432
|
-
ok: true,
|
|
2433
|
-
filePath,
|
|
2434
|
-
state: {
|
|
2435
|
-
accessToken,
|
|
2436
|
-
accountId: resolveString(
|
|
2437
|
-
parsed.account_id,
|
|
2438
|
-
parsed.accountId,
|
|
2439
|
-
getNestedString(parsed, ["tokens", "account_id"]),
|
|
2440
|
-
getNestedString(parsed, ["tokens", "accountId"])
|
|
2441
|
-
),
|
|
2442
|
-
authMode: resolveString(
|
|
2443
|
-
parsed.auth_mode,
|
|
2444
|
-
parsed.authMode,
|
|
2445
|
-
getNestedString(parsed, ["tokens", "auth_mode"]),
|
|
2446
|
-
getNestedString(parsed, ["tokens", "authMode"])
|
|
2447
|
-
),
|
|
2448
|
-
lastRefresh: resolveString(
|
|
2449
|
-
parsed.last_refresh,
|
|
2450
|
-
parsed.lastRefresh,
|
|
2451
|
-
parsed.refresh_at,
|
|
2452
|
-
parsed.refreshAt
|
|
2453
|
-
)
|
|
2454
|
-
}
|
|
2433
|
+
const authState = await readAccountAuthState({
|
|
2434
|
+
account: input.account,
|
|
2435
|
+
logger: input.logger
|
|
2436
|
+
});
|
|
2437
|
+
if (!authState.ok) {
|
|
2438
|
+
input.logger.warn("selection.usage_probe.auth_missing", {
|
|
2439
|
+
accountId: input.account.id,
|
|
2440
|
+
label: input.account.label,
|
|
2441
|
+
category: authState.category,
|
|
2442
|
+
filePath: authState.filePath
|
|
2443
|
+
});
|
|
2444
|
+
return {
|
|
2445
|
+
ok: false,
|
|
2446
|
+
account: input.account,
|
|
2447
|
+
category: "auth-missing",
|
|
2448
|
+
message: authState.message,
|
|
2449
|
+
source: "fresh"
|
|
2455
2450
|
};
|
|
2456
|
-
|
|
2451
|
+
}
|
|
2452
|
+
try {
|
|
2453
|
+
const response = await fetchImpl(WHAM_USAGE_URL, {
|
|
2454
|
+
method: "GET",
|
|
2455
|
+
headers: buildUsageHeaders({
|
|
2456
|
+
accessToken: authState.state.accessToken,
|
|
2457
|
+
accountId: authState.state.accountId,
|
|
2458
|
+
useAccountIdHeader: input.probeConfig.useAccountIdHeader
|
|
2459
|
+
}),
|
|
2460
|
+
signal: AbortSignal.timeout(input.probeConfig.probeTimeoutMs)
|
|
2461
|
+
});
|
|
2462
|
+
input.logger.debug("selection.usage_probe.http_complete", {
|
|
2457
2463
|
accountId: input.account.id,
|
|
2458
2464
|
label: input.account.label,
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
authAccountId: result.state.accountId,
|
|
2462
|
-
authMode: result.state.authMode,
|
|
2463
|
-
hasRefreshMetadata: result.state.lastRefresh !== null
|
|
2465
|
+
status: response.status,
|
|
2466
|
+
ok: response.ok
|
|
2464
2467
|
});
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
if (isNodeErrorWithCode(error, "ENOENT")) {
|
|
2468
|
-
input.logger.warn("selection.account_auth_state.missing_file", {
|
|
2468
|
+
if (!response.ok) {
|
|
2469
|
+
input.logger.warn("selection.usage_probe.http_error", {
|
|
2469
2470
|
accountId: input.account.id,
|
|
2470
2471
|
label: input.account.label,
|
|
2471
|
-
|
|
2472
|
+
status: response.status
|
|
2472
2473
|
});
|
|
2473
2474
|
return {
|
|
2474
2475
|
ok: false,
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
message:
|
|
2476
|
+
account: input.account,
|
|
2477
|
+
category: "http-error",
|
|
2478
|
+
message: `Usage probe returned HTTP ${response.status}.`,
|
|
2479
|
+
source: "fresh"
|
|
2478
2480
|
};
|
|
2479
2481
|
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
+
const body = await response.json();
|
|
2483
|
+
if (!isRecord3(body)) {
|
|
2484
|
+
input.logger.warn("selection.usage_probe.invalid_response", {
|
|
2482
2485
|
accountId: input.account.id,
|
|
2483
2486
|
label: input.account.label,
|
|
2484
|
-
|
|
2485
|
-
message: error.message
|
|
2487
|
+
bodyType: typeof body
|
|
2486
2488
|
});
|
|
2487
2489
|
return {
|
|
2488
2490
|
ok: false,
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
message:
|
|
2491
|
+
account: input.account,
|
|
2492
|
+
category: "invalid-response",
|
|
2493
|
+
message: "Usage probe returned a non-object JSON payload.",
|
|
2494
|
+
source: "fresh"
|
|
2492
2495
|
};
|
|
2493
2496
|
}
|
|
2494
|
-
|
|
2497
|
+
const snapshot = normalizeWhamUsageResponse({
|
|
2498
|
+
accountIdHint: authState.state.accountId ?? input.account.id,
|
|
2499
|
+
logger: input.logger,
|
|
2500
|
+
raw: body
|
|
2501
|
+
});
|
|
2502
|
+
input.logger.info("selection.usage_probe.success", {
|
|
2503
|
+
accountId: input.account.id,
|
|
2504
|
+
label: input.account.label,
|
|
2505
|
+
snapshotStatus: snapshot.status,
|
|
2506
|
+
dailyRemaining: snapshot.dailyRemaining,
|
|
2507
|
+
weeklyRemaining: snapshot.weeklyRemaining,
|
|
2508
|
+
limitReached: snapshot.limitReached
|
|
2509
|
+
});
|
|
2510
|
+
return {
|
|
2511
|
+
ok: true,
|
|
2512
|
+
account: input.account,
|
|
2513
|
+
snapshot,
|
|
2514
|
+
source: "fresh"
|
|
2515
|
+
};
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
if (isAbortError(error)) {
|
|
2518
|
+
input.logger.warn("selection.usage_probe.timeout", {
|
|
2519
|
+
accountId: input.account.id,
|
|
2520
|
+
label: input.account.label,
|
|
2521
|
+
timeoutMs: input.probeConfig.probeTimeoutMs
|
|
2522
|
+
});
|
|
2523
|
+
return {
|
|
2524
|
+
ok: false,
|
|
2525
|
+
account: input.account,
|
|
2526
|
+
category: "timeout",
|
|
2527
|
+
message: `Usage probe timed out after ${input.probeConfig.probeTimeoutMs}ms.`,
|
|
2528
|
+
source: "fresh"
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
input.logger.error("selection.usage_probe.request_failed", {
|
|
2495
2532
|
accountId: input.account.id,
|
|
2496
2533
|
label: input.account.label,
|
|
2497
|
-
filePath,
|
|
2498
2534
|
message: error instanceof Error ? error.message : String(error)
|
|
2499
2535
|
});
|
|
2500
2536
|
return {
|
|
2501
2537
|
ok: false,
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
message: error instanceof Error ? error.message : String(error)
|
|
2538
|
+
account: input.account,
|
|
2539
|
+
category: "invalid-response",
|
|
2540
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2541
|
+
source: "fresh"
|
|
2505
2542
|
};
|
|
2506
2543
|
}
|
|
2507
2544
|
}
|
|
2508
|
-
function
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2545
|
+
function buildUsageHeaders(input) {
|
|
2546
|
+
const headers = new Headers({
|
|
2547
|
+
accept: "application/json",
|
|
2548
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
2549
|
+
"user-agent": "codexes/0.1 experimental-usage-probe"
|
|
2550
|
+
});
|
|
2551
|
+
if (input.useAccountIdHeader && input.accountId) {
|
|
2552
|
+
headers.set("OpenAI-Account-ID", input.accountId);
|
|
2553
|
+
}
|
|
2554
|
+
return headers;
|
|
2555
|
+
}
|
|
2556
|
+
function isAbortError(error) {
|
|
2557
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
2558
|
+
}
|
|
2559
|
+
function isRecord3(value) {
|
|
2560
|
+
return typeof value === "object" && value !== null;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// src/selection/usage-cache.ts
|
|
2564
|
+
import { mkdir as mkdir6, readFile as readFile7, rename as rename2, writeFile as writeFile5 } from "node:fs/promises";
|
|
2565
|
+
import path10 from "node:path";
|
|
2566
|
+
var USAGE_CACHE_SCHEMA_VERSION = 1;
|
|
2567
|
+
async function loadUsageCache(input) {
|
|
2568
|
+
try {
|
|
2569
|
+
const raw = await readFile7(input.cacheFilePath, "utf8");
|
|
2570
|
+
const parsed = JSON.parse(raw);
|
|
2571
|
+
const normalized = normalizeUsageCacheDocument(parsed);
|
|
2572
|
+
input.logger.debug("selection.usage_cache.load_success", {
|
|
2573
|
+
cacheFilePath: input.cacheFilePath,
|
|
2574
|
+
entryCount: normalized.entries.length,
|
|
2575
|
+
compatibilitySummary: normalized.entries.map((entry) => ({
|
|
2576
|
+
accountId: entry.accountId,
|
|
2577
|
+
shape: describeSnapshotShape(entry.snapshot)
|
|
2578
|
+
}))
|
|
2579
|
+
});
|
|
2580
|
+
return normalized.entries;
|
|
2581
|
+
} catch (error) {
|
|
2582
|
+
if (isNodeErrorWithCode2(error, "ENOENT")) {
|
|
2583
|
+
input.logger.debug("selection.usage_cache.missing", {
|
|
2584
|
+
cacheFilePath: input.cacheFilePath
|
|
2585
|
+
});
|
|
2586
|
+
return [];
|
|
2513
2587
|
}
|
|
2514
|
-
|
|
2588
|
+
const backupPath = `${input.cacheFilePath}.corrupt-${Date.now()}`;
|
|
2589
|
+
await rename2(input.cacheFilePath, backupPath).catch(() => void 0);
|
|
2590
|
+
input.logger.warn("selection.usage_cache.corrupt", {
|
|
2591
|
+
cacheFilePath: input.cacheFilePath,
|
|
2592
|
+
backupPath,
|
|
2593
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2594
|
+
});
|
|
2595
|
+
return [];
|
|
2515
2596
|
}
|
|
2516
|
-
return typeof current === "string" && current.trim().length > 0 ? current : null;
|
|
2517
2597
|
}
|
|
2518
|
-
function
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2598
|
+
async function persistUsageCache(input) {
|
|
2599
|
+
await mkdir6(path10.dirname(input.cacheFilePath), { recursive: true });
|
|
2600
|
+
const document = {
|
|
2601
|
+
schemaVersion: USAGE_CACHE_SCHEMA_VERSION,
|
|
2602
|
+
entries: input.entries
|
|
2603
|
+
};
|
|
2604
|
+
const tempFile = `${input.cacheFilePath}.tmp`;
|
|
2605
|
+
const serialized = JSON.stringify(document, null, 2);
|
|
2606
|
+
await writeFile5(tempFile, serialized, "utf8");
|
|
2607
|
+
await rename2(tempFile, input.cacheFilePath);
|
|
2608
|
+
input.logger.debug("selection.usage_cache.persisted", {
|
|
2609
|
+
cacheFilePath: input.cacheFilePath,
|
|
2610
|
+
entryCount: input.entries.length
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
function resolveFreshUsageCacheEntry(input) {
|
|
2614
|
+
const entry = input.entries.find((candidate) => candidate.accountId === input.accountId) ?? null;
|
|
2615
|
+
if (!entry) {
|
|
2616
|
+
input.logger.debug("selection.usage_cache.miss", {
|
|
2617
|
+
accountId: input.accountId,
|
|
2618
|
+
ttlMs: input.ttlMs,
|
|
2619
|
+
snapshotSource: "fresh-required"
|
|
2620
|
+
});
|
|
2621
|
+
return null;
|
|
2622
|
+
}
|
|
2623
|
+
const ageMs = input.now - new Date(entry.cachedAt).valueOf();
|
|
2624
|
+
if (!Number.isFinite(ageMs) || ageMs > input.ttlMs) {
|
|
2625
|
+
input.logger.debug("selection.usage_cache.expired", {
|
|
2626
|
+
accountId: input.accountId,
|
|
2627
|
+
cachedAt: entry.cachedAt,
|
|
2628
|
+
ageMs: Number.isFinite(ageMs) ? ageMs : null,
|
|
2629
|
+
ttlMs: input.ttlMs,
|
|
2630
|
+
primaryRemainingPercent: entry.snapshot.dailyRemaining,
|
|
2631
|
+
secondaryRemainingPercent: entry.snapshot.weeklyRemaining,
|
|
2632
|
+
snapshotStatus: entry.snapshot.status,
|
|
2633
|
+
compatibilityPath: describeSnapshotShape(entry.snapshot)
|
|
2634
|
+
});
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2637
|
+
input.logger.debug("selection.usage_cache.hit", {
|
|
2638
|
+
accountId: input.accountId,
|
|
2639
|
+
cachedAt: entry.cachedAt,
|
|
2640
|
+
ageMs,
|
|
2641
|
+
ttlMs: input.ttlMs,
|
|
2642
|
+
primaryRemainingPercent: entry.snapshot.dailyRemaining,
|
|
2643
|
+
secondaryRemainingPercent: entry.snapshot.weeklyRemaining,
|
|
2644
|
+
snapshotStatus: entry.snapshot.status,
|
|
2645
|
+
compatibilityPath: describeSnapshotShape(entry.snapshot)
|
|
2646
|
+
});
|
|
2647
|
+
return entry;
|
|
2648
|
+
}
|
|
2649
|
+
function normalizeUsageCacheDocument(value) {
|
|
2650
|
+
if (!isRecord4(value)) {
|
|
2651
|
+
throw new Error("Usage cache document is not an object.");
|
|
2652
|
+
}
|
|
2653
|
+
const schemaVersion = typeof value.schemaVersion === "number" ? value.schemaVersion : USAGE_CACHE_SCHEMA_VERSION;
|
|
2654
|
+
if (schemaVersion !== USAGE_CACHE_SCHEMA_VERSION) {
|
|
2655
|
+
throw new Error(`Unsupported usage cache schema version ${schemaVersion}.`);
|
|
2656
|
+
}
|
|
2657
|
+
const entries = Array.isArray(value.entries) ? value.entries.filter(isUsageCacheEntry) : [];
|
|
2658
|
+
return {
|
|
2659
|
+
schemaVersion: USAGE_CACHE_SCHEMA_VERSION,
|
|
2660
|
+
entries
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
function isUsageCacheEntry(value) {
|
|
2664
|
+
return isRecord4(value) && typeof value.accountId === "string" && typeof value.accountLabel === "string" && typeof value.cachedAt === "string" && isRecord4(value.snapshot);
|
|
2665
|
+
}
|
|
2666
|
+
function isRecord4(value) {
|
|
2667
|
+
return typeof value === "object" && value !== null;
|
|
2668
|
+
}
|
|
2669
|
+
function isNodeErrorWithCode2(error, code) {
|
|
2670
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
2671
|
+
}
|
|
2672
|
+
function describeSnapshotShape(snapshot) {
|
|
2673
|
+
if (typeof snapshot === "object" && snapshot !== null && "windows" in snapshot && typeof snapshot.windows === "object" && snapshot.windows !== null) {
|
|
2674
|
+
return "current";
|
|
2675
|
+
}
|
|
2676
|
+
return "legacy-compatible";
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/selection/usage-probe-coordinator.ts
|
|
2680
|
+
async function resolveAccountUsageSnapshots(input) {
|
|
2681
|
+
const now = Date.now();
|
|
2682
|
+
input.logger.info("selection.usage_probe_coordinator.start", {
|
|
2683
|
+
accountCount: input.accounts.length,
|
|
2684
|
+
cacheFilePath: input.cacheFilePath,
|
|
2685
|
+
cacheTtlMs: input.probeConfig.cacheTtlMs,
|
|
2686
|
+
timeoutMs: input.probeConfig.probeTimeoutMs
|
|
2687
|
+
});
|
|
2688
|
+
const cacheEntries = await loadUsageCache({
|
|
2689
|
+
cacheFilePath: input.cacheFilePath,
|
|
2690
|
+
logger: input.logger
|
|
2691
|
+
});
|
|
2692
|
+
const freshCacheEntries = [...cacheEntries];
|
|
2693
|
+
const resolutions = await Promise.all(
|
|
2694
|
+
input.accounts.map(async (account) => {
|
|
2695
|
+
const cached = resolveFreshUsageCacheEntry({
|
|
2696
|
+
accountId: account.id,
|
|
2697
|
+
entries: freshCacheEntries,
|
|
2698
|
+
logger: input.logger,
|
|
2699
|
+
now,
|
|
2700
|
+
ttlMs: input.probeConfig.cacheTtlMs
|
|
2701
|
+
});
|
|
2702
|
+
if (cached) {
|
|
2703
|
+
return {
|
|
2704
|
+
ok: true,
|
|
2705
|
+
account,
|
|
2706
|
+
snapshot: cached.snapshot,
|
|
2707
|
+
source: "cache"
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
const fresh = await probeAccountUsage({
|
|
2711
|
+
account,
|
|
2712
|
+
fetchImpl: input.fetchImpl,
|
|
2713
|
+
logger: input.logger,
|
|
2714
|
+
probeConfig: input.probeConfig
|
|
2715
|
+
});
|
|
2716
|
+
if (fresh.ok) {
|
|
2717
|
+
upsertCacheEntry(freshCacheEntries, {
|
|
2718
|
+
accountId: account.id,
|
|
2719
|
+
accountLabel: account.label,
|
|
2720
|
+
cachedAt: new Date(now).toISOString(),
|
|
2721
|
+
snapshot: fresh.snapshot
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
return fresh;
|
|
2725
|
+
})
|
|
2726
|
+
);
|
|
2727
|
+
await persistUsageCache({
|
|
2728
|
+
cacheFilePath: input.cacheFilePath,
|
|
2729
|
+
entries: freshCacheEntries,
|
|
2730
|
+
logger: input.logger
|
|
2731
|
+
});
|
|
2732
|
+
input.logger.info("selection.usage_probe_coordinator.complete", {
|
|
2733
|
+
accountCount: input.accounts.length,
|
|
2734
|
+
cacheHitCount: resolutions.filter((entry) => entry.ok && entry.source === "cache").length,
|
|
2735
|
+
freshSuccessCount: resolutions.filter((entry) => entry.ok && entry.source === "fresh").length,
|
|
2736
|
+
failureCount: resolutions.filter((entry) => !entry.ok).length,
|
|
2737
|
+
resolutionSummary: resolutions.map(
|
|
2738
|
+
(entry) => entry.ok ? {
|
|
2739
|
+
accountId: entry.account.id,
|
|
2740
|
+
source: entry.source,
|
|
2741
|
+
snapshotStatus: entry.snapshot.status,
|
|
2742
|
+
primaryRemainingPercent: entry.snapshot.dailyRemaining,
|
|
2743
|
+
secondaryRemainingPercent: entry.snapshot.weeklyRemaining
|
|
2744
|
+
} : {
|
|
2745
|
+
accountId: entry.account.id,
|
|
2746
|
+
source: entry.source,
|
|
2747
|
+
failureCategory: entry.category
|
|
2748
|
+
}
|
|
2749
|
+
)
|
|
2750
|
+
});
|
|
2751
|
+
return resolutions;
|
|
2752
|
+
}
|
|
2753
|
+
function upsertCacheEntry(entries, nextEntry) {
|
|
2754
|
+
const existingIndex = entries.findIndex((entry) => entry.accountId === nextEntry.accountId);
|
|
2755
|
+
if (existingIndex >= 0) {
|
|
2756
|
+
entries.splice(existingIndex, 1, nextEntry);
|
|
2757
|
+
return;
|
|
2523
2758
|
}
|
|
2524
|
-
|
|
2525
|
-
}
|
|
2526
|
-
function isRecord(value) {
|
|
2527
|
-
return typeof value === "object" && value !== null;
|
|
2528
|
-
}
|
|
2529
|
-
function isNodeErrorWithCode(error, code) {
|
|
2530
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
2759
|
+
entries.push(nextEntry);
|
|
2531
2760
|
}
|
|
2532
2761
|
|
|
2533
|
-
// src/selection/
|
|
2534
|
-
function
|
|
2535
|
-
input.
|
|
2536
|
-
|
|
2537
|
-
|
|
2762
|
+
// src/selection/selection-summary.ts
|
|
2763
|
+
async function resolveSelectionSummary(input) {
|
|
2764
|
+
const mode = input.mode ?? "execution";
|
|
2765
|
+
const accounts = await input.registry.listAccounts();
|
|
2766
|
+
input.logger.info("selection.summary.start", {
|
|
2767
|
+
mode,
|
|
2768
|
+
strategy: input.strategy,
|
|
2769
|
+
accountCount: accounts.length
|
|
2538
2770
|
});
|
|
2539
|
-
|
|
2540
|
-
|
|
2771
|
+
if (accounts.length === 0) {
|
|
2772
|
+
input.logger.warn("selection.none", {
|
|
2773
|
+
mode,
|
|
2774
|
+
strategy: input.strategy
|
|
2775
|
+
});
|
|
2776
|
+
throw new Error("No accounts configured. Add one with `codexes account add <label>`.");
|
|
2777
|
+
}
|
|
2778
|
+
const defaultAccount = await input.registry.getDefaultAccount();
|
|
2779
|
+
const summary = await resolveStrategySummary({
|
|
2780
|
+
accounts,
|
|
2781
|
+
defaultAccount,
|
|
2782
|
+
experimentalSelection: input.experimentalSelection,
|
|
2783
|
+
fetchImpl: input.fetchImpl,
|
|
2541
2784
|
logger: input.logger,
|
|
2542
|
-
|
|
2543
|
-
|
|
2785
|
+
mode,
|
|
2786
|
+
registry: input.registry,
|
|
2787
|
+
selectionCacheFilePath: input.selectionCacheFilePath,
|
|
2788
|
+
strategy: input.strategy
|
|
2544
2789
|
});
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2790
|
+
input.logger.info("selection.summary.complete", {
|
|
2791
|
+
mode: summary.mode,
|
|
2792
|
+
strategy: summary.strategy,
|
|
2793
|
+
selectedAccountId: summary.selectedAccount?.id ?? null,
|
|
2794
|
+
selectedBy: summary.selectedBy,
|
|
2795
|
+
fallbackReason: summary.fallbackReason,
|
|
2796
|
+
executionBlockedReason: summary.executionBlockedReason,
|
|
2797
|
+
entryCount: summary.entries.length
|
|
2550
2798
|
});
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2799
|
+
return summary;
|
|
2800
|
+
}
|
|
2801
|
+
async function resolveStrategySummary(input) {
|
|
2802
|
+
switch (input.strategy) {
|
|
2803
|
+
case "manual-default":
|
|
2804
|
+
return buildManualDefaultSummary(input.registry, input.logger, input.accounts, input.defaultAccount, input.mode);
|
|
2805
|
+
case "single-account":
|
|
2806
|
+
return buildSingleAccountSummary(input.registry, input.logger, input.accounts, input.defaultAccount, input.mode);
|
|
2807
|
+
case "remaining-limit":
|
|
2808
|
+
case "remaining-limit-experimental":
|
|
2809
|
+
return buildExperimentalSummary(input);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
async function buildManualDefaultSummary(registry, logger, accounts, defaultAccount, mode) {
|
|
2813
|
+
const selection = await resolveManualDefaultSelection({
|
|
2814
|
+
accounts,
|
|
2815
|
+
logger,
|
|
2816
|
+
mode,
|
|
2817
|
+
registry,
|
|
2818
|
+
strategy: "manual-default"
|
|
2559
2819
|
});
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
dailyPercentUsed: daily.percentUsed,
|
|
2569
|
-
weeklyPercentUsed: weekly.percentUsed,
|
|
2570
|
-
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2571
|
-
status,
|
|
2572
|
-
statusReason: describeUsageStatus(status),
|
|
2573
|
-
windows: {
|
|
2574
|
-
daily,
|
|
2575
|
-
weekly
|
|
2576
|
-
}
|
|
2820
|
+
return {
|
|
2821
|
+
entries: createUnavailableEntries(accounts, defaultAccount, selection.selectedAccount),
|
|
2822
|
+
executionBlockedReason: selection.executionBlockedReason,
|
|
2823
|
+
fallbackReason: null,
|
|
2824
|
+
mode,
|
|
2825
|
+
selectedAccount: selection.selectedAccount,
|
|
2826
|
+
selectedBy: selection.selectedBy,
|
|
2827
|
+
strategy: "manual-default"
|
|
2577
2828
|
};
|
|
2578
|
-
input.logger.debug("selection.usage_normalize.complete", {
|
|
2579
|
-
accountId: snapshot.accountId,
|
|
2580
|
-
allowed: snapshot.allowed,
|
|
2581
|
-
limitReached: snapshot.limitReached,
|
|
2582
|
-
dailyRemaining: snapshot.dailyRemaining,
|
|
2583
|
-
weeklyRemaining: snapshot.weeklyRemaining,
|
|
2584
|
-
dailyResetsAt: snapshot.dailyResetsAt,
|
|
2585
|
-
weeklyResetsAt: snapshot.weeklyResetsAt,
|
|
2586
|
-
status: snapshot.status,
|
|
2587
|
-
statusReason: snapshot.statusReason
|
|
2588
|
-
});
|
|
2589
|
-
return snapshot;
|
|
2590
2829
|
}
|
|
2591
|
-
function
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2830
|
+
async function buildSingleAccountSummary(registry, logger, accounts, defaultAccount, mode) {
|
|
2831
|
+
const selectedAccount = await selectSingleAccountOnly(registry, logger, accounts, mode);
|
|
2832
|
+
return {
|
|
2833
|
+
entries: createUnavailableEntries(accounts, defaultAccount, selectedAccount),
|
|
2834
|
+
executionBlockedReason: null,
|
|
2835
|
+
fallbackReason: null,
|
|
2836
|
+
mode,
|
|
2837
|
+
selectedAccount,
|
|
2838
|
+
selectedBy: "single-account",
|
|
2839
|
+
strategy: "single-account"
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
async function buildExperimentalSummary(input) {
|
|
2843
|
+
if (!input.experimentalSelection?.enabled || !input.selectionCacheFilePath) {
|
|
2844
|
+
input.logger.warn("selection.experimental_config_missing", {
|
|
2845
|
+
enabled: input.experimentalSelection?.enabled ?? false,
|
|
2846
|
+
hasSelectionCacheFilePath: Boolean(input.selectionCacheFilePath),
|
|
2847
|
+
mode: input.mode
|
|
2848
|
+
});
|
|
2849
|
+
const fallbackSelection = await resolveManualDefaultSelection({
|
|
2850
|
+
accounts: input.accounts,
|
|
2851
|
+
fallbackReason: "experimental-config-missing",
|
|
2852
|
+
logger: input.logger,
|
|
2853
|
+
mode: input.mode,
|
|
2854
|
+
registry: input.registry,
|
|
2855
|
+
strategy: input.strategy
|
|
2596
2856
|
});
|
|
2597
2857
|
return {
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2858
|
+
entries: createUnavailableEntries(input.accounts, input.defaultAccount, fallbackSelection.selectedAccount),
|
|
2859
|
+
executionBlockedReason: fallbackSelection.executionBlockedReason,
|
|
2860
|
+
fallbackReason: "experimental-config-missing",
|
|
2861
|
+
mode: input.mode,
|
|
2862
|
+
selectedAccount: fallbackSelection.selectedAccount,
|
|
2863
|
+
selectedBy: fallbackSelection.selectedBy,
|
|
2864
|
+
strategy: input.strategy
|
|
2605
2865
|
};
|
|
2606
2866
|
}
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
input.
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
const
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2867
|
+
const probeResults = await resolveAccountUsageSnapshots({
|
|
2868
|
+
accounts: input.accounts,
|
|
2869
|
+
cacheFilePath: input.selectionCacheFilePath,
|
|
2870
|
+
fetchImpl: input.fetchImpl,
|
|
2871
|
+
logger: input.logger,
|
|
2872
|
+
probeConfig: input.experimentalSelection
|
|
2873
|
+
});
|
|
2874
|
+
const failedProbes = probeResults.filter((entry) => !entry.ok);
|
|
2875
|
+
if (failedProbes.length > 0) {
|
|
2876
|
+
const fallbackReason = failedProbes.length === probeResults.length ? "all-probes-failed" : "mixed-probe-outcomes";
|
|
2877
|
+
input.logger.warn(
|
|
2878
|
+
fallbackReason === "all-probes-failed" ? "selection.experimental_fallback_all_probes_failed" : "selection.experimental_fallback_mixed_probe_outcomes",
|
|
2879
|
+
{
|
|
2880
|
+
failedAccountIds: failedProbes.map((entry) => entry.account.id),
|
|
2881
|
+
failureCategories: failedProbes.map((entry) => entry.category),
|
|
2882
|
+
mode: input.mode,
|
|
2883
|
+
successfulAccountIds: probeResults.filter((entry) => entry.ok).map((entry) => entry.account.id)
|
|
2884
|
+
}
|
|
2885
|
+
);
|
|
2886
|
+
const fallbackSelection = await resolveManualDefaultSelection({
|
|
2887
|
+
accounts: input.accounts,
|
|
2888
|
+
fallbackReason,
|
|
2889
|
+
logger: input.logger,
|
|
2890
|
+
mode: input.mode,
|
|
2891
|
+
registry: input.registry,
|
|
2892
|
+
strategy: input.strategy
|
|
2893
|
+
});
|
|
2894
|
+
logExperimentalFallbackSelection(input.logger, {
|
|
2895
|
+
fallbackReason,
|
|
2896
|
+
mode: input.mode,
|
|
2897
|
+
selectedAccount: fallbackSelection.selectedAccount,
|
|
2898
|
+
selectedBy: fallbackSelection.selectedBy
|
|
2899
|
+
});
|
|
2900
|
+
return {
|
|
2901
|
+
entries: createExperimentalEntries({
|
|
2902
|
+
defaultAccount: input.defaultAccount,
|
|
2903
|
+
probeResults,
|
|
2904
|
+
selectedAccount: fallbackSelection.selectedAccount,
|
|
2905
|
+
selectedCandidateIds: []
|
|
2906
|
+
}),
|
|
2907
|
+
executionBlockedReason: fallbackSelection.executionBlockedReason,
|
|
2908
|
+
fallbackReason,
|
|
2909
|
+
mode: input.mode,
|
|
2910
|
+
selectedAccount: fallbackSelection.selectedAccount,
|
|
2911
|
+
selectedBy: fallbackSelection.selectedBy,
|
|
2912
|
+
strategy: input.strategy
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
const successfulProbes = probeResults.filter((entry) => entry.ok);
|
|
2916
|
+
const candidates = successfulProbes.filter((entry) => entry.snapshot.status === "usable").sort(
|
|
2917
|
+
(left, right) => compareExperimentalCandidates({
|
|
2918
|
+
defaultAccountId: input.defaultAccount?.id ?? null,
|
|
2919
|
+
left,
|
|
2920
|
+
registryOrder: input.accounts,
|
|
2921
|
+
right
|
|
2922
|
+
})
|
|
2623
2923
|
);
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2924
|
+
input.logger.info("selection.experimental_ranked", {
|
|
2925
|
+
candidateOrder: candidates.map((entry) => ({
|
|
2926
|
+
accountId: entry.account.id,
|
|
2927
|
+
label: entry.account.label,
|
|
2928
|
+
primaryRemainingPercent: entry.snapshot.dailyRemaining,
|
|
2929
|
+
secondaryRemainingPercent: entry.snapshot.weeklyRemaining,
|
|
2930
|
+
source: entry.source
|
|
2931
|
+
})),
|
|
2932
|
+
defaultAccountId: input.defaultAccount?.id ?? null,
|
|
2933
|
+
mode: input.mode,
|
|
2934
|
+
rankingSignal: "remaining-percent",
|
|
2935
|
+
tieBreakOrder: [
|
|
2936
|
+
"primary_remaining_percent_desc",
|
|
2937
|
+
"secondary_remaining_percent_desc",
|
|
2938
|
+
"default_account",
|
|
2939
|
+
"registry_order"
|
|
2940
|
+
]
|
|
2941
|
+
});
|
|
2942
|
+
const selected = candidates[0];
|
|
2943
|
+
if (!selected) {
|
|
2944
|
+
const allExhausted = successfulProbes.every(
|
|
2945
|
+
(entry) => entry.snapshot.limitReached || entry.snapshot.status === "limit-reached"
|
|
2946
|
+
);
|
|
2947
|
+
const fallbackReason = allExhausted ? "all-accounts-exhausted" : "ambiguous-usage";
|
|
2948
|
+
input.logger.warn(
|
|
2949
|
+
allExhausted ? "selection.experimental_fallback_all_accounts_exhausted" : "selection.experimental_fallback_ambiguous_usage",
|
|
2950
|
+
{
|
|
2951
|
+
mode: input.mode,
|
|
2952
|
+
usableProbeCount: candidates.length,
|
|
2953
|
+
probeStatuses: successfulProbes.map((entry) => ({
|
|
2954
|
+
accountId: entry.account.id,
|
|
2955
|
+
snapshotStatus: entry.snapshot.status,
|
|
2956
|
+
limitReached: entry.snapshot.limitReached,
|
|
2957
|
+
dailyRemaining: entry.snapshot.dailyRemaining,
|
|
2958
|
+
weeklyRemaining: entry.snapshot.weeklyRemaining
|
|
2959
|
+
}))
|
|
2960
|
+
}
|
|
2961
|
+
);
|
|
2962
|
+
const fallbackSelection = await resolveManualDefaultSelection({
|
|
2963
|
+
accounts: input.accounts,
|
|
2964
|
+
fallbackReason,
|
|
2965
|
+
logger: input.logger,
|
|
2966
|
+
mode: input.mode,
|
|
2967
|
+
registry: input.registry,
|
|
2968
|
+
strategy: input.strategy
|
|
2969
|
+
});
|
|
2970
|
+
logExperimentalFallbackSelection(input.logger, {
|
|
2971
|
+
fallbackReason,
|
|
2972
|
+
mode: input.mode,
|
|
2973
|
+
selectedAccount: fallbackSelection.selectedAccount,
|
|
2974
|
+
selectedBy: fallbackSelection.selectedBy
|
|
2975
|
+
});
|
|
2976
|
+
return {
|
|
2977
|
+
entries: createExperimentalEntries({
|
|
2978
|
+
defaultAccount: input.defaultAccount,
|
|
2979
|
+
probeResults,
|
|
2980
|
+
selectedAccount: fallbackSelection.selectedAccount,
|
|
2981
|
+
selectedCandidateIds: []
|
|
2982
|
+
}),
|
|
2983
|
+
executionBlockedReason: fallbackSelection.executionBlockedReason,
|
|
2984
|
+
fallbackReason,
|
|
2985
|
+
mode: input.mode,
|
|
2986
|
+
selectedAccount: fallbackSelection.selectedAccount,
|
|
2987
|
+
selectedBy: fallbackSelection.selectedBy,
|
|
2988
|
+
strategy: input.strategy
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
input.logger.info("selection.experimental_selected", {
|
|
2992
|
+
accountId: selected.account.id,
|
|
2993
|
+
label: selected.account.label,
|
|
2994
|
+
primaryRemainingPercent: selected.snapshot.dailyRemaining,
|
|
2995
|
+
secondaryRemainingPercent: selected.snapshot.weeklyRemaining,
|
|
2996
|
+
source: selected.source,
|
|
2997
|
+
mode: input.mode,
|
|
2998
|
+
selectedBy: "experimental-ranked",
|
|
2999
|
+
rankingSignal: "remaining-percent"
|
|
2635
3000
|
});
|
|
2636
3001
|
return {
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
3002
|
+
entries: createExperimentalEntries({
|
|
3003
|
+
defaultAccount: input.defaultAccount,
|
|
3004
|
+
probeResults,
|
|
3005
|
+
selectedAccount: selected.account,
|
|
3006
|
+
selectedCandidateIds: candidates.map((entry) => entry.account.id)
|
|
3007
|
+
}),
|
|
3008
|
+
executionBlockedReason: null,
|
|
3009
|
+
fallbackReason: null,
|
|
3010
|
+
mode: input.mode,
|
|
3011
|
+
selectedAccount: selected.account,
|
|
3012
|
+
selectedBy: "experimental-ranked",
|
|
3013
|
+
strategy: input.strategy
|
|
2644
3014
|
};
|
|
2645
3015
|
}
|
|
2646
|
-
function
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
3016
|
+
async function resolveManualDefaultSelection(input) {
|
|
3017
|
+
input.logger.debug("selection.manual_default.requirement", {
|
|
3018
|
+
mode: input.mode,
|
|
3019
|
+
strategy: input.strategy,
|
|
3020
|
+
fallbackReason: input.fallbackReason ?? null,
|
|
3021
|
+
accountCount: input.accounts.length
|
|
3022
|
+
});
|
|
3023
|
+
const defaultAccount = await input.registry.getDefaultAccount();
|
|
3024
|
+
if (defaultAccount) {
|
|
3025
|
+
input.logger.info("selection.manual_default", {
|
|
3026
|
+
accountId: defaultAccount.id,
|
|
3027
|
+
label: defaultAccount.label,
|
|
3028
|
+
mode: input.mode,
|
|
3029
|
+
strategy: input.strategy
|
|
3030
|
+
});
|
|
3031
|
+
return {
|
|
3032
|
+
executionBlockedReason: null,
|
|
3033
|
+
selectedAccount: defaultAccount,
|
|
3034
|
+
selectedBy: "manual-default"
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
if (input.accounts.length === 1) {
|
|
3038
|
+
const [singleAccount] = input.accounts;
|
|
3039
|
+
if (!singleAccount) {
|
|
3040
|
+
throw new Error("No accounts configured.");
|
|
2651
3041
|
}
|
|
3042
|
+
input.logger.info("selection.manual_default_fallback_single", {
|
|
3043
|
+
accountId: singleAccount.id,
|
|
3044
|
+
label: singleAccount.label,
|
|
3045
|
+
mode: input.mode,
|
|
3046
|
+
strategy: input.strategy
|
|
3047
|
+
});
|
|
3048
|
+
return {
|
|
3049
|
+
executionBlockedReason: null,
|
|
3050
|
+
selectedAccount: await input.registry.selectAccount(singleAccount.id),
|
|
3051
|
+
selectedBy: "manual-default-fallback-single"
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
const executionBlockedReason = "Multiple accounts are configured but no default account is selected. Use `codexes account use <account-id-or-label>` first.";
|
|
3055
|
+
input.logger.warn("selection.manual_default_missing", {
|
|
3056
|
+
accountCount: input.accounts.length,
|
|
3057
|
+
mode: input.mode,
|
|
3058
|
+
strategy: input.strategy,
|
|
3059
|
+
fallbackReason: input.fallbackReason ?? null
|
|
3060
|
+
});
|
|
3061
|
+
if (input.mode === "display-only") {
|
|
3062
|
+
input.logger.info("selection.display_only_missing_execution_account", {
|
|
3063
|
+
accountCount: input.accounts.length,
|
|
3064
|
+
strategy: input.strategy,
|
|
3065
|
+
fallbackReason: input.fallbackReason ?? null
|
|
3066
|
+
});
|
|
3067
|
+
return {
|
|
3068
|
+
executionBlockedReason,
|
|
3069
|
+
selectedAccount: null,
|
|
3070
|
+
selectedBy: null
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
input.logger.warn("selection.execution_blocked_missing_default", {
|
|
3074
|
+
accountCount: input.accounts.length,
|
|
3075
|
+
strategy: input.strategy,
|
|
3076
|
+
fallbackReason: input.fallbackReason ?? null
|
|
3077
|
+
});
|
|
3078
|
+
throw new Error(executionBlockedReason);
|
|
3079
|
+
}
|
|
3080
|
+
async function selectSingleAccountOnly(registry, logger, accounts, mode) {
|
|
3081
|
+
logger.debug("selection.single_account.requirement", {
|
|
3082
|
+
mode,
|
|
3083
|
+
accountCount: accounts.length
|
|
3084
|
+
});
|
|
3085
|
+
if (accounts.length !== 1) {
|
|
3086
|
+
logger.warn("selection.single_account_invalid", {
|
|
3087
|
+
accountCount: accounts.length,
|
|
3088
|
+
mode
|
|
3089
|
+
});
|
|
3090
|
+
throw new Error(
|
|
3091
|
+
"The single-account strategy requires exactly one configured account."
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
3094
|
+
const [singleAccount] = accounts;
|
|
3095
|
+
if (!singleAccount) {
|
|
3096
|
+
throw new Error("No accounts configured.");
|
|
3097
|
+
}
|
|
3098
|
+
logger.info("selection.single_account", {
|
|
3099
|
+
accountId: singleAccount.id,
|
|
3100
|
+
label: singleAccount.label,
|
|
3101
|
+
mode
|
|
3102
|
+
});
|
|
3103
|
+
const defaultAccount = await registry.getDefaultAccount();
|
|
3104
|
+
if (defaultAccount?.id === singleAccount.id) {
|
|
3105
|
+
return singleAccount;
|
|
3106
|
+
}
|
|
3107
|
+
return registry.selectAccount(singleAccount.id);
|
|
3108
|
+
}
|
|
3109
|
+
function createUnavailableEntries(accounts, defaultAccount, selectedAccount) {
|
|
3110
|
+
return accounts.map((account) => ({
|
|
3111
|
+
account,
|
|
3112
|
+
failureCategory: null,
|
|
3113
|
+
failureMessage: null,
|
|
3114
|
+
isDefault: account.id === defaultAccount?.id,
|
|
3115
|
+
isEligibleForRanking: false,
|
|
3116
|
+
isSelected: account.id === selectedAccount?.id,
|
|
3117
|
+
rankingPosition: null,
|
|
3118
|
+
snapshot: null,
|
|
3119
|
+
source: "unavailable"
|
|
3120
|
+
}));
|
|
3121
|
+
}
|
|
3122
|
+
function createExperimentalEntries(input) {
|
|
3123
|
+
return input.probeResults.map((entry) => ({
|
|
3124
|
+
account: entry.account,
|
|
3125
|
+
failureCategory: entry.ok ? null : entry.category,
|
|
3126
|
+
failureMessage: entry.ok ? null : entry.message,
|
|
3127
|
+
isDefault: entry.account.id === input.defaultAccount?.id,
|
|
3128
|
+
isEligibleForRanking: entry.ok && entry.snapshot.status === "usable",
|
|
3129
|
+
isSelected: entry.account.id === input.selectedAccount?.id,
|
|
3130
|
+
rankingPosition: entry.ok ? resolveRankingPosition(input.selectedCandidateIds, entry.account.id) : null,
|
|
3131
|
+
snapshot: entry.ok ? entry.snapshot : null,
|
|
3132
|
+
source: entry.ok ? entry.source : "fresh"
|
|
3133
|
+
}));
|
|
3134
|
+
}
|
|
3135
|
+
function resolveRankingPosition(selectedCandidateIds, accountId) {
|
|
3136
|
+
const index = selectedCandidateIds.indexOf(accountId);
|
|
3137
|
+
if (index < 0) {
|
|
3138
|
+
return null;
|
|
3139
|
+
}
|
|
3140
|
+
return index + 1;
|
|
3141
|
+
}
|
|
3142
|
+
function compareExperimentalCandidates(input) {
|
|
3143
|
+
const dailyDelta = (input.right.snapshot.dailyRemaining ?? Number.NEGATIVE_INFINITY) - (input.left.snapshot.dailyRemaining ?? Number.NEGATIVE_INFINITY);
|
|
3144
|
+
if (dailyDelta !== 0) {
|
|
3145
|
+
return dailyDelta;
|
|
3146
|
+
}
|
|
3147
|
+
const weeklyDelta = (input.right.snapshot.weeklyRemaining ?? Number.NEGATIVE_INFINITY) - (input.left.snapshot.weeklyRemaining ?? Number.NEGATIVE_INFINITY);
|
|
3148
|
+
if (weeklyDelta !== 0) {
|
|
3149
|
+
return weeklyDelta;
|
|
2652
3150
|
}
|
|
2653
|
-
|
|
3151
|
+
const leftIsDefault = input.left.account.id === input.defaultAccountId;
|
|
3152
|
+
const rightIsDefault = input.right.account.id === input.defaultAccountId;
|
|
3153
|
+
if (leftIsDefault !== rightIsDefault) {
|
|
3154
|
+
return leftIsDefault ? -1 : 1;
|
|
3155
|
+
}
|
|
3156
|
+
return input.registryOrder.findIndex((account) => account.id === input.left.account.id) - input.registryOrder.findIndex((account) => account.id === input.right.account.id);
|
|
2654
3157
|
}
|
|
2655
|
-
function
|
|
2656
|
-
|
|
2657
|
-
|
|
3158
|
+
function logExperimentalFallbackSelection(logger, input) {
|
|
3159
|
+
logger.info("selection.experimental_fallback_selected", {
|
|
3160
|
+
fallbackReason: input.fallbackReason,
|
|
3161
|
+
mode: input.mode,
|
|
3162
|
+
selectedAccountId: input.selectedAccount?.id ?? null,
|
|
3163
|
+
selectedBy: input.selectedBy,
|
|
3164
|
+
rankingSignal: input.selectedBy === null ? null : "manual-default-fallback"
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// src/commands/account-list/run-account-list-command.ts
|
|
3169
|
+
async function runAccountListCommand(context) {
|
|
3170
|
+
const logger = createLogger({
|
|
3171
|
+
level: context.logging.level,
|
|
3172
|
+
name: "account_list",
|
|
3173
|
+
sink: context.logging.sink
|
|
3174
|
+
});
|
|
3175
|
+
const registry = createAccountRegistry({
|
|
3176
|
+
accountRoot: context.paths.accountRoot,
|
|
3177
|
+
logger,
|
|
3178
|
+
registryFile: context.paths.registryFile
|
|
3179
|
+
});
|
|
3180
|
+
const accounts = await registry.listAccounts();
|
|
3181
|
+
logger.info("command.start", {
|
|
3182
|
+
accountCount: accounts.length
|
|
3183
|
+
});
|
|
3184
|
+
if (accounts.length === 0) {
|
|
3185
|
+
context.io.stdout.write(
|
|
3186
|
+
[
|
|
3187
|
+
"No accounts configured.",
|
|
3188
|
+
"Add one with: codexes account add <label>"
|
|
3189
|
+
].join("\n") + "\n"
|
|
3190
|
+
);
|
|
3191
|
+
logger.info("command.empty");
|
|
3192
|
+
return 0;
|
|
2658
3193
|
}
|
|
2659
|
-
|
|
2660
|
-
|
|
3194
|
+
const summary = await resolveSelectionSummary({
|
|
3195
|
+
experimentalSelection: context.wrapperConfig.experimentalSelection,
|
|
3196
|
+
fetchImpl: fetch,
|
|
3197
|
+
logger,
|
|
3198
|
+
mode: "display-only",
|
|
3199
|
+
registry,
|
|
3200
|
+
selectionCacheFilePath: context.paths.selectionCacheFile,
|
|
3201
|
+
strategy: context.wrapperConfig.accountSelectionStrategy
|
|
3202
|
+
});
|
|
3203
|
+
const formattedSummary = formatSelectionSummary({
|
|
3204
|
+
capabilities: {
|
|
3205
|
+
stdoutIsTTY: context.output.stdoutIsTTY,
|
|
3206
|
+
useColor: context.output.stdoutIsTTY
|
|
3207
|
+
},
|
|
3208
|
+
logger,
|
|
3209
|
+
summary
|
|
3210
|
+
});
|
|
3211
|
+
context.io.stdout.write(formattedSummary);
|
|
3212
|
+
logger.info("summary_rendered", {
|
|
3213
|
+
mode: summary.mode,
|
|
3214
|
+
strategy: summary.strategy,
|
|
3215
|
+
useColor: context.output.stdoutIsTTY,
|
|
3216
|
+
selectedAccountId: summary.selectedAccount?.id ?? null,
|
|
3217
|
+
fallbackReason: summary.fallbackReason,
|
|
3218
|
+
executionBlockedReason: summary.executionBlockedReason
|
|
3219
|
+
});
|
|
3220
|
+
if (summary.fallbackReason || summary.executionBlockedReason) {
|
|
3221
|
+
logger.warn("fallback_announced", {
|
|
3222
|
+
fallbackReason: summary.fallbackReason,
|
|
3223
|
+
selectedAccountId: summary.selectedAccount?.id ?? null,
|
|
3224
|
+
executionBlockedReason: summary.executionBlockedReason
|
|
3225
|
+
});
|
|
2661
3226
|
}
|
|
2662
|
-
|
|
3227
|
+
logger.info("command.complete", {
|
|
3228
|
+
accountIds: summary.entries.map(({ account }) => account.id)
|
|
3229
|
+
});
|
|
3230
|
+
return 0;
|
|
2663
3231
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
3232
|
+
|
|
3233
|
+
// src/commands/account-remove/run-account-remove-command.ts
|
|
3234
|
+
import { rm as rm3 } from "node:fs/promises";
|
|
3235
|
+
|
|
3236
|
+
// src/accounts/account-resolution.ts
|
|
3237
|
+
function resolveAccountBySelector(input) {
|
|
3238
|
+
const normalizedSelector = input.selector.trim();
|
|
3239
|
+
const matches = input.accounts.filter(
|
|
3240
|
+
(account) => account.id === normalizedSelector || account.label.toLowerCase() === normalizedSelector.toLowerCase()
|
|
3241
|
+
);
|
|
3242
|
+
input.logger.debug("account_resolution.lookup", {
|
|
3243
|
+
selector: normalizedSelector,
|
|
3244
|
+
accountCount: input.accounts.length,
|
|
3245
|
+
matchCount: matches.length,
|
|
3246
|
+
matchedAccountIds: matches.map((account) => account.id)
|
|
3247
|
+
});
|
|
3248
|
+
if (matches.length === 0) {
|
|
3249
|
+
throw new Error(`No account matches "${normalizedSelector}".`);
|
|
2667
3250
|
}
|
|
2668
|
-
if (
|
|
2669
|
-
|
|
3251
|
+
if (matches.length > 1) {
|
|
3252
|
+
throw new Error(
|
|
3253
|
+
`Selector "${normalizedSelector}" matched multiple accounts; use the account id instead.`
|
|
3254
|
+
);
|
|
2670
3255
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
3256
|
+
const [match] = matches;
|
|
3257
|
+
if (!match) {
|
|
3258
|
+
throw new Error(`No account matches "${normalizedSelector}".`);
|
|
2673
3259
|
}
|
|
2674
|
-
return
|
|
3260
|
+
return match;
|
|
2675
3261
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
3262
|
+
|
|
3263
|
+
// src/commands/account-remove/run-account-remove-command.ts
|
|
3264
|
+
async function runAccountRemoveCommand(context, argv) {
|
|
3265
|
+
const logger = createLogger({
|
|
3266
|
+
level: context.logging.level,
|
|
3267
|
+
name: "account_remove",
|
|
3268
|
+
sink: context.logging.sink
|
|
3269
|
+
});
|
|
3270
|
+
if (argv.includes("--help")) {
|
|
3271
|
+
context.io.stdout.write(`${buildAccountRemoveHelpText()}
|
|
3272
|
+
`);
|
|
3273
|
+
logger.info("help.rendered");
|
|
3274
|
+
return 0;
|
|
2686
3275
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
if (typeof value === "string") {
|
|
2691
|
-
const parsed = new Date(value);
|
|
2692
|
-
if (!Number.isNaN(parsed.valueOf())) {
|
|
2693
|
-
return parsed.toISOString();
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2697
|
-
const normalizedValue = value > 1e10 ? value : value * 1e3;
|
|
2698
|
-
const parsed = new Date(normalizedValue);
|
|
2699
|
-
if (!Number.isNaN(parsed.valueOf())) {
|
|
2700
|
-
return parsed.toISOString();
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
3276
|
+
const selector = argv[0]?.trim();
|
|
3277
|
+
if (!selector || argv.length > 1) {
|
|
3278
|
+
throw new Error(buildAccountRemoveHelpText());
|
|
2703
3279
|
}
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3280
|
+
const registry = createAccountRegistry({
|
|
3281
|
+
accountRoot: context.paths.accountRoot,
|
|
3282
|
+
logger,
|
|
3283
|
+
registryFile: context.paths.registryFile
|
|
3284
|
+
});
|
|
3285
|
+
const accounts = await registry.listAccounts();
|
|
3286
|
+
if (accounts.length === 0) {
|
|
3287
|
+
context.io.stdout.write("No accounts configured.\n");
|
|
3288
|
+
logger.info("command.empty");
|
|
3289
|
+
return 0;
|
|
2709
3290
|
}
|
|
2710
|
-
|
|
3291
|
+
const account = resolveAccountBySelector({ accounts, logger, selector });
|
|
3292
|
+
logger.info("command.start", {
|
|
3293
|
+
requestedSelector: selector,
|
|
3294
|
+
resolvedAccountId: account.id,
|
|
3295
|
+
label: account.label
|
|
3296
|
+
});
|
|
3297
|
+
await registry.removeAccount(account.id);
|
|
3298
|
+
await rm3(account.authDirectory, { force: true, recursive: true });
|
|
3299
|
+
context.io.stdout.write(`Removed account "${account.label}" (${account.id}).
|
|
3300
|
+
`);
|
|
3301
|
+
logger.info("command.complete", {
|
|
3302
|
+
requestedSelector: selector,
|
|
3303
|
+
resolvedAccountId: account.id
|
|
3304
|
+
});
|
|
3305
|
+
return 0;
|
|
2711
3306
|
}
|
|
2712
|
-
function
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
3307
|
+
function buildAccountRemoveHelpText() {
|
|
3308
|
+
return [
|
|
3309
|
+
"Usage:",
|
|
3310
|
+
" codexes account remove <account-id-or-label>"
|
|
3311
|
+
].join("\n");
|
|
2717
3312
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
3313
|
+
|
|
3314
|
+
// src/commands/account-use/run-account-use-command.ts
|
|
3315
|
+
async function runAccountUseCommand(context, argv) {
|
|
3316
|
+
const logger = createLogger({
|
|
3317
|
+
level: context.logging.level,
|
|
3318
|
+
name: "account_use",
|
|
3319
|
+
sink: context.logging.sink
|
|
3320
|
+
});
|
|
3321
|
+
if (argv.includes("--help")) {
|
|
3322
|
+
context.io.stdout.write(`${buildAccountUseHelpText()}
|
|
3323
|
+
`);
|
|
3324
|
+
logger.info("help.rendered");
|
|
3325
|
+
return 0;
|
|
2721
3326
|
}
|
|
2722
|
-
const
|
|
2723
|
-
|
|
2724
|
-
|
|
3327
|
+
const registry = createAccountRegistry({
|
|
3328
|
+
accountRoot: context.paths.accountRoot,
|
|
3329
|
+
logger,
|
|
3330
|
+
registryFile: context.paths.registryFile
|
|
3331
|
+
});
|
|
3332
|
+
const accounts = await registry.listAccounts();
|
|
3333
|
+
if (accounts.length === 0) {
|
|
3334
|
+
context.io.stdout.write(
|
|
3335
|
+
[
|
|
3336
|
+
"No accounts configured.",
|
|
3337
|
+
"Add one with: codexes account add <label>"
|
|
3338
|
+
].join("\n") + "\n"
|
|
3339
|
+
);
|
|
3340
|
+
logger.info("command.empty");
|
|
3341
|
+
return 0;
|
|
2725
3342
|
}
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
for (const value of values) {
|
|
2730
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
2731
|
-
return value;
|
|
2732
|
-
}
|
|
3343
|
+
const selector = argv[0]?.trim() ?? null;
|
|
3344
|
+
if (argv.length > 1) {
|
|
3345
|
+
throw new Error(buildAccountUseHelpText());
|
|
2733
3346
|
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
3347
|
+
let targetAccount = null;
|
|
3348
|
+
if (!selector) {
|
|
3349
|
+
if (accounts.length === 1) {
|
|
3350
|
+
const [singleAccount] = accounts;
|
|
3351
|
+
if (!singleAccount) {
|
|
3352
|
+
throw new Error("No accounts configured.");
|
|
3353
|
+
}
|
|
3354
|
+
targetAccount = singleAccount;
|
|
3355
|
+
logger.info("command.single_account_default", {
|
|
3356
|
+
resolvedAccountId: targetAccount.id,
|
|
3357
|
+
label: targetAccount.label
|
|
3358
|
+
});
|
|
3359
|
+
} else {
|
|
3360
|
+
throw new Error(
|
|
3361
|
+
"Multiple accounts exist. Specify which one to use: codexes account use <account-id-or-label>"
|
|
3362
|
+
);
|
|
2740
3363
|
}
|
|
3364
|
+
} else {
|
|
3365
|
+
targetAccount = resolveAccountBySelector({ accounts, logger, selector });
|
|
2741
3366
|
}
|
|
2742
|
-
|
|
3367
|
+
if (!targetAccount) {
|
|
3368
|
+
throw new Error("Could not resolve the account to use.");
|
|
3369
|
+
}
|
|
3370
|
+
logger.info("command.start", {
|
|
3371
|
+
requestedSelector: selector,
|
|
3372
|
+
resolvedAccountId: targetAccount.id,
|
|
3373
|
+
label: targetAccount.label
|
|
3374
|
+
});
|
|
3375
|
+
const selectedAccount = await registry.selectAccount(targetAccount.id);
|
|
3376
|
+
context.io.stdout.write(
|
|
3377
|
+
`Using account "${selectedAccount.label}" (${selectedAccount.id}) as the default.
|
|
3378
|
+
`
|
|
3379
|
+
);
|
|
3380
|
+
logger.info("command.complete", {
|
|
3381
|
+
requestedSelector: selector,
|
|
3382
|
+
resolvedAccountId: selectedAccount.id
|
|
3383
|
+
});
|
|
3384
|
+
return 0;
|
|
2743
3385
|
}
|
|
2744
|
-
function
|
|
2745
|
-
return
|
|
3386
|
+
function buildAccountUseHelpText() {
|
|
3387
|
+
return [
|
|
3388
|
+
"Usage:",
|
|
3389
|
+
" codexes account use <account-id-or-label>",
|
|
3390
|
+
" codexes account use",
|
|
3391
|
+
"",
|
|
3392
|
+
"When only one account exists, `codexes account use` selects it automatically."
|
|
3393
|
+
].join("\n");
|
|
2746
3394
|
}
|
|
2747
3395
|
|
|
2748
|
-
// src/
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
const
|
|
2759
|
-
|
|
2760
|
-
|
|
3396
|
+
// src/runtime/lock/runtime-lock.ts
|
|
3397
|
+
import os3 from "node:os";
|
|
3398
|
+
import path11 from "node:path";
|
|
3399
|
+
import { mkdir as mkdir7, readFile as readFile8, rm as rm4, stat as stat5, writeFile as writeFile6 } from "node:fs/promises";
|
|
3400
|
+
var DEFAULT_WAIT_TIMEOUT_MS = 15e3;
|
|
3401
|
+
var DEFAULT_STALE_LOCK_MS = 5 * 60 * 1e3;
|
|
3402
|
+
var DEFAULT_POLL_INTERVAL_MS = 250;
|
|
3403
|
+
async function acquireRuntimeLock(input) {
|
|
3404
|
+
const lockRoot = path11.join(input.runtimeRoot, "lock");
|
|
3405
|
+
const ownerFile = path11.join(lockRoot, "owner.json");
|
|
3406
|
+
const waitTimeoutMs = input.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
3407
|
+
const staleLockMs = input.staleLockMs ?? DEFAULT_STALE_LOCK_MS;
|
|
3408
|
+
const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3409
|
+
const startedAt = Date.now();
|
|
3410
|
+
input.logger.info("runtime_lock.acquire.start", {
|
|
3411
|
+
lockRoot,
|
|
3412
|
+
waitTimeoutMs,
|
|
3413
|
+
staleLockMs,
|
|
3414
|
+
pollIntervalMs
|
|
2761
3415
|
});
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
};
|
|
2776
|
-
}
|
|
2777
|
-
try {
|
|
2778
|
-
const response = await fetchImpl(WHAM_USAGE_URL, {
|
|
2779
|
-
method: "GET",
|
|
2780
|
-
headers: buildUsageHeaders({
|
|
2781
|
-
accessToken: authState.state.accessToken,
|
|
2782
|
-
accountId: authState.state.accountId,
|
|
2783
|
-
useAccountIdHeader: input.probeConfig.useAccountIdHeader
|
|
2784
|
-
}),
|
|
2785
|
-
signal: AbortSignal.timeout(input.probeConfig.probeTimeoutMs)
|
|
2786
|
-
});
|
|
2787
|
-
input.logger.debug("selection.usage_probe.http_complete", {
|
|
2788
|
-
accountId: input.account.id,
|
|
2789
|
-
label: input.account.label,
|
|
2790
|
-
status: response.status,
|
|
2791
|
-
ok: response.ok
|
|
2792
|
-
});
|
|
2793
|
-
if (!response.ok) {
|
|
2794
|
-
input.logger.warn("selection.usage_probe.http_error", {
|
|
2795
|
-
accountId: input.account.id,
|
|
2796
|
-
label: input.account.label,
|
|
2797
|
-
status: response.status
|
|
3416
|
+
while (true) {
|
|
3417
|
+
try {
|
|
3418
|
+
await mkdir7(lockRoot);
|
|
3419
|
+
const owner = {
|
|
3420
|
+
pid: process.pid,
|
|
3421
|
+
host: os3.hostname(),
|
|
3422
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3423
|
+
};
|
|
3424
|
+
await writeFile6(ownerFile, JSON.stringify(owner, null, 2), "utf8");
|
|
3425
|
+
input.logger.info("runtime_lock.acquire.complete", {
|
|
3426
|
+
lockRoot,
|
|
3427
|
+
waitedMs: Date.now() - startedAt,
|
|
3428
|
+
owner
|
|
2798
3429
|
});
|
|
2799
3430
|
return {
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3431
|
+
async release() {
|
|
3432
|
+
input.logger.info("runtime_lock.release.start", { lockRoot });
|
|
3433
|
+
await rm4(lockRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
3434
|
+
input.logger.info("runtime_lock.release.complete", { lockRoot });
|
|
3435
|
+
}
|
|
2805
3436
|
};
|
|
3437
|
+
} catch (error) {
|
|
3438
|
+
if (!isAlreadyExistsError(error)) {
|
|
3439
|
+
input.logger.error("runtime_lock.acquire.failed", {
|
|
3440
|
+
lockRoot,
|
|
3441
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3442
|
+
});
|
|
3443
|
+
throw error;
|
|
3444
|
+
}
|
|
2806
3445
|
}
|
|
2807
|
-
const
|
|
2808
|
-
if (
|
|
2809
|
-
input.logger.warn("
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
bodyType: typeof body
|
|
3446
|
+
const lockAgeMs = await readLockAgeMs(lockRoot, ownerFile);
|
|
3447
|
+
if (lockAgeMs !== null && lockAgeMs > staleLockMs) {
|
|
3448
|
+
input.logger.warn("runtime_lock.stale_detected", {
|
|
3449
|
+
lockRoot,
|
|
3450
|
+
lockAgeMs
|
|
2813
3451
|
});
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
account: input.account,
|
|
2817
|
-
category: "invalid-response",
|
|
2818
|
-
message: "Usage probe returned a non-object JSON payload.",
|
|
2819
|
-
source: "fresh"
|
|
2820
|
-
};
|
|
3452
|
+
await rm4(lockRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
3453
|
+
continue;
|
|
2821
3454
|
}
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
input.logger.info("selection.usage_probe.success", {
|
|
2828
|
-
accountId: input.account.id,
|
|
2829
|
-
label: input.account.label,
|
|
2830
|
-
snapshotStatus: snapshot.status,
|
|
2831
|
-
dailyRemaining: snapshot.dailyRemaining,
|
|
2832
|
-
weeklyRemaining: snapshot.weeklyRemaining,
|
|
2833
|
-
limitReached: snapshot.limitReached
|
|
3455
|
+
const waitedMs = Date.now() - startedAt;
|
|
3456
|
+
input.logger.debug("runtime_lock.acquire.waiting", {
|
|
3457
|
+
lockRoot,
|
|
3458
|
+
waitedMs,
|
|
3459
|
+
lockAgeMs
|
|
2834
3460
|
});
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
};
|
|
2841
|
-
} catch (error) {
|
|
2842
|
-
if (isAbortError(error)) {
|
|
2843
|
-
input.logger.warn("selection.usage_probe.timeout", {
|
|
2844
|
-
accountId: input.account.id,
|
|
2845
|
-
label: input.account.label,
|
|
2846
|
-
timeoutMs: input.probeConfig.probeTimeoutMs
|
|
3461
|
+
if (waitedMs >= waitTimeoutMs) {
|
|
3462
|
+
input.logger.error("runtime_lock.acquire.timeout", {
|
|
3463
|
+
lockRoot,
|
|
3464
|
+
waitedMs,
|
|
3465
|
+
lockAgeMs
|
|
2847
3466
|
});
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
category: "timeout",
|
|
2852
|
-
message: `Usage probe timed out after ${input.probeConfig.probeTimeoutMs}ms.`,
|
|
2853
|
-
source: "fresh"
|
|
2854
|
-
};
|
|
3467
|
+
throw new Error(
|
|
3468
|
+
`Timed out waiting for the shared runtime lock after ${waitTimeoutMs}ms.`
|
|
3469
|
+
);
|
|
2855
3470
|
}
|
|
2856
|
-
|
|
2857
|
-
accountId: input.account.id,
|
|
2858
|
-
label: input.account.label,
|
|
2859
|
-
message: error instanceof Error ? error.message : String(error)
|
|
2860
|
-
});
|
|
2861
|
-
return {
|
|
2862
|
-
ok: false,
|
|
2863
|
-
account: input.account,
|
|
2864
|
-
category: "invalid-response",
|
|
2865
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2866
|
-
source: "fresh"
|
|
2867
|
-
};
|
|
3471
|
+
await sleep(pollIntervalMs);
|
|
2868
3472
|
}
|
|
2869
3473
|
}
|
|
2870
|
-
function
|
|
2871
|
-
const
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
headers.set("OpenAI-Account-ID", input.accountId);
|
|
3474
|
+
async function readLockAgeMs(lockRoot, ownerFile) {
|
|
3475
|
+
const ownerContents = await readFile8(ownerFile, "utf8").catch(() => null);
|
|
3476
|
+
if (ownerContents) {
|
|
3477
|
+
const parsed = JSON.parse(ownerContents);
|
|
3478
|
+
if (typeof parsed.createdAt === "string") {
|
|
3479
|
+
return Date.now() - new Date(parsed.createdAt).getTime();
|
|
3480
|
+
}
|
|
2878
3481
|
}
|
|
2879
|
-
|
|
3482
|
+
const lockStats = await stat5(lockRoot).catch(() => null);
|
|
3483
|
+
return lockStats ? Date.now() - lockStats.mtimeMs : null;
|
|
2880
3484
|
}
|
|
2881
|
-
function
|
|
2882
|
-
return error
|
|
3485
|
+
function isAlreadyExistsError(error) {
|
|
3486
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
2883
3487
|
}
|
|
2884
|
-
function
|
|
2885
|
-
return
|
|
3488
|
+
function sleep(durationMs) {
|
|
3489
|
+
return new Promise((resolve) => {
|
|
3490
|
+
setTimeout(resolve, durationMs);
|
|
3491
|
+
});
|
|
2886
3492
|
}
|
|
2887
3493
|
|
|
2888
|
-
// src/
|
|
2889
|
-
import { mkdir as mkdir8, readFile as
|
|
2890
|
-
import
|
|
2891
|
-
|
|
2892
|
-
async function
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
input.
|
|
2898
|
-
|
|
2899
|
-
|
|
3494
|
+
// src/runtime/activate-account/activate-account.ts
|
|
3495
|
+
import { copyFile as copyFile3, cp as cp3, mkdir as mkdir8, readFile as readFile9, rm as rm5, stat as stat6 } from "node:fs/promises";
|
|
3496
|
+
import path12 from "node:path";
|
|
3497
|
+
import { createHash } from "node:crypto";
|
|
3498
|
+
async function activateAccountIntoSharedRuntime(input) {
|
|
3499
|
+
const runtimePaths = resolveAccountRuntimePaths(input.runtimeContract, input.account.id);
|
|
3500
|
+
const accountStateRoot = runtimePaths.accountStateDirectory;
|
|
3501
|
+
const backupRoot = path12.join(runtimePaths.runtimeBackupDirectory, "active");
|
|
3502
|
+
input.logger.info("account_activation.start", {
|
|
3503
|
+
accountId: input.account.id,
|
|
3504
|
+
label: input.account.label,
|
|
3505
|
+
accountStateRoot,
|
|
3506
|
+
sharedCodexHome: input.sharedCodexHome,
|
|
3507
|
+
backupRoot
|
|
3508
|
+
});
|
|
3509
|
+
await rm5(backupRoot, { force: true, recursive: true }).catch(() => void 0);
|
|
3510
|
+
await mkdir8(backupRoot, { recursive: true });
|
|
3511
|
+
const accountRules = input.runtimeContract.fileRules.filter(
|
|
3512
|
+
(rule) => rule.classification === "account"
|
|
3513
|
+
);
|
|
3514
|
+
const authSourcePath = path12.join(accountStateRoot, "auth.json");
|
|
3515
|
+
if (!await pathExists4(authSourcePath)) {
|
|
3516
|
+
input.logger.error("account_activation.missing_auth", {
|
|
3517
|
+
accountId: input.account.id,
|
|
3518
|
+
authSourcePath
|
|
2900
3519
|
});
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3520
|
+
throw new Error(
|
|
3521
|
+
`Account "${input.account.label}" has no stored auth.json; add the account again.`
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
try {
|
|
3525
|
+
for (const rule of accountRules) {
|
|
3526
|
+
await backupRuntimeArtifact({
|
|
3527
|
+
backupRoot,
|
|
3528
|
+
logger: input.logger,
|
|
3529
|
+
rule,
|
|
3530
|
+
sharedCodexHome: input.sharedCodexHome
|
|
3531
|
+
});
|
|
3532
|
+
await replaceRuntimeArtifact({
|
|
3533
|
+
accountStateRoot,
|
|
3534
|
+
logger: input.logger,
|
|
3535
|
+
rule,
|
|
3536
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2906
3537
|
});
|
|
2907
|
-
return [];
|
|
2908
3538
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
cacheFilePath: input.cacheFilePath,
|
|
2913
|
-
backupPath,
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
input.logger.error("account_activation.failed", {
|
|
3541
|
+
accountId: input.account.id,
|
|
2914
3542
|
message: error instanceof Error ? error.message : String(error)
|
|
2915
3543
|
});
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
schemaVersion: USAGE_CACHE_SCHEMA_VERSION,
|
|
2923
|
-
entries: input.entries
|
|
2924
|
-
};
|
|
2925
|
-
const tempFile = `${input.cacheFilePath}.tmp`;
|
|
2926
|
-
const serialized = JSON.stringify(document, null, 2);
|
|
2927
|
-
await writeFile6(tempFile, serialized, "utf8");
|
|
2928
|
-
await rename2(tempFile, input.cacheFilePath);
|
|
2929
|
-
input.logger.debug("selection.usage_cache.persisted", {
|
|
2930
|
-
cacheFilePath: input.cacheFilePath,
|
|
2931
|
-
entryCount: input.entries.length
|
|
2932
|
-
});
|
|
2933
|
-
}
|
|
2934
|
-
function resolveFreshUsageCacheEntry(input) {
|
|
2935
|
-
const entry = input.entries.find((candidate) => candidate.accountId === input.accountId) ?? null;
|
|
2936
|
-
if (!entry) {
|
|
2937
|
-
input.logger.debug("selection.usage_cache.miss", {
|
|
2938
|
-
accountId: input.accountId,
|
|
2939
|
-
ttlMs: input.ttlMs
|
|
2940
|
-
});
|
|
2941
|
-
return null;
|
|
2942
|
-
}
|
|
2943
|
-
const ageMs = input.now - new Date(entry.cachedAt).valueOf();
|
|
2944
|
-
if (!Number.isFinite(ageMs) || ageMs > input.ttlMs) {
|
|
2945
|
-
input.logger.debug("selection.usage_cache.expired", {
|
|
2946
|
-
accountId: input.accountId,
|
|
2947
|
-
cachedAt: entry.cachedAt,
|
|
2948
|
-
ageMs: Number.isFinite(ageMs) ? ageMs : null,
|
|
2949
|
-
ttlMs: input.ttlMs
|
|
3544
|
+
await restoreSharedRuntimeFromBackup({
|
|
3545
|
+
account: input.account,
|
|
3546
|
+
backupRoot,
|
|
3547
|
+
logger: input.logger,
|
|
3548
|
+
runtimeContract: input.runtimeContract,
|
|
3549
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2950
3550
|
});
|
|
2951
|
-
|
|
3551
|
+
throw error;
|
|
2952
3552
|
}
|
|
2953
|
-
input.logger.
|
|
2954
|
-
accountId: input.
|
|
2955
|
-
|
|
2956
|
-
ageMs,
|
|
2957
|
-
ttlMs: input.ttlMs
|
|
3553
|
+
input.logger.info("account_activation.complete", {
|
|
3554
|
+
accountId: input.account.id,
|
|
3555
|
+
sharedCodexHome: input.sharedCodexHome
|
|
2958
3556
|
});
|
|
2959
|
-
return entry;
|
|
2960
|
-
}
|
|
2961
|
-
function normalizeUsageCacheDocument(value) {
|
|
2962
|
-
if (!isRecord4(value)) {
|
|
2963
|
-
throw new Error("Usage cache document is not an object.");
|
|
2964
|
-
}
|
|
2965
|
-
const schemaVersion = typeof value.schemaVersion === "number" ? value.schemaVersion : USAGE_CACHE_SCHEMA_VERSION;
|
|
2966
|
-
if (schemaVersion !== USAGE_CACHE_SCHEMA_VERSION) {
|
|
2967
|
-
throw new Error(`Unsupported usage cache schema version ${schemaVersion}.`);
|
|
2968
|
-
}
|
|
2969
|
-
const entries = Array.isArray(value.entries) ? value.entries.filter(isUsageCacheEntry) : [];
|
|
2970
3557
|
return {
|
|
2971
|
-
|
|
2972
|
-
|
|
3558
|
+
account: input.account,
|
|
3559
|
+
backupRoot,
|
|
3560
|
+
runtimeContract: input.runtimeContract,
|
|
3561
|
+
sharedCodexHome: input.sharedCodexHome,
|
|
3562
|
+
sourceAccountStateRoot: accountStateRoot
|
|
2973
3563
|
};
|
|
2974
3564
|
}
|
|
2975
|
-
function
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
// src/selection/usage-probe-coordinator.ts
|
|
2986
|
-
async function resolveAccountUsageSnapshots(input) {
|
|
2987
|
-
const now = Date.now();
|
|
2988
|
-
input.logger.info("selection.usage_probe_coordinator.start", {
|
|
2989
|
-
accountCount: input.accounts.length,
|
|
2990
|
-
cacheFilePath: input.cacheFilePath,
|
|
2991
|
-
cacheTtlMs: input.probeConfig.cacheTtlMs,
|
|
2992
|
-
timeoutMs: input.probeConfig.probeTimeoutMs
|
|
3565
|
+
async function syncSharedRuntimeBackToAccount(input) {
|
|
3566
|
+
const accountRules = input.session.runtimeContract.fileRules.filter(
|
|
3567
|
+
(rule) => rule.classification === "account"
|
|
3568
|
+
);
|
|
3569
|
+
input.logger.info("account_sync.start", {
|
|
3570
|
+
accountId: input.session.account.id,
|
|
3571
|
+
sharedCodexHome: input.session.sharedCodexHome,
|
|
3572
|
+
accountStateRoot: input.session.sourceAccountStateRoot
|
|
2993
3573
|
});
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
3574
|
+
for (const rule of accountRules) {
|
|
3575
|
+
await syncRuntimeArtifact({
|
|
3576
|
+
accountStateRoot: input.session.sourceAccountStateRoot,
|
|
3577
|
+
logger: input.logger,
|
|
3578
|
+
rule,
|
|
3579
|
+
sharedCodexHome: input.session.sharedCodexHome
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
input.logger.info("account_sync.complete", {
|
|
3583
|
+
accountId: input.session.account.id
|
|
2997
3584
|
});
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
accountId: account.id,
|
|
3003
|
-
entries: freshCacheEntries,
|
|
3004
|
-
logger: input.logger,
|
|
3005
|
-
now,
|
|
3006
|
-
ttlMs: input.probeConfig.cacheTtlMs
|
|
3007
|
-
});
|
|
3008
|
-
if (cached) {
|
|
3009
|
-
return {
|
|
3010
|
-
ok: true,
|
|
3011
|
-
account,
|
|
3012
|
-
snapshot: cached.snapshot,
|
|
3013
|
-
source: "cache"
|
|
3014
|
-
};
|
|
3015
|
-
}
|
|
3016
|
-
const fresh = await probeAccountUsage({
|
|
3017
|
-
account,
|
|
3018
|
-
fetchImpl: input.fetchImpl,
|
|
3019
|
-
logger: input.logger,
|
|
3020
|
-
probeConfig: input.probeConfig
|
|
3021
|
-
});
|
|
3022
|
-
if (fresh.ok) {
|
|
3023
|
-
upsertCacheEntry(freshCacheEntries, {
|
|
3024
|
-
accountId: account.id,
|
|
3025
|
-
accountLabel: account.label,
|
|
3026
|
-
cachedAt: new Date(now).toISOString(),
|
|
3027
|
-
snapshot: fresh.snapshot
|
|
3028
|
-
});
|
|
3029
|
-
}
|
|
3030
|
-
return fresh;
|
|
3031
|
-
})
|
|
3585
|
+
}
|
|
3586
|
+
async function restoreSharedRuntimeFromBackup(input) {
|
|
3587
|
+
const accountRules = input.runtimeContract.fileRules.filter(
|
|
3588
|
+
(rule) => rule.classification === "account"
|
|
3032
3589
|
);
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3590
|
+
input.logger.warn("account_activation.restore.start", {
|
|
3591
|
+
accountId: input.account.id,
|
|
3592
|
+
backupRoot: input.backupRoot,
|
|
3593
|
+
sharedCodexHome: input.sharedCodexHome
|
|
3037
3594
|
});
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3595
|
+
for (const rule of accountRules) {
|
|
3596
|
+
await restoreRuntimeArtifact({
|
|
3597
|
+
backupRoot: input.backupRoot,
|
|
3598
|
+
logger: input.logger,
|
|
3599
|
+
rule,
|
|
3600
|
+
sharedCodexHome: input.sharedCodexHome
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
input.logger.warn("account_activation.restore.complete", {
|
|
3604
|
+
accountId: input.account.id
|
|
3043
3605
|
});
|
|
3044
|
-
return resolutions;
|
|
3045
3606
|
}
|
|
3046
|
-
function
|
|
3047
|
-
const
|
|
3048
|
-
|
|
3049
|
-
|
|
3607
|
+
async function backupRuntimeArtifact(input) {
|
|
3608
|
+
const sourcePath = path12.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
|
|
3609
|
+
const backupPath = path12.join(input.backupRoot, normalizedPattern(input.rule.pathPattern));
|
|
3610
|
+
if (!await pathExists4(sourcePath)) {
|
|
3611
|
+
input.logger.debug("account_activation.backup.skip", {
|
|
3612
|
+
pathPattern: input.rule.pathPattern,
|
|
3613
|
+
sourcePath,
|
|
3614
|
+
reason: "missing"
|
|
3615
|
+
});
|
|
3050
3616
|
return;
|
|
3051
3617
|
}
|
|
3052
|
-
|
|
3618
|
+
await mkdir8(path12.dirname(backupPath), { recursive: true });
|
|
3619
|
+
if (isDirectoryPattern(input.rule)) {
|
|
3620
|
+
await cp3(sourcePath, backupPath, { recursive: true });
|
|
3621
|
+
} else {
|
|
3622
|
+
await copyFile3(sourcePath, backupPath);
|
|
3623
|
+
}
|
|
3624
|
+
input.logger.debug("account_activation.backup.complete", {
|
|
3625
|
+
pathPattern: input.rule.pathPattern,
|
|
3626
|
+
sourcePath,
|
|
3627
|
+
backupPath
|
|
3628
|
+
});
|
|
3053
3629
|
}
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3630
|
+
async function replaceRuntimeArtifact(input) {
|
|
3631
|
+
const sourcePath = path12.join(input.accountStateRoot, normalizedPattern(input.rule.pathPattern));
|
|
3632
|
+
const targetPath = path12.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
|
|
3633
|
+
await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
|
|
3634
|
+
if (!await pathExists4(sourcePath)) {
|
|
3635
|
+
input.logger.debug("account_activation.replace.skip", {
|
|
3636
|
+
pathPattern: input.rule.pathPattern,
|
|
3637
|
+
sourcePath,
|
|
3638
|
+
reason: "missing"
|
|
3639
|
+
});
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
await mkdir8(path12.dirname(targetPath), { recursive: true });
|
|
3643
|
+
if (isDirectoryPattern(input.rule)) {
|
|
3644
|
+
await cp3(sourcePath, targetPath, { recursive: true });
|
|
3645
|
+
} else {
|
|
3646
|
+
await copyFile3(sourcePath, targetPath);
|
|
3647
|
+
}
|
|
3648
|
+
input.logger.debug("account_activation.replace.complete", {
|
|
3649
|
+
pathPattern: input.rule.pathPattern,
|
|
3650
|
+
sourcePath,
|
|
3651
|
+
targetPath
|
|
3061
3652
|
});
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3653
|
+
}
|
|
3654
|
+
async function syncRuntimeArtifact(input) {
|
|
3655
|
+
const sourcePath = path12.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
|
|
3656
|
+
const targetPath = path12.join(input.accountStateRoot, normalizedPattern(input.rule.pathPattern));
|
|
3657
|
+
if (!await pathExists4(sourcePath)) {
|
|
3658
|
+
input.logger.debug("account_sync.skip", {
|
|
3659
|
+
pathPattern: input.rule.pathPattern,
|
|
3660
|
+
sourcePath,
|
|
3661
|
+
reason: "missing"
|
|
3662
|
+
});
|
|
3663
|
+
return;
|
|
3065
3664
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3665
|
+
const changed = await hasArtifactChanged(sourcePath, targetPath, isDirectoryPattern(input.rule));
|
|
3666
|
+
if (!changed) {
|
|
3667
|
+
input.logger.debug("account_sync.no_change", {
|
|
3668
|
+
pathPattern: input.rule.pathPattern,
|
|
3669
|
+
sourcePath,
|
|
3670
|
+
targetPath
|
|
3671
|
+
});
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
|
|
3675
|
+
await mkdir8(path12.dirname(targetPath), { recursive: true });
|
|
3676
|
+
if (isDirectoryPattern(input.rule)) {
|
|
3677
|
+
await cp3(sourcePath, targetPath, { recursive: true });
|
|
3678
|
+
} else {
|
|
3679
|
+
await copyFile3(sourcePath, targetPath);
|
|
3080
3680
|
}
|
|
3681
|
+
input.logger.info("account_sync.updated", {
|
|
3682
|
+
pathPattern: input.rule.pathPattern,
|
|
3683
|
+
sourcePath,
|
|
3684
|
+
targetPath
|
|
3685
|
+
});
|
|
3081
3686
|
}
|
|
3082
|
-
async function
|
|
3083
|
-
const
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3687
|
+
async function restoreRuntimeArtifact(input) {
|
|
3688
|
+
const backupPath = path12.join(input.backupRoot, normalizedPattern(input.rule.pathPattern));
|
|
3689
|
+
const targetPath = path12.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
|
|
3690
|
+
await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
|
|
3691
|
+
if (!await pathExists4(backupPath)) {
|
|
3692
|
+
input.logger.debug("account_activation.restore.skip", {
|
|
3693
|
+
pathPattern: input.rule.pathPattern,
|
|
3694
|
+
backupPath,
|
|
3695
|
+
reason: "missing"
|
|
3088
3696
|
});
|
|
3089
|
-
return
|
|
3697
|
+
return;
|
|
3090
3698
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
logger.info("selection.manual_default_fallback_single", {
|
|
3097
|
-
accountId: singleAccount.id,
|
|
3098
|
-
label: singleAccount.label
|
|
3099
|
-
});
|
|
3100
|
-
return registry.selectAccount(singleAccount.id);
|
|
3699
|
+
await mkdir8(path12.dirname(targetPath), { recursive: true });
|
|
3700
|
+
if (isDirectoryPattern(input.rule)) {
|
|
3701
|
+
await cp3(backupPath, targetPath, { recursive: true });
|
|
3702
|
+
} else {
|
|
3703
|
+
await copyFile3(backupPath, targetPath);
|
|
3101
3704
|
}
|
|
3102
|
-
logger.
|
|
3103
|
-
|
|
3705
|
+
input.logger.debug("account_activation.restore.complete_artifact", {
|
|
3706
|
+
pathPattern: input.rule.pathPattern,
|
|
3707
|
+
backupPath,
|
|
3708
|
+
targetPath
|
|
3104
3709
|
});
|
|
3105
|
-
throw new Error(
|
|
3106
|
-
"Multiple accounts are configured but no default account is selected. Use `codexes account use <account-id-or-label>` first."
|
|
3107
|
-
);
|
|
3108
3710
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3711
|
+
function isDirectoryPattern(rule) {
|
|
3712
|
+
return rule.pathPattern.endsWith("/**");
|
|
3713
|
+
}
|
|
3714
|
+
function normalizedPattern(pattern) {
|
|
3715
|
+
return pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
|
|
3716
|
+
}
|
|
3717
|
+
async function hasArtifactChanged(sourcePath, targetPath, isDirectory) {
|
|
3718
|
+
if (!await pathExists4(targetPath)) {
|
|
3719
|
+
return true;
|
|
3117
3720
|
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3721
|
+
if (isDirectory) {
|
|
3722
|
+
const [sourceHash2, targetHash2] = await Promise.all([
|
|
3723
|
+
hashDirectory(sourcePath),
|
|
3724
|
+
hashDirectory(targetPath)
|
|
3725
|
+
]);
|
|
3726
|
+
return sourceHash2 !== targetHash2;
|
|
3121
3727
|
}
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3728
|
+
const [sourceHash, targetHash] = await Promise.all([
|
|
3729
|
+
hashFile(sourcePath),
|
|
3730
|
+
hashFile(targetPath)
|
|
3731
|
+
]);
|
|
3732
|
+
return sourceHash !== targetHash;
|
|
3733
|
+
}
|
|
3734
|
+
async function hashFile(filePath) {
|
|
3735
|
+
const content = await readFile9(filePath);
|
|
3736
|
+
return createHash("sha256").update(content).digest("hex");
|
|
3737
|
+
}
|
|
3738
|
+
async function hashDirectory(directoryPath) {
|
|
3739
|
+
const entries = await collectFiles(directoryPath);
|
|
3740
|
+
const hash = createHash("sha256");
|
|
3741
|
+
for (const entry of entries.sort()) {
|
|
3742
|
+
hash.update(entry.relativePath);
|
|
3743
|
+
hash.update(await readFile9(entry.absolutePath));
|
|
3129
3744
|
}
|
|
3130
|
-
return
|
|
3745
|
+
return hash.digest("hex");
|
|
3131
3746
|
}
|
|
3132
|
-
async function
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
hasSelectionCacheFilePath: Boolean(input.selectionCacheFilePath)
|
|
3137
|
-
});
|
|
3138
|
-
return selectManualDefaultAccount(input.registry, input.logger, input.accounts);
|
|
3747
|
+
async function collectFiles(root) {
|
|
3748
|
+
const rootStats = await stat6(root).catch(() => null);
|
|
3749
|
+
if (!rootStats) {
|
|
3750
|
+
return [];
|
|
3139
3751
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
accounts: input.accounts,
|
|
3143
|
-
cacheFilePath: input.selectionCacheFilePath,
|
|
3144
|
-
fetchImpl: input.fetchImpl,
|
|
3145
|
-
logger: input.logger,
|
|
3146
|
-
probeConfig: input.experimentalSelection
|
|
3147
|
-
});
|
|
3148
|
-
const failedProbes = probeResults.filter((entry) => !entry.ok);
|
|
3149
|
-
if (failedProbes.length > 0) {
|
|
3150
|
-
const eventName = failedProbes.length === probeResults.length ? "selection.experimental_fallback_all_probes_failed" : "selection.experimental_fallback_mixed_probe_outcomes";
|
|
3151
|
-
input.logger.warn(eventName, {
|
|
3152
|
-
failedAccountIds: failedProbes.map((entry) => entry.account.id),
|
|
3153
|
-
failureCategories: failedProbes.map((entry) => entry.category),
|
|
3154
|
-
successfulAccountIds: probeResults.filter((entry) => entry.ok).map((entry) => entry.account.id)
|
|
3155
|
-
});
|
|
3156
|
-
return selectManualDefaultAccount(input.registry, input.logger, input.accounts);
|
|
3752
|
+
if (!rootStats.isDirectory()) {
|
|
3753
|
+
return [{ absolutePath: root, relativePath: path12.basename(root) }];
|
|
3157
3754
|
}
|
|
3158
|
-
const
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
input.logger.info("selection.experimental_ranked", {
|
|
3168
|
-
candidateOrder: candidates.map((entry) => ({
|
|
3169
|
-
accountId: entry.account.id,
|
|
3170
|
-
label: entry.account.label,
|
|
3171
|
-
dailyRemaining: entry.snapshot.dailyRemaining,
|
|
3172
|
-
weeklyRemaining: entry.snapshot.weeklyRemaining,
|
|
3173
|
-
source: entry.source
|
|
3174
|
-
})),
|
|
3175
|
-
defaultAccountId: defaultAccount?.id ?? null
|
|
3176
|
-
});
|
|
3177
|
-
const selected = candidates[0];
|
|
3178
|
-
if (!selected) {
|
|
3179
|
-
const allExhausted = successfulProbes.every(
|
|
3180
|
-
(entry) => entry.snapshot.limitReached || entry.snapshot.status === "limit-reached"
|
|
3755
|
+
const results = [];
|
|
3756
|
+
const stack = [root];
|
|
3757
|
+
while (stack.length > 0) {
|
|
3758
|
+
const current = stack.pop();
|
|
3759
|
+
if (!current) {
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
const entries = await import("node:fs/promises").then(
|
|
3763
|
+
(fs) => fs.readdir(current, { withFileTypes: true })
|
|
3181
3764
|
);
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
{
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
accountId: entry.account.id,
|
|
3188
|
-
snapshotStatus: entry.snapshot.status,
|
|
3189
|
-
limitReached: entry.snapshot.limitReached,
|
|
3190
|
-
dailyRemaining: entry.snapshot.dailyRemaining,
|
|
3191
|
-
weeklyRemaining: entry.snapshot.weeklyRemaining
|
|
3192
|
-
}))
|
|
3765
|
+
for (const entry of entries) {
|
|
3766
|
+
const absolutePath = path12.join(current, entry.name);
|
|
3767
|
+
if (entry.isDirectory()) {
|
|
3768
|
+
stack.push(absolutePath);
|
|
3769
|
+
continue;
|
|
3193
3770
|
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3771
|
+
if (entry.isFile()) {
|
|
3772
|
+
results.push({
|
|
3773
|
+
absolutePath,
|
|
3774
|
+
relativePath: path12.relative(root, absolutePath).split(path12.sep).join("/")
|
|
3775
|
+
});
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3196
3778
|
}
|
|
3197
|
-
|
|
3198
|
-
accountId: selected.account.id,
|
|
3199
|
-
label: selected.account.label,
|
|
3200
|
-
dailyRemaining: selected.snapshot.dailyRemaining,
|
|
3201
|
-
weeklyRemaining: selected.snapshot.weeklyRemaining,
|
|
3202
|
-
source: selected.source
|
|
3203
|
-
});
|
|
3204
|
-
return selected.account;
|
|
3779
|
+
return results;
|
|
3205
3780
|
}
|
|
3206
|
-
function
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
return
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
const leftIsDefault = input.left.account.id === input.defaultAccountId;
|
|
3216
|
-
const rightIsDefault = input.right.account.id === input.defaultAccountId;
|
|
3217
|
-
if (leftIsDefault !== rightIsDefault) {
|
|
3218
|
-
return leftIsDefault ? -1 : 1;
|
|
3781
|
+
async function pathExists4(targetPath) {
|
|
3782
|
+
try {
|
|
3783
|
+
await stat6(targetPath);
|
|
3784
|
+
return true;
|
|
3785
|
+
} catch (error) {
|
|
3786
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
3787
|
+
return false;
|
|
3788
|
+
}
|
|
3789
|
+
throw error;
|
|
3219
3790
|
}
|
|
3220
|
-
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
// src/process/spawn-codex-command.ts
|
|
3794
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3795
|
+
async function spawnCodexCommand(input) {
|
|
3796
|
+
const launchSpec = await resolveCodexLaunchSpec(input.codexBinaryPath, input.argv);
|
|
3797
|
+
input.logger.info("spawn_codex.start", {
|
|
3798
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
3799
|
+
resolvedCommand: launchSpec.command,
|
|
3800
|
+
codexHome: input.codexHome,
|
|
3801
|
+
argv: launchSpec.args,
|
|
3802
|
+
stdinIsTTY: process.stdin.isTTY ?? false,
|
|
3803
|
+
stdoutIsTTY: process.stdout.isTTY ?? false,
|
|
3804
|
+
stderrIsTTY: process.stderr.isTTY ?? false
|
|
3805
|
+
});
|
|
3806
|
+
return new Promise((resolve, reject) => {
|
|
3807
|
+
const child = spawn2(launchSpec.command, launchSpec.args, {
|
|
3808
|
+
env: {
|
|
3809
|
+
...process.env,
|
|
3810
|
+
CODEX_HOME: input.codexHome
|
|
3811
|
+
},
|
|
3812
|
+
shell: false,
|
|
3813
|
+
stdio: "inherit",
|
|
3814
|
+
windowsHide: false
|
|
3815
|
+
});
|
|
3816
|
+
let settled = false;
|
|
3817
|
+
const forwardSignal = (signal) => {
|
|
3818
|
+
input.logger.warn("spawn_codex.parent_signal", {
|
|
3819
|
+
signal,
|
|
3820
|
+
pid: child.pid ?? null
|
|
3821
|
+
});
|
|
3822
|
+
child.kill(signal);
|
|
3823
|
+
};
|
|
3824
|
+
const signalHandlers = {
|
|
3825
|
+
SIGINT: () => forwardSignal("SIGINT"),
|
|
3826
|
+
SIGTERM: () => forwardSignal("SIGTERM")
|
|
3827
|
+
};
|
|
3828
|
+
process.on("SIGINT", signalHandlers.SIGINT);
|
|
3829
|
+
process.on("SIGTERM", signalHandlers.SIGTERM);
|
|
3830
|
+
const cleanup = () => {
|
|
3831
|
+
process.off("SIGINT", signalHandlers.SIGINT);
|
|
3832
|
+
process.off("SIGTERM", signalHandlers.SIGTERM);
|
|
3833
|
+
};
|
|
3834
|
+
child.on("error", (error) => {
|
|
3835
|
+
if (settled) {
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
settled = true;
|
|
3839
|
+
cleanup();
|
|
3840
|
+
input.logger.error("spawn_codex.error", {
|
|
3841
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
3842
|
+
message: error.message
|
|
3843
|
+
});
|
|
3844
|
+
reject(error);
|
|
3845
|
+
});
|
|
3846
|
+
child.on("exit", (exitCode2, signal) => {
|
|
3847
|
+
if (settled) {
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
settled = true;
|
|
3851
|
+
cleanup();
|
|
3852
|
+
input.logger.info("spawn_codex.complete", {
|
|
3853
|
+
codexBinaryPath: input.codexBinaryPath,
|
|
3854
|
+
exitCode: exitCode2,
|
|
3855
|
+
signal
|
|
3856
|
+
});
|
|
3857
|
+
resolve(exitCode2 ?? 1);
|
|
3858
|
+
});
|
|
3859
|
+
});
|
|
3221
3860
|
}
|
|
3222
3861
|
|
|
3223
3862
|
// src/commands/root/run-root-command.ts
|
|
@@ -3240,6 +3879,7 @@ async function runRootCommand(context) {
|
|
|
3240
3879
|
createdRuntimeFiles: context.runtimeInitialization.createdFiles,
|
|
3241
3880
|
credentialStoreMode: context.wrapperConfig.credentialStoreMode,
|
|
3242
3881
|
accountSelectionStrategy: context.wrapperConfig.accountSelectionStrategy,
|
|
3882
|
+
accountSelectionStrategySource: context.wrapperConfig.accountSelectionStrategySource,
|
|
3243
3883
|
experimentalSelection: context.wrapperConfig.experimentalSelection,
|
|
3244
3884
|
codexBinaryPath: context.codexBinary.path,
|
|
3245
3885
|
recursionGuardSource: context.executablePath
|
|
@@ -3306,23 +3946,68 @@ async function runRootCommand(context) {
|
|
|
3306
3946
|
logger,
|
|
3307
3947
|
registryFile: context.paths.registryFile
|
|
3308
3948
|
});
|
|
3309
|
-
|
|
3310
|
-
|
|
3949
|
+
logger.info("selection.strategy_active", {
|
|
3950
|
+
strategy: context.wrapperConfig.accountSelectionStrategy,
|
|
3951
|
+
source: context.wrapperConfig.accountSelectionStrategySource
|
|
3952
|
+
});
|
|
3953
|
+
if (context.wrapperConfig.accountSelectionStrategy === "remaining-limit" || context.wrapperConfig.accountSelectionStrategy === "remaining-limit-experimental") {
|
|
3954
|
+
logger.info("selection.experimental_enabled", {
|
|
3311
3955
|
endpoint: "https://chatgpt.com/backend-api/wham/usage",
|
|
3312
3956
|
fallbackStrategy: "manual-default",
|
|
3313
3957
|
timeoutMs: context.wrapperConfig.experimentalSelection.probeTimeoutMs,
|
|
3314
3958
|
cacheTtlMs: context.wrapperConfig.experimentalSelection.cacheTtlMs,
|
|
3315
|
-
useAccountIdHeader: context.wrapperConfig.experimentalSelection.useAccountIdHeader
|
|
3959
|
+
useAccountIdHeader: context.wrapperConfig.experimentalSelection.useAccountIdHeader,
|
|
3960
|
+
source: context.wrapperConfig.accountSelectionStrategySource
|
|
3316
3961
|
});
|
|
3317
3962
|
}
|
|
3318
|
-
const
|
|
3963
|
+
const selectionSummary = await resolveSelectionSummary({
|
|
3319
3964
|
experimentalSelection: context.wrapperConfig.experimentalSelection,
|
|
3320
3965
|
fetchImpl: fetch,
|
|
3321
3966
|
logger,
|
|
3967
|
+
mode: "execution",
|
|
3322
3968
|
registry,
|
|
3323
3969
|
selectionCacheFilePath: context.paths.selectionCacheFile,
|
|
3324
3970
|
strategy: context.wrapperConfig.accountSelectionStrategy
|
|
3325
3971
|
});
|
|
3972
|
+
const activeAccount = selectionSummary.selectedAccount;
|
|
3973
|
+
if (!activeAccount || !selectionSummary.selectedBy) {
|
|
3974
|
+
logger.error("selection.execution_summary_incomplete", {
|
|
3975
|
+
strategy: selectionSummary.strategy,
|
|
3976
|
+
fallbackReason: selectionSummary.fallbackReason,
|
|
3977
|
+
executionBlockedReason: selectionSummary.executionBlockedReason
|
|
3978
|
+
});
|
|
3979
|
+
throw new Error(
|
|
3980
|
+
selectionSummary.executionBlockedReason ?? "Execution selection did not resolve an account."
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
const formattedSummary = formatSelectionSummary({
|
|
3984
|
+
capabilities: {
|
|
3985
|
+
stdoutIsTTY: context.output.stdoutIsTTY,
|
|
3986
|
+
useColor: context.output.stdoutIsTTY
|
|
3987
|
+
},
|
|
3988
|
+
logger,
|
|
3989
|
+
summary: selectionSummary
|
|
3990
|
+
});
|
|
3991
|
+
context.io.stdout.write(formattedSummary);
|
|
3992
|
+
logger.info("summary_rendered", {
|
|
3993
|
+
mode: selectionSummary.mode,
|
|
3994
|
+
strategy: selectionSummary.strategy,
|
|
3995
|
+
useColor: context.output.stdoutIsTTY,
|
|
3996
|
+
selectedAccountId: activeAccount.id,
|
|
3997
|
+
fallbackReason: selectionSummary.fallbackReason,
|
|
3998
|
+
executionBlockedReason: selectionSummary.executionBlockedReason
|
|
3999
|
+
});
|
|
4000
|
+
logger.info("selected_account_announced", {
|
|
4001
|
+
accountId: activeAccount.id,
|
|
4002
|
+
label: activeAccount.label,
|
|
4003
|
+
selectedBy: selectionSummary.selectedBy
|
|
4004
|
+
});
|
|
4005
|
+
if (selectionSummary.fallbackReason) {
|
|
4006
|
+
logger.warn("fallback_announced", {
|
|
4007
|
+
fallbackReason: selectionSummary.fallbackReason,
|
|
4008
|
+
selectedAccountId: activeAccount.id
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
3326
4011
|
const lock = await acquireRuntimeLock({
|
|
3327
4012
|
logger,
|
|
3328
4013
|
runtimeRoot: context.paths.runtimeRoot
|
|
@@ -3379,8 +4064,10 @@ function buildHelpText() {
|
|
|
3379
4064
|
"",
|
|
3380
4065
|
"Current status:",
|
|
3381
4066
|
" Account management and default Codex passthrough are implemented.",
|
|
3382
|
-
"
|
|
3383
|
-
"
|
|
4067
|
+
" Default selection strategy: remaining-limit.",
|
|
4068
|
+
" Available overrides: manual-default, single-account, remaining-limit.",
|
|
4069
|
+
" Legacy compatibility override: remaining-limit-experimental.",
|
|
4070
|
+
" Remaining-limit mode probes https://chatgpt.com/backend-api/wham/usage and falls back to manual-default when ranking is unreliable."
|
|
3384
4071
|
].join("\n");
|
|
3385
4072
|
}
|
|
3386
4073
|
|