feat(codex): add --instances and --project flags for project-grouped usage reports by ambiguouStr · Pull Request #925 · ccusage/ccusage · GitHub
Skip to content

feat(codex): add --instances and --project flags for project-grouped usage reports#925

Closed
ambiguouStr wants to merge 4 commits into
ccusage:mainfrom
ambiguouStr:feat/codex-project-filter
Closed

feat(codex): add --instances and --project flags for project-grouped usage reports#925
ambiguouStr wants to merge 4 commits into
ccusage:mainfrom
ambiguouStr:feat/codex-project-filter

Conversation

@ambiguouStr

@ambiguouStr ambiguouStr commented Apr 3, 2026

Copy link
Copy Markdown

Summary

  • Add --instances (-i) flag to show usage breakdown grouped by project across daily, monthly, and session commands
  • Add --project (-p) flag to filter reports to a specific project (exact match on normalized path)
  • Extract project from Codex session_meta and turn_context cwd with mutable tracking for mid-session directory changes
  • Scan archived_sessions/ alongside live sessions/ directory
  • Handle mixed-project sessions: mark as (mixed) in default view, split by project when --instances is used
  • Safe home-path normalization (~/...) with boundary check to prevent corruption of similarly-prefixed paths

Verification

  • pnpm --filter @ccusage/codex typecheck passes
  • pnpm --filter @ccusage/codex run test — 26/26 tests pass (7 files)
  • Verified --instances table output with real local Codex data
  • Verified --project ~/path exact-match filtering
  • Verified --instances --json outputs stable { projects: {}, totals } schema

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added --project (-p) and --instances (-i) CLI options to filter and group reports by project.
    • Reports (daily, monthly, sessions) can include a project field and optionally group results by project.
  • Behavior Changes

    • Command outputs change shape in instances mode ({ projects: ... } vs legacy arrays).
    • Empty-report JSON adapts to instances mode.
  • Other

    • Optional archived-sessions discovery and automatic project extraction/normalization from session metadata.

…usage reports

Support per-project usage breakdown across daily, monthly, and session commands.
Extract project from session_meta/turn_context cwd with mutable tracking for
mid-session directory changes. Scan archived_sessions alongside live sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 3, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
apps/codex/src/monthly-report.ts (2)

76-79: Repeated type assertion suggests missing type definition.

The pattern (summary as MonthlyUsageSummary & { project?: string }) appears three times. Consider defining a proper type alias to avoid repetition and improve maintainability.

♻️ Suggested refactor

Define a local type near the function:

+type MonthlyUsageSummaryWithProject = MonthlyUsageSummary & { project?: string };

 const summaries = new Map<string, MonthlyUsageSummary>();
+// When groupByProject is true, summaries will have the project field populated

Then use the type consistently:

 if (groupByProject) {
-  (summary as MonthlyUsageSummary & { project?: string }).project =
+  (summary as MonthlyUsageSummaryWithProject).project =
     project ?? UNKNOWN_PROJECT_LABEL;
 }

Also applies to: 111-112, 138-138

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/monthly-report.ts` around lines 76 - 79, The repeated inline
cast (summary as MonthlyUsageSummary & { project?: string }) in
monthly-report.ts (seen around the summary assignments when groupByProject is
true) indicates a missing local type alias; create a single type alias (e.g.,
MonthlyUsageSummaryWithProject) near the top of the function or module that
extends MonthlyUsageSummary with an optional project?: string, then replace all
three occurrences of the inline intersection with that alias when assigning to
summary (used alongside groupByProject and UNKNOWN_PROJECT_LABEL) to remove
duplication and improve readability.

9-9: Import os is only used in test code.

The node:os import is only referenced within the import.meta.vitest test block (line 243). Consider moving this import inside the test block to clarify its scope and avoid loading the module in production.

♻️ Suggested refactor
-import os from 'node:os';

Then inside the test block:

import os from 'node:os';
const home = os.homedir();

Or use dynamic import within the test if preferred.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/monthly-report.ts` at line 9, Remove the top-level "import os
from 'node:os';" and instead import it only inside the test block that uses it
(the import.meta.vitest block around the tests near the bottom of
monthly-report.ts); either add "import os from 'node:os';" at the start of that
test block or perform a dynamic import there and call os.homedir() where the
test computes home — this ensures os is not loaded in production while keeping
the test reference local to import.meta.vitest.
apps/codex/src/commands/monthly.ts (1)

