Skip to content

refactor: share forecast and report command helpers#382

Closed
ndycode wants to merge 1 commit intomainfrom
fix/dedupe-forecast-report
Closed

refactor: share forecast and report command helpers#382
ndycode wants to merge 1 commit intomainfrom
fix/dedupe-forecast-report

Conversation

@ndycode
Copy link
Copy Markdown
Owner

@ndycode ndycode commented Apr 6, 2026

Problem

The forecast and report commands duplicated the same refreshed-account persistence flow and forecast-result serialization logic.

That increases maintenance cost and makes future fixes easy to miss in one path.

Fix

Extract the shared logic into a single helper module and keep both commands delegating to the same implementation.

Changes

  • added lib/codex-manager/forecast-report-shared.ts
    • shared refreshed-account patch persistence
    • shared forecast-result serialization
  • updated lib/codex-manager/commands/forecast.ts
  • updated lib/codex-manager/commands/report.ts

Validation

npx vitest run test/codex-manager-forecast-command.test.ts test/codex-manager-report-command.test.ts
Test Files  2 passed (2)
Tests      16 passed (16)

npx vitest run
Test Files  222 passed (222)
Tests      3313 passed (3313)

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this PR extracts the duplicated token-persistence and forecast-serialization logic from forecast.ts and report.ts into a new lib/codex-manager/forecast-report-shared.ts module. the extraction is structurally correct and a good maintenance improvement — but report.ts is missing the type AccountIdentityMatch and type RefreshedAccountPatch imports required for its explicit type annotations, which will break npm run typecheck. the new shared module also lacks dedicated unit tests.

  • report.ts missing type importsRefreshedAccountPatch and AccountIdentityMatch are annotated at lines 277/289 but not imported; forecast.ts imports them correctly and report.ts was overlooked — this is a guaranteed tsc failure
  • no unit tests for forecast-report-shared.ts — the EBUSY/EPERM retry contract and patch-application logic are critical windows-filesystem safety invariants per AGENTS.md; they need an isolated test file
  • duplicate write-retry constants in report.tsRETRYABLE_WRITE_CODES / isRetryableWriteError mirror the shared module's pattern; the two concerns are separate but could drift
  • non-atomic read-modify-write in persistRefreshedAccountPatch — no lock between the loadAccounts reload and saveAccountsWithRetry; concurrent forecast --live + report --live runs can race and drop a token patch, especially on windows where EBUSY retries extend the write window

Confidence Score: 2/5

not safe to merge — report.ts has missing type imports that will fail tsc

the logic extraction is correct but the omission of RefreshedAccountPatch and AccountIdentityMatch type imports in report.ts is a guaranteed typecheck failure; tests pass because vitest uses esbuild which strips types without checking them. additionally no unit tests exist for the new shared module and the non-atomic write in persistRefreshedAccountPatch is now the shared path for both commands without a concurrency guard.

lib/codex-manager/commands/report.ts (missing type imports — blocks tsc), lib/codex-manager/forecast-report-shared.ts (no unit tests, non-atomic write)

Important Files Changed

Filename Overview
lib/codex-manager/forecast-report-shared.ts new shared module; extraction is clean and logic is correct, but missing type exports cause a tsc failure in report.ts, no dedicated unit tests exist, and persistRefreshedAccountPatch has a non-atomic read-modify-write under concurrent command runs
lib/codex-manager/commands/report.ts missing type imports for RefreshedAccountPatch and AccountIdentityMatch at lines 277/289 — will fail tsc; also retains duplicate write-retry constants vs. the shared module
lib/codex-manager/commands/forecast.ts correctly delegates to shared helpers with complete type imports including AccountIdentityMatch and RefreshedAccountPatch; no issues found

Sequence Diagram

