osdy-pi 1.11.10 → 1.11.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,11 +8,13 @@ Osdy Pi gives [Pi](https://github.com/earendil-works/pi) a themed, responsive te
8
8
 
9
9
  **The Control Center consolidates Osdy controls in `/osdyConfig`**: ten categories cover appearance, Git, sounds, accounts, usage, and TODO/Agents provider settings. Account quota previews query a selected stored Codex profile without switching it. The header shows the installed Osdy package version when it fits; constrained layouts show only the centered mascot. See [Control Center in Pi](#control-center-in-pi) for confirmation and persistence boundaries.
10
10
 
11
+ **Codex banked resets show counts and per-reset local expiry** in `/usage` and the Control Center. Press **u** in `/usage` or select **Usage → Use or check banked reset** for Cancel-first, active-account use or pending-attempt checks; stored-profile previews stay read-only. See [Use or check a banked reset](#use-or-check-a-banked-reset).
12
+
11
13
  **1.11.0 adds three Spider-Man-inspired dark palettes**, bringing the bundle to **23 themes**. It retains the four-page local usage analytics introduced in 1.10.0, with shared period and profile/provider/model filters, keyboard navigation, and metadata-only history. These version introductions are historical, not a fixed latest-version claim. After npm Trusted Publishing is configured, pushes to main (including direct pushes) run checks and attempt to publish an automatically allocated patch to npm. Failed checks publish nothing; authentication or registry failures can also stop publication. Verify the resolved npm version before installing. Maintainers: [automatic npm publishing setup](https://github.com/OsdyOrtiz/Osdy-Pi/blob/main/docs/npm-publishing.md).
12
14
 
13
15
  | Area | What ships |
14
16
  | --- | --- |
15
- | Control Center | `/osdyConfig` opens ten categories, including confirmed TODO/Agents provider selection and explicit per-profile Codex quota previews. |
17
+ | Control Center | **Ctrl+Option+O** or `/osdyConfig` opens ten categories, including confirmed TODO/Agents provider selection and explicit per-profile Codex quota previews. |
16
18
  | Local analytics (1.10.0) | `/osdy-usage` opens Summary, Profiles, Models, and History pages for recorded tokens and estimated cost—not billing or subscription quota. |
17
19
  | Themes introduced in 1.11.0 | Spider-Man Classic, Miles Morales, and Spider-Verse add three distinct dark palettes. |
18
20
  | Themes introduced in 1.9.0 | Halloween and Halloween Killer add two dark palettes. |
@@ -20,7 +22,7 @@ Osdy Pi gives [Pi](https://github.com/earendil-works/pi) a themed, responsive te
20
22
  | Message cards | Pi-native colored user cards without emojis and plain assistant Markdown preceded by a separate `🦝` transcript row. |
21
23
  | Header and mascot | Independent header/mascot choices, an installed-package version label, and mascot-only fallback when full artwork cannot fit. |
22
24
  | Accounts | `/osdy-account` switches Codex profiles in place with atomic activation, rollback, bounded auth files, and process-safe locks. |
23
- | Quota | `/usage` and compact bars emphasize remaining Codex quota at warning (40% or less) and error (15% or less) thresholds. |
25
+ | Quota | **Ctrl+Option+U** or `/usage` opens subscription quota; compact bars emphasize remaining Codex quota at warning (40% or less) and error (15% or less) thresholds. |
24
26
  | TODO (starting in 1.5.0, opt-in) | Default-off first-party `todo` snapshots track the Pi session branch; `/todos` reads them without editing. ODD Markdown remains separate. |
25
27
 
26
28
  ## Prerequisites
@@ -57,11 +59,26 @@ Osdy Pi's own extension and themes are bundled—do not install them separately.
57
59
 
58
60
  ### Control Center in Pi
59
61
 
60
- Run **`/osdyConfig`** in interactive Pi to open the Osdy Control Center. The ten categories—**Theme, Header, Mascot, Editor, Git, Sounds, Account, Usage, TODO, and Agents**—provide inline controls or data in the same modal. Existing commands remain available.
62
+ Press **Ctrl+Option+O** or run **`/osdyConfig`** in interactive Pi to open the Osdy Control Center. The ten categories—**Theme, Header, Mascot, Editor, Git, Sounds, Account, Usage, TODO, and Agents**—provide inline controls or data in the same modal. Existing commands remain available.
63
+
64
+ | Shortcut | Panel | Command fallback |
65
+ | --- | --- | --- |
66
+ | **Ctrl+Option+O** | Osdy Control Center | `/osdyConfig` |
67
+ | **Ctrl+Option+U** | Active Codex subscription quota—not local token/cost analytics | `/usage` |
68
+
69
+ On macOS, **Option = Alt** in Pi's shortcut names: these bindings register as `ctrl+alt+o` and `ctrl+alt+u`. On other platforms, use **Ctrl+Alt+O** and **Ctrl+Alt+U**. Your terminal must send Option as **Alt/Meta** and support **Ctrl+Alt chords**; registration does not guarantee actual hardware delivery. Check **`/hotkeys`** for bindings, and use **`/osdyConfig`** or **`/usage`** if the terminal, OS, or another extension intercepts or cannot deliver the chord.
70
+
71
+ **Ctrl+Option+O capability limits:** Account **Switch**, **Set default**, and **Clear default** refuse changes while Pi is busy; retry when idle. The same actions through `/osdyConfig` retain idle waiting, as does switching through `/osdy-account`. Read-only account quota previews remain available while busy. Successful TODO/Agents selection from Ctrl+Option+O saves settings, closes/disposes the panel, releases its editor hold, then asks you to run **`/reload` manually** (or restart Pi). From `/osdyConfig`, the same sequence still reloads automatically once; restart Pi if reload fails.
72
+
73
+ Ctrl+Option+U opens the existing `/usage` dashboard with the same refresh behavior; it does not open `/osdy-usage`.
74
+
75
+ When Codex reports an available banked reset count, `/usage`, **Account → View usage**, and the **Usage** category show **Banked resets: N**, including zero. Unknown counts are omitted. Banked resets are separate from paid **Credits** and legacy **Credit resets**; local analytics filters do not change the count. Displaying it never consumes a reset or changes the compact footer.
76
+
77
+ Positive counts also trigger a best-effort read-only reset-details GET within the same timeout budget. Dashboards label listed resets **Reset 1:**, **Reset 2:**, and so on, with expiry date/time in your machine's **local timezone**; the Usage count row includes these lines in its expanded help (`?`). Missing or invalid expiry shows **Expiry not provided**. Past dates show **expired** with **(cached; local time)** and are excluded from new-reset selection. **Reset expiry details unavailable** or **Partial expiry details: M of N resets listed** identifies missing or incomplete details. The usage summary remains the count authority even when details are empty, capped, inconsistent, or fail. Quotas and the count remain visible on details failure.
61
78
 
62
79
  - Use **↑/↓**, **Home/End**, or **Page Up/Down** to navigate; **Tab** or **←/→** switches between categories and details. **Enter** selects. Single blank lines separate the heading, summary, compact options, selected-item help, status, and action hints—not individual options. Gaps yield on short viewports so the selected option and controls remain reachable. Press **?** to expand/collapse the selected help and view notes; resize if expanded help is clipped. Confirmation effects remain complete, and **?** never toggles help during confirmation, path editing, loading, querying, or saving.
63
- - TODO shows configured selection, current eligibility, and actually loaded SDK registration separately. Opt-in/out requires an idle session and a Cancel-first inline confirmation naming the target, owned Gentle filter effects, and reload. Successful selection closes/disposes the panel and releases its editor hold before reloading once; restart Pi if reload fails. Rejections stay in the panel. No installs or task-history changes occur; legacy `/osdy-pi todo on|off|status` remains available.
64
- - Agents shows concise **Joker / Gentle** choices without activating anything. With detail focus, visible lowercase-only **j Joker / g Gentle** letters open the existing confirmation directly; only available, unambiguous rows get shortcuts. **↑/↓ + Enter** remains the fallback. Profile and Agent selections survive category navigation; unavailable choices fall back to the current row. Letters never bypass category focus, confirmation, path input, loading, querying, or saving. Selection is idle-only and Cancel-first confirmed, naming the real target, possible Joker installation, owned agent filters, and preserved unrelated resources. Normal personal Pi only: `pi:dev` and isolated/override sessions are blocked, never redirected to personal settings. Success closes/disposes the panel and releases its editor hold before reloading once; restart Pi if reload fails. Legacy `/osdy-pi agents setup|on|off|status` is unchanged.
80
+ - TODO shows configured selection, current eligibility, and actually loaded SDK registration separately. Opt-in/out requires an idle session and a Cancel-first inline confirmation naming the target, owned Gentle filter effects, and reload. Successful selection closes/disposes the panel and releases its editor hold before automatic command reload or the Ctrl+Option+O manual-reload notice described above; restart Pi if reload fails. Rejections stay in the panel. No installs or task-history changes occur; legacy `/osdy-pi todo on|off|status` remains available.
81
+ - Agents shows concise **Joker / Gentle** choices without activating anything. With detail focus, visible lowercase-only **j Joker / g Gentle** letters open the existing confirmation directly; only available, unambiguous rows get shortcuts. **↑/↓ + Enter** remains the fallback. Profile and Agent selections survive category navigation; unavailable choices fall back to the current row. Letters never bypass category focus, confirmation, path input, loading, querying, or saving. Selection is idle-only and Cancel-first confirmed, naming the real target, possible Joker installation, owned agent filters, and preserved unrelated resources. Normal personal Pi only: `pi:dev` and isolated/override sessions are blocked, never redirected to personal settings. Success closes/disposes the panel and releases its editor hold before automatic command reload or the Ctrl+Option+O manual-reload notice described above; restart Pi if reload fails. Legacy `/osdy-pi agents setup|on|off|status` is unchanged.
65
82
  - Theme lists Pi's available themes and marks the current theme. Explicit **Page x/N** pages contain at most **8 themes**, fewer in short viewports or with expanded help. **Page Up/Down** changes pages, **Home/End** reaches the first/last theme, and arrows cross page boundaries. Selection survives category navigation and resizing; resizing adjusts the page around the selected theme. A successful selection applies live and saves globally in the active Pi agent directory; project settings can override it at startup. Failures appear in the modal.
66
83
  - Header offers **osdy-theme / neon**; Mascot offers **current / Bts / Osdy-Halloween**; Editor offers **auto / extended / simple**, with the current preference marked. Editor also shows the effective mode: small terminals stay simple/native even with extended selected.
67
84
  - Preferences save immediately through the same runtime and global Osdy settings store as `/osdy-pi` commands (`<agent-dir>/extensions/osdy-pi/settings.json`). Header/Mascot apply live without closing the panel. Editor replacement waits until all overlays close; if another overlay remains, the next existing responsive watcher tick applies the latest choice. The effective mode reports the actually mounted editor while queued. Opening a category only reads values; while Osdy is disabled, choices save for later without enabling it.
@@ -69,8 +86,8 @@ Run **`/osdyConfig`** in interactive Pi to open the Osdy Control Center. The ten
69
86
  - **Git** shows the current branch (or detached HEAD), changed-entry count/clean status, and the current widget position as a read-only session detail. Working-tree enablement applies and saves through the same owner as `/osdy-pi working-tree`; Git inspection never writes Git. `/osdy-pi diff` and position commands are unchanged.
70
87
  - **Sounds** shows each event's saved and effective path/source. Configure opens a single-line path editor **inside this overlay**: enter a readable `.mp3`/`.wav` path, then **Enter** to validate and save immediately, or **Esc** to cancel editing. Relative paths resolve against the project directory and save as absolute paths. Clear removes only that event's saved path from the existing v1 audio store (`<agent-dir>/extensions/osdy-pi/audio-notifications.json`). Notifications read this store afresh; refresh failures are reported separately from save failures.
71
88
  - Sounds' master enablement is **unavailable**: the existing schema has paths, not an enable switch or presets. Startup `--osdy-pi-sound-<event>` flags override saved paths, so **clearing a saved path does not disable flag-configured playback**. Only **Test effective** explicitly plays a sound; opening categories and configuring paths never play. Tests report unavailable files/platforms or playback failures; playback remains macOS/Windows only. The legacy `/osdy-pi sound setup` wizard is preserved.
72
- - **Account** shows one row per validated profile, marked **Active** and **Default** where applicable. Select a profile, then use its visible context bar: **v View usage**, **s Switch** (inactive profiles), **d Set default**, and **c Clear default** (when a default exists). **v** or **Enter** explicitly queries that profile's stored Codex credentials without activating it; no second action list appears. Opening, navigating or highlighting never queries; stored profiles are never polled. A dedicated quota view reuses `/usage` dashboard content: Session/Weekly cards, additional bucket groups, remaining-capacity bars, resets, and the quotas/account summary with plan and credits when available. Identity belongs to the selected stored profile; unknown provider/model metadata is labeled honestly. Checked and updated times use local time; absent data is unknown, not zero. Refresh/Back controls and query feedback stay pinned while long dashboard content scrolls. **r Refresh** repeats the selected profile query; **s Switch**, when available, confirms activation of the currently viewed inactive profile. Cancelling that confirmation preserves the quota view and scroll position. **b Back** or **Esc** returns to the overview with the profile selected, without closing the overlay. In quota details, **↑/↓** selects Refresh/Back and **Enter** activates it; **Page Up/Down** and **Home/End** scroll long quota data. Switch, set default, and clear default require an **in-screen confirmation** with Cancel selected initially; **Esc** cancels confirmation without closing the overlay. Single blank separators group the confirmation title, complete effects, choices, and instructions; gaps yield on short viewports. Resize if the full confirmation cannot fit—acceptance stays hidden and blocked. Switching waits for Pi to become idle, activates through the existing backend, and refreshes subscription usage. Login remains Pi-owned (`/login`); create/rename/remove remain available in `/osdy-account`. Category navigation never changes profiles or credentials.
73
- - **Usage** shows the runtime's cached active Codex subscription quota (remaining-capacity bars and percentages, window, reset/fetch times) and the existing local history owner's recorded token/cache totals, cost gaps, and model detail. Select **Range: day/week/month** for the current local calendar period; toggle **Accounts: all/current** for local analytics (unmanaged when no valid current profile exists). Quota always belongs to the active account, not the analytics filter; local token/cost totals never become quota percentages. Select **Refresh quota and local history** explicitly; rendering and filters never fetch. Empty, loading, unavailable, and limited/incomplete coverage are reported honestly. Estimates are not billing or subscription quota. `/usage` and `/osdy-usage` remain unchanged.
89
+ - **Account** shows one row per validated profile, marked **Active** and **Default** where applicable. Select a profile, then use its visible context bar: **v View usage**, **s Switch** (inactive profiles), **d Set default**, and **c Clear default** (when a default exists). **v** or **Enter** explicitly queries that profile's stored Codex credentials without activating it; no second action list appears. Opening, navigating or highlighting never queries; stored profiles are never polled. A dedicated quota view reuses `/usage` dashboard content: Session/Weekly cards, additional bucket groups, remaining-capacity bars, resets, and the quotas/account summary with plan and credits when available. Identity belongs to the selected stored profile; unknown provider/model metadata is labeled honestly. Checked and updated times use local time; absent data is unknown, not zero. Refresh/Back controls and query feedback stay pinned while long dashboard content scrolls. **r Refresh** repeats the selected profile query; **s Switch**, when available, confirms activation of the currently viewed inactive profile. Cancelling that confirmation preserves the quota view and scroll position. **b Back** or **Esc** returns to the overview with the profile selected, without closing the overlay. In quota details, **↑/↓** selects Refresh/Back and **Enter** activates it; **Page Up/Down** and **Home/End** scroll long quota data. Switch, set default, and clear default require an **in-screen confirmation** with Cancel selected initially; **Esc** cancels confirmation without closing the overlay. Single blank separators group the confirmation title, complete effects, choices, and instructions; gaps yield on short viewports. Resize if the full confirmation cannot fit—acceptance stays hidden and blocked. Switching through `/osdyConfig` waits for Pi to become idle; Ctrl+Option+O refuses busy account changes. Successful switching activates through the existing backend and refreshes subscription usage. Login remains Pi-owned (`/login`); create/rename/remove remain available in `/osdy-account`. Category navigation never changes profiles or credentials.
90
+ - **Usage** shows the runtime's cached active Codex subscription quota (remaining-capacity bars and percentages, window, reset/fetch times) and the existing local history owner's recorded token/cache totals, cost gaps, and model detail. Select **Range: day/week/month** for the current local calendar period; toggle **Accounts: all/current** for local analytics (unmanaged when no valid current profile exists). Quota always belongs to the active account, not the analytics filter; local token/cost totals never become quota percentages. Select **Refresh quota and local history** explicitly; rendering and filters never fetch. Empty, loading, unavailable, and limited/incomplete coverage are reported honestly. Estimates are not billing or subscription quota. `/usage` remains the subscription dashboard; `/osdy-usage` remains local analytics.
74
91
  - **Esc** closes without undoing live or saved changes, except when cancelling a confirmation/input or returning from Account quota details. Account read-only queries are cancelled on Back/Esc, category exit or close; late results are ignored even if cancellation is not honored. Other in-flight operations still finish and are not claimed cancelled. Late results cannot replace a closed overlay or another category. On narrow terminals, categories and details share the viewport; use Tab to switch.
75
92
 
76
93
  This modal requires Pi's terminal UI, not RPC, JSON, or print mode. `/osdy-pi` remains the legacy extension command; the external shell command `osdy` (`bin/osdy.mjs`) remains the isolated launcher described below.
@@ -218,7 +235,7 @@ For a project-local installation, run `pi remove -l npm:@juicesharp/rpiv-todo` i
218
235
 
219
236
  **Quick path:** `/osdyConfig` → **Account** → select a profile → **v** (or **Enter**) for quota bars. Use **r Refresh** to query again or **b Back** / **Esc** to return with the profile selected. From the overview or quota dashboard, **s Switch** opens the same Cancel-first confirmation for the eligible inactive profile; cancelling from quota preserves the view and scroll position. From the overview, **d** sets the default and **c** clears an existing default, also with Cancel-first confirmation. The active profile also supports explicit stored-credential queries.
220
237
 
221
- Preview is **Codex only**, a live query using the selected profile's **stored credentials snapshot**, with no automatic refresh or stored-profile polling. Only **View usage** and the quota view's **Refresh** query; profile navigation does not. It never activates, borrows active credentials, refreshes tokens, writes auth, changes models/session state or overwrites active quota. Missing, malformed, expired or remotely rejected snapshots are unavailable—not zero quota. Even the current profile's stored snapshot may be stale: log in and save through the normal account workflow outside preview. Profile labels and decoded JWT claims do not verify human identity or server validity. Local history remains token/cost estimates, not subscription quota. Legacy `/osdy-account`, `/usage` and `/osdy-usage` remain unchanged; Agents configuration remains normal personal Pi only.
238
+ Preview is **Codex only**, a live query using the selected profile's **stored credentials snapshot**, with no automatic refresh or stored-profile polling. Only **View usage** and the quota view's **Refresh** query; profile navigation does not. It never activates, borrows active credentials, refreshes tokens, writes auth, changes models/session state or overwrites active quota. Missing, malformed, expired or remotely rejected snapshots are unavailable—not zero quota. Even the current profile's stored snapshot may be stale: log in and save through the normal account workflow outside preview. Profile labels and decoded JWT claims do not verify human identity or server validity. Local history remains token/cost estimates, not subscription quota. Legacy `/osdy-account`, `/usage` and `/osdy-usage` remain available; Agents configuration remains normal personal Pi only.
222
239
 
223
240
  Osdy Pi can keep multiple ChatGPT Plus/Pro accounts authenticated and let you choose which one starts Pi. `personal` and `work` are only examples—you can create as many named profiles as you need.
224
241
 
@@ -283,7 +300,7 @@ To resume a specific session directly from the terminal:
283
300
  osdy-pi account use work -- --session /absolute/path/to/session.jsonl
284
301
  ```
285
302
 
286
- > **Privacy:** each profile keeps a private `auth.json`; Pi runs against its canonical shared `auth.json`. Account activation/preservation atomically copies opaque auth files. The separate explicit Account **View usage / Refresh** query reads a bounded stored auth snapshot, parses its Codex OAuth fields and unverified JWT claims, and sends the access token/account ID only to the fixed HTTPS Codex usage endpoint. It never refreshes tokens, logs in, switches accounts, writes auth, or queries automatically; it does not print or log credentials. Session history, settings, installed packages, and extension resources are shared, so every profile can access that local state. It keeps Pi's canonical `openai-codex` provider and delegates authentication to Pi's built-in `/login` flow.
303
+ > **Privacy:** each profile keeps a private `auth.json`; Pi runs against its canonical shared `auth.json`. Account activation/preservation atomically copies opaque auth files. The separate explicit Account **View usage / Refresh** query reads a bounded stored auth snapshot, parses its Codex OAuth fields and unverified JWT claims, and sends the access token/account ID only to the fixed HTTPS Codex usage and optional reset-details endpoints. It never refreshes tokens, logs in, switches accounts, writes auth, or queries automatically; it does not print or log credentials. Session history, settings, installed packages, and extension resources are shared, so every profile can access that local state. It keeps Pi's canonical `openai-codex` provider and delegates authentication to Pi's built-in `/login` flow.
287
304
 
288
305
  Account switching is serialized in-process and protected by a private cross-process lock. Osdy Pi accepts only bounded regular auth files, refuses symlink-based auth sources, writes replacements with private permissions, and restores the previous canonical auth and account metadata when activation fails. A separate namespace lock protects concurrent profile creation, rename, and removal. Launchers activate the selected auth before starting Pi while continuing to use the shared agent directory; existing managed-directory links from earlier profile layouts are resolved for compatibility.
289
306
 
@@ -402,7 +419,7 @@ The enabled state, editor mode, working-tree visibility preference, header, and
402
419
 
403
420
  | Group | Command |
404
421
  | --- | --- |
405
- | Control Center | `/osdyConfig` (interactive terminal UI) |
422
+ | Control Center | **Ctrl+Option+O** or `/osdyConfig` (interactive terminal UI) |
406
423
  | Main | `/osdy-pi` |
407
424
  | Main | `/osdy-pi enable\|disable\|on\|off\|status` |
408
425
  | Accounts | `/osdy-account` |
@@ -416,7 +433,7 @@ The enabled state, editor mode, working-tree visibility preference, header, and
416
433
  | Audio | `/osdy-pi sound setup` |
417
434
  | Diff | `/osdy-pi diff` |
418
435
  | Package | `/osdy-pi uninstall` |
419
- | Codex subscription | `/usage` |
436
+ | Codex subscription | **Ctrl+Option+U** or `/usage` |
420
437
  | Local usage analytics (starting in 1.10.0) | `/osdy-usage` |
421
438
 
422
439
  `/osdy-pi uninstall` finds a unique Osdy Pi package in Pi's configured package list and asks you to confirm its exact source and user/project scope before calling Pi's `remove` command. Run it from a trusted project with interactive UI. Git registrations are checked against Pi's host/path identity; a local registration must point to this running extension's package root and have a matching manifest. Ambiguous or changed registrations, unrecognized sources, and cancelled confirmation remove nothing. It removes only the Pi package registration, not Osdy profiles, accounts, the globally installed CLI, or other packages. Restart Pi afterward to unload the extension. If no unique source is found, inspect `pi list` and use `pi remove <source> [-l]` manually.
@@ -425,17 +442,27 @@ The enabled state, editor mode, working-tree visibility preference, header, and
425
442
 
426
443
  ### Codex subscription usage
427
444
 
428
- Run `/usage` to open the Codex subscription dashboard for the active managed `openai-codex` profile and model. Press `r` to refresh; `esc` or `q` closes it. The dashboard shows these controls at the bottom.
445
+ Press **Ctrl+Option+U** or run `/usage` to open the same Codex subscription dashboard for the active managed `openai-codex` profile and model. Press `r` to refresh; `esc` or `q` closes it. The dashboard shows these controls at the bottom.
429
446
 
430
447
  The main windows are labeled **Session** and **Weekly** (the API/domain remains primary/secondary). Their remaining-capacity bars are full at 100% remaining and empty at 0%; labels use the active theme's bold accent, Session uses the accent fill, Weekly uses `mdLink`, and empty segments are muted. The remaining percentage stays muted above 40%, changes to the theme's warning color at 40% or less, and changes to its error color at 15% or less. The same thresholds apply in the `/usage` dashboard and compact footer/editor quota bars. A double themed frame, section dividers, and spacing separate the display. Detail cards show each duration and relative, local, and UTC reset times. Plan, availability, credits/reset count, and additional buckets appear only when the service supplies them; absent optional values are omitted.
431
448
 
432
449
  At wide widths, the modal pairs the Session and Weekly detail cards. Below 72 content columns, cards and bars stack and account/model data wraps. The native footer and extended editor also show compact Session/Weekly remaining-capacity bars below model and thinking metadata whenever a current Codex snapshot exists. Wide widths combine those bars; narrow widths stack them. Additional buckets appear in `/usage` and the selected-profile Control Center quota view, not compact editor/footer bars.
433
450
 
434
- Active quota loads when an enabled session starts and refreshes when `/usage` opens or `r` is pressed. It also refreshes while idle on a **60-second** schedule, only after settings are loaded, with Osdy enabled, an interactive terminal UI, an `openai-codex` model selected, and no quota request already in flight. Automatic failures double the retry delay up to **10 minutes**; a successful automatic query restores the 60-second interval. This scheduler never polls stored profiles.
451
+ Active quota loads when an enabled session starts and refreshes when **Ctrl+Option+U** or `/usage` opens the dashboard, or `r` is pressed. It also refreshes while idle on a **60-second** schedule, only after settings are loaded, with Osdy enabled, an interactive terminal UI, an `openai-codex` model selected, and no quota request already in flight. Automatic failures double the retry delay up to **10 minutes**; a successful automatic query restores the 60-second interval. This scheduler never polls stored profiles.
435
452
 
436
453
  Automatic refresh retains the last snapshot during loading and on error, so an older reading can remain visible until a successful query replaces it. Manual refresh clears the previous snapshot before authentication resolves and supersedes any in-flight automatic request. Account switching refreshes active quota through that same clearing path. Session replacement and shutdown stop the timer, cancel pending requests and clear quota state; late results are ignored.
437
454
 
438
- > **Privacy:** this active-account `/usage` path resolves OAuth through Pi's `modelRegistry`, rather than directly reading auth files. The separate Control Center Account **View usage / Refresh** path reads the selected profile's bounded stored credentials snapshot; it never borrows active credentials or refreshes tokens, logs in, switches accounts, writes auth, or queries automatically. Both paths transmit the access token/account ID only to the fixed HTTPS Codex usage endpoint, reject redirects, and bound timeout and response size. Quota previews do not persist or log credentials or display account IDs, tokens, raw response bodies, or endpoint internals.
455
+ > **Privacy:** this active-account `/usage` path resolves OAuth through Pi's `modelRegistry`, rather than directly reading auth files. The separate Control Center Account **View usage / Refresh** path reads the selected profile's bounded stored credentials snapshot; it never borrows active credentials or refreshes tokens, logs in, switches accounts, writes auth, or queries automatically. Both paths transmit the access token/account ID only to the fixed HTTPS Codex usage and optional reset-details endpoints, reject redirects, and bound timeout and response size. Quota previews do not persist or log credentials or display account IDs, tokens, raw response bodies, or endpoint internals. Only an explicitly selected and confirmed active-account reset additionally POSTs to the fixed Codex reset-consume endpoint.
456
+
457
+ #### Use or check a banked reset
458
+
459
+ 1. In `/usage`, press **u**; or select **Usage → Use or check banked reset** in `/osdyConfig`. Account → View usage remains read-only, including previews of the current profile.
460
+ 2. The overlay closes first. Choose an indexed reset with its **local expiry date/time** (or **Expiry not provided**), then explicitly select **Use reset**. **Cancel is first**; Cancel/Esc sends nothing. The confirmation affects only the **active Codex account** and explains that a successful reset consumes one banked reset. Auth/account/session are checked again before POST.
461
+ 3. `/usage` reopens with the result snapshot if the same context remains active; Control Center exits, so reopen Usage to inspect it. A failed quota refresh or local bookkeeping failure is reported separately from a confirmed server outcome. `no_credit` and `nothing_to_reset` do not consume a reset; `already_redeemed` confirms prior success.
462
+
463
+ **Unknown outcome:** do not start another reset or infer success from a changed count. Use **u** or the Usage row again and explicitly choose **Check pending attempt** to recheck the **same stored request UUID and reset ID**, which may finish an earlier unconfirmed reset—not start a new attempt. A private, durable local pending journal stores attempt identifiers, never credentials; reload does not automatically retry or replay it. Checking remains available with active Codex authentication even when the displayed count is zero or quota is unavailable.
464
+
465
+ **Blocked storage/lock:** stop for manual verification of the previous attempt and any other running Pi process. Lock acquisition or pre-dispatch journal-save failure prevents a reset POST. Locks are not automatically reclaimed, even when apparently stale; orphaned locks can require manual recovery. There is no automatic lock cleanup or guarantee of perfect crash recovery. This flow has mocked transport tests, not live API, real-reset, or manual TUI verification.
439
466
 
440
467
  ### Local usage analytics (starting in 1.10.0)
441
468
 
@@ -450,7 +477,7 @@ Run `/osdy-usage` in interactive terminal Pi. **Summary** opens by default; use
450
477
 
451
478
  Narrow terminals stack metrics and wrap names; every group remains reachable by scrolling. Shares use the selected total tokens; bars scale to each ranking's maximum. Loading, stale, scan-limit/warning and unpriced/partial-cost states remain on every page and in help. Sticky status lines wrap instead of concealing cost status behind scan warnings, including at End. When height is scarce, statuses take priority over optional headings, charts and footer controls; the active page remains visible when space permits. Press `?` for methodology and full controls, then `?` again to return. Choosing a page also leaves help without discarding filters.
452
479
 
453
- This is separate from the unchanged `/usage` subscription quota panel. RPC, JSON and print modes receive a short notification instead of charts. The four-page UI passed independent scripted synthetic PTY checks for navigation, shared selection and responsive statuses at 12/32/100 columns, including a five-row terminal. Human visual confirmation and provider-backed capture remain unverified.
480
+ This is separate from the `/usage` subscription quota panel. RPC, JSON and print modes receive a short notification instead of charts. The four-page UI passed independent scripted synthetic PTY checks for navigation, shared selection and responsive statuses at 12/32/100 columns, including a five-row terminal. Human visual confirmation and provider-backed capture remain unverified.
454
481
 
455
482
  | Control | Action |
456
483
  | --- | --- |
@@ -606,13 +633,14 @@ This launches `pi` from `PATH`, loads this checkout with `-e`, and uses `.pi-dev
606
633
  The explicit argument bypasses the launcher's default-account activation; Pi's `--no-session` disables session persistence, **not settings writes or provider access**.
607
634
  An isolated agent directory does **not** make custom audio paths or real provider accounts harmless. Use intentional disposable test profiles/files for profile writes, switches, default confirmations, and audio tests; do not assume an isolated account store makes real credentials safe.
608
635
 
609
- - Open `/osdyConfig` in regular and fullscreen Pi. Check arrows/Home/End/Page Up/Down, Tab or left/right, Enter, and Esc; shrink and restore the terminal, checking both panes remain reachable.
636
+ - Check `/hotkeys` lists `ctrl+alt+o` and `ctrl+alt+u`. On macOS, configure Option to send Alt/Meta and verify the terminal supports Ctrl+Alt chords; actual Ctrl+Option delivery still needs a hardware check. Open both **Ctrl+Option+O** and `/osdyConfig` in regular and fullscreen Pi (Ctrl+Alt+O on other platforms); use the slash command if the chord is intercepted or unsupported. Check arrows/Home/End/Page Up/Down, Tab or left/right, Enter, and Esc; shrink and restore the terminal, checking both panes remain reachable. With disposable TODO/Agents settings, verify Ctrl+Option+O asks for manual `/reload` only after closing, while `/osdyConfig` reloads automatically.
610
637
  - In disposable settings, change a visual preference, wait for **Saved globally**, close and reopen, then reload/restart to check persistence. Esc does not undo changes; distinguish save/application errors from success.
611
- - Account (manual): with disposable profiles only, check Cancel-first switch/set-default/clear-default confirmations, Esc cancellation, idle switching, and current/default labels. Check one row per profile with **v/s/d/c** context actions and **Enter** usage fallback, without a second action list. From **v** quota details, check eligible **s Switch** opens the same Cancel-first confirmation, with blank separators between the title/effects, choices, and instructions; Cancel/Esc must retain the quota view and scroll position. Check gaps yield on short viewports without hiding safety content or enabling acceptance when the full confirmation cannot fit. Check remaining-capacity bars, **r Refresh**, **b Back/Esc** returning to the selected profile, long-view scrolling, no query on navigation, no stored-profile polling, and cancellation on exit. Create/rename/remove stay in `/osdy-account`, login in `/login`.
638
+ - Account (manual): with disposable profiles only, check Cancel-first switch/set-default/clear-default confirmations, Esc cancellation, idle switching, and current/default labels. Verify Ctrl+Option+O refuses all three changes while busy, succeeds when idle, and `/osdyConfig` still waits for idle. Check one row per profile with **v/s/d/c** context actions and **Enter** usage fallback, without a second action list. From **v** quota details, check eligible **s Switch** opens the same Cancel-first confirmation, with blank separators between the title/effects, choices, and instructions; Cancel/Esc must retain the quota view and scroll position. Check gaps yield on short viewports without hiding safety content or enabling acceptance when the full confirmation cannot fit. Check remaining-capacity bars, **r Refresh**, **b Back/Esc** returning to the selected profile, long-view scrolling, no query on navigation, no stored-profile polling, and cancellation on exit. Create/rename/remove stay in `/osdy-account`, login in `/login`.
639
+ - Subscription quota (manual): compare **Ctrl+Option+U** and `/usage` (Ctrl+Alt+U on other platforms), including **r Refresh** and Esc/q closing. Verify actual terminal chord delivery; use `/usage` if intercepted or unsupported.
612
640
  - Usage (manual): check day/week/month and all/current filters, empty/unavailable/limited states, and explicit refresh. Authenticated quota and post-switch account correctness need an intentionally authorized live provider check; local estimates are not billing.
613
641
  - Sounds (manual): use disposable readable `.mp3`/`.wav` files to check configure, cancel, clear, and startup-flag precedence. Only choose **Test effective** when playback is intended; actual macOS/Windows playback still needs a live check.
614
642
 
615
- Live TUI appearance, real account switching/authenticated quota, and actual audio playback remain unverified; this checklist is not a recorded pass.
643
+ The user confirmed physical Ctrl+Option+O and Ctrl+Option+U delivery on one Mac terminal; other terminals may intercept or not support these chords. Live TUI appearance, real account switching/authenticated quota, and actual audio playback remain unverified; this checklist is not a recorded full pass.
616
644
 
617
645
  Install a local checkout into Pi with:
618
646
 
@@ -460,12 +460,14 @@ void test("manages profile rename and permanent removal prompts through authorit
460
460
 
461
461
  void test("switches in place after idle without spawning or shutting down", async () => {
462
462
  const events: string[] = [];
463
+ let idle = false;
463
464
  await switchAccountInPlace(
464
465
  {
465
- isIdle: () => false,
466
+ isIdle: () => idle,
466
467
  waitForIdle: () =>
467
468
  Promise.resolve().then(() => {
468
469
  events.push("idle");
470
+ idle = true;
469
471
  }),
470
472
  ui: {
471
473
  notify: (message: string) => events.push(message),
@@ -495,6 +497,46 @@ void test("switches in place after idle without spawning or shutting down", asyn
495
497
  ]);
496
498
  });
497
499
 
500
+ void test("base-context account switches require idle without a command wait hook", async () => {
501
+ const previous = process.env.OSDY_PI_PROFILE_NAME;
502
+ try {
503
+ for (const idle of [false, true]) {
504
+ process.env.OSDY_PI_PROFILE_NAME = "personal";
505
+ const events: string[] = [];
506
+ const notices: string[] = [];
507
+ const ctx = { isIdle: () => idle, ui: {
508
+ notify: (message: string) => notices.push(message),
509
+ select: () => Promise.resolve(undefined), input: () => Promise.resolve(undefined),
510
+ } };
511
+ assert.equal("waitForIdle" in ctx, false);
512
+ const switched = await switchAccountInPlace(ctx, "work",
513
+ profile => { events.push(`activate:${profile}`); return Promise.resolve(); },
514
+ () => { events.push("refresh"); return Promise.resolve(); },
515
+ () => { events.push("render"); });
516
+ assert.equal(switched, idle);
517
+ assert.deepEqual(events, idle ? ["activate:work", "render", "refresh"] : []);
518
+ assert.equal(process.env.OSDY_PI_PROFILE_NAME, idle ? "work" : "personal");
519
+ assert.match(notices.join(" "), idle ? /Switched to work/ : /require Pi to be idle/);
520
+ }
521
+ } finally {
522
+ if (previous === undefined) delete process.env.OSDY_PI_PROFILE_NAME;
523
+ else process.env.OSDY_PI_PROFILE_NAME = previous;
524
+ }
525
+ });
526
+
527
+ void test("a command wait hook cannot activate an account if Pi remains busy", async () => {
528
+ const events: string[] = [];
529
+ const switched = await switchAccountInPlace({ isIdle: () => false,
530
+ waitForIdle: () => { events.push("wait"); return Promise.resolve(); },
531
+ ui: { notify: message => events.push(message), select: () => Promise.resolve(undefined), input: () => Promise.resolve(undefined) },
532
+ }, "work", () => { events.push("activate"); return Promise.resolve(); },
533
+ () => { events.push("refresh"); return Promise.resolve(); }, () => { events.push("render"); });
534
+ assert.equal(switched, false);
535
+ assert.equal(events[0], "wait");
536
+ assert.match(events[1] ?? "", /require Pi to be idle/);
537
+ assert.equal(events.length, 2);
538
+ });
539
+
498
540
  void test("keeps the current account when in-place activation fails", async () => {
499
541
  const notices: string[] = [];
500
542
  let renderRequested = false;
@@ -12,10 +12,9 @@ function sameProfile(left: string | undefined, right: string | undefined): boole
12
12
  return left?.toLowerCase() === right?.toLowerCase();
13
13
  }
14
14
 
15
- export type AccountContext = Pick<
16
- ExtensionCommandContext,
17
- "isIdle" | "waitForIdle"
18
- > & {
15
+ export type AccountContext = Pick<ExtensionContext, "isIdle"> & {
16
+ /** Only slash-command contexts can wait; base shortcut contexts must already be idle. */
17
+ waitForIdle?: ExtensionCommandContext["waitForIdle"];
19
18
  shutdown?: ExtensionCommandContext["shutdown"];
20
19
  sessionManager?: Pick<
21
20
  ExtensionCommandContext["sessionManager"],
@@ -128,7 +127,14 @@ export async function switchAccountInPlace(
128
127
  );
129
128
  return false;
130
129
  }
131
- if (!ctx.isIdle()) await ctx.waitForIdle();
130
+ if (!ctx.isIdle() && ctx.waitForIdle) await ctx.waitForIdle();
131
+ if (!ctx.isIdle()) {
132
+ ctx.ui.notify(
133
+ "Account changes require Pi to be idle. Wait for active work to finish and retry.",
134
+ "warning",
135
+ );
136
+ return false;
137
+ }
132
138
  try {
133
139
  await activate(profile);
134
140
  } catch {
@@ -0,0 +1,236 @@
1
+ import assert from "node:assert/strict";
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync } from "node:fs";
4
+ import { chmod, mkdtemp, open, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
5
+ import type { FileHandle } from "node:fs/promises";
6
+ import { registerHooks } from "node:module";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import test from "node:test";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ registerHooks({
13
+ resolve(specifier, context, nextResolve) {
14
+ if (specifier.startsWith(".") && specifier.endsWith(".js") && context.parentURL?.endsWith(".ts")) {
15
+ const url = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
16
+ if (existsSync(fileURLToPath(url))) return { shortCircuit: true, url: url.href };
17
+ }
18
+ return nextResolve(specifier, context);
19
+ },
20
+ });
21
+
22
+ const { createCodexResetStore } = await import("./codex-reset-store.js");
23
+ const accountId = "synthetic-account/../not-a-path";
24
+ const pending = {
25
+ version: 1 as const, accountId, creditId: "synthetic-credit",
26
+ requestId: "11111111-1111-4111-8111-111111111111", expiresAt: 2_000, status: "pending" as const,
27
+ };
28
+ const basename = createHash("sha256").update(accountId).digest("hex");
29
+
30
+ async function fixture(t: test.TestContext) {
31
+ const directory = await mkdtemp(join(tmpdir(), "osdy-reset-test-"));
32
+ t.after(() => rm(directory, { recursive: true, force: true })); // Only this synthetic fixture.
33
+ const root = join(directory, "journal");
34
+ return { root, store: createCodexResetStore(root) };
35
+ }
36
+
37
+ void test("private hashed journals round-trip and resolved attempts permit a new request", async (t) => {
38
+ const { root, store } = await fixture(t);
39
+ const lease = await store.acquire(accountId);
40
+ assert.equal(await lease.read(), undefined);
41
+ await lease.write(pending);
42
+ assert.deepEqual(await lease.read(), pending);
43
+ assert.equal((await stat(root)).mode & 0o777, 0o700);
44
+ for (const name of await readdir(root)) {
45
+ assert.match(name, /^[a-f0-9]{64}\.(json|lock)$/);
46
+ assert.equal((await stat(join(root, name))).mode & 0o777, 0o600);
47
+ }
48
+ assert.equal((await readFile(join(root, `${basename}.json`), "utf8")).includes("accessToken"), false);
49
+ await lease.write({ ...pending, status: "resolved", confirmedCode: "reset" });
50
+ await lease.write({ ...pending, requestId: "22222222-2222-4222-8222-222222222222" });
51
+ await lease.release();
52
+ const restored = await store.acquire(accountId);
53
+ assert.equal((await restored.read())?.requestId, "22222222-2222-4222-8222-222222222222");
54
+ await restored.release();
55
+ assert.deepEqual(await readdir(root), [`${basename}.json`]);
56
+ });
57
+
58
+ void test("account leases are exclusive, isolate accounts, and never reclaim existing stale locks", async (t) => {
59
+ const { root, store } = await fixture(t);
60
+ const lease = await store.acquire(accountId);
61
+ await assert.rejects(store.acquire(accountId), /^Error: Codex reset journal is unavailable$/);
62
+ const other = await store.acquire("synthetic-other");
63
+ assert.equal(await other.read(), undefined);
64
+ await lease.write(pending);
65
+ assert.equal(await other.read(), undefined);
66
+ await other.release();
67
+ await lease.release();
68
+ await writeFile(join(root, `${basename}.lock`), "", { mode: 0o600 });
69
+ await assert.rejects(store.acquire(accountId), /^Error: Codex reset journal is unavailable$/);
70
+ assert.equal(await readFile(join(root, `${basename}.lock`), "utf8"), "");
71
+ });
72
+
73
+ for (const content of [
74
+ "{broken", "x".repeat(8_193), JSON.stringify({ ...pending, version: 2 }),
75
+ JSON.stringify({ ...pending, accountId: "wrong-account" }), JSON.stringify({ ...pending, requestId: "bad" }),
76
+ JSON.stringify({ ...pending, expiresAt: -1 }), JSON.stringify({ ...pending, accessToken: "forbidden" }),
77
+ JSON.stringify({ ...pending, status: "resolved", confirmedCode: "invented" }),
78
+ JSON.stringify({ ...pending, status: "pending", confirmedCode: "reset" }),
79
+ ]) {
80
+ void test(`rejects corrupt or unbounded journal fixture ${content.length}:${content.slice(0, 12)}`, async (t) => {
81
+ const { root, store } = await fixture(t);
82
+ const lease = await store.acquire(accountId);
83
+ await writeFile(join(root, `${basename}.json`), content, { mode: 0o600 });
84
+ await assert.rejects(lease.read(), /^Error: Codex reset journal is unavailable$/);
85
+ await lease.release();
86
+ });
87
+ }
88
+
89
+ void test("refuses final-file symlinks on both read and atomic write, preserving the target", async (t) => {
90
+ const { root, store } = await fixture(t);
91
+ const lease = await store.acquire(accountId);
92
+ const target = join(root, "synthetic-target");
93
+ await writeFile(target, "untouched", { mode: 0o600 });
94
+ await symlink(target, join(root, `${basename}.json`));
95
+ await assert.rejects(lease.read());
96
+ await assert.rejects(lease.write(pending));
97
+ assert.equal(await readFile(target, "utf8"), "untouched");
98
+ await lease.release();
99
+ assert.equal((await readdir(root)).some((name) => name.endsWith(".tmp")), false);
100
+ });
101
+
102
+ void test("refuses symlink roots and non-private journal files or directories", async (t) => {
103
+ const { root, store } = await fixture(t);
104
+ const lease = await store.acquire(accountId);
105
+ await lease.write(pending);
106
+ await chmod(join(root, `${basename}.json`), 0o644);
107
+ await assert.rejects(lease.read());
108
+ await lease.release();
109
+ await chmod(root, 0o755);
110
+ await assert.rejects(store.acquire(accountId));
111
+ const link = `${root}-link`;
112
+ await symlink(root, link);
113
+ await assert.rejects(createCodexResetStore(link).acquire(accountId));
114
+ });
115
+
116
+ void test("validation/write failure cannot replace pending; terminal outcomes must match the logical attempt", async (t) => {
117
+ const { root, store } = await fixture(t);
118
+ const lease = await store.acquire(accountId);
119
+ await lease.write(pending);
120
+ await assert.rejects(lease.write({ ...pending, creditId: "different" }));
121
+ await assert.rejects(lease.write({ ...pending, status: "resolved", requestId: "22222222-2222-4222-8222-222222222222" }));
122
+ await assert.rejects(lease.write({ ...pending, creditId: "x".repeat(8_193) }));
123
+ assert.deepEqual(await lease.read(), pending);
124
+ assert.equal((await readdir(root)).some((name) => name.endsWith(".tmp")), false);
125
+ await lease.write({ ...pending, status: "resolved" }); // Definitely not sent.
126
+ await lease.release();
127
+ });
128
+
129
+ void test("invalid UTF-8 must not silently change an opaque stored credit ID", async (t) => {
130
+ const { root, store } = await fixture(t);
131
+ const lease = await store.acquire(accountId);
132
+ const content = Buffer.from(JSON.stringify(pending));
133
+ content[content.indexOf("synthetic-credit")] = 0xff;
134
+ await writeFile(join(root, `${basename}.json`), content, { mode: 0o600 });
135
+ await assert.rejects(lease.read());
136
+ await lease.release();
137
+ });
138
+
139
+ void test("resolved request IDs cannot be reused for a new logical attempt", async (t) => {
140
+ const { store } = await fixture(t);
141
+ const lease = await store.acquire(accountId);
142
+ await lease.write(pending);
143
+ await lease.write({ ...pending, status: "resolved", confirmedCode: "reset" });
144
+ await assert.rejects(lease.write(pending));
145
+ await lease.release();
146
+ });
147
+
148
+ async function fileHandlePrototype(root: string): Promise<FileHandle> {
149
+ const probe = await open(join(root, "synthetic-probe"), "wx", 0o600);
150
+ const prototype = Object.getPrototypeOf(probe) as FileHandle;
151
+ await probe.close();
152
+ return prototype;
153
+ }
154
+
155
+ void test("failed temporary write leaves pending intact and cleans only its own temporary file", async (t) => {
156
+ const { root, store } = await fixture(t);
157
+ const lease = await store.acquire(accountId);
158
+ await lease.write(pending);
159
+ const prototype = await fileHandlePrototype(root);
160
+ const fault = t.mock.method(prototype, "writeFile", () => Promise.reject(new Error("synthetic write fault")));
161
+ await assert.rejects(lease.write({ ...pending, status: "resolved", confirmedCode: "reset" }));
162
+ fault.mock.restore();
163
+ assert.deepEqual(await lease.read(), pending);
164
+ assert.equal((await readdir(root)).some((name) => name.endsWith(".tmp")), false);
165
+ await lease.release();
166
+ });
167
+
168
+ for (const restoreFails of [false, true]) {
169
+ void test(`post-rename terminal fsync failure restores pending or retains lock (${restoreFails})`, async (t) => {
170
+ const { root, store } = await fixture(t);
171
+ const lease = await store.acquire(accountId);
172
+ await lease.write(pending);
173
+ const prototype = await fileHandlePrototype(root);
174
+ const original = Reflect.get(prototype, "sync");
175
+ let directorySyncs = 0;
176
+ const fault = t.mock.method(prototype, "sync", async function (this: FileHandle) {
177
+ if ((await this.stat()).isDirectory() && (++directorySyncs === 1 || restoreFails)) throw new Error("synthetic fsync fault");
178
+ await original.call(this);
179
+ });
180
+ await assert.rejects(lease.write({ ...pending, status: "resolved", confirmedCode: "reset" }));
181
+ fault.mock.restore();
182
+ assert.deepEqual(JSON.parse(await readFile(join(root, `${basename}.json`), "utf8")) as unknown, pending);
183
+ if (restoreFails) {
184
+ await assert.rejects(lease.release());
185
+ await assert.rejects(store.acquire(accountId));
186
+ } else {
187
+ await lease.release();
188
+ const next = await store.acquire(accountId);
189
+ assert.deepEqual(await next.read(), pending);
190
+ await next.release();
191
+ }
192
+ assert.equal((await readdir(root)).some((name) => name.endsWith(".tmp")), false);
193
+ });
194
+ }
195
+
196
+ void test("accepts the byte limit but never reads beyond it", async (t) => {
197
+ const { root, store } = await fixture(t);
198
+ const lease = await store.acquire(accountId);
199
+ const serialized = JSON.stringify(pending);
200
+ await writeFile(join(root, `${basename}.json`), serialized.padEnd(8_192, " "), { mode: 0o600 });
201
+ assert.deepEqual(await lease.read(), pending);
202
+ await writeFile(join(root, `${basename}.json`), serialized.padEnd(8_193, " "), { mode: 0o600 });
203
+ await assert.rejects(lease.read());
204
+ await lease.release();
205
+ });
206
+
207
+ void test("no terminal record without pending, no operations after lease release", async (t) => {
208
+ const { store } = await fixture(t);
209
+ const lease = await store.acquire(accountId);
210
+ await assert.rejects(lease.write({ ...pending, status: "resolved", confirmedCode: "reset" }));
211
+ assert.equal(await lease.read(), undefined);
212
+ await lease.release();
213
+ await assert.rejects(lease.read());
214
+ await assert.rejects(lease.write(pending));
215
+ });
216
+
217
+ void test("write snapshots validated fields before asynchronous IO and cannot serialize inherited credentials", async (t) => {
218
+ const { root, store } = await fixture(t);
219
+ const lease = await store.acquire(accountId);
220
+ const input = { ...pending };
221
+ Object.setPrototypeOf(input, { toJSON: () => ({ ...pending, accessToken: "synthetic-forbidden" }) });
222
+ const saving = lease.write(input);
223
+ Object.assign(input, { creditId: "x".repeat(8_193), accessToken: "synthetic-forbidden" });
224
+ await saving;
225
+ assert.deepEqual(await lease.read(), pending);
226
+ assert.equal((await readFile(join(root, `${basename}.json`), "utf8")).includes("synthetic-forbidden"), false);
227
+ await lease.release();
228
+ });
229
+
230
+ void test("lock release failure is generic and refuses a subsequent lease", async (t) => {
231
+ const { root, store } = await fixture(t);
232
+ const lease = await store.acquire(accountId);
233
+ await chmod(join(root, `${basename}.lock`), 0o644);
234
+ await assert.rejects(lease.release(), /^Error: Codex reset journal is unavailable$/);
235
+ await assert.rejects(store.acquire(accountId));
236
+ });