151-206: Table row rendering logic is duplicated between branches.

The row rendering code (lines 164-181 and 186-204) is nearly identical. The only difference is iterating over grouped projectRows vs flat rows. Consider extracting a helper function to reduce duplication.

♻️ Suggested refactor

Extract a helper function for rendering rows:

const renderRow = (row: MonthlyReportRow) => {
  const split = splitUsageTokens(row);
  totalsForDisplay.inputTokens += split.inputTokens;
  totalsForDisplay.outputTokens += split.outputTokens;
  totalsForDisplay.reasoningTokens += split.reasoningTokens;
  totalsForDisplay.cacheReadTokens += split.cacheReadTokens;
  totalsForDisplay.totalTokens += row.totalTokens;
  totalsForDisplay.costUSD += row.costUSD;
  table.push([
    row.month,
    formatModelsDisplayMultiline(formatModelsList(row.models)),
    formatNumber(split.inputTokens),
    formatNumber(split.outputTokens),
    formatNumber(split.reasoningTokens),
    formatNumber(split.cacheReadTokens),
    formatNumber(row.totalTokens),
    formatCurrency(row.costUSD),
  ]);
};

Then both branches become simpler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/commands/monthly.ts` around lines 151 - 206, The row-rendering
logic is duplicated between the grouped-branches and the flat-rows branch;
extract a single helper (e.g., renderRow or renderRows) that accepts a
MonthlyReportRow (or an array) and performs the splitUsageTokens call, updates
totalsForDisplay (inputTokens, outputTokens, reasoningTokens, cacheReadTokens,
totalTokens, costUSD) and pushes the formatted row into table using
formatModelsDisplayMultiline(formatModelsList(...)), formatNumber(...), and
formatCurrency(...); then call this helper inside the grouped loop (for each
projectRows) and in the else branch (for each rows) so
groupRowsByProject/sortedProjects and the flat iteration reuse the same render
function.
apps/codex/src/commands/daily.ts (1)

151-206: Same table rendering duplication as monthly.ts.

Both daily.ts and monthly.ts have nearly identical table rendering logic with duplicated code between the project-grouped and ungrouped branches. Consider extracting this pattern to a shared utility in command-utils.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/commands/daily.ts` around lines 151 - 206, The table rendering
in daily.ts duplicates the same project-grouped vs ungrouped logic found in
monthly.ts; extract a shared function (e.g., renderUsageTable) into
command-utils.ts that accepts parameters like rows, table, totalsForDisplay,
useInstances, and TABLE_COLUMN_COUNT and encapsulates the grouping
(groupRowsByProject), per-row token splitting (splitUsageTokens), totals
accumulation (totalsForDisplay.*), row formatting (formatModelsDisplayMultiline,
formatModelsList, formatNumber, formatCurrency) and separators
(addEmptySeparatorRow); then replace the duplicated blocks in daily.ts (and
monthly.ts) to call renderUsageTable(...) to avoid repetition and keep the same
behavior and symbols.
apps/codex/src/session-report.ts (2)

9-9: Import os is only used in test code.

