@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/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: resolveAccountSelectionStrategy(input.env),
80
- experimentalSelection: resolveExperimentalSelectionConfig(input.env, input.logger)
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(env, logger) {
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 = resolveAccountSelectionStrategy(env) === "remaining-limit-experimental";
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
- switch (env.CODEXES_ACCOUNT_SELECTION_STRATEGY?.trim().toLowerCase()) {
129
+ function resolveAccountSelectionStrategy(env, logger) {
130
+ const rawOverride = env.CODEXES_ACCOUNT_SELECTION_STRATEGY?.trim().toLowerCase();
131
+ switch (rawOverride) {
124
132
  case "single-account":
125
- return "single-account";
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
- return "remaining-limit-experimental";
128
- case "manual-default":
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
- return "manual-default";
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
- return "manual-default";
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/accounts/account-resolution.ts
1668
- import { readFile as readFile6 } from "node:fs/promises";
1669
- import path9 from "node:path";
1670
- function resolveAccountBySelector(input) {
1671
- const normalizedSelector = input.selector.trim();
1672
- const matches = input.accounts.filter(
1673
- (account) => account.id === normalizedSelector || account.label.toLowerCase() === normalizedSelector.toLowerCase()
1674
- );
1675
- input.logger.debug("account_resolution.lookup", {
1676
- selector: normalizedSelector,
1677
- accountCount: input.accounts.length,
1678
- matchCount: matches.length,
1679
- matchedAccountIds: matches.map((account) => account.id)
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
- if (matches.length === 0) {
1682
- throw new Error(`No account matches "${normalizedSelector}".`);
1683
- }
1684
- if (matches.length > 1) {
1685
- throw new Error(
1686
- `Selector "${normalizedSelector}" matched multiple accounts; use the account id instead.`
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
- const [match] = matches;
1690
- if (!match) {
1691
- throw new Error(`No account matches "${normalizedSelector}".`);
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
- async function buildAccountPresentations(input) {
1696
- const presentations = [];
1697
- for (const account of input.accounts) {
1698
- presentations.push({
1699
- account,
1700
- ...await readAccountMetadataSummary(account, input.logger)
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
- return presentations;
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
- async function readAccountMetadataSummary(account, logger) {
1706
- const metadataFile = path9.join(account.authDirectory, "account.json");
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(metadataFile, "utf8");
1910
+ const raw = await readFile6(filePath, "utf8");
1709
1911
  const parsed = JSON.parse(raw);
1710
- const summary = {
1711
- authAccountId: typeof parsed.authAccountId === "string" ? parsed.authAccountId : null,
1712
- authMode: typeof parsed.authMode === "string" ? parsed.authMode : null
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("account_resolution.metadata_loaded", {
1715
- accountId: account.id,
1716
- metadataFile,
1717
- authAccountId: summary.authAccountId,
1718
- authMode: summary.authMode
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 summary;
1979
+ return result;
1721
1980
  } catch (error) {
1722
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
1723
- logger.debug("account_resolution.metadata_missing", {
1724
- accountId: account.id,
1725
- metadataFile
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 { authAccountId: null, authMode: null };
2001
+ return {
2002
+ ok: false,
2003
+ category: "malformed-json",
2004
+ filePath,
2005
+ message: error.message
2006
+ };
1728
2007
  }
1729
- logger.warn("account_resolution.metadata_failed", {
1730
- accountId: account.id,
1731
- metadataFile,
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 { authAccountId: null, authMode: null };
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/commands/account-list/run-account-list-command.ts
1739
- async function runAccountListCommand(context) {
1740
- const logger = createLogger({
1741
- level: context.logging.level,
1742
- name: "account_list",
1743
- sink: context.logging.sink
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 registry = createAccountRegistry({
1746
- accountRoot: context.paths.accountRoot,
1747
- logger,
1748
- registryFile: context.paths.registryFile
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 [accounts, defaultAccount] = await Promise.all([
1751
- registry.listAccounts(),
1752
- registry.getDefaultAccount()
1753
- ]);
1754
- logger.info("command.start", {
1755
- accountCount: accounts.length,
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
- if (accounts.length === 0) {
1759
- context.io.stdout.write(
1760
- [
1761
- "No accounts configured.",
1762
- "Add one with: codexes account add <label>"
1763
- ].join("\n") + "\n"
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.logger.warn("account_activation.restore.start", {
2114
- accountId: input.account.id,
2115
- backupRoot: input.backupRoot,
2116
- sharedCodexHome: input.sharedCodexHome
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
- for (const rule of accountRules) {
2119
- await restoreRuntimeArtifact({
2120
- backupRoot: input.backupRoot,
2121
- logger: input.logger,
2122
- rule,
2123
- sharedCodexHome: input.sharedCodexHome
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
- input.logger.warn("account_activation.restore.complete", {
2127
- accountId: input.account.id
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
- async function backupRuntimeArtifact(input) {
2131
- const sourcePath = path11.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
2132
- const backupPath = path11.join(input.backupRoot, normalizedPattern(input.rule.pathPattern));
2133
- if (!await pathExists4(sourcePath)) {
2134
- input.logger.debug("account_activation.backup.skip", {
2135
- pathPattern: input.rule.pathPattern,
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
- await mkdir7(path11.dirname(backupPath), { recursive: true });
2142
- if (isDirectoryPattern(input.rule)) {
2143
- await cp3(sourcePath, backupPath, { recursive: true });
2144
- } else {
2145
- await copyFile3(sourcePath, backupPath);
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.logger.debug("account_activation.backup.complete", {
2148
- pathPattern: input.rule.pathPattern,
2149
- sourcePath,
2150
- backupPath
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
- async function replaceRuntimeArtifact(input) {
2154
- const sourcePath = path11.join(input.accountStateRoot, normalizedPattern(input.rule.pathPattern));
2155
- const targetPath = path11.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
2156
- await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
2157
- if (!await pathExists4(sourcePath)) {
2158
- input.logger.debug("account_activation.replace.skip", {
2159
- pathPattern: input.rule.pathPattern,
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
- await mkdir7(path11.dirname(targetPath), { recursive: true });
2166
- if (isDirectoryPattern(input.rule)) {
2167
- await cp3(sourcePath, targetPath, { recursive: true });
2168
- } else {
2169
- await copyFile3(sourcePath, targetPath);
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
- input.logger.debug("account_activation.replace.complete", {
2172
- pathPattern: input.rule.pathPattern,
2173
- sourcePath,
2174
- targetPath
2175
- });
2229
+ return {
2230
+ raw: null,
2231
+ source: null
2232
+ };
2176
2233
  }
2177
- async function syncRuntimeArtifact(input) {
2178
- const sourcePath = path11.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
2179
- const targetPath = path11.join(input.accountStateRoot, normalizedPattern(input.rule.pathPattern));
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
- const changed = await hasArtifactChanged(sourcePath, targetPath, isDirectoryPattern(input.rule));
2189
- if (!changed) {
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
- await rm5(targetPath, { force: true, recursive: true }).catch(() => void 0);
2198
- await mkdir7(path11.dirname(targetPath), { recursive: true });
2199
- if (isDirectoryPattern(input.rule)) {
2200
- await cp3(sourcePath, targetPath, { recursive: true });
2201
- } else {
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
- input.logger.info("account_sync.updated", {
2205
- pathPattern: input.rule.pathPattern,
2206
- sourcePath,
2207
- targetPath
2208
- });
2247
+ return "legacy";
2209
2248
  }
2210
- async function restoreRuntimeArtifact(input) {
2211
- const backupPath = path11.join(input.backupRoot, normalizedPattern(input.rule.pathPattern));
2212
- const targetPath = path11.join(input.sharedCodexHome, normalizedPattern(input.rule.pathPattern));
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
- await mkdir7(path11.dirname(targetPath), { recursive: true });
2223
- if (isDirectoryPattern(input.rule)) {
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.logger.debug("account_activation.restore.complete_artifact", {
2229
- pathPattern: input.rule.pathPattern,
2230
- backupPath,
2231
- targetPath
2232
- });
2256
+ if (input.dailyRemaining === null && input.weeklyRemaining === null) {
2257
+ return "missing-usage-data";
2258
+ }
2259
+ return "usable";
2233
2260
  }
2234
- function isDirectoryPattern(rule) {
2235
- return rule.pathPattern.endsWith("/**");
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 normalizedPattern(pattern) {
2238
- return pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
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
- async function hasArtifactChanged(sourcePath, targetPath, isDirectory) {
2241
- if (!await pathExists4(targetPath)) {
2242
- return true;
2291
+ function calculateResetAtFromSeconds(value) {
2292
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
2293
+ return null;
2243
2294
  }
2244
- if (isDirectory) {
2245
- const [sourceHash2, targetHash2] = await Promise.all([
2246
- hashDirectory(sourcePath),
2247
- hashDirectory(targetPath)
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
- const [sourceHash, targetHash] = await Promise.all([
2252
- hashFile(sourcePath),
2253
- hashFile(targetPath)
2254
- ]);
2255
- return sourceHash !== targetHash;
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
- async function hashFile(filePath) {
2258
- const content = await readFile8(filePath);
2259
- return createHash("sha256").update(content).digest("hex");
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
- async function hashDirectory(directoryPath) {
2262
- const entries = await collectFiles(directoryPath);
2263
- const hash = createHash("sha256");
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 hash.digest("hex");
2323
+ return Number((100 - percentUsed).toFixed(2));
2269
2324
  }
2270
- async function collectFiles(root) {
2271
- const rootStats = await stat6(root).catch(() => null);
2272
- if (!rootStats) {
2273
- return [];
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
- if (!rootStats.isDirectory()) {
2276
- return [{ absolutePath: root, relativePath: path11.basename(root) }];
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 results = [];
2279
- const stack = [root];
2280
- while (stack.length > 0) {
2281
- const current = stack.pop();
2282
- if (!current) {
2283
- continue;
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
- const entries = await import("node:fs/promises").then(
2286
- (fs) => fs.readdir(current, { withFileTypes: true })
2287
- );
2288
- for (const entry of entries) {
2289
- const absolutePath = path11.join(current, entry.name);
2290
- if (entry.isDirectory()) {
2291
- stack.push(absolutePath);
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 results;
2409
+ return null;
2303
2410
  }
2304
- async function pathExists4(targetPath) {
2305
- try {
2306
- await stat6(targetPath);
2307
- return true;
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
- // src/process/spawn-codex-command.ts
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/account-auth-state.ts
2386
- import { readFile as readFile9 } from "node:fs/promises";
2387
- import path12 from "node:path";
2388
- async function readAccountAuthState(input) {
2389
- const filePath = path12.join(input.account.authDirectory, "state", "auth.json");
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
- filePath
2430
+ timeoutMs: input.probeConfig.probeTimeoutMs,
2431
+ useAccountIdHeader: input.probeConfig.useAccountIdHeader
2394
2432
  });
2395
- try {
2396
- const raw = await readFile9(filePath, "utf8");
2397
- const parsed = JSON.parse(raw);
2398
- if (!isRecord(parsed)) {
2399
- input.logger.warn("selection.account_auth_state.unsupported_shape", {
2400
- accountId: input.account.id,
2401
- label: input.account.label,
2402
- filePath,
2403
- topLevelType: typeof parsed
2404
- });
2405
- return {
2406
- ok: false,
2407
- category: "unsupported-auth-shape",
2408
- filePath,
2409
- message: "auth.json is not a JSON object."
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
- input.logger.debug("selection.account_auth_state.read_complete", {
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
- filePath,
2460
- hasAccessToken: true,
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
- return result;
2466
- } catch (error) {
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
- filePath
2472
+ status: response.status
2472
2473
  });
2473
2474
  return {
2474
2475
  ok: false,
2475
- category: "missing-file",
2476
- filePath,
2477
- message: "auth.json was not found for the account profile."
2476
+ account: input.account,
2477
+ category: "http-error",
2478
+ message: `Usage probe returned HTTP ${response.status}.`,
2479
+ source: "fresh"
2478
2480
  };
2479
2481
  }
2480
- if (error instanceof SyntaxError) {
2481
- input.logger.warn("selection.account_auth_state.malformed_json", {
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
- filePath,
2485
- message: error.message
2487
+ bodyType: typeof body
2486
2488
  });
2487
2489
  return {
2488
2490
  ok: false,
2489
- category: "malformed-json",
2490
- filePath,
2491
- message: error.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
- input.logger.warn("selection.account_auth_state.unsupported_shape", {
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
- category: "unsupported-auth-shape",
2503
- filePath,
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 getNestedString(value, pathParts) {
2509
- let current = value;
2510
- for (const part of pathParts) {
2511
- if (!isRecord(current) || typeof current[part] === "undefined") {
2512
- return null;
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
- current = current[part];
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 resolveString(...values) {
2519
- for (const value of values) {
2520
- if (typeof value === "string" && value.trim().length > 0) {
2521
- return value;
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
- return null;
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/usage-normalize.ts
2534
- function normalizeWhamUsageResponse(input) {
2535
- input.logger.debug("selection.usage_normalize.start", {
2536
- accountIdHint: input.accountIdHint ?? null,
2537
- topLevelKeys: Object.keys(input.raw).sort()
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
- const daily = normalizeUsageWindow({
2540
- accountIdHint: input.accountIdHint,
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
- raw: resolveUsageWindow(input.raw, "daily"),
2543
- window: "daily"
2785
+ mode,
2786
+ registry: input.registry,
2787
+ selectionCacheFilePath: input.selectionCacheFilePath,
2788
+ strategy: input.strategy
2544
2789
  });
2545
- const weekly = normalizeUsageWindow({
2546
- accountIdHint: input.accountIdHint,
2547
- logger: input.logger,
2548
- raw: resolveUsageWindow(input.raw, "weekly"),
2549
- window: "weekly"
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
- const accountId = pickString(input.raw.account_id, input.raw.accountId, input.accountIdHint);
2552
- const allowed = typeof input.raw.allowed === "boolean" ? input.raw.allowed : true;
2553
- const limitReached = typeof input.raw.limit_reached === "boolean" ? input.raw.limit_reached : daily.limitReached || weekly.limitReached;
2554
- const status = classifyUsageStatus({
2555
- allowed,
2556
- dailyRemaining: daily.remaining,
2557
- limitReached,
2558
- weeklyRemaining: weekly.remaining
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
- const snapshot = {
2561
- accountId,
2562
- allowed,
2563
- limitReached,
2564
- dailyRemaining: daily.remaining,
2565
- weeklyRemaining: weekly.remaining,
2566
- dailyResetsAt: daily.resetsAt,
2567
- weeklyResetsAt: weekly.resetsAt,
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 normalizeUsageWindow(input) {
2592
- if (!input.raw) {
2593
- input.logger.debug("selection.usage_normalize.window_missing", {
2594
- accountIdHint: input.accountIdHint ?? null,
2595
- window: input.window
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
- limit: null,
2599
- used: null,
2600
- remaining: null,
2601
- limitReached: false,
2602
- resetsAt: null,
2603
- percentUsed: null,
2604
- source: null
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 limit = pickNumber(input.raw.limit);
2608
- const used = pickNumber(input.raw.used, calculateUsed(limit, pickNumber(input.raw.remaining)));
2609
- const remaining = pickNumber(
2610
- input.raw.remaining,
2611
- calculateRemaining(limit, used)
2612
- );
2613
- const limitReached = typeof input.raw.limit_reached === "boolean" ? input.raw.limit_reached : remaining !== null ? remaining <= 0 : false;
2614
- const percentUsed = pickNumber(
2615
- input.raw.percent_used,
2616
- input.raw.percentage_used,
2617
- calculatePercentUsed(limit, used, remaining)
2618
- );
2619
- const resetsAt = normalizeTimestamp(
2620
- input.raw.reset_at,
2621
- input.raw.resets_at,
2622
- input.raw.next_reset_at
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
- const source = resolveWindowSource(input.raw);
2625
- input.logger.debug("selection.usage_normalize.window_complete", {
2626
- accountIdHint: input.accountIdHint ?? null,
2627
- window: input.window,
2628
- limit,
2629
- used,
2630
- remaining,
2631
- limitReached,
2632
- percentUsed,
2633
- resetsAt,
2634
- source
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
- limit,
2638
- used,
2639
- remaining,
2640
- limitReached,
2641
- resetsAt,
2642
- percentUsed,
2643
- source
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 resolveUsageWindow(raw, window) {
2647
- const candidates = [raw[window], raw.usage?.[window], raw.quotas?.[window]];
2648
- for (const candidate of candidates) {
2649
- if (isRecord2(candidate)) {
2650
- return candidate;
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
- return null;
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 resolveWindowSource(raw) {
2656
- if (typeof raw.source === "string") {
2657
- return raw.source;
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
- if (typeof raw.kind === "string") {
2660
- return raw.kind;
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
- return null;
3227
+ logger.info("command.complete", {
3228
+ accountIds: summary.entries.map(({ account }) => account.id)
3229
+ });
3230
+ return 0;
2663
3231
  }
2664
- function classifyUsageStatus(input) {
2665
- if (!input.allowed) {
2666
- return "not-allowed";
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 (input.limitReached) {
2669
- return "limit-reached";
3251
+ if (matches.length > 1) {
3252
+ throw new Error(
3253
+ `Selector "${normalizedSelector}" matched multiple accounts; use the account id instead.`
3254
+ );
2670
3255
  }
2671
- if (input.dailyRemaining === null && input.weeklyRemaining === null) {
2672
- return "missing-usage-data";
3256
+ const [match] = matches;
3257
+ if (!match) {
3258
+ throw new Error(`No account matches "${normalizedSelector}".`);
2673
3259
  }
2674
- return "usable";
3260
+ return match;
2675
3261
  }
2676
- function describeUsageStatus(status) {
2677
- switch (status) {
2678
- case "not-allowed":
2679
- return "usage endpoint reported that the account is not allowed to launch";
2680
- case "limit-reached":
2681
- return "usage endpoint reported an exhausted limit window";
2682
- case "missing-usage-data":
2683
- return "usage endpoint did not expose enough quota fields to rank this account";
2684
- case "usable":
2685
- return "usage endpoint exposed enough quota fields to rank this account";
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
- function normalizeTimestamp(...values) {
2689
- for (const value of values) {
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
- return null;
2705
- }
2706
- function calculateRemaining(limit, used) {
2707
- if (limit === null || used === null) {
2708
- return null;
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
- return limit - used;
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 calculateUsed(limit, remaining) {
2713
- if (limit === null || remaining === null) {
2714
- return null;
2715
- }
2716
- return limit - remaining;
3307
+ function buildAccountRemoveHelpText() {
3308
+ return [
3309
+ "Usage:",
3310
+ " codexes account remove <account-id-or-label>"
3311
+ ].join("\n");
2717
3312
  }
2718
- function calculatePercentUsed(limit, used, remaining) {
2719
- if (limit === null || limit <= 0) {
2720
- return null;
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 numerator = used ?? calculateUsed(limit, remaining);
2723
- if (numerator === null) {
2724
- return null;
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
- return Number((numerator / limit * 100).toFixed(2));
2727
- }
2728
- function pickString(...values) {
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
- return null;
2735
- }
2736
- function pickNumber(...values) {
2737
- for (const value of values) {
2738
- if (typeof value === "number" && Number.isFinite(value)) {
2739
- return value;
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
- return null;
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 isRecord2(value) {
2745
- return typeof value === "object" && value !== null;
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/selection/usage-client.ts
2749
- var WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
2750
- async function probeAccountUsage(input) {
2751
- const fetchImpl = input.fetchImpl ?? fetch;
2752
- input.logger.info("selection.usage_probe.start", {
2753
- accountId: input.account.id,
2754
- label: input.account.label,
2755
- timeoutMs: input.probeConfig.probeTimeoutMs,
2756
- useAccountIdHeader: input.probeConfig.useAccountIdHeader
2757
- });
2758
- const authState = await readAccountAuthState({
2759
- account: input.account,
2760
- logger: input.logger
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
- if (!authState.ok) {
2763
- input.logger.warn("selection.usage_probe.auth_missing", {
2764
- accountId: input.account.id,
2765
- label: input.account.label,
2766
- category: authState.category,
2767
- filePath: authState.filePath
2768
- });
2769
- return {
2770
- ok: false,
2771
- account: input.account,
2772
- category: "auth-missing",
2773
- message: authState.message,
2774
- source: "fresh"
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
- ok: false,
2801
- account: input.account,
2802
- category: "http-error",
2803
- message: `Usage probe returned HTTP ${response.status}.`,
2804
- source: "fresh"
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 body = await response.json();
2808
- if (!isRecord3(body)) {
2809
- input.logger.warn("selection.usage_probe.invalid_response", {
2810
- accountId: input.account.id,
2811
- label: input.account.label,
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
- return {
2815
- ok: false,
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 snapshot = normalizeWhamUsageResponse({
2823
- accountIdHint: authState.state.accountId ?? input.account.id,
2824
- logger: input.logger,
2825
- raw: body
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
- return {
2836
- ok: true,
2837
- account: input.account,
2838
- snapshot,
2839
- source: "fresh"
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
- return {
2849
- ok: false,
2850
- account: input.account,
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
- input.logger.error("selection.usage_probe.request_failed", {
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 buildUsageHeaders(input) {
2871
- const headers = new Headers({
2872
- accept: "application/json",
2873
- authorization: `Bearer ${input.accessToken}`,
2874
- "user-agent": "codexes/0.1 experimental-usage-probe"
2875
- });
2876
- if (input.useAccountIdHeader && input.accountId) {
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
- return headers;
3482
+ const lockStats = await stat5(lockRoot).catch(() => null);
3483
+ return lockStats ? Date.now() - lockStats.mtimeMs : null;
2880
3484
  }
2881
- function isAbortError(error) {
2882
- return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
3485
+ function isAlreadyExistsError(error) {
3486
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
2883
3487
  }
2884
- function isRecord3(value) {
2885
- return typeof value === "object" && value !== null;
3488
+ function sleep(durationMs) {
3489
+ return new Promise((resolve) => {
3490
+ setTimeout(resolve, durationMs);
3491
+ });
2886
3492
  }
2887
3493
 
2888
- // src/selection/usage-cache.ts
2889
- import { mkdir as mkdir8, readFile as readFile10, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
2890
- import path13 from "node:path";
2891
- var USAGE_CACHE_SCHEMA_VERSION = 1;
2892
- async function loadUsageCache(input) {
2893
- try {
2894
- const raw = await readFile10(input.cacheFilePath, "utf8");
2895
- const parsed = JSON.parse(raw);
2896
- const normalized = normalizeUsageCacheDocument(parsed);
2897
- input.logger.debug("selection.usage_cache.load_success", {
2898
- cacheFilePath: input.cacheFilePath,
2899
- entryCount: normalized.entries.length
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
- return normalized.entries;
2902
- } catch (error) {
2903
- if (isNodeErrorWithCode2(error, "ENOENT")) {
2904
- input.logger.debug("selection.usage_cache.missing", {
2905
- cacheFilePath: input.cacheFilePath
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
- const backupPath = `${input.cacheFilePath}.corrupt-${Date.now()}`;
2910
- await rename2(input.cacheFilePath, backupPath).catch(() => void 0);
2911
- input.logger.warn("selection.usage_cache.corrupt", {
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
- return [];
2917
- }
2918
- }
2919
- async function persistUsageCache(input) {
2920
- await mkdir8(path13.dirname(input.cacheFilePath), { recursive: true });
2921
- const document = {
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
- return null;
3551
+ throw error;
2952
3552
  }
2953
- input.logger.debug("selection.usage_cache.hit", {
2954
- accountId: input.accountId,
2955
- cachedAt: entry.cachedAt,
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
- schemaVersion: USAGE_CACHE_SCHEMA_VERSION,
2972
- entries
3558
+ account: input.account,
3559
+ backupRoot,
3560
+ runtimeContract: input.runtimeContract,
3561
+ sharedCodexHome: input.sharedCodexHome,
3562
+ sourceAccountStateRoot: accountStateRoot
2973
3563
  };
2974
3564
  }
2975
- function isUsageCacheEntry(value) {
2976
- return isRecord4(value) && typeof value.accountId === "string" && typeof value.accountLabel === "string" && typeof value.cachedAt === "string" && isRecord4(value.snapshot);
2977
- }
2978
- function isRecord4(value) {
2979
- return typeof value === "object" && value !== null;
2980
- }
2981
- function isNodeErrorWithCode2(error, code) {
2982
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
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 cacheEntries = await loadUsageCache({
2995
- cacheFilePath: input.cacheFilePath,
2996
- logger: input.logger
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
- const freshCacheEntries = [...cacheEntries];
2999
- const resolutions = await Promise.all(
3000
- input.accounts.map(async (account) => {
3001
- const cached = resolveFreshUsageCacheEntry({
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
- await persistUsageCache({
3034
- cacheFilePath: input.cacheFilePath,
3035
- entries: freshCacheEntries,
3036
- logger: input.logger
3590
+ input.logger.warn("account_activation.restore.start", {
3591
+ accountId: input.account.id,
3592
+ backupRoot: input.backupRoot,
3593
+ sharedCodexHome: input.sharedCodexHome
3037
3594
  });
3038
- input.logger.info("selection.usage_probe_coordinator.complete", {
3039
- accountCount: input.accounts.length,
3040
- cacheHitCount: resolutions.filter((entry) => entry.ok && entry.source === "cache").length,
3041
- freshSuccessCount: resolutions.filter((entry) => entry.ok && entry.source === "fresh").length,
3042
- failureCount: resolutions.filter((entry) => !entry.ok).length
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 upsertCacheEntry(entries, nextEntry) {
3047
- const existingIndex = entries.findIndex((entry) => entry.accountId === nextEntry.accountId);
3048
- if (existingIndex >= 0) {
3049
- entries.splice(existingIndex, 1, nextEntry);
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
- entries.push(nextEntry);
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
- // src/selection/select-account.ts
3056
- async function selectAccountForExecution(input) {
3057
- const accounts = await input.registry.listAccounts();
3058
- input.logger.info("selection.start", {
3059
- strategy: input.strategy,
3060
- accountCount: accounts.length
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
- if (accounts.length === 0) {
3063
- input.logger.warn("selection.none");
3064
- throw new Error("No accounts configured. Add one with `codexes account add <label>`.");
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
- switch (input.strategy) {
3067
- case "manual-default":
3068
- return selectManualDefaultAccount(input.registry, input.logger, accounts);
3069
- case "single-account":
3070
- return selectSingleAccountOnly(input.registry, input.logger, accounts);
3071
- case "remaining-limit-experimental":
3072
- return selectExperimentalRemainingLimitAccount({
3073
- accounts,
3074
- experimentalSelection: input.experimentalSelection,
3075
- fetchImpl: input.fetchImpl,
3076
- logger: input.logger,
3077
- registry: input.registry,
3078
- selectionCacheFilePath: input.selectionCacheFilePath
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 selectManualDefaultAccount(registry, logger, accounts) {
3083
- const defaultAccount = await registry.getDefaultAccount();
3084
- if (defaultAccount) {
3085
- logger.info("selection.manual_default", {
3086
- accountId: defaultAccount.id,
3087
- label: defaultAccount.label
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 defaultAccount;
3697
+ return;
3090
3698
  }
3091
- if (accounts.length === 1) {
3092
- const [singleAccount] = accounts;
3093
- if (!singleAccount) {
3094
- throw new Error("No accounts configured.");
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.warn("selection.manual_default_missing", {
3103
- accountCount: accounts.length
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
- async function selectSingleAccountOnly(registry, logger, accounts) {
3110
- if (accounts.length !== 1) {
3111
- logger.warn("selection.single_account_invalid", {
3112
- accountCount: accounts.length
3113
- });
3114
- throw new Error(
3115
- "The single-account strategy requires exactly one configured account."
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
- const [singleAccount] = accounts;
3119
- if (!singleAccount) {
3120
- throw new Error("No accounts configured.");
3721
+ if (isDirectory) {
3722
+ const [sourceHash2, targetHash2] = await Promise.all([
3723
+ hashDirectory(sourcePath),
3724
+ hashDirectory(targetPath)
3725
+ ]);
3726
+ return sourceHash2 !== targetHash2;
3121
3727
  }
3122
- logger.info("selection.single_account", {
3123
- accountId: singleAccount.id,
3124
- label: singleAccount.label
3125
- });
3126
- const defaultAccount = await registry.getDefaultAccount();
3127
- if (defaultAccount?.id === singleAccount.id) {
3128
- return singleAccount;
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 registry.selectAccount(singleAccount.id);
3745
+ return hash.digest("hex");
3131
3746
  }
3132
- async function selectExperimentalRemainingLimitAccount(input) {
3133
- if (!input.experimentalSelection?.enabled || !input.selectionCacheFilePath) {
3134
- input.logger.warn("selection.experimental_config_missing", {
3135
- enabled: input.experimentalSelection?.enabled ?? false,
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
- const defaultAccount = await input.registry.getDefaultAccount();
3141
- const probeResults = await resolveAccountUsageSnapshots({
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 successfulProbes = probeResults.filter((entry) => entry.ok);
3159
- const candidates = successfulProbes.filter((entry) => entry.snapshot.status === "usable").sort(
3160
- (left, right) => compareExperimentalCandidates({
3161
- defaultAccountId: defaultAccount?.id ?? null,
3162
- left,
3163
- right,
3164
- registryOrder: input.accounts
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
- input.logger.warn(
3183
- allExhausted ? "selection.experimental_fallback_all_accounts_exhausted" : "selection.experimental_fallback_ambiguous_usage",
3184
- {
3185
- usableProbeCount: candidates.length,
3186
- probeStatuses: successfulProbes.map((entry) => ({
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
- return selectManualDefaultAccount(input.registry, input.logger, input.accounts);
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
- input.logger.info("selection.experimental_selected", {
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 compareExperimentalCandidates(input) {
3207
- const dailyDelta = (input.right.snapshot.dailyRemaining ?? Number.NEGATIVE_INFINITY) - (input.left.snapshot.dailyRemaining ?? Number.NEGATIVE_INFINITY);
3208
- if (dailyDelta !== 0) {
3209
- return dailyDelta;
3210
- }
3211
- const weeklyDelta = (input.right.snapshot.weeklyRemaining ?? Number.NEGATIVE_INFINITY) - (input.left.snapshot.weeklyRemaining ?? Number.NEGATIVE_INFINITY);
3212
- if (weeklyDelta !== 0) {
3213
- return weeklyDelta;
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
- return input.registryOrder.findIndex((account) => account.id === input.left.account.id) - input.registryOrder.findIndex((account) => account.id === input.right.account.id);
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
- if (context.wrapperConfig.accountSelectionStrategy === "remaining-limit-experimental") {
3310
- logger.warn("selection.experimental_enabled", {
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 activeAccount = await selectAccountForExecution({
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
- " Selection strategies: manual-default, single-account, remaining-limit-experimental.",
3383
- " Experimental mode probes https://chatgpt.com/backend-api/wham/usage and falls back to manual-default when ranking is unreliable."
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