Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ Codex Desktop, its local app-server, Claude Code, and Anthropic's API remain gov

Optional Codex account management reads local Codex `auth.json` for explicit registration, switching, and identity checks while refreshing registered-account usage. Replacement happens only on an explicit switch. Additional accounts use official Codex browser login in a restricted temporary home; temporary credentials are removed after import or cancellation. The separate account vault and recovery transaction are DPAPI encrypted and ACL restricted to the Windows user; the login staging directory also permits SYSTEM. They are never sent to this project's developers. DPAPI protects data at rest; it does not protect against other software already running as the same Windows user. The active Codex credential remains owned by the live authentication file: saved snapshots never override its newer tokens during recovery. Uninstall intentionally preserves the separate account vault to avoid losing saved logins.

An explicit **사용량 조회** request for an inactive account temporarily places that account's credential in a separate, user-only private home for the official local app-server. It never replaces the active login. The helper may refresh authentication while reading usage; after its confirmed exit, updated credentials are atomically saved back to the DPAPI vault even on request failure or cancellation. A DPAPI recovery journal retains the selected commit credentials before the vault write, including recovery interrupted by another crash. Temporary plaintext files are removed after verified persistence. If the process crashes or cleanup cannot finish, the restricted staging directory and journal remain until **중단된 조회 복구** completes after Codex writers stop. Each account retains only its latest successful percentages, reset times, and check time; the application does not attribute usage to people or retain a usage timeline.
An explicit **사용량 조회** request for an inactive account temporarily places that account's credential in a separate, user-only private home for the official local app-server. It never replaces the active login. The helper may refresh authentication while reading usage; after its confirmed exit, updated credentials are atomically saved back to the DPAPI vault even on request failure or cancellation. A DPAPI recovery journal retains the selected commit credentials before the vault write, including recovery interrupted by another crash. Temporary plaintext files are removed after verified persistence. An interruption before credential commit retains staging and requires **중단된 조회 복구** after Codex writers stop. If only post-commit file cleanup remains, **임시 파일 정리** can retry while Codex stays open without modifying authentication or the vault. A failed cleanup retains only a safe error category and numeric code in the encrypted journal; raw exception text and paths are not logged. Each account retains only its latest successful percentages, reset times, and check time; the application does not attribute usage to people or retain a usage timeline.

When reporting a bug, do not attach Codex or Claude logs, credential/configuration files, tokens, or screenshots containing information you do not want to publish.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ Select the saved account and click **이 계정으로 전환**. The preparation

Select any registered account and click **사용량 조회** to fetch its latest weekly and 5-hour remaining usage, reset times, and last successful check time. **목록 갱신** only reloads saved values. Inactive accounts are queried only on an explicit click, using a short-lived official app-server in a private isolated home. Codex Desktop and the current account stay signed in; the current widget helper is not suspended. Active-account queries reuse that helper and require Codex Desktop to be open. **조회 취소**, a timeout, or a failed request preserves the last successful usage observation. Refreshed credentials are saved even if the usage request fails or is canceled. Expired logins can be renewed through **+ 다른 계정 추가** using the same inactive account; for the active account, sign in again in Codex.

Only explicit selections cause a switch. There is no automatic quota rotation, proxy, inactive-account background polling, quota pooling, usage history, or per-person attribution. Values past their reset time are marked **갱신 필요**. A pending switch transaction blocks polling until **미완료 전환 복구** reconciles it with actual live authentication. A query interrupted before credential cleanup offers **중단된 조회 복구**; finish Codex work and close remaining Codex writers before this exceptional recovery. Its encrypted journal preserves refreshed credentials before staging is removed. Recovery never overwrites newer live authentication. The management window supports display scaling, and its details scroll when the window is made smaller.
Only explicit selections cause a switch. There is no automatic quota rotation, proxy, inactive-account background polling, quota pooling, usage history, or per-person attribution. Values past their reset time are marked **갱신 필요**. A pending switch transaction blocks polling until **미완료 전환 복구** reconciles it with actual live authentication. A query interrupted before credentials are safely saved offers **중단된 조회 복구**; finish Codex work and close remaining Codex writers before this exceptional recovery. Its encrypted journal preserves refreshed credentials before staging is removed. Recovery never overwrites newer live authentication.