Same as monthly-report.ts, the node:os import is only used within the test block. Consider moving it inside the test scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/session-report.ts` at line 9, The top-level import "import os
from 'node:os'" is only used in test code; move the import into the test scope
to avoid importing node:os in production. Locate the "os" import in
apps/codex/src/session-report.ts and remove the top-level import, then add a
local import inside the test block (e.g., within the describe/it or conditional
that references os) so only tests require the module.

166-172: Complex nested ternary reduces readability.

This ternary chain is difficult to parse at a glance. Consider extracting to a helper function for clarity.

♻️ Suggested refactor
+function resolveProjectLabel(
+  groupByProject: boolean,
+  summaryProject: string | undefined,
+  projectKeys: Set<string>,
+): string | undefined {
+  if (groupByProject) {
+    return summaryProject;
+  }
+  if (projectKeys.size !== 1) {
+    return MIXED_PROJECT_LABEL;
+  }
+  const singleKey = Array.from(projectKeys)[0];
+  return singleKey === UNKNOWN_PROJECT_LABEL ? undefined : singleKey;
+}

 rows.push({
   sessionId: summary.sessionId,
-  project: groupByProject
-    ? summary.project
-    : summary.projectKeys.size === 1
-      ? Array.from(summary.projectKeys)[0] === UNKNOWN_PROJECT_LABEL
-        ? undefined
-        : Array.from(summary.projectKeys)[0]
-      : MIXED_PROJECT_LABEL,
+  project: resolveProjectLabel(groupByProject, summary.project, summary.projectKeys),
   lastActivity: summary.lastTimestamp,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/session-report.ts` around lines 166 - 172, The nested ternary