sequenceDiagram
    participant FC as forecast.ts
    participant RC as report.ts
    participant SH as forecast-report-shared.ts
    participant ST as Storage

    Note over FC,RC: both commands run --live; per-account loop (serial within each command)

    FC->>ST: loadAccounts() [initial]
    RC->>ST: loadAccounts() [initial]

    FC->>FC: hasUsableAccessToken? → false
    FC->>FC: queuedRefresh(refreshToken)
    FC->>SH: applyRefreshedAccountPatch(account, patch) [in-memory only]
    FC->>SH: persistRefreshedAccountPatch(storage, accountMatch, patch, deps)
    SH->>ST: loadAccounts() [reload latest to avoid clobbering concurrent writes]
    SH->>SH: structuredClone(latestStorage)
    SH->>SH: findMatchingAccountIndex × 2 (identity then patch fallback)
    SH->>SH: applyRefreshedAccountPatch(targetAccount, patch)
    SH->>ST: saveAccountsWithRetry [EBUSY/EPERM retry up to 3x, exponential backoff]
    SH-->>FC: return
    FC->>ST: fetchCodexQuotaSnapshot(probeAccessToken, probeAccountId)

    RC->>RC: hasUsableAccessToken? → false
    RC->>RC: queuedRefresh(refreshToken)
    RC->>SH: applyRefreshedAccountPatch(account, patch) [in-memory only]
    RC->>SH: persistRefreshedAccountPatch(storage, accountMatch, patch, deps)
    SH->>ST: loadAccounts() [reload latest]
    SH->>SH: structuredClone(latestStorage)
    SH->>SH: findMatchingAccountIndex × 2
    SH->>SH: applyRefreshedAccountPatch(targetAccount, patch)
    SH->>ST: saveAccountsWithRetry [EBUSY/EPERM retry up to 3x]
    SH-->>RC: return
    RC->>ST: fetchCodexQuotaSnapshot(probeAccessToken, probeAccountId)

    FC->>SH: serializeForecastResults(results, liveQuotaByIndex, refreshFailures, formatFn)
    RC->>SH: serializeForecastResults(results, liveQuotaByIndex, refreshFailures, formatFn)
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/codex-manager/commands/report.ts
Line: 15-18

Comment:
**Missing type imports for `RefreshedAccountPatch` and `AccountIdentityMatch`**

`report.ts` uses both types as explicit annotations (`const refreshPatch: RefreshedAccountPatch` at line 277 and `const accountMatch: AccountIdentityMatch` at line 289) but neither is in the import block. `forecast.ts` imports them correctly with `type AccountIdentityMatch` and `type RefreshedAccountPatch` — this was missed during the extraction. `npm run typecheck` will fail with "Cannot find name" errors.