If credentials are already saved and only temporary files remain, the manager instead shows **임시 파일 정리 대기** with an **임시 파일 정리** button. This cleanup can run while Codex stays open and does not block account editing or switching. The next inactive usage request also retries cleanup before starting. A temporary file lock is retried automatically; a persistent failure displays its category and code. Cleanup does not turn a failed or canceled usage request into a success, and periodic list refresh preserves the original result. The management window supports display scaling, and its details scroll when the window is made smaller.

The first version supports local Windows file-based ChatGPT authentication. Unsupported keyring/managed configurations fail closed. The vault is stored separately at `%LOCALAPPDATA%\CodexWeeklyUsageIndicator.Accounts`; uninstall preserves it. Delete inactive accounts from the manager before removing the app if you no longer want their saved credentials. This convenience tool does not establish that any particular multi-account usage pattern is permitted by the service terms.

Expand Down
6 changes: 5 additions & 1 deletion docs/manual-accounts-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ Inactive requests hold the existing process-wide mutex and vault file lock on on

Rate-limit reads can implicitly refresh authentication even with `account/read.refreshToken=false`. Query staging therefore has a separate encrypted journal, never the disposable `login-*` cleanup path. The owned helper uses a non-breakaway kill-on-close Job and must exit before credential read-back. Success, protocol failure, and cancellation all save its validated same-account credentials before removing staging. Shutdown uncertainty retains staging and journal. Before writing the vault, the chosen auth and expected prior digest are durably written to the encrypted journal; recovery can be interrupted repeatedly and the desktop can independently change accounts without reverting a committed refresh. Active live authentication remains authoritative and is never written by this path.

Crash recovery is explicit and conservatively requires all potential Codex writers to exit, accounting for the narrow process-start/Job-assignment interval. A pending query is announced on startup and blocks account mutations, while active-account usage polling remains available. Missing/corrupt credentials or inconsistent vault revisions preserve recovery evidence and fail closed.
Before credential commit, crash recovery is explicit and conservatively requires all potential Codex writers to exit, accounting for the narrow process-start/Job-assignment interval. This state blocks account mutations while active-account usage polling remains available. Missing/corrupt credentials or inconsistent vault revisions preserve recovery evidence and fail closed.

After the vault write and credential read-back, a committed journal means only temporary-file cleanup remains; it does not imply a successful usage request. Version 1.6.1 keeps that state separate from authentication recovery. Cleanup never reads or rewrites the vault or live auth, so account rename/removal and active snapshot saves remain available. The explicit cleanup button does not suspend Desktop or its helper. The next inactive query first retries cleanup and starts only after the fixed home is reclaimed. Cleanup retries I/O failures over a bounded 1.5-second backoff, revalidating the tree each time; path-validation and access errors remain visible. Only a safe failure category and numeric code are retained. Original query success, failure, and cancellation messages survive periodic list refresh.

The isolated helper disables plugin and bundled-skill startup work through supported configuration overrides. Shutdown retains its Job handle, terminates the owned process group, confirms zero active descendants, then waits for the parent and output readers before releasing ownership. Process exit and successful file cleanup remain separate checks.

## Authentication and recovery

Expand Down
26 changes: 26 additions & 0 deletions docs/query-cleanup-delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Usage query cleanup fix — 1.6.1

A successful manual usage refresh in 1.6.0 could leave a committed query journal when temporary-file deletion failed. The manager treated every remaining journal as authentication recovery, disabled account actions, and replaced the original error with a generic banner during reload.

## Diagnosis and decision

The reported installation had a committed encrypted journal, matching saved credentials, and a new usage observation. Staged authentication had already been removed; remaining files included SQLite and plugin startup artifacts. The original deletion error was overwritten, so its exact file or locking process is not established.

Two inherited reviewers examined transaction semantics and helper lifetime. A fixed staging home with separate pre-commit recovery and post-commit cleanup was selected. A unique-home cleanup queue would introduce additional credential-bearing directories and migration states without evidence that they are necessary. Disabling startup work alone cannot address unrelated transient file locks, so it accompanies bounded deletion retries and explicit descendant shutdown verification.

Committed cleanup never reloads or replays saved authentication, including after the target is renamed or removed. Query outcomes and cleanup notices are independent; committed credentials do not prove a successful usage read. Only pre-commit recovery requires all Codex writers to stop.

A fresh reviewer inspected the actual candidate without the chair's preferred conclusion and found no remaining P0. The review checked blocking-state classification, possible replay after account deletion, and descendant shutdown with file cleanup. The original locking cause remains unconfirmed.

## Validation

- The required release build passed all 25 regression groups, with no compiler warnings or errors. The release EXE passed the username and absolute build-path scan in UTF-8 and UTF-16.
- File-lock cases cover automatic retry, persistent cleanup on success/failure/cancel, original-result preservation, account mutation during cleanup, and rejection of a new inactive query until its fixed home is reclaimed.
- A fake app-server leaves an independently running child holding a staging file after the parent exits. Job shutdown and the production cleanup path must terminate the child and remove staging.
- Native manager tests check enabled actions, a separate cleanup button with no query/suspend/resume, and preservation of the original failure through the five-second reload. Existing focus and account recovery coverage remains in the suite.
- An explicit live upgrade check cleaned the existing 1.6.0 committed journal through the new manager button while Desktop remained running. Both live authentication and the encrypted vault stayed byte-for-byte unchanged, with no query or helper restart.
- A second explicit live check queried the selected inactive account through the manager. A new observation was saved, current authentication and active snapshot remained unchanged, Desktop stayed running, and the helper/staging/journal were removed.

## Release and rollback

Build the complete test suite, review the actual change, and verify the GitHub draft EXE against both SHA256SUMS and the ZIP before installation and publication. Keep the existing interactive per-user scheduled-task installer and its final-path guard. Version 1.6.1 reads existing version-1 query journals; resolve pending recovery or cleanup before downgrading. No vault schema migration is required.
52 changes: 41 additions & 11 deletions src/AccountManagerForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ internal sealed class AccountManagerForm : Form
private bool _busy;
private bool _reloading;
private string? _desktopPath;
private UsageQueryStatus _usageQueryStatus = new(UsageQueryState.None);
private string? _operationMessage;
private bool _operationError;
private bool _operationSuccess;
internal bool IsOperationInProgress => _busy;
private SavedCodexAccount? Selected => _accounts.SelectedItem as SavedCodexAccount;

Expand All @@ -59,7 +63,7 @@ public AccountManagerForm(CodexAccountStore store, Func<Task> suspend, Action re
var root = AccountUiTheme.Stack(4);
root.Padding = new Padding(24);
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 76));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 78));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 100));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 28));

Expand Down Expand Up @@ -295,6 +299,18 @@ private void DeleteSelected()

private async Task RecoverAsync()
{
if (!_store.HasPendingRecovery && _usageQueryStatus.State == UsageQueryState.CleanupPending)
{
await RunAsync(false, "임시 파일을 정리하고 있습니다…", async () =>
{
await Task.Run(_store.RetryUsageCleanup);
Reload();
SetStatus(_usageQueryStatus.State == UsageQueryState.None ? "임시 파일 정리를 마쳤습니다."
: "임시 파일을 아직 정리하지 못했습니다. 잠시 후 다시 시도하세요.",
success: _usageQueryStatus.State == UsageQueryState.None);
}, acquireGate: false);
return;
}
await RunAsync(true, "복구 조건을 확인하고 있습니다…", async () =>
{
using var dialog = new AccountSwitchDialog("", "", recovery: true);
Expand Down Expand Up @@ -358,12 +374,11 @@ private bool Reload(string? selectId = null, bool quiet = false)
}
_count.Text = $"계정 {_items.Count}개";
_empty.Visible = _items.Count == 0; _detail.Visible = _items.Count > 0;
_usageQueryStatus = _store.GetUsageQueryStatus();
if (!quiet) { _operationMessage = null; _operationError = false; _operationSuccess = false; }
ShowSelected(); UpdateActions();
if (_store.HasPendingRecovery) SetStatus("미완료 전환이 있습니다. 복구를 완료하면 다시 사용할 수 있습니다.", error: true);
else if (_store.HasPendingUsageQuery) SetStatus("중단된 사용량 조회가 있습니다. 복구하여 로그인 정보를 보존해 주세요.", error: true);
else if (_items.Count > 0 && !_items.Any(a => a.IsActive)) SetStatus("현재 로그인은 아직 등록되지 않았습니다. 전환하려면 현재 계정을 먼저 등록하세요.");
else if (!quiet && _items.Count == 0) SetStatus("현재 계정을 먼저 등록하세요. 이름은 자동으로 지정됩니다.");
return !_store.HasPendingRecovery && !_store.HasPendingUsageQuery && (_items.Count == 0 || _items.Any(a => a.IsActive));
RenderStatus();
return !_store.HasPendingRecovery && _usageQueryStatus.State != UsageQueryState.RecoveryRequired && (_items.Count == 0 || _items.Any(a => a.IsActive));
}
catch (Exception ex) { SetStatus(ex.Message, error: true); return false; }
}
Expand Down Expand Up @@ -391,7 +406,7 @@ private void ShowSelected()

private void UpdateActions()
{
var pending = _store.HasPendingRecovery || _store.HasPendingUsageQuery;
var pending = _store.HasPendingRecovery || _usageQueryStatus.State == UsageQueryState.RecoveryRequired;
_accounts.Enabled = !_busy;
_refresh.Enabled = !_busy;
_readUsage.Enabled = !_busy && !pending && Selected is not null;
Expand All @@ -402,14 +417,29 @@ private void UpdateActions()
_rename.Enabled = !_busy && !pending && Selected is not null;
_switch.Enabled = !_busy && !pending && _items.Any(a => a.IsActive) && Selected is { IsActive: false };
_delete.Enabled = !_busy && !pending && Selected is { IsActive: false };
_recover.Visible = pending; _recover.Enabled = !_busy;
_recover.Text = _store.HasPendingUsageQuery ? "중단된 조회 복구" : "미완료 전환 복구";
_recover.Visible = pending || _usageQueryStatus.State == UsageQueryState.CleanupPending; _recover.Enabled = !_busy;
_recover.Text = _store.HasPendingRecovery ? "미완료 전환 복구" : _usageQueryStatus.State == UsageQueryState.CleanupPending ? "임시 파일 정리" : "중단된 조회 복구";
}

private void SetStatus(string message, bool error = false, bool success = false)
{
_status.Text = message;
_status.ForeColor = error ? AccountUiTheme.Error : success ? AccountUiTheme.Accent : AccountUiTheme.Text;
_operationMessage = message; _operationError = error; _operationSuccess = success;
RenderStatus();
}

private void RenderStatus()
{
var recovery = _store.HasPendingRecovery ? "미완료 전환이 있습니다. 복구를 완료해 주세요."
: _usageQueryStatus.State == UsageQueryState.RecoveryRequired ? "중단된 조회의 로그인 정보 복구가 필요합니다." : null;
var cleanup = _usageQueryStatus.State == UsageQueryState.CleanupPending
? "로그인 정보 저장 완료 · 임시 파일 정리 대기" + (_usageQueryStatus.Failure is { } failure ? $" ({failure.Summary})" : "") : null;
var fallback = _items.Count == 0 ? "현재 계정을 먼저 등록하세요. 이름은 자동으로 지정됩니다."
: !_items.Any(a => a.IsActive) ? "현재 로그인은 아직 등록되지 않았습니다. 전환하려면 현재 계정을 먼저 등록하세요."
: "계정을 선택해 상태를 확인하세요.";
_status.Text = string.Join("\n", new[] { _operationMessage, recovery, cleanup }.Where(text => text is not null));
if (_status.Text.Length == 0) _status.Text = fallback;
_status.ForeColor = _operationError || recovery is not null ? AccountUiTheme.Error
: _operationSuccess && cleanup is null ? AccountUiTheme.Accent : AccountUiTheme.Text;
}

protected override void Dispose(bool disposing)
Expand Down
Loading