that computes the project field is hard to read; extract its logic into a small
helper (e.g., getProjectValue or determineProjectLabel) that accepts summary and
groupByProject and returns either summary.project, undefined, a single project
key, or MIXED_PROJECT_LABEL based on the current checks (use
summary.projectKeys, UNKNOWN_PROJECT_LABEL, and groupByProject). Replace the
inline ternary in the project property with a call to this helper so the
conditional logic is isolated and clearer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/codex/src/command-utils.ts`:
- Around line 39-47: The accumulator object in groupRowsByProject can inherit
Object.prototype and be corrupted when project names like "__proto__" are used;
change the accumulator creation to use a null-prototype map (e.g., create
projects with Object.create(null)) and ensure when you initialize a bucket for a
new project key you always create an own array (the existing (projects[project]
??= []).push(row) logic should be updated to use the null-prototype projects so
the bucket creation and push operate on own properties of the map).

In `@apps/codex/src/data-loader.ts`:
- Around line 203-205: The code currently always includes the default
archivedSessionsDir in sessionDirs causing a warning when it doesn't exist;
change the directory existence/validation logic (the code that builds/filters
missingDirectories for sessionDirs) to treat archivedSessionsDir as optional
when callers did not provide explicit dirs (i.e., when providedDirs is
null/undefined): if a missing path equals archivedSessionsDir and providedDirs
is falsy, suppress the ENOENT "missing directory" warning for that path but
still surface/log any non-ENOENT errors for archivedSessionsDir and preserve
existing warnings for all paths when providedDirs was explicitly given; update
checks around defaultSessionsDir, archivedSessionsDir, and sessionDirs
accordingly.

In `@apps/codex/src/project-utils.ts`:
- Around line 19-36: The equality/startsWith checks in normalizeProjectPath fail
on Windows due to case differences; modify normalizeProjectPath to perform
case-insensitive comparisons on Windows by creating comparison copies (e.g.,
normalizedForCompare and homeForCompare) that call toLowerCase() when path.sep
=== "\\" and use those for the checks (normalizedForCompare === homeForCompare
and normalizedForCompare.startsWith(homeForCompare + path.sep)), but keep
returning the original normalized value (or '~' with the original slice) so
casing is preserved; also add a unit test for normalizeProjectPath that verifies
a home-path with different casing on Windows yields '~' (or the tilde-prefixed
path when under home).

---

Nitpick comments:
In `@apps/codex/src/commands/daily.ts`:
- Around line 151-206: The table rendering in daily.ts duplicates the same
project-grouped vs ungrouped logic found in monthly.ts; extract a shared
function (e.g., renderUsageTable) into command-utils.ts that accepts parameters
like rows, table, totalsForDisplay, useInstances, and TABLE_COLUMN_COUNT and
encapsulates the grouping (groupRowsByProject), per-row token splitting
(splitUsageTokens), totals accumulation (totalsForDisplay.*), row formatting
(formatModelsDisplayMultiline, formatModelsList, formatNumber, formatCurrency)
and separators (addEmptySeparatorRow); then replace the duplicated blocks in
daily.ts (and monthly.ts) to call renderUsageTable(...) to avoid repetition and
keep the same behavior and symbols.

In `@apps/codex/src/commands/monthly.ts`:
- Around line 151-206: The row-rendering logic is duplicated between the
grouped-branches and the flat-rows branch; extract a single helper (e.g.,
renderRow or renderRows) that accepts a MonthlyReportRow (or an array) and
performs the splitUsageTokens call, updates totalsForDisplay (inputTokens,
outputTokens, reasoningTokens, cacheReadTokens, totalTokens, costUSD) and pushes
the formatted row into table using
formatModelsDisplayMultiline(formatModelsList(...)), formatNumber(...), and
formatCurrency(...); then call this helper inside the grouped loop (for each
projectRows) and in the else branch (for each rows) so
groupRowsByProject/sortedProjects and the flat iteration reuse the same render
function.

In `@apps/codex/src/monthly-report.ts`:
- Around line 76-79: The repeated inline cast (summary as MonthlyUsageSummary &
{ project?: string }) in monthly-report.ts (seen around the summary assignments
when groupByProject is true) indicates a missing local type alias; create a
single type alias (e.g., MonthlyUsageSummaryWithProject) near the top of the
function or module that extends MonthlyUsageSummary with an optional project?:
string, then replace all three occurrences of the inline intersection with that
alias when assigning to summary (used alongside groupByProject and
UNKNOWN_PROJECT_LABEL) to remove duplication and improve readability.
- Line 9: Remove the top-level "import os from 'node:os';" and instead import it
only inside the test block that uses it (the import.meta.vitest block around the
tests near the bottom of monthly-report.ts); either add "import os from
'node:os';" at the start of that test block or perform a dynamic import there
and call os.homedir() where the test computes home — this ensures os is not
loaded in production while keeping the test reference local to
import.meta.vitest.

In `@apps/codex/src/session-report.ts`:
- Line 9: The top-level import "import os from 'node:os'" is only used in test
code; move the import into the test scope to avoid importing node:os in
production. Locate the "os" import in apps/codex/src/session-report.ts and
remove the top-level import, then add a local import inside the test block
(e.g., within the describe/it or conditional that references os) so only tests
require the module.
- Around line 166-172: The nested ternary that computes the project field is
hard to read; extract its logic into a small helper (e.g., getProjectValue or
determineProjectLabel) that accepts summary and groupByProject and returns
either summary.project, undefined, a single project key, or MIXED_PROJECT_LABEL
based on the current checks (use summary.projectKeys, UNKNOWN_PROJECT_LABEL, and
groupByProject). Replace the inline ternary in the project property with a call
to this helper so the conditional logic is isolated and clearer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecca52b9-b049-42a5-a266-79e9dd5ed008

📥 Commits

Reviewing files that changed from the base of the PR and between 61ee04d and b57f624.

📒 Files selected for processing (12)
  • apps/codex/src/_consts.ts
  • apps/codex/src/_shared-args.ts
  • apps/codex/src/_types.ts
  • apps/codex/src/command-utils.ts
  • apps/codex/src/commands/daily.ts
  • apps/codex/src/commands/monthly.ts
  • apps/codex/src/commands/session.ts
  • apps/codex/src/daily-report.ts
  • apps/codex/src/data-loader.ts
  • apps/codex/src/monthly-report.ts
  • apps/codex/src/project-utils.ts
  • apps/codex/src/session-report.ts

Comment thread apps/codex/src/command-utils.ts
Comment thread apps/codex/src/data-loader.ts
Comment on lines +19 to +36
export function normalizeProjectPath(value: string): string {
let normalized = path.normalize(expandHomeDirectory(value.trim()));
const home = path.normalize(os.homedir());
const root = path.parse(normalized).root;

while (normalized.length > root.length && normalized.endsWith(path.sep)) {
normalized = normalized.slice(0, -path.sep.length);
}

if (normalized === home) {
return '~';
}

if (normalized.startsWith(home + path.sep)) {
return `~${normalized.slice(home.length)}`;
}

return normalized;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files "apps/codex/src/project-utils.ts"

Repository: ryoppippi/ccusage

Length of output: 92


🏁 Script executed:

fd "project-utils.ts" --type f

Repository: ryoppippi/ccusage

Length of output: 92


🏁 Script executed:

cat -n apps/codex/src/project-utils.ts

Repository: ryoppippi/ccusage

Length of output: 2746


🏁 Script executed:

# Let me check if there are any related tests or utilities that handle Windows path comparison
rg "toLowerCase" apps/codex --type ts

Repository: ryoppippi/ccusage

Length of output: 134


🏁 Script executed:

# Check if there's any Windows-specific path handling elsewhere
rg "win32|Windows" apps/codex --type ts -A 2 -B 2

Repository: ryoppippi/ccusage

Length of output: 43


🏁 Script executed:

# Search for how normalizeProjectPath is used
rg "normalizeProjectPath" --type ts -A 2 -B 2

Repository: ryoppippi/ccusage

Length of output: 2895


🏁 Script executed:

# Check Node.js documentation on path.normalize behavior and case sensitivity
# Let's also check if there are any other path comparison functions being used
rg "path\." apps/codex --type ts | head -30

Repository: ryoppippi/ccusage

Length of output: 1464


🏁 Script executed:

# Check the actual platform detection and usage in the codebase
rg "process\.platform|platform ===" --type ts -B 2 -A 2

Repository: ryoppippi/ccusage

Length of output: 43


🏁 Script executed:

# Let's verify the behavior of path.normalize() on different platforms
# Check Node.js docs about case preservation
python3 << 'EOF'
import subprocess
import sys

# Test path.normalize behavior description
test_code = """
import path from 'node:path';

// On Windows, path.normalize preserves casing
const test1 = path.normalize('C:\\\\Users\\\\Me\\\\Project');
const test2 = path.normalize('c:\\\\users\\\\me\\\\project');

console.log('test1:', test1);
console.log('test2:', test2);
console.log('Equal?', test1 === test2);
"""

print("Node.js path.normalize() behavior:")
print("- Preserves casing on Windows (case-insensitive filesystem, case-preserving paths)")
print("- path.normalize('C:\\\\Users\\\\Me') !== path.normalize('c:\\\\users\\\\me')")
print("\nThis confirms the review comment's concern is technically valid.")
EOF

Repository: ryoppippi/ccusage

Length of output: 314


🏁 Script executed:

# Check if there are platform-specific tests or Windows path handling elsewhere
rg "path\." apps/codex/src/project-utils.ts -A 1 -B 1

Repository: ryoppippi/ccusage

Length of output: 623


🏁 Script executed:

# Check the actual Node.js path module behavior documentation by looking at usage patterns
# Let's see how paths are being compared elsewhere in the codebase
rg "===" apps/codex/src --type ts | grep -i path | head -20

Repository: ryoppippi/ccusage

Length of output: 160


🏁 Script executed:

# Let's verify if there are any other similar path comparison patterns in the codebase
rg "startsWith.*path|path.*startsWith" --type ts -B 2 -A 2

Repository: ryoppippi/ccusage

Length of output: 320


🏁 Script executed:

# Check if the suggested fix has already been applied to the file
sed -n '19,36p' apps/codex/src/project-utils.ts | grep -i "lowercase\|comparable"

Repository: ryoppippi/ccusage

Length of output: 43


Case-fold the home-path comparison on Windows.

On Windows, path.normalize() preserves casing, so the exact string equality checks at lines 28 and 32 fail when the user provides a path with different casing than os.homedir() returns. This breaks --project exact matching unless the user types the path with identical casing. Use toLowerCase() for comparison purposes on Windows (when path.sep === "\\"), and add a test case covering this scenario.

Suggested fix
 export function normalizeProjectPath(value: string): string {
 	let normalized = path.normalize(expandHomeDirectory(value.trim()));
 	const home = path.normalize(os.homedir());
 	const root = path.parse(normalized).root;
 
 	while (normalized.length > root.length && normalized.endsWith(path.sep)) {
 		normalized = normalized.slice(0, -path.sep.length);
 	}
+
+	const comparableNormalized = path.sep === "\\" ? normalized.toLowerCase() : normalized;
+	const comparableHome = path.sep === "\\" ? home.toLowerCase() : home;
 
-	if (normalized === home) {
+	if (comparableNormalized === comparableHome) {
 		return "~";
 	}
 
-	if (normalized.startsWith(home + path.sep)) {
+	if (comparableNormalized.startsWith(comparableHome + path.sep)) {
 		return `~${normalized.slice(home.length)}`;
 	}
 
 	return normalized;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function normalizeProjectPath(value: string): string {
let normalized = path.normalize(expandHomeDirectory(value.trim()));
const home = path.normalize(os.homedir());
const root = path.parse(normalized).root;
while (normalized.length > root.length && normalized.endsWith(path.sep)) {
normalized = normalized.slice(0, -path.sep.length);
}
if (normalized === home) {
return '~';
}
if (normalized.startsWith(home + path.sep)) {
return `~${normalized.slice(home.length)}`;
}
return normalized;
export function normalizeProjectPath(value: string): string {
let normalized = path.normalize(expandHomeDirectory(value.trim()));
const home = path.normalize(os.homedir());
const root = path.parse(normalized).root;
while (normalized.length > root.length && normalized.endsWith(path.sep)) {
normalized = normalized.slice(0, -path.sep.length);
}
const comparableNormalized = path.sep === "\\" ? normalized.toLowerCase() : normalized;
const comparableHome = path.sep === "\\" ? home.toLowerCase() : home;
if (comparableNormalized === comparableHome) {
return "~";
}
if (comparableNormalized.startsWith(comparableHome + path.sep)) {
return `~${normalized.slice(home.length)}`;
}
return normalized;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/project-utils.ts` around lines 19 - 36, The
equality/startsWith checks in normalizeProjectPath fail on Windows due to case
differences; modify normalizeProjectPath to perform case-insensitive comparisons
on Windows by creating comparison copies (e.g., normalizedForCompare and
homeForCompare) that call toLowerCase() when path.sep === "\\" and use those for
the checks (normalizedForCompare === homeForCompare and
normalizedForCompare.startsWith(homeForCompare + path.sep)), but keep returning
the original normalized value (or '~' with the original slice) so casing is
preserved; also add a unit test for normalizeProjectPath that verifies a
home-path with different casing on Windows yields '~' (or the tilde-prefixed
path when under home).