```suggestion
import {
	applyRefreshedAccountPatch,
	persistRefreshedAccountPatch,
	serializeForecastResults,
	type AccountIdentityMatch,
	type RefreshedAccountPatch,
} from "../forecast-report-shared.js";
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/codex-manager/commands/report.ts
Line: 53-89

Comment:
**Duplicate write-retry constants and helper not unified with shared module**

`RETRYABLE_WRITE_CODES` and `isRetryableWriteError` here implement the exact same two-code set (`EBUSY`/`EPERM`) and the same guard pattern as `isRetryableStorageWriteError` in `forecast-report-shared.ts`. these guard a different concern (output-file writes in `defaultWriteFile` vs. storage writes), but since both target the same windows antivirus lock codes, splitting them risks the sets drifting. consider importing `isRetryableStorageWriteError` from the shared module and reusing it here, or rename the local constant (`RETRYABLE_OUTPUT_WRITE_CODES`) to make the intentional split obvious to future maintainers.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/codex-manager/forecast-report-shared.ts
Line: 32-47

Comment:
**No dedicated unit tests for `forecast-report-shared.ts`**

the extracted helpers are exercised only indirectly through the command integration tests. per the windows filesystem safety rules in `AGENTS.md`, the `EBUSY`/`EPERM` retry contract is a critical invariant. a `test/codex-manager-forecast-report-shared.test.ts` should cover:
- `isRetryableStorageWriteError` with `EBUSY`, `EPERM`, and an unrelated error code
- `saveAccountsWithRetry` stops after 3 retries and re-throws non-retryable errors immediately
- `applyRefreshedAccountPatch` mutates `refreshToken`/`accessToken`/`expiresAt` unconditionally, and only touches `email`/`accountId`/`accountIdSource` when the patch fields are truthy
- `persistRefreshedAccountPatch` happy path (load → clone → find → apply → save) and the "account not found" throw path

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/codex-manager/forecast-report-shared.ts
Line: 63-88

Comment:
**Non-atomic read-modify-write — concurrent command runs can silently drop a token patch**

`persistRefreshedAccountPatch` calls `loadAccounts()` (line 70), clones the result, finds the target account, applies the patch, then calls `saveAccountsWithRetry`. there is no lock between the reload and the save. if `forecast --live` and `report --live` run simultaneously, the second caller's `saveAccountsWithRetry` will write a clone that does not contain the first caller's token update, silently dropping a refreshed `refreshToken`/`accessToken` — a token safety risk on windows where `EBUSY` retries mean the total write window is hundreds of milliseconds.

this was a pre-existing per-command risk; consolidating both commands into the same shared path without adding a concurrency guard makes the concurrent exposure larger. consider a per-storage-path write-queue similar to the queue already used in `unified-settings.ts`, or at minimum document the known race in a comment.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "refactor: share forecast and report comm..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

The forecast and report commands duplicated the same refreshed-account
persistence flow and forecast-result serialization logic. That increases
maintenance cost and makes future fixes easy to miss in one path.

Extract the shared pieces into a single helper module and keep both
commands delegating to the same implementation.
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

Warning

Rate limit exceeded

@ndycode has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 14 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 7 minutes and 14 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 337a8edb-1b9d-4307-9ec6-372a6c91aef1

📥 Commits

Reviewing files that changed from the base of the PR and between 478f44c and c559cea.

📒 Files selected for processing (3)
  • lib/codex-manager/commands/forecast.ts
  • lib/codex-manager/commands/report.ts
  • lib/codex-manager/forecast-report-shared.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dedupe-forecast-report
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/dedupe-forecast-report

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +15 to +18
applyRefreshedAccountPatch,
persistRefreshedAccountPatch,
serializeForecastResults,
} from "../forecast-report-shared.js";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 Missing type imports for RefreshedAccountPatch and AccountIdentityMatch

report.ts uses both types as explicit annotations (const refreshPatch: RefreshedAccountPatch at line 277 and const accountMatch: AccountIdentityMatch at line 289) but neither is in the import block. forecast.ts imports them correctly with type AccountIdentityMatch and type RefreshedAccountPatch — this was missed during the extraction. npm run typecheck will fail with "Cannot find name" errors.

Suggested change
applyRefreshedAccountPatch,
persistRefreshedAccountPatch,
serializeForecastResults,
} from "../forecast-report-shared.js";
import {
applyRefreshedAccountPatch,
persistRefreshedAccountPatch,
serializeForecastResults,
type AccountIdentityMatch,
type RefreshedAccountPatch,
} from "../forecast-report-shared.js";
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/codex-manager/commands/report.ts
Line: 15-18

Comment:
**Missing type imports for `RefreshedAccountPatch` and `AccountIdentityMatch`**

`report.ts` uses both types as explicit annotations (`const refreshPatch: RefreshedAccountPatch` at line 277 and `const accountMatch: AccountIdentityMatch` at line 289) but neither is in the import block. `forecast.ts` imports them correctly with `type AccountIdentityMatch` and `type RefreshedAccountPatch` — this was missed during the extraction. `npm run typecheck` will fail with "Cannot find name" errors.

```suggestion
import {
	applyRefreshedAccountPatch,
	persistRefreshedAccountPatch,
	serializeForecastResults,
	type AccountIdentityMatch,
	type RefreshedAccountPatch,
} from "../forecast-report-shared.js";
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@ndycode
Copy link
Copy Markdown
Owner Author

ndycode commented Apr 6, 2026

Superseded by #387, which rebuilds the full open PR stack onto one reviewed integration branch.

@ndycode
Copy link
Copy Markdown
Owner Author

ndycode commented Apr 6, 2026

Closing in favor of #387.

@ndycode ndycode closed this Apr 6, 2026
@ndycode ndycode deleted the fix/dedupe-forecast-report branch April 12, 2026 06:00
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.

1 participant