@iowarp/clio-coder 0.5.0 → 0.5.1

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +12 -9
  3. package/dist/{acp-KS7ARGV6.js → acp-X3N7JDGS.js} +2 -2
  4. package/dist/{agents-YOGJ2YEJ.js → agents-UYGSMAWR.js} +3 -3
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{chunk-EOJPDNUP.js → chunk-3OODBJ45.js} +1 -1
  7. package/dist/{chunk-R7FGRZNF.js → chunk-4D7QM7S5.js} +2 -2
  8. package/dist/{chunk-X4CEFOSG.js → chunk-6B76U65H.js} +4 -4
  9. package/dist/{chunk-LTWH2ACR.js → chunk-7FHSA7VB.js} +5 -5
  10. package/dist/{chunk-GRHOXDKB.js → chunk-7KI4ZSEB.js} +2 -2
  11. package/dist/{chunk-LYJJ6546.js → chunk-7VNLPEUG.js} +6 -6
  12. package/dist/{chunk-D65E25JG.js → chunk-A7G7OUND.js} +2 -2
  13. package/dist/{chunk-3JLTRPN7.js → chunk-AKQ3N3L4.js} +2 -2
  14. package/dist/{chunk-AVSR2HKI.js → chunk-BIQLF3NZ.js} +3 -3
  15. package/dist/{chunk-D3KWFOUT.js → chunk-DGMY2DWE.js} +2 -2
  16. package/dist/{chunk-IN7DGBVS.js → chunk-FBXTHPNZ.js} +2 -2
  17. package/dist/{chunk-EX57VVWW.js → chunk-GGEEG7CD.js} +2 -2
  18. package/dist/{chunk-V35KZDUZ.js → chunk-GW2KVAVI.js} +2 -2
  19. package/dist/{chunk-SJGS3GDI.js → chunk-KIAUZGKF.js} +2 -2
  20. package/dist/{chunk-CBAQPTDU.js → chunk-LFYA3HYH.js} +2 -2
  21. package/dist/{chunk-4VDHLCF5.js → chunk-M5FPIZQW.js} +2 -2
  22. package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
  23. package/dist/{chunk-QNUQ7K7D.js → chunk-OXWRE2QJ.js} +2 -2
  24. package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
  25. package/dist/{chunk-HQVN7G4F.js → chunk-QM7HFPQQ.js} +4 -4
  26. package/dist/{chunk-ZR5YA6ZF.js → chunk-QTYRMBW6.js} +2 -2
  27. package/dist/{chunk-3LT34CAM.js → chunk-SAL2SBP4.js} +8 -8
  28. package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
  29. package/dist/{chunk-U6YTUVOX.js → chunk-VMTCZ2GR.js} +2 -2
  30. package/dist/{chunk-OQ2EOAHA.js → chunk-VWKR2NN5.js} +5 -5
  31. package/dist/{chunk-QDUUOZ3S.js → chunk-WAISUJNQ.js} +2 -2
  32. package/dist/{chunk-XDHUDE5K.js → chunk-XOH3SD7H.js} +3 -3
  33. package/dist/{chunk-62UK7AGW.js → chunk-ZNTGRDFA.js} +281 -14
  34. package/dist/cli/index.js +17 -17
  35. package/dist/{clio-K2PBVCEM.js → clio-Z3JTEL2I.js} +2 -2
  36. package/dist/{config-Q5J7UBAB.js → config-QYIRZHI7.js} +8 -8
  37. package/dist/{config-graph-P3555574.js → config-graph-TS3SGGQW.js} +8 -8
  38. package/dist/{configure-KIJ32ZO6.js → configure-OX7KVJTQ.js} +2 -2
  39. package/dist/{context-LFBQOSXT.js → context-2BNF6HTP.js} +12 -12
  40. package/dist/{context-WBH3KSVR.js → context-GTMUGAR4.js} +7 -7
  41. package/dist/{context-clear-YOHMQ6SQ.js → context-clear-FVJQBBL7.js} +7 -7
  42. package/dist/{context-working-set-MRAEVDNV.js → context-working-set-MOE72SD6.js} +2 -2
  43. package/dist/{detail-RHJOTGN4.js → detail-QZM3QAAU.js} +8 -8
  44. package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-2KSQO5FU.js} +8 -8
  45. package/dist/{doctor-S5KZ2GTN.js → doctor-2U6YR5NH.js} +3 -3
  46. package/dist/{eval-3D6M36N7.js → eval-ZKK77JWZ.js} +3 -3
  47. package/dist/{evidence-6IYN52ET.js → evidence-FMP7KZZ2.js} +7 -7
  48. package/dist/{evidence-JVECPI3F.js → evidence-PJTOE7JA.js} +7 -7
  49. package/dist/{evidence-XNVPPLVK.js → evidence-POSEQW4Q.js} +9 -9
  50. package/dist/{evolve-TLPUDJDM.js → evolve-JT6PAMZE.js} +7 -7
  51. package/dist/{fleet-WKVBF5SZ.js → fleet-4XR2ZEOF.js} +16 -16
  52. package/dist/{fleet-6RJWIOEO.js → fleet-6JJKNIMV.js} +8 -8
  53. package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-SWJMFVDP.js} +9 -9
  54. package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-KSIHWQAU.js} +4 -4
  55. package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-QM6XU2KL.js} +7 -7
  56. package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-RLKFHFCM.js} +8 -8
  57. package/dist/gui/reads-worker.js +4 -4
  58. package/dist/{init-DV5B6HQG.js → init-VBUPHABR.js} +11 -11
  59. package/dist/{interactive-OXGYZYAS.js → interactive-JCVBPVYT.js} +1597 -503
  60. package/dist/{inventory-OEGIL6PY.js → inventory-54G7FSVY.js} +8 -8
  61. package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
  62. package/dist/{memory-J3STUUIK.js → memory-5BPQSSOP.js} +7 -7
  63. package/dist/{models-Y3GMFJ2V.js → models-R6HILX6X.js} +3 -3
  64. package/dist/{monitor-LHNH577J.js → monitor-HVRKSFEQ.js} +10 -10
  65. package/dist/{orchestrator-DTAPXULA.js → orchestrator-OR7V4YWK.js} +26 -26
  66. package/dist/{preload-OJBMAIXF.js → preload-KLXFOX2F.js} +7 -7
  67. package/dist/{run-H64MEVTA.js → run-UIDRUYAY.js} +16 -16
  68. package/dist/{skills-eval-CQ23MYQK.js → skills-eval-JDAXOPWC.js} +7 -7
  69. package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-MNKQ5QP3.js} +4 -4
  70. package/dist/{startup-background-KDWCQMWT.js → startup-background-PJMWTKVO.js} +7 -7
  71. package/dist/{system-ZDW44ZGX.js → system-IVVZRKGI.js} +2 -2
  72. package/dist/{targets-AJWA6E7Z.js → targets-RFRUMCQ5.js} +4 -4
  73. package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
  74. package/dist/{usage-XSILDHKF.js → usage-LA33SG3R.js} +9 -9
  75. package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-HL73TICA.js} +10 -10
  76. package/dist/worker/entry.js +4 -4
  77. package/docs/architecture/architecture.md +1 -0
  78. package/docs/architecture/context-engine.md +2 -2
  79. package/docs/architecture/observability.md +6 -4
  80. package/docs/architecture/prompt-envelope-and-tools.md +1 -1
  81. package/docs/architecture/session-lifecycle.md +1 -1
  82. package/docs/architecture/tui-design.md +12 -9
  83. package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
  84. package/docs/guide/commands-and-modes.md +91 -5
  85. package/docs/guide/configuration-and-targets.md +2 -2
  86. package/docs/guide/proactive-memory.md +3 -3
  87. package/package.json +1 -1
  88. package/src/cli/usage.ts +2 -2
  89. package/src/domains/gateway/mcp/client.ts +1 -1
  90. package/src/domains/observability/background-memory-usage.ts +1 -1
  91. package/src/domains/observability/cost.ts +3 -3
  92. package/src/domains/observability/extension.ts +1 -1
  93. package/src/domains/observability/metrics.ts +1 -1
  94. package/src/domains/quota/anthropic-max-provider.ts +134 -0
  95. package/src/domains/quota/anthropic-usage.ts +220 -0
  96. package/src/domains/quota/antigravity-provider.ts +339 -0
  97. package/src/domains/quota/cache.ts +87 -0
  98. package/src/domains/quota/claude-code-provider.ts +169 -0
  99. package/src/domains/quota/codex-provider.ts +237 -0
  100. package/src/domains/quota/presentation.ts +240 -0
  101. package/src/domains/quota/registry.ts +23 -0
  102. package/src/domains/quota/service.ts +93 -0
  103. package/src/domains/quota/summary-feed.ts +86 -0
  104. package/src/domains/quota/types.ts +84 -0
  105. package/src/domains/session/usage.ts +2 -2
  106. package/src/engine/acp/commands.ts +1 -1
  107. package/src/entry/orchestrator.ts +2 -2
  108. package/src/interactive/chat-loop-messages.ts +2 -2
  109. package/src/interactive/chat-loop.ts +3 -3
  110. package/src/interactive/dispatch-board.ts +21 -3
  111. package/src/interactive/footer/dashboard.ts +13 -0
  112. package/src/interactive/footer/key-hints.ts +2 -2
  113. package/src/interactive/footer/pages.ts +55 -20
  114. package/src/interactive/footer-panel.ts +1 -1
  115. package/src/interactive/interactive-application.ts +5 -2
  116. package/src/interactive/interactive-input-runtime.ts +2 -2
  117. package/src/interactive/interactive-presentation.ts +15 -0
  118. package/src/interactive/interactive-slash-runtime.ts +2 -2
  119. package/src/interactive/interactive-tickers.ts +7 -1
  120. package/src/interactive/overlay-general-openers.ts +12 -8
  121. package/src/interactive/overlay-key-routing.ts +6 -9
  122. package/src/interactive/overlay-lifecycle.ts +10 -6
  123. package/src/interactive/overlay-session-lifecycle.ts +3 -3
  124. package/src/interactive/quota-view.ts +229 -0
  125. package/src/interactive/session-last-turn.ts +1 -1
  126. package/src/interactive/session-usage-reseed.ts +2 -2
  127. package/src/interactive/side-question.ts +2 -2
  128. package/src/interactive/slash-commands.ts +8 -8
  129. package/src/interactive/turn-context.ts +1 -1
  130. package/src/interactive/turn-prewarm.ts +1 -1
  131. package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
  132. package/src/interactive/welcome-dashboard.ts +26 -3
@@ -250,7 +250,7 @@ The registry table below lists the available interactive slash commands. On a ba
250
250
  | `/oracle` | `/oracle <question>` | Ask a read-only advisor to challenge a question against this session's settled decisions |
251
251
  | `/council` | `/council [--roster <name>] [--rounds <n>] [--synthesis <judge\|vote\|none>] <task>` | Ask a roster of read-only members the same task, with an optional vote or judge synthesis |
252
252
  | `/agents` | `/agents` | Open the Library on Agents. |
253
- | `/cost` | `/cost` | Show session token and cost totals |
253
+ | `/usage` | `/usage` | Show subscription quota, credits, and session token and cost totals |
254
254
  | `/doctor` | `/doctor [deep]` | Show a diagnostic report with errors and warnings first and full wrapped check details; `deep` adds live tool probes on the session's targets and a validation-contract dry run at the session's autonomy. See [Doctor](doctor.md). |
255
255
  | `/context` | `/context compact [instructions] \| /context recall <ref> \| /context init [--preview] [--heuristic] [--adopt] [--global] [--propose\|--apply\|--rewrite] \| /context refresh \| /context reset` | Context hub: window overlay plus compact, recall, init, refresh, and reset |
256
256
  | `/fleet` | `/fleet [run [--var <key=value>] <name>]` | Open Fleet Runs, or run a fleet contract with an approval preview. Configure fleets with `/settings fleet`. |
@@ -270,6 +270,92 @@ The registry table below lists the available interactive slash commands. On a ba
270
270
  | `/fork` | `/fork` | Fork from an assistant turn |
271
271
  | `/export` | `/export [path]` | Export a self-contained HTML transcript by default; a `.md` path writes Markdown |
272
272
 
273
+ ### Subscription quota and session usage
274
+
275
+ `/usage` replaces `/cost`. There is no alias: `/cost` is no longer a command.
276
+ The overlay keeps the token and cost accounting `/cost` carried and adds what
277
+ the connected subscription accounts report. It is a live view of this session
278
+ and of your accounts right now; `clio-coder usage report` remains the separate
279
+ command for folding token and cost facts across past sessions.
280
+
281
+ | View | What it shows |
282
+ | --- | --- |
283
+ | Accounts | Subscription and credit meters, every reported model group, remaining capacity, reset countdowns and local reset times with an explicit time zone, and stale or expired readings |
284
+ | Session | Recorded token and cost totals, cache traffic, reasoning, and the background-call categories (side questions, handoffs, pre-warms, memory steps) |
285
+ | Models | Each model's share of recorded processed tokens, including cache traffic, plus its detailed token and cost breakdown |
286
+ | Workers | Active and recent runs, recorded tokens and cost, context usage, and the shared account limit when the local credential owner is known |
287
+
288
+ Press 1–4, Tab/Shift+Tab, or ←/→ to change views; ↑/↓ or PgUp/PgDn to scroll;
289
+ Home/End or Ctrl+Home/Ctrl+End to jump to either end; and Esc to close. Each
290
+ view keeps its own scroll position while the overlay stays open.
291
+
292
+ #### Used, left, and what a percentage describes
293
+
294
+ One direction per label, on every surface. A filled meter cell always means
295
+ consumed capacity, and the detailed meters in Accounts and Status label that
296
+ number `used` and print the complement beside it as `remaining`. Compact badges
297
+ carry the other direction and say so: `weekly 9% used` in a detailed meter is
298
+ the same reading as `weekly 91% left` in a footer badge.
299
+
300
+ Subscription percentages describe an account-wide provider window shared across
301
+ your sessions and devices. Session tokens and costs describe only what this
302
+ process recorded, and the two cannot be converted into each other. Context
303
+ occupancy is a third quantity again: it measures one request against a model's
304
+ window, not an account against its plan.
305
+
306
+ #### Where quota appears outside the overlay
307
+
308
+ The welcome header, the compact footer, the expanded dashboard, worker cards,
309
+ and fleet islands read the same cached readings the overlay does.
310
+
311
+ - The **welcome launchpad** carries a `Subscriptions` field beside the wordmark,
312
+ wrapped so every connected account and the free local-inference row stay visible.
313
+ - The **compact footer** shows the selected model's weekly headroom on line 1,
314
+ beside the model identity, and only when the account and model group are
315
+ identifiable. Line 2 always keeps the working directory and Git branch or dirty
316
+ state; notices, tips, and urgent prompts borrow the rotating hint area beside
317
+ them. Quota never takes line 2, and the footer never promotes an unrelated
318
+ account's busier window in place of the selected model's.
319
+ - The expanded dashboard's **Status** page puts the session total above the
320
+ account meters, **Activity** shows shared account headroom on worker cards, and
321
+ **Context** states that request occupancy and account limits are different
322
+ quantities.
323
+
324
+ #### What Clio does not claim
325
+
326
+ Worker badges are shared account headroom, not a measured per-worker share.
327
+ Clio does not infer a worker's subscription consumption from its token counts or
328
+ from account deltas, and a badge appears only for a supported local credential
329
+ owner and the matching model group. Antigravity's Gemini group stays distinct
330
+ from its Claude and GPT group. Remote workers and generic transports stay
331
+ unlinked rather than being attributed to an account that may not be theirs.
332
+ Clio's own `openai-codex` sign-in is separate storage from the Codex CLI's
333
+ `auth.json`, so a Codex CLI reading is never attributed to an `openai-codex`
334
+ worker.
335
+
336
+ Local inference is listed at `$0.00` with no subscription window consumed. No
337
+ saved-quota figure is claimed, because per-target token attribution does not
338
+ exist yet.
339
+
340
+ #### Reads, caching, and failures
341
+
342
+ Quota reads are read-only. They read stored credentials and never refresh a
343
+ token, write a credential, or start a sign-in. An expired credential is reported
344
+ as expired with the provider's own sign-in guidance rather than repaired.
345
+
346
+ A reading is cached for about five minutes and refreshed lazily when a surface
347
+ that shows it is rendered, never on a background timer. A failed refresh keeps
348
+ the last good reading and marks it `STALE` rather than blanking the account. A
349
+ provider that reports no window for a period reports nothing for it: Codex sends
350
+ a weekly window and no 5h window, and Clio prints the rows a provider actually
351
+ sent instead of a fixed shape. Where a provider sends its own severity word,
352
+ that word wins over Clio's thresholds.
353
+
354
+ Two credentials on one Anthropic subscription (an `anthropic-max` sign-in and a
355
+ Claude Code sign-in) report identical windows, so they are folded into one
356
+ account and the apparent budget is not doubled. Accounts that merely happen to
357
+ show equal percentages are never merged.
358
+
273
359
  ### Notes on individual commands
274
360
 
275
361
  The notes below explain commands whose behavior needs more than a usage line.
@@ -345,7 +431,7 @@ context every worker inherits. Esc closes the overlay, and cancels the round if
345
431
  is still streaming. `/btw` during an in-flight turn is refused with a notice
346
432
  rather than queued, because a side question answered after the run it was asked
347
433
  during has already missed its moment. The round's token usage still shows in
348
- `/cost`, labeled as a side question, because it was a real call and cost real
434
+ `/usage`, labeled as a side question, because it was a real call and cost real
349
435
  money; it is deliberately not counted as a turn.
350
436
 
351
437
  `/council [--roster <name>] [--rounds <n>] [--synthesis judge|vote|none] <task>`
@@ -564,7 +650,7 @@ not a potentially double-counted total. Supported GPU drivers expose utilization
564
650
  and VRAM through sysfs. Missing counters stay unavailable; WSL measurements are
565
651
  labeled as guest measurements and do not describe the Windows host or remote GPU.
566
652
  Counter resets wait for a fresh baseline instead of showing negative rates. Memory
567
- bank and context-engine activity remain visible. `/cost`, `/mcp`, `/library`, `/context`, and
653
+ bank and context-engine activity remain visible. `/usage`, `/mcp`, `/library`, `/context`, and
568
654
  Fleet Runs retain deeper inspection. Narrow or short terminals explicitly indicate
569
655
  when detail does not fit.
570
656
 
@@ -1047,9 +1133,9 @@ The footer owns live activity. Transcript actions have static running or outcome
1047
1133
 
1048
1134
  The Clio TUI maximizes operational focus, ergonomics, and discoverability:
1049
1135
 
1050
- - **Welcome Launchpad & Session Header:** Prior to your first prompt, Clio renders a framed launchpad dashboard (`src/interactive/welcome-dashboard.ts`) with 11 detail rows (model route status, workspace path, permissions/autonomy, guidance, target inventory, and fleet recipes), accompanied by ASCII wordmark art at ≥76 columns and rotating hints at ≥160 columns. A bottom action row indicates the immediate step or blocker (`describe a task · Enter to send · / for commands`, or route blockers such as `/model` or `/settings targets`). On prompt submission, the launchpad collapses to a single live header row (`>C_ Clio Coder v0.5.0 · <target/model> · <workspace> · <branch>*`) tracking mid-session route or branch changes. `/new` restores the launchpad; `/resume`, `/tree`, `/fork`, and `/handoff` collapse it after transition. See [tui-design.md](../architecture/tui-design.md#51-welcome-launchpad--session-header).
1136
+ - **Welcome Launchpad & Session Header:** Prior to your first prompt, Clio renders a framed launchpad dashboard (`src/interactive/welcome-dashboard.ts`) with 11 detail rows (model route status, workspace path, permissions/autonomy, guidance, target inventory, and fleet recipes) plus a wrapping `Subscriptions` field once a connected account reports quota, accompanied by ASCII wordmark art at ≥76 columns and rotating hints at ≥160 columns. A bottom action row indicates the immediate step or blocker (`describe a task · Enter to send · / for commands`, or route blockers such as `/model` or `/settings targets`). On prompt submission, the launchpad collapses to a single live header row (`>C_ Clio Coder v0.5.1 · <target/model> · <workspace> · <branch>*`) tracking mid-session route or branch changes. `/new` restores the launchpad; `/resume`, `/tree`, `/fork`, and `/handoff` collapse it after transition. See [tui-design.md](../architecture/tui-design.md#51-welcome-launchpad--session-header).
1051
1137
  - **Quiet, Focused Clio Composer:** Normal composition rails remain clean and uncluttered (`src/interactive/clio-editor.ts`), omitting duplicate model identity or newline hints already provided by the footer. Exceptional operational states elevate to the top rail: `CONFIRM` (`warning` token, or `CONFIRM · FULL-AUTO` in `editorDanger`) when an approval prompt owns input, `PREPARING` during turn dispatch, `COMPACTING` during context compaction, or `FULL-AUTO` (`editorDanger`). Native editor scroll indicators (`↑/↓` line counts) remain on line 0 when drafts scroll. When the draft is empty, line 1 shows context-aware dim placeholder text (`Ask Clio… / for commands`, `A parked call is waiting for your decision`, `Clio has your prompt and is preparing the turn`, or `Clio is compacting the session context`). During parked permission prompts, the bottom rail carries fitted decision keys (`confirmRailHint` via `src/interactive/permission-hint.ts`: `Enter` allow, or `Backspace` clear draft to allow if a draft is present; `Esc` deny; `s` stop turn; `v` inspect mutation; standing approval terms toggle via `?` on the permission card itself).
1052
- - **Two-Line Compact Footer & Expanded Dashboard:** The compact footer (`src/interactive/footer/pages.ts`) renders an ambient two-line strip: Line 1 displays live activity phase (`accent`) and active worker counts (`agent`), model/target identity, thinking level, context meter bar, and token usage; Line 2 surfaces urgent input prompts (`Ctrl+C again to quit`, `Ctrl+G choose key`), active notices, or rotating shortcut hints alternating with workspace cwd and git dirty state on the left, alongside session throughput and dashboard toggle keys on the right. Pressing `Alt+U` expands the dashboard into 1/4 of the viewport (minimum 8 rows), cycling through `Activity` (live worker cards and finished history), `Context` (meter bar/grid, category swatches, headroom), `Status` (Cost & Connections, Local Machine metrics, memory bank), and closed.
1138
+ - **Two-Line Compact Footer & Expanded Dashboard:** The compact footer (`src/interactive/footer/pages.ts`) renders an ambient two-line strip: Line 1 displays live activity phase (`accent`) and active worker counts (`agent`), model/target identity, thinking level, context meter bar, and token usage; Line 1 also shows the selected model’s weekly quota remaining when its account and model group are known; Line 2 preserves workspace cwd and Git branch/dirty state on the left, with rotating shortcuts, tips, or urgent notices on the right. Pressing `Alt+U` expands the dashboard into 1/4 of the viewport (minimum 8 rows), cycling through `Activity` (live worker cards and finished history), `Context` (meter bar/grid, category swatches, headroom), `Status` (Cost & Connections, Local Machine metrics, memory bank), and closed.
1053
1139
  - **Grouped Slash Command Palette:** Typing `/` opens an autocomplete command palette grouped by operational category (`Run`, `Inspect`, `Configure`, `Sessions`) with compact argument hints. Every suggestion is the command's one canonical spelling.
1054
1140
  - **Voice-First Transcript & Receipts:** User (`› `) and assistant (`✦ `) prose are formatted with a two-cell hanging indent, ensuring wrapped continuation lines remain visually tied to their voice prefix. Tool ledgers maintain full terminal width. Completed turn receipts honor Output style: Compact omits the separate receipt, Standard shows a small completion line with available duration, and Detailed includes available call counts, token usage and reasoning provenance.
1055
1141
  - **Transactional Settings Center:** Open `/settings` or deep-link to one of `chat`, `fleet`, `targets`, `context`, `safety`, `interface`, or `integrations`. Value edits construct change plans offering `Apply this session`, `Apply and save globally`, or `Cancel`; narrow terminals use a drill-down layout below 72 columns.
@@ -687,7 +687,7 @@ Every new assistant ledger entry carries `responseModelIdObservation` in one of
687
687
 
688
688
  </details>
689
689
 
690
- The adapter retains `responseModel` as the differing response id because providers outside the stream tap still supply that fact. `clio-coder usage report` emits `attributedModelId`, `requestedModelIds`, and `responseModelIdObservationCounts`. Its text table and the `/cost` overlay use the labels `attributed model`, `requested model ids`, and `response model id observation`; requested ids are printed as ids rather than as `same`.
690
+ The adapter retains `responseModel` as the differing response id because providers outside the stream tap still supply that fact. `clio-coder usage report` emits `attributedModelId`, `requestedModelIds`, and `responseModelIdObservationCounts`. Its text table and the `/usage` overlay use the labels `attributed model`, `requested model ids`, and `response model id observation`; requested ids are printed as ids rather than as `same`.
691
691
 
692
692
  The footer's last-turn line uses `response model id observation <state>`, with the id after `reported` or a historical `legacy difference-only` state. Dispatch receipt `upstreamResponses` entries carry `requestedModelId`, `responseModelIdObservation`, `differingResponseModelId`, and `providerResponseId`. The peer warning is said once per process per distinct fact (target, requested id, resolved instance, peer set), not once per turn.
693
693
 
@@ -1235,7 +1235,7 @@ integrations:
1235
1235
  ```
1236
1236
  Then invoke it using `/delegate claude-code <task>`.
1237
1237
 
1238
- `/delegate [--share] <agent-id> <task>` does not accept `--model` in v0.5.0.
1238
+ `/delegate [--share] <agent-id> <task>` does not accept `--model` in v0.5.1.
1239
1239
  `/run --model <id>` selects a model for a Clio fleet worker; it does not configure
1240
1240
  an external ACP session. External model selection depends on the adapter's own
1241
1241
  supported configuration. Clio does not currently expose an ACP model selector.
@@ -19,7 +19,7 @@ writes, model resolution, reminders, handoff offers, and handoff seeding.
19
19
  | --- | --- |
20
20
  | Inspect the bank and recent memory steps | `/memory` |
21
21
  | Select a background model or change memory controls | `/settings` → **Context & Memory** |
22
- | Review recorded cost | `/cost` or `clio-coder usage report` |
22
+ | Review recorded cost | `/usage` or `clio-coder usage report` |
23
23
  | Look up the default values and routing example | [Operator setup](#operator-setup) |
24
24
  | Understand what survives a handoff | [Handoff continuity](#handoff-continuity) |
25
25
 
@@ -234,7 +234,7 @@ The LLM tier costs real tokens, real seconds of model time, and a request slot o
234
234
  a server that is usually the same machine the operator's own turns run on.
235
235
 
236
236
  Every step is therefore accounted for the way a `/btw` side question is: one cost
237
- entry under the `background-memory` label, which `/cost` shows as its own
237
+ entry under the `background-memory` label, which `/usage` shows as its own
238
238
  `memory steps` row, and one durable row in `<stateDir>/usage/out-of-turn.jsonl`
239
239
  carrying the usage, the call's duration, and the backend's prefill facts.
240
240
  `clio-coder usage report` folds those rows after the process exits, and `/memory`
@@ -582,7 +582,7 @@ Every exact-schema record has:
582
582
  - count of cited entries, input/output/total memory-model tokens, and latency.
583
583
 
584
584
  The same steps are also billed. See "Cost and the default decision" for the
585
- `/cost` row, the durable out-of-turn usage row, and the lifetime figures `/memory`
585
+ `/usage` row, the durable out-of-turn usage row, and the lifetime figures `/memory`
586
586
  folds out of this file.
587
587
 
588
588
  `dropped` is the one outcome that ran no step. It has two causes, separated by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iowarp/clio-coder",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "An open-source terminal coding agent for scientific software: your models, bounded workers, and inspectable evidence.",
5
5
  "keywords": [
6
6
  "ai",
package/src/cli/usage.ts CHANGED
@@ -114,7 +114,7 @@ function addResponseModelIdObservation(counts: ResponseModelIdObservationCounts,
114
114
  * session pre-warm, and a proactive-memory step are real spend and belong in
115
115
  * the token and cost totals, but none of them is a turn: they never entered
116
116
  * the session. `turns` therefore subtracts them from the folded row count,
117
- * which is exactly what the `/cost` overlay does.
117
+ * which is exactly what the `/usage` overlay does.
118
118
  */
119
119
  interface CallOrigins {
120
120
  rows: number;
@@ -435,7 +435,7 @@ export async function runUsageCommand(argv: ReadonlyArray<string>): Promise<numb
435
435
  }
436
436
  }
437
437
 
438
- // Token and cost facts fold the same per-call usage the `/cost` overlay
438
+ // Token and cost facts fold the same per-call usage the `/usage` overlay
439
439
  // reseeds from, through the same session-domain function, so the report and
440
440
  // the overlay cannot disagree about what a session spent.
441
441
  // Keyed by the model id accounting attributes the call to. A reported LM
@@ -35,7 +35,7 @@ import {
35
35
  } from "./protocol.js";
36
36
 
37
37
  export const MCP_CLIENT_NAME = "clio-coder";
38
- export const DEFAULT_MCP_CLIENT_VERSION = "0.5.0";
38
+ export const DEFAULT_MCP_CLIENT_VERSION = "0.5.1";
39
39
  export const DEFAULT_INITIALIZE_TIMEOUT_MS = 15_000;
40
40
  export const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
41
41
  export const DEFAULT_STDERR_TAIL_BYTES = 16 * 1024;
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * The two writes are the same pair the side-question path makes: the
12
12
  * in-process cost tracker under the `background-memory` label, which is what
13
- * `/cost` folds, and one durable row in the out-of-turn usage store, which is
13
+ * `/usage` folds, and one durable row in the out-of-turn usage store, which is
14
14
  * what an archive reader folds after the process is gone.
15
15
  */
16
16
 
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Per-entry token breakdown (input/output/cacheRead/cacheWrite/reasoning)
9
9
  * matches the shape of pi-ai's `Usage` plus provider-specific reasoning detail
10
- * fields. The /cost overlay aggregates it via `aggregateCostEntries`; the TUI
10
+ * fields. The /usage overlay aggregates it via `aggregateCostEntries`; the TUI
11
11
  * footer consumes the session sum through `ObservabilityContract.sessionTokens()`.
12
12
  */
13
13
 
@@ -92,7 +92,7 @@ export function formatCostAggregate(cost: CostAggregate | null | undefined): str
92
92
 
93
93
  /**
94
94
  * What a fixed-width surface says when there is no cost claim. The footer and
95
- * `/cost` drop their cost field instead; a table cell that owns a column cannot,
95
+ * `/usage` drop their cost field instead; a table cell that owns a column cannot,
96
96
  * so it says the same thing in words rather than inventing `$0.00`.
97
97
  */
98
98
  export const COST_NOT_MEASURED = "not measured";
@@ -134,7 +134,7 @@ export interface UsageBreakdown {
134
134
  }
135
135
 
136
136
  /**
137
- * What produced a priced call, when it was not an ordinary turn. `/cost` and
137
+ * What produced a priced call, when it was not an ordinary turn. `/usage` and
138
138
  * the usage surfaces separate these out so an operator can see that money was
139
139
  * spent beside the session rather than inside it.
140
140
  */
@@ -53,7 +53,7 @@ function recordDispatchCost(
53
53
  }
54
54
  telemetry.record("counter", "tokens.total", payload.tokenCount);
55
55
  // Dispatch terminal payloads carry the same full split as receipts. Preserve
56
- // it so /cost and the footer agree with the fleet board instead of showing
56
+ // it so /usage and the footer agree with the fleet board instead of showing
57
57
  // zero input/output/cache for worker-only sessions.
58
58
  cost.accumulate(
59
59
  payload.targetId,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Thin reducer over TelemetrySnapshot. Produces a view the TUI + /cost overlay
2
+ * Thin reducer over TelemetrySnapshot. Produces a view the TUI + /usage overlay
3
3
  * consume. Stays pure so tests can hand-roll snapshots and assert the shape.
4
4
  */
5
5
 
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Quota adapter for the `anthropic-max` runtime, the account Clio itself
3
+ * spends when it runs a native turn.
4
+ *
5
+ * Unlike `claude-code`, this credential belongs to Clio: it is the OAuth
6
+ * record her own auth storage persists under the `anthropic` provider id.
7
+ * The adapter reads the stored credential without triggering a refresh,
8
+ * because a quota read must never mutate an authentication record.
9
+ *
10
+ * Both adapters can point at the same Anthropic account. That is a display
11
+ * concern rather than a correctness one: each reports what its own
12
+ * credential sees, and a consumer that shows both should say which
13
+ * credential produced which row.
14
+ */
15
+
16
+ import { openAuthStorage } from "../providers/auth/index.js";
17
+ import { fetchAnthropicUsage } from "./anthropic-usage.js";
18
+ import type { QuotaProvider, UsageSnapshot } from "./types.js";
19
+
20
+ export const ANTHROPIC_MAX_QUOTA_PROVIDER_ID = "anthropic-max";
21
+
22
+ /** The provider id Clio's auth storage files an Anthropic OAuth credential under. */
23
+ const AUTH_PROVIDER_ID = "anthropic";
24
+ const DISPLAY_NAME = "Anthropic Max";
25
+ const REQUEST_TIMEOUT_MS = 15_000;
26
+
27
+ /** The subset of Clio's stored OAuth credential this adapter needs. */
28
+ export interface AnthropicMaxCredentials {
29
+ accessToken: string;
30
+ /** Epoch milliseconds the access token expires, or null when the record omits it. */
31
+ expiresAtMs: number | null;
32
+ hasRefreshToken: boolean;
33
+ }
34
+
35
+ export interface AnthropicMaxQuotaOptions {
36
+ readCredentials?: () => Promise<AnthropicMaxCredentials | null>;
37
+ fetch?: typeof globalThis.fetch;
38
+ timeoutMs?: number;
39
+ now?: () => Date;
40
+ }
41
+
42
+ /** Shape a stored auth credential into the fields a usage read needs. */
43
+ export function parseStoredAnthropicCredential(raw: unknown): AnthropicMaxCredentials | null {
44
+ if (!isRecord(raw) || raw.type !== "oauth") return null;
45
+ const access = raw.access;
46
+ if (typeof access !== "string" || access.length === 0) return null;
47
+ const refresh = raw.refresh;
48
+ return {
49
+ accessToken: access,
50
+ expiresAtMs: asNumber(raw.expires),
51
+ hasRefreshToken: typeof refresh === "string" && refresh.length > 0,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Read Clio's stored Anthropic credential.
57
+ *
58
+ * `AuthStorage.get` is the accessor that does no refresh, unlike
59
+ * `resolveApiKey`, which renews an expiring credential as a side effect.
60
+ */
61
+ async function readStoredCredential(): Promise<AnthropicMaxCredentials | null> {
62
+ try {
63
+ return parseStoredAnthropicCredential(openAuthStorage().get(AUTH_PROVIDER_ID));
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /** Build the read-only `anthropic-max` quota adapter. */
70
+ export function createAnthropicMaxQuotaProvider(options: AnthropicMaxQuotaOptions = {}): QuotaProvider {
71
+ const readCredentials = options.readCredentials ?? readStoredCredential;
72
+ const doFetch = options.fetch ?? globalThis.fetch;
73
+ const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
74
+ const now = options.now ?? (() => new Date());
75
+
76
+ const snapshot = (
77
+ status: UsageSnapshot["status"],
78
+ message: string | null,
79
+ extra: Partial<UsageSnapshot> = {},
80
+ ): UsageSnapshot => ({
81
+ providerId: ANTHROPIC_MAX_QUOTA_PROVIDER_ID,
82
+ displayName: DISPLAY_NAME,
83
+ status,
84
+ windows: [],
85
+ message,
86
+ fetchedAt: now().toISOString(),
87
+ ...extra,
88
+ });
89
+
90
+ return {
91
+ id: ANTHROPIC_MAX_QUOTA_PROVIDER_ID,
92
+ displayName: DISPLAY_NAME,
93
+
94
+ async detect(): Promise<boolean> {
95
+ return (await readCredentials()) !== null;
96
+ },
97
+
98
+ async fetch(): Promise<UsageSnapshot> {
99
+ const credentials = await readCredentials();
100
+ if (credentials === null) {
101
+ return snapshot("no_credentials", "Clio holds no Anthropic OAuth credential");
102
+ }
103
+ if (credentials.expiresAtMs !== null && credentials.expiresAtMs <= now().getTime()) {
104
+ return snapshot(
105
+ "expired",
106
+ credentials.hasRefreshToken
107
+ ? "Clio's Anthropic token expired; the next turn refreshes it"
108
+ : "Run clio-coder auth login anthropic again",
109
+ );
110
+ }
111
+
112
+ const usage = await fetchAnthropicUsage(credentials.accessToken, { fetch: doFetch, timeoutMs });
113
+ if (usage.status !== "ok") {
114
+ return snapshot(usage.status, usage.message ?? "Run clio-coder auth login anthropic again", {
115
+ retryAfterSeconds: usage.retryAfterSeconds,
116
+ });
117
+ }
118
+ return snapshot("ok", null, { windows: usage.windows, credits: usage.credits });
119
+ },
120
+ };
121
+ }
122
+
123
+ function isRecord(value: unknown): value is Record<string, unknown> {
124
+ return typeof value === "object" && value !== null && !Array.isArray(value);
125
+ }
126
+
127
+ function asNumber(value: unknown): number | null {
128
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
129
+ if (typeof value === "string") {
130
+ const parsed = Number.parseFloat(value.trim());
131
+ return Number.isFinite(parsed) ? parsed : null;
132
+ }
133
+ return null;
134
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Shared reader for Anthropic's OAuth usage endpoint.
3
+ *
4
+ * Two runtimes spend an Anthropic subscription: the `claude-code` CLI with
5
+ * its own stored login, and `anthropic-max` with the OAuth credential Clio
6
+ * itself holds. The account surface is the same endpoint and the same two
7
+ * response shapes, so only the token source differs. This module owns the
8
+ * request and the parsing; each adapter owns its credential.
9
+ *
10
+ * The endpoint has shipped both a `limits[]` array and flat
11
+ * `five_hour`/`seven_day` buckets, and has been observed serving both at
12
+ * once. The array wins when present because only it carries severity,
13
+ * the active flag, and per-model scope.
14
+ */
15
+
16
+ import { parseRetryAfterSeconds, type UsageCredits, type UsageSnapshot, type UsageWindow } from "./types.js";
17
+
18
+ export const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
19
+ export const ANTHROPIC_OAUTH_BETA = "oauth-2025-04-20";
20
+
21
+ const WINDOW_ORDER: Record<string, number> = { session: 0, weekly: 1 };
22
+
23
+ /** What one usage read produced, before an adapter stamps its own identity on it. */
24
+ export interface AnthropicUsageOutcome {
25
+ status: UsageSnapshot["status"];
26
+ message: string | null;
27
+ windows: UsageWindow[];
28
+ credits: UsageCredits | null;
29
+ retryAfterSeconds: number | null;
30
+ }
31
+
32
+ export interface AnthropicUsageDependencies {
33
+ fetch: typeof globalThis.fetch;
34
+ timeoutMs: number;
35
+ }
36
+
37
+ function outcome(
38
+ status: UsageSnapshot["status"],
39
+ message: string | null,
40
+ extra: Partial<AnthropicUsageOutcome> = {},
41
+ ): AnthropicUsageOutcome {
42
+ return { status, message, windows: [], credits: null, retryAfterSeconds: null, ...extra };
43
+ }
44
+
45
+ /** Read current Anthropic usage with a bearer token, reporting failure as a status. */
46
+ export async function fetchAnthropicUsage(
47
+ token: string,
48
+ dependencies: AnthropicUsageDependencies,
49
+ ): Promise<AnthropicUsageOutcome> {
50
+ const controller = new AbortController();
51
+ const timer = setTimeout(() => controller.abort(), dependencies.timeoutMs);
52
+ let response: Response;
53
+ try {
54
+ response = await dependencies.fetch(ANTHROPIC_USAGE_URL, {
55
+ method: "GET",
56
+ headers: {
57
+ Authorization: `Bearer ${token}`,
58
+ "anthropic-beta": ANTHROPIC_OAUTH_BETA,
59
+ },
60
+ signal: controller.signal,
61
+ });
62
+ } catch (error) {
63
+ return outcome("error", describeError(error, dependencies.timeoutMs));
64
+ } finally {
65
+ clearTimeout(timer);
66
+ }
67
+
68
+ if (response.status === 401 || response.status === 403) {
69
+ await discardBody(response);
70
+ return outcome("expired", null);
71
+ }
72
+ if (response.status === 429) {
73
+ await discardBody(response);
74
+ return outcome("error", "HTTP 429", {
75
+ retryAfterSeconds: parseRetryAfterSeconds(response.headers.get("Retry-After")),
76
+ });
77
+ }
78
+ if (response.status !== 200) {
79
+ await discardBody(response);
80
+ return outcome("error", `HTTP ${response.status}`);
81
+ }
82
+
83
+ let body: unknown;
84
+ try {
85
+ body = await response.json();
86
+ } catch {
87
+ return outcome("error", "Unexpected usage response");
88
+ }
89
+ if (!isRecord(body)) {
90
+ return outcome("error", "Unexpected usage response");
91
+ }
92
+
93
+ return outcome("ok", null, { windows: usageWindows(body), credits: usageCredits(body.spend) });
94
+ }
95
+
96
+ /** Windows from `limits[]` when the endpoint sends it, otherwise the flat buckets. */
97
+ function usageWindows(body: Record<string, unknown>): UsageWindow[] {
98
+ const fromLimits = windowsFromLimits(body.limits);
99
+ const windows = fromLimits.length > 0 ? fromLimits : windowsFromFlat(body);
100
+ return windows.sort((left, right) => {
101
+ const order = (WINDOW_ORDER[left.key] ?? 2) - (WINDOW_ORDER[right.key] ?? 2);
102
+ return order !== 0 ? order : left.label.localeCompare(right.label);
103
+ });
104
+ }
105
+
106
+ function windowsFromLimits(raw: unknown): UsageWindow[] {
107
+ if (!Array.isArray(raw)) return [];
108
+ const windows: UsageWindow[] = [];
109
+ for (const entry of raw) {
110
+ if (!isRecord(entry)) continue;
111
+ const percent = asNumber(entry.percent);
112
+ if (percent === null) continue;
113
+ const common: Pick<UsageWindow, "usedPct" | "resetsAt" | "severity" | "active"> = {
114
+ usedPct: percent,
115
+ resetsAt: asIsoString(entry.resets_at),
116
+ };
117
+ if (typeof entry.severity === "string") common.severity = entry.severity;
118
+ if (entry.is_active === true) common.active = true;
119
+ if (entry.kind === "session") {
120
+ windows.push({ key: "session", label: "5h", short: "5h", ...common });
121
+ } else if (entry.kind === "weekly_all") {
122
+ windows.push({ key: "weekly", label: "Weekly", short: "wk", ...common });
123
+ } else if (entry.kind === "weekly_scoped") {
124
+ const name = scopeName(entry.scope);
125
+ if (name === null) continue;
126
+ windows.push({ key: `weekly_scoped.${slug(name)}`, label: name, scope: name, ...common });
127
+ }
128
+ }
129
+ return windows;
130
+ }
131
+
132
+ function windowsFromFlat(body: Record<string, unknown>): UsageWindow[] {
133
+ const windows: UsageWindow[] = [];
134
+ const fiveHour = bucketWindow(body.five_hour, "session", "5h", "5h");
135
+ if (fiveHour !== null) windows.push(fiveHour);
136
+ const sevenDay = bucketWindow(body.seven_day, "weekly", "Weekly", "wk");
137
+ if (sevenDay !== null) windows.push(sevenDay);
138
+ return windows;
139
+ }
140
+
141
+ function bucketWindow(raw: unknown, key: string, label: string, short: string): UsageWindow | null {
142
+ if (!isRecord(raw)) return null;
143
+ const usedPct = asNumber(raw.utilization);
144
+ if (usedPct === null) return null;
145
+ return { key, label, short, usedPct, resetsAt: asIsoString(raw.resets_at) };
146
+ }
147
+
148
+ function scopeName(raw: unknown): string | null {
149
+ if (!isRecord(raw)) return null;
150
+ const model = raw.model;
151
+ if (!isRecord(model)) return null;
152
+ const name = typeof model.display_name === "string" ? model.display_name.trim() : "";
153
+ if (name.length === 0) return null;
154
+ return slug(name) === "all-models" || slug(name) === "all" ? null : name;
155
+ }
156
+
157
+ /** Credit balance from the `spend` block, present only for credit-billed plans. */
158
+ function usageCredits(raw: unknown): UsageCredits | null {
159
+ if (!isRecord(raw) || raw.enabled !== true) return null;
160
+ const percent = asNumber(raw.percent);
161
+ const limit = majorAmount(raw.limit);
162
+ if (limit === null) {
163
+ return percent === null ? null : { display: `${percent.toFixed(0)}% used`, usedPct: percent };
164
+ }
165
+ const used = majorAmount(raw.used);
166
+ if (used === null) return null;
167
+ const currency = isRecord(raw.limit) && typeof raw.limit.currency === "string" ? raw.limit.currency : "USD";
168
+ const symbol = currency === "USD" ? "$" : "";
169
+ const usedPct = percent ?? (limit > 0 ? (used / limit) * 100 : null);
170
+ return { display: `${symbol}${used.toFixed(2)} / ${symbol}${limit.toFixed(2)}`, usedPct };
171
+ }
172
+
173
+ function majorAmount(raw: unknown): number | null {
174
+ if (!isRecord(raw)) return null;
175
+ const minor = asNumber(raw.amount_minor);
176
+ if (minor === null) return null;
177
+ const exponent = asNumber(raw.exponent) ?? 0;
178
+ return minor / 10 ** exponent;
179
+ }
180
+
181
+ function isRecord(value: unknown): value is Record<string, unknown> {
182
+ return typeof value === "object" && value !== null && !Array.isArray(value);
183
+ }
184
+
185
+ function asNumber(value: unknown): number | null {
186
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
187
+ if (typeof value === "string") {
188
+ const parsed = Number.parseFloat(value.trim());
189
+ return Number.isFinite(parsed) ? parsed : null;
190
+ }
191
+ return null;
192
+ }
193
+
194
+ function asIsoString(value: unknown): string | null {
195
+ if (typeof value !== "string" || value.trim().length === 0) return null;
196
+ const parsed = new Date(value);
197
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
198
+ }
199
+
200
+ function slug(value: string): string {
201
+ return value
202
+ .toLowerCase()
203
+ .replace(/[^a-z0-9]+/g, "-")
204
+ .replace(/^-+|-+$/g, "");
205
+ }
206
+
207
+ function describeError(error: unknown, timeoutMs: number): string {
208
+ if (error instanceof Error) {
209
+ return error.name === "AbortError" ? `timeout after ${timeoutMs}ms` : error.message;
210
+ }
211
+ return String(error);
212
+ }
213
+
214
+ async function discardBody(response: Response): Promise<void> {
215
+ try {
216
+ await response.body?.cancel();
217
+ } catch {
218
+ return;
219
+ }
220
+ }