- groupRowsByProject now uses a null-prototype object so project names like
  `__proto__` or `constructor` cannot collide with inherited members.
- normalizeProjectPath compares the home prefix case-insensitively on Windows
  (case-sensitive elsewhere) so mixed-case drive letters still normalize to ~.
- loadTokenUsageEvents treats the auto-discovered archived_sessions/ directory
  as optional — missing it no longer surfaces in missingDirectories warnings,
  while explicitly provided sessionDirs still report as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/codex/src/data-loader.ts (1)

221-224: ⚠️ Potential issue | 🟠 Major

Only suppress auto-archived warnings for missing-directory (ENOENT) cases

Line 221 currently suppresses all failures for the optional archived directory, including permission/configuration failures. That can hide real filesystem problems.

Suggested fix
 		if (Result.isFailure(statResult)) {
-			if (!optionalDirs.has(directoryPath)) {
+			const errorCode =
+				typeof statResult.error === "object" &&
+				statResult.error != null &&
+				"code" in statResult.error
+					? (statResult.error as NodeJS.ErrnoException).code
+					: undefined;
+			const isOptionalMissing = optionalDirs.has(directoryPath) && errorCode === "ENOENT";
+			if (!isOptionalMissing) {
 				missingDirectories.push(directoryPath);
 			}
 			continue;
 		}
 
 		if (!statResult.value.isDirectory()) {
 			if (!optionalDirs.has(directoryPath)) {
 				missingDirectories.push(directoryPath);
 			}
 			continue;
 		}

Also applies to: 229-231

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/codex/src/data-loader.ts` around lines 221 - 224, The current check
treating any Result.isFailure(statResult) for optionalDirs as non-fatal hides
real FS errors; update the logic around the statResult handling (the block that
references statResult, optionalDirs, and missingDirectories) to only suppress
the error when the underlying error code equals 'ENOENT' (or its platform
equivalent) — for other failures, either rethrow or surface/log the error
instead of pushing directoryPath to missingDirectories; apply the same change to
the similar block that covers lines 229-231 so both statResult checks only
swallow ENOENT and not permission/configuration errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/codex/src/data-loader.ts`:
- Around line 221-224: The current check treating any
Result.isFailure(statResult) for optionalDirs as non-fatal hides real FS errors;
update the logic around the statResult handling (the block that references
statResult, optionalDirs, and missingDirectories) to only suppress the error
when the underlying error code equals 'ENOENT' (or its platform equivalent) —
for other failures, either rethrow or surface/log the error instead of pushing
directoryPath to missingDirectories; apply the same change to the similar block
that covers lines 229-231 so both statResult checks only swallow ENOENT and not
permission/configuration errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e94a33d-72d3-409c-8aa6-bec117693a69

📥 Commits

Reviewing files that changed from the base of the PR and between b57f624 and dba618c.

📒 Files selected for processing (3)
  • apps/codex/src/command-utils.ts
  • apps/codex/src/data-loader.ts
  • apps/codex/src/project-utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/codex/src/project-utils.ts

ambiguouStr and others added 2 commits April 7, 2026 10:08
Per follow-up review: the optional-dir suppression should skip only "not
found" errors (ENOENT/ENOTDIR). Permission failures and other stat errors
still propagate to missingDirectories so users can diagnose real problems.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@XiHeGoddess

Copy link
Copy Markdown

Been looking for exactly this. The per-project breakdown is what I need to figure out if my AI spend is actually going to the right things or just burning tokens on context loading.

@ambiguouStr did you end up implementing the --instances approach? Curious how you handled mixed-project sessions where Claude jumps between codebases mid-conversation.

@ambiguouStr

ambiguouStr commented Apr 15, 2026

Copy link
Copy Markdown
Author

Been looking for exactly this. The per-project breakdown is what I need to figure out if my AI spend is actually going to the right things or just burning tokens on context loading.

@ambiguouStr did you end up implementing the --instances approach? Curious how you handled mixed-project sessions where Claude jumps between codebases mid-conversation.

Hi @XiHeGoddess, thanks for the feedback.

Regarding --instances: Yes, it is included in this PR alongside --project. --instances produces a per-project breakdown of Codex usage, while --project <path> restricts the report to a single project.

Regarding mixed-project sessions: This case is handled. Each Codex turn emits a turn_context event carrying the current cwd. The data loader updates its tracked sessionCwd whenever that value changes, so subsequent token_count events are
attributed to the project that was active at the time of the turn. As a result, tokens are correctly split across projects even within a single JSONL file.

A dedicated test covers this behavior — updates project when turn_context cwd changes mid-session in apps/codex/src/data-loader.ts (around line 536). Please let me know if you run into any edge cases that aren't covered.

@ryoppippi

ryoppippi commented May 19, 2026

Copy link
Copy Markdown
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants