sessiontools 0.3.1__tar.gz → 0.3.2__tar.gz

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 (93) hide show
  1. {sessiontools-0.3.1 → sessiontools-0.3.2}/PKG-INFO +88 -31
  2. {sessiontools-0.3.1 → sessiontools-0.3.2}/README.md +87 -30
  3. {sessiontools-0.3.1 → sessiontools-0.3.2}/pyproject.toml +1 -1
  4. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/cli.py +52 -13
  5. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/limits.py +6 -4
  6. sessiontools-0.3.2/src/sessiontools/account/profile.py +147 -0
  7. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/status.py +139 -35
  8. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/use.py +26 -6
  9. sessiontools-0.3.2/src/sessiontools/account/verify.py +419 -0
  10. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/band.py +1 -1
  11. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/cli.py +25 -1
  12. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/default_model_cli.py +1 -1
  13. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/default_model_watch.py +6 -3
  14. sessiontools-0.3.2/src/sessiontools/identity_block.py +169 -0
  15. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/listing.py +101 -6
  16. sessiontools-0.3.2/src/sessiontools/migrate.py +572 -0
  17. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/pane_state_agent.py +8 -3
  18. sessiontools-0.3.2/src/sessiontools/paths.py +165 -0
  19. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/reap.py +7 -5
  20. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/session_info.py +52 -5
  21. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/settings.py +214 -13
  22. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/snapshot.py +5 -7
  23. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/sound/__init__.py +6 -8
  24. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/sound/engine.swift +1 -1
  25. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/sound/service.py +29 -8
  26. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/__init__.py +135 -26
  27. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/agents.py +23 -15
  28. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/args.py +5 -3
  29. sessiontools-0.3.2/src/sessiontools/start/boot_identity.py +427 -0
  30. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/claude_settings.py +71 -5
  31. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/default_seat.py +5 -2
  32. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/environment.py +10 -1
  33. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/holders.py +89 -13
  34. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/placement.py +35 -3
  35. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/plan.py +69 -20
  36. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/run.py +33 -1
  37. {sessiontools-0.3.1 → sessiontools-0.3.2}/LICENSE +0 -0
  38. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/__init__.py +0 -0
  39. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/__init__.py +0 -0
  40. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/keychain.py +0 -0
  41. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/leakscan.py +0 -0
  42. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/movehome.py +0 -0
  43. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/new.py +0 -0
  44. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/registry.py +0 -0
  45. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/setuptoken.py +0 -0
  46. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/tokenfile.py +0 -0
  47. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/account/tokenstore.py +0 -0
  48. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/busy.py +0 -0
  49. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/claude_code.py +0 -0
  50. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/clockline.py +0 -0
  51. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/completion.py +0 -0
  52. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/__init__.py +0 -0
  53. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/boot.py +0 -0
  54. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/breakdown.py +0 -0
  55. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/hooks.py +0 -0
  56. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/nnls.py +0 -0
  57. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/render.py +0 -0
  58. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/slash_context.py +0 -0
  59. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/tools.py +0 -0
  60. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/context_parts/transcript.py +0 -0
  61. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/default_model.py +0 -0
  62. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/get.py +0 -0
  63. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/ledger/__init__.py +0 -0
  64. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/ledger/_tiktoken_child.py +0 -0
  65. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/ledger/count.py +0 -0
  66. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/ledger/render.py +0 -0
  67. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/ledger/transcript.py +0 -0
  68. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/operator.py +0 -0
  69. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/pane_state.py +0 -0
  70. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/park.py +0 -0
  71. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/procs.py +0 -0
  72. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/py.typed +0 -0
  73. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/send.py +0 -0
  74. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/settle.py +0 -0
  75. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/sound/event.swift +0 -0
  76. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/agentfile.py +0 -0
  77. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/context.py +0 -0
  78. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/default_seat.md +0 -0
  79. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/errors.py +0 -0
  80. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/homefolder.py +0 -0
  81. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/identity.py +0 -0
  82. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/policy.py +0 -0
  83. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/record.py +0 -0
  84. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/resume.py +0 -0
  85. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/secrets.py +0 -0
  86. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/start/vendor.py +0 -0
  87. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/statusline/__init__.py +0 -0
  88. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/statusline/sample.json +0 -0
  89. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/statusline/statusline.sh +0 -0
  90. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/tmux.py +0 -0
  91. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/turn.py +0 -0
  92. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/verbs.py +0 -0
  93. {sessiontools-0.3.1 → sessiontools-0.3.2}/src/sessiontools/window.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sessiontools
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: The harness layer: sessions, status and idle, one driver per harness - a library first
5
5
  Author: Mike Bailey
6
6
  Author-email: Mike Bailey <mike@sessionkit.ai>
@@ -61,6 +61,8 @@ eval "$(session completion bash)"
61
61
  category.
62
62
  - `session ledger --short`: each file read and command run, by tokens.
63
63
  - `session account status`: which Claude account this Mac is on.
64
+ - `session account verify`: which account the API bills for this seat's
65
+ token, from one 1-token request.
64
66
  - `session start --list-agents`: the agents it can start.
65
67
  - `session reap`: seats idle past 1h, each CLOSE or KEEP, and why. A dry
66
68
  run.
@@ -129,8 +131,10 @@ it. Each line says what puts it there and what takes it away.
129
131
  sessiontools` drops this package's entries, and `uv cache prune` drops
130
132
  the ones nothing uses.
131
133
  - **Folders, each made by a verb you run:**
132
- - `~/.session/state/list/`: the list cache, `snapshot.json` and
133
- `list.lock`, at the first `session list`.
134
+ - `~/.sessionkit/tools/state/list/`: the list cache, `snapshot.json`
135
+ and `list.lock`, at the first `session list`. The state folders here
136
+ and below are on a fresh host; a host that still has
137
+ `~/.session/state/` keeps using it until it is moved.
134
138
  - `~/.session/sessions/<session-id>/`: one for each `session start`,
135
139
  holding `launch/`, `log/settings.jsonl` and `session-id`. It also
136
140
  gets `settings/<tool>.d/50-env.json`, one for each tool whose
@@ -139,14 +143,19 @@ it. Each line says what puts it there and what takes it away.
139
143
  `log/settings.jsonl` names each variable it read, never its value.
140
144
  The plugin's hooks and `session park` write only inside a folder
141
145
  that is already there.
142
- - `~/.session/state/reap.log`: `session reap --yes`.
143
- - `~/.session/state/pane-state/watch.lock`: the pane signal's service.
144
- - `~/.session/state/default-model/`: `backups/` and `log.jsonl`, the
145
- first time `session default-model enforce` changes settings.json.
146
+ - `~/.sessionkit/tools/state/pane-state/watch.lock`: the pane signal's
147
+ service.
148
+ - `~/.sessionkit/tools/state/default-model/`: `backups/` and
149
+ `log.jsonl`, the first time `session default-model enforce` changes
150
+ settings.json.
146
151
  - `~/.sessionkit/tools/accounts/`: `session account new` and `use`.
147
- - `~/.local/share/sessiontools/sound/bin/`: `session sound engine on`
148
- builds the sound engine there with `swiftc`.
149
- - `~/Library/Logs/sessiontools/`: the two services' logs.
152
+ - `~/.sessionkit/tools/state/sound/bin/`: `session sound engine on`
153
+ builds the sound engine there with `swiftc` (in
154
+ `~/.local/share/sessiontools/sound/bin/` while that old folder is
155
+ still there).
156
+ - `~/.sessionkit/tools/log/`: the services' logs and `reap.log`
157
+ (Logs, below). An older release wrote them elsewhere, and those
158
+ files stay where they were.
150
159
  - `SESSIONS_DIR`, `SESSION_STATE_DIR` and `SESSIONTOOLS_SOUND_HOME`
151
160
  move these folders when set.
152
161
  - **Services (launchd agents):** none until you turn one on. Each `on`
@@ -188,6 +197,12 @@ it. Each line says what puts it there and what takes it away.
188
197
  With `model.default` set, `session default-model enforce` (and the
189
198
  unit or `watch` that runs it) rewrites the top-level `"model"` in
190
199
  `~/.claude/settings.json`, after a backup.
200
+ - **Not changed by an upgrade: a live tmux server.** One born inside the
201
+ default seat by sessiontools 0.3.1 or earlier still hands
202
+ `CLAUDE_CODE_AGENT=claude` to every pane it makes, so a raw `claude`
203
+ there hears the seat's mail. Restarting that server, or running
204
+ `tmux set-environment -g -u CLAUDE_CODE_AGENT` on it, is yours to do
205
+ ([the default seat][default-seat]).
191
206
 
192
207
  ## Uninstall
193
208
 
@@ -208,23 +223,27 @@ uv tool uninstall sessiontools
208
223
  ```
209
224
 
210
225
  That leaves what the verbs made, listed above. The plugin's cached
211
- copy, the services' logs and the engine's folder are sessiontools'
212
- alone. To delete them:
226
+ copy and the engine's folder are sessiontools' alone. To delete them:
213
227
 
214
228
  ```bash
215
229
  rm -rf "${HOME:?}/.claude/plugins/cache/sessiontools"
216
- rm -rf "${HOME:?}/Library/Logs/sessiontools"
217
230
  rm -rf "${HOME:?}/.local/share/sessiontools"
231
+ rm -rf "${HOME:?}/.sessionkit/tools/state/sound"
218
232
  ```
219
233
 
234
+ The logs are left in place: the new ones in `~/.sessionkit/tools/log/`,
235
+ and an older release's in `~/Library/Logs/sessiontools/`. Nothing here
236
+ deletes from `~/Library/Logs`.
237
+
220
238
  uv's cache stays too, with the other things uv installed; `uv cache
221
239
  clean sessiontools` drops this package's entries.
222
240
 
223
241
  The rest is your data: delete it by hand only if you want it gone.
224
- `~/.sessionkit/tools/` holds your settings and accounts, and the
225
- accounts folder keeps the backups of every token `session account use`
226
- replaced. `~/.session/` holds the session folders and the list cache,
227
- and the kit's other packages keep their files in it too.
242
+ `~/.sessionkit/tools/` holds your settings, accounts, state and logs,
243
+ and the accounts folder keeps the backups of every token `session
244
+ account use` replaced. `~/.session/` holds the session folders (and, on
245
+ a host that has not moved it, the old state folder), and the kit's other
246
+ packages keep their files in it too.
228
247
 
229
248
  ## Docs
230
249
 
@@ -236,6 +255,10 @@ and the kit's other packages keep their files in it too.
236
255
  - [The `session list` cache][cache]: one computation, many readers
237
256
  - [Operator and privileged seats][operator]: who seats ask
238
257
  - [Closing idle seats][reap]: `session reap`, a dry run by default
258
+ - [Where tools keeps its state][state-paths]: each folder, old and new,
259
+ the rule, and `session path`, the verb peers ask
260
+ - [Moving tools' state][state-migrate]: `session path migrate`, one host
261
+ at a time, dry run first
239
262
  - [Shell completion][completion]: `session completion bash|zsh`, a static
240
263
  script built from the verbs' own parsers
241
264
  - [The Claude Code plugin][plugin]: the skill and the hooks
@@ -244,6 +267,8 @@ and the kit's other packages keep their files in it too.
244
267
  - [The busy sound][busy]: a sound while a tool batch runs long
245
268
  - [The session-info hooks][session-info]: the short session id as the
246
269
  session title, and the session's facts in context, both off by default
270
+ - [The identity block][identity]: who a launched session is, in its
271
+ context on turn one and from `session identity`
247
272
  - [The sound engine][engine]: the busy sound from one resident process
248
273
  - [The status line][status-line]: `session statusline install` puts it in the
249
274
  kit's claude-code layer, never `~/.claude`. `session start` seats show it;
@@ -266,11 +291,28 @@ be set, and where each one lives ([configuration][configuration]):
266
291
  - **Settings files**, JSON in layers: `~/.sessionkit/tools/settings/session.json`
267
292
  (the operator, the list cache, the pane signal, accounts, a session's
268
293
  home, the default model), `~/.sessionkit/tools/settings/sounds.json` (the busy sound), and
269
- `~/.sessionkit/tools/settings/claude-code.json` (the model, effort
270
- and status line `session start` passes on). Every key, its default and
294
+ `~/.sessionkit/tools/settings/claude-code.json` (the model, effort,
295
+ status line and renderer `session start` passes on). Every key, its default and
271
296
  which layers it is read from: [settings][settings].
272
- - **Folders of its own:** `~/.session/` (sessions, seats, agent files, the
273
- state dir) and `~/.sessionkit/tools/accounts/` (`session account`).
297
+ - **Folders of its own:** `~/.sessionkit/tools/accounts/` (`session
298
+ account`), and its state, each leaf with an old and a new folder. A
299
+ fresh host starts `state` and `sound` in the new folder; an existing
300
+ one stays put until it is moved, and a moved one is followed
301
+ ([where tools keeps its state][state-paths]):
302
+ - `state`: `~/.session/state/`, new `~/.sessionkit/tools/state/`.
303
+ - `sessions`, `seats`, `agent` and `backups`: `~/.session/<leaf>/`, new
304
+ `~/.sessionkit/tools/state/<leaf>/`.
305
+ - `sound`: `~/.local/share/sessiontools/sound/`, new
306
+ `~/.sessionkit/tools/state/sound/`.
307
+ - **The rule, per leaf:** its variable when set; else the old folder
308
+ while it is real; else the new one when it exists; else the new one
309
+ for `state` and `sound`, the old one for the rest.
310
+ - **To see what is used:** `session path LEAF`, or `session path --json`.
311
+ - **To move `state` and `sound`:** `session path migrate --dry-run`,
312
+ then a real run with that host's go ([moving tools' state][state-migrate]).
313
+ - **Upgrade every copy of sessiontools on a host together,** other
314
+ packages' copies included: an older copy writes the old `state`
315
+ folder, and `session path --json` shows `stranded` when one lags.
274
316
  - **Environment variables:** the settle alert's `SESSION_SETTLE_AT`, the
275
317
  clock line's `SESSION_CLOCK_WARN_AT`, the hooks' `SESSIONTOOLS_HOOK_*`
276
318
  switches (`SESSIONTOOLS_HOOK_CLOCK=on` switches the clock line on), and
@@ -280,31 +322,43 @@ be set, and where each one lives ([configuration][configuration]):
280
322
 
281
323
  ## Logs
282
324
 
283
- - **The sound engine:** `~/Library/Logs/sessiontools/sound-engine.log`,
325
+ - **The sound engine:** `~/.sessionkit/tools/log/sound-engine.log`,
284
326
  once `session sound engine on` has run.
285
- - **The pane signal's agent:** `~/Library/Logs/sessiontools/pane-state.log`,
327
+ - **The pane signal's agent:** `~/.sessionkit/tools/log/pane-state.log`,
286
328
  once `session pane-state agent on` has run.
287
- - **The default model:** `~/.session/state/default-model/log.jsonl`, one
288
- line per write to settings.json, and
289
- `~/Library/Logs/sessiontools/default-model.log`, once you have loaded
329
+ - **The default model:** `~/.sessionkit/tools/state/default-model/log.jsonl`
330
+ (`~/.session/state/default-model/log.jsonl` on a Mac that still has
331
+ its old state folder), one line per write to settings.json, and
332
+ `~/.sessionkit/tools/log/default-model.log`, once you have loaded
290
333
  the launchd agent `session default-model unit launchd` prints.
291
- - **`session reap --yes`:** `~/.session/state/reap.log`, one line for each
292
- seat it closed or failed to close.
334
+ - **`session reap --yes`:** `~/.sessionkit/tools/log/reap.log`, one line
335
+ for each seat it closed or failed to close.
293
336
  - **Each session's settings at birth:**
294
337
  `~/.session/sessions/<session-id>/log/settings.jsonl`, what `session
295
338
  start` read into that session's settings: each environment variable
296
339
  by name, never its value.
297
340
  - **The status line:** `~/.config/session/statusline-log.jsonl`, one line
298
341
  per render, only when `~/.config/session/` already exists.
342
+ - **Old logs, 0.3.1 and before:** the three services logged to
343
+ `~/Library/Logs/sessiontools/`, and `session reap --yes` to
344
+ `$SESSION_STATE_DIR/reap.log` (`~/.session/state/reap.log`). Those
345
+ files are left in place. A service an older release installed keeps
346
+ writing its old log, at every login too, until its plist is written
347
+ again: `on` for the sound engine and the pane signal, the steps in
348
+ [the default model][default-model] for its agent. The sound engine's
349
+ and the pane signal's `status` name an old log while it is still there.
299
350
 
300
351
  Follow one as it grows with `tail -F`, for example
301
- `tail -F ~/Library/Logs/sessiontools/pane-state.log`.
352
+ `tail -F ~/.sessionkit/tools/log/pane-state.log`.
302
353
 
303
354
  - **Health:** `session list` prints a row for each live session, with its
304
- state and context. A session you know is running that is not listed is
305
- one sessiontools cannot see.
355
+ state and context, and SEAT names the seat a session holds: `claude`
356
+ for the default seat, or an agent's name for its hand-launched
357
+ holder. A session you know is running that is not listed is one
358
+ sessiontools cannot see.
306
359
 
307
360
  [verbs]: docs/session.md
361
+ [default-seat]: docs/session.md#the-default-seat
308
362
  [start]: docs/session-start-launcher.md
309
363
  [credentials]: docs/session-start-launcher.md#credentials-are-never-handed-down
310
364
  [account]: docs/account.md
@@ -312,11 +366,14 @@ Follow one as it grows with `tail -F`, for example
312
366
  [operator]: docs/operator-privileged-seats.md
313
367
  [reap]: docs/session-reap.md
314
368
  [completion]: docs/shell-completion.md
369
+ [state-paths]: docs/tools-state-paths.md
370
+ [state-migrate]: docs/tools-state-migrate.md
315
371
  [plugin]: docs/claude-code-plugin.md
316
372
  [settle]: docs/settle-alert-hook.md
317
373
  [clock]: docs/clock-line-hook.md
318
374
  [busy]: docs/busy-sound-hook.md
319
375
  [session-info]: docs/session-info-hook.md
376
+ [identity]: docs/identity-block.md
320
377
  [engine]: docs/sound-engine-agent.md
321
378
  [status-line]: docs/status-line-script.md
322
379
  [pane-state]: docs/pane-state-signal.md
@@ -45,6 +45,8 @@ eval "$(session completion bash)"
45
45
  category.
46
46
  - `session ledger --short`: each file read and command run, by tokens.
47
47
  - `session account status`: which Claude account this Mac is on.
48
+ - `session account verify`: which account the API bills for this seat's
49
+ token, from one 1-token request.
48
50
  - `session start --list-agents`: the agents it can start.
49
51
  - `session reap`: seats idle past 1h, each CLOSE or KEEP, and why. A dry
50
52
  run.
@@ -113,8 +115,10 @@ it. Each line says what puts it there and what takes it away.
113
115
  sessiontools` drops this package's entries, and `uv cache prune` drops
114
116
  the ones nothing uses.
115
117
  - **Folders, each made by a verb you run:**
116
- - `~/.session/state/list/`: the list cache, `snapshot.json` and
117
- `list.lock`, at the first `session list`.
118
+ - `~/.sessionkit/tools/state/list/`: the list cache, `snapshot.json`
119
+ and `list.lock`, at the first `session list`. The state folders here
120
+ and below are on a fresh host; a host that still has
121
+ `~/.session/state/` keeps using it until it is moved.
118
122
  - `~/.session/sessions/<session-id>/`: one for each `session start`,
119
123
  holding `launch/`, `log/settings.jsonl` and `session-id`. It also
120
124
  gets `settings/<tool>.d/50-env.json`, one for each tool whose
@@ -123,14 +127,19 @@ it. Each line says what puts it there and what takes it away.
123
127
  `log/settings.jsonl` names each variable it read, never its value.
124
128
  The plugin's hooks and `session park` write only inside a folder
125
129
  that is already there.
126
- - `~/.session/state/reap.log`: `session reap --yes`.
127
- - `~/.session/state/pane-state/watch.lock`: the pane signal's service.
128
- - `~/.session/state/default-model/`: `backups/` and `log.jsonl`, the
129
- first time `session default-model enforce` changes settings.json.
130
+ - `~/.sessionkit/tools/state/pane-state/watch.lock`: the pane signal's
131
+ service.
132
+ - `~/.sessionkit/tools/state/default-model/`: `backups/` and
133
+ `log.jsonl`, the first time `session default-model enforce` changes
134
+ settings.json.
130
135
  - `~/.sessionkit/tools/accounts/`: `session account new` and `use`.
131
- - `~/.local/share/sessiontools/sound/bin/`: `session sound engine on`
132
- builds the sound engine there with `swiftc`.
133
- - `~/Library/Logs/sessiontools/`: the two services' logs.
136
+ - `~/.sessionkit/tools/state/sound/bin/`: `session sound engine on`
137
+ builds the sound engine there with `swiftc` (in
138
+ `~/.local/share/sessiontools/sound/bin/` while that old folder is
139
+ still there).
140
+ - `~/.sessionkit/tools/log/`: the services' logs and `reap.log`
141
+ (Logs, below). An older release wrote them elsewhere, and those
142
+ files stay where they were.
134
143
  - `SESSIONS_DIR`, `SESSION_STATE_DIR` and `SESSIONTOOLS_SOUND_HOME`
135
144
  move these folders when set.
136
145
  - **Services (launchd agents):** none until you turn one on. Each `on`
@@ -172,6 +181,12 @@ it. Each line says what puts it there and what takes it away.
172
181
  With `model.default` set, `session default-model enforce` (and the
173
182
  unit or `watch` that runs it) rewrites the top-level `"model"` in
174
183
  `~/.claude/settings.json`, after a backup.
184
+ - **Not changed by an upgrade: a live tmux server.** One born inside the
185
+ default seat by sessiontools 0.3.1 or earlier still hands
186
+ `CLAUDE_CODE_AGENT=claude` to every pane it makes, so a raw `claude`
187
+ there hears the seat's mail. Restarting that server, or running
188
+ `tmux set-environment -g -u CLAUDE_CODE_AGENT` on it, is yours to do
189
+ ([the default seat][default-seat]).
175
190
 
176
191
  ## Uninstall
177
192
 
@@ -192,23 +207,27 @@ uv tool uninstall sessiontools
192
207
  ```
193
208
 
194
209
  That leaves what the verbs made, listed above. The plugin's cached
195
- copy, the services' logs and the engine's folder are sessiontools'
196
- alone. To delete them:
210
+ copy and the engine's folder are sessiontools' alone. To delete them:
197
211
 
198
212
  ```bash
199
213
  rm -rf "${HOME:?}/.claude/plugins/cache/sessiontools"
200
- rm -rf "${HOME:?}/Library/Logs/sessiontools"
201
214
  rm -rf "${HOME:?}/.local/share/sessiontools"
215
+ rm -rf "${HOME:?}/.sessionkit/tools/state/sound"
202
216
  ```
203
217
 
218
+ The logs are left in place: the new ones in `~/.sessionkit/tools/log/`,
219
+ and an older release's in `~/Library/Logs/sessiontools/`. Nothing here
220
+ deletes from `~/Library/Logs`.
221
+
204
222
  uv's cache stays too, with the other things uv installed; `uv cache
205
223
  clean sessiontools` drops this package's entries.
206
224
 
207
225
  The rest is your data: delete it by hand only if you want it gone.
208
- `~/.sessionkit/tools/` holds your settings and accounts, and the
209
- accounts folder keeps the backups of every token `session account use`
210
- replaced. `~/.session/` holds the session folders and the list cache,
211
- and the kit's other packages keep their files in it too.
226
+ `~/.sessionkit/tools/` holds your settings, accounts, state and logs,
227
+ and the accounts folder keeps the backups of every token `session
228
+ account use` replaced. `~/.session/` holds the session folders (and, on
229
+ a host that has not moved it, the old state folder), and the kit's other
230
+ packages keep their files in it too.
212
231
 
213
232
  ## Docs
214
233
 
@@ -220,6 +239,10 @@ and the kit's other packages keep their files in it too.
220
239
  - [The `session list` cache][cache]: one computation, many readers
221
240
  - [Operator and privileged seats][operator]: who seats ask
222
241
  - [Closing idle seats][reap]: `session reap`, a dry run by default
242
+ - [Where tools keeps its state][state-paths]: each folder, old and new,
243
+ the rule, and `session path`, the verb peers ask
244
+ - [Moving tools' state][state-migrate]: `session path migrate`, one host
245
+ at a time, dry run first
223
246
  - [Shell completion][completion]: `session completion bash|zsh`, a static
224
247
  script built from the verbs' own parsers
225
248
  - [The Claude Code plugin][plugin]: the skill and the hooks
@@ -228,6 +251,8 @@ and the kit's other packages keep their files in it too.
228
251
  - [The busy sound][busy]: a sound while a tool batch runs long
229
252
  - [The session-info hooks][session-info]: the short session id as the
230
253
  session title, and the session's facts in context, both off by default
254
+ - [The identity block][identity]: who a launched session is, in its
255
+ context on turn one and from `session identity`
231
256
  - [The sound engine][engine]: the busy sound from one resident process
232
257
  - [The status line][status-line]: `session statusline install` puts it in the
233
258
  kit's claude-code layer, never `~/.claude`. `session start` seats show it;
@@ -250,11 +275,28 @@ be set, and where each one lives ([configuration][configuration]):
250
275
  - **Settings files**, JSON in layers: `~/.sessionkit/tools/settings/session.json`
251
276
  (the operator, the list cache, the pane signal, accounts, a session's
252
277
  home, the default model), `~/.sessionkit/tools/settings/sounds.json` (the busy sound), and
253
- `~/.sessionkit/tools/settings/claude-code.json` (the model, effort
254
- and status line `session start` passes on). Every key, its default and
278
+ `~/.sessionkit/tools/settings/claude-code.json` (the model, effort,
279
+ status line and renderer `session start` passes on). Every key, its default and
255
280
  which layers it is read from: [settings][settings].
256
- - **Folders of its own:** `~/.session/` (sessions, seats, agent files, the
257
- state dir) and `~/.sessionkit/tools/accounts/` (`session account`).
281
+ - **Folders of its own:** `~/.sessionkit/tools/accounts/` (`session
282
+ account`), and its state, each leaf with an old and a new folder. A
283
+ fresh host starts `state` and `sound` in the new folder; an existing
284
+ one stays put until it is moved, and a moved one is followed
285
+ ([where tools keeps its state][state-paths]):
286
+ - `state`: `~/.session/state/`, new `~/.sessionkit/tools/state/`.
287
+ - `sessions`, `seats`, `agent` and `backups`: `~/.session/<leaf>/`, new
288
+ `~/.sessionkit/tools/state/<leaf>/`.
289
+ - `sound`: `~/.local/share/sessiontools/sound/`, new
290
+ `~/.sessionkit/tools/state/sound/`.
291
+ - **The rule, per leaf:** its variable when set; else the old folder
292
+ while it is real; else the new one when it exists; else the new one
293
+ for `state` and `sound`, the old one for the rest.
294
+ - **To see what is used:** `session path LEAF`, or `session path --json`.
295
+ - **To move `state` and `sound`:** `session path migrate --dry-run`,
296
+ then a real run with that host's go ([moving tools' state][state-migrate]).
297
+ - **Upgrade every copy of sessiontools on a host together,** other
298
+ packages' copies included: an older copy writes the old `state`
299
+ folder, and `session path --json` shows `stranded` when one lags.
258
300
  - **Environment variables:** the settle alert's `SESSION_SETTLE_AT`, the
259
301
  clock line's `SESSION_CLOCK_WARN_AT`, the hooks' `SESSIONTOOLS_HOOK_*`
260
302
  switches (`SESSIONTOOLS_HOOK_CLOCK=on` switches the clock line on), and
@@ -264,31 +306,43 @@ be set, and where each one lives ([configuration][configuration]):
264
306
 
265
307
  ## Logs
266
308
 
267
- - **The sound engine:** `~/Library/Logs/sessiontools/sound-engine.log`,
309
+ - **The sound engine:** `~/.sessionkit/tools/log/sound-engine.log`,
268
310
  once `session sound engine on` has run.
269
- - **The pane signal's agent:** `~/Library/Logs/sessiontools/pane-state.log`,
311
+ - **The pane signal's agent:** `~/.sessionkit/tools/log/pane-state.log`,
270
312
  once `session pane-state agent on` has run.
271
- - **The default model:** `~/.session/state/default-model/log.jsonl`, one
272
- line per write to settings.json, and
273
- `~/Library/Logs/sessiontools/default-model.log`, once you have loaded
313
+ - **The default model:** `~/.sessionkit/tools/state/default-model/log.jsonl`
314
+ (`~/.session/state/default-model/log.jsonl` on a Mac that still has
315
+ its old state folder), one line per write to settings.json, and
316
+ `~/.sessionkit/tools/log/default-model.log`, once you have loaded
274
317
  the launchd agent `session default-model unit launchd` prints.
275
- - **`session reap --yes`:** `~/.session/state/reap.log`, one line for each
276
- seat it closed or failed to close.
318
+ - **`session reap --yes`:** `~/.sessionkit/tools/log/reap.log`, one line
319
+ for each seat it closed or failed to close.
277
320
  - **Each session's settings at birth:**
278
321
  `~/.session/sessions/<session-id>/log/settings.jsonl`, what `session
279
322
  start` read into that session's settings: each environment variable
280
323
  by name, never its value.
281
324
  - **The status line:** `~/.config/session/statusline-log.jsonl`, one line
282
325
  per render, only when `~/.config/session/` already exists.
326
+ - **Old logs, 0.3.1 and before:** the three services logged to
327
+ `~/Library/Logs/sessiontools/`, and `session reap --yes` to
328
+ `$SESSION_STATE_DIR/reap.log` (`~/.session/state/reap.log`). Those
329
+ files are left in place. A service an older release installed keeps
330
+ writing its old log, at every login too, until its plist is written
331
+ again: `on` for the sound engine and the pane signal, the steps in
332
+ [the default model][default-model] for its agent. The sound engine's
333
+ and the pane signal's `status` name an old log while it is still there.
283
334
 
284
335
  Follow one as it grows with `tail -F`, for example
285
- `tail -F ~/Library/Logs/sessiontools/pane-state.log`.
336
+ `tail -F ~/.sessionkit/tools/log/pane-state.log`.
286
337
 
287
338
  - **Health:** `session list` prints a row for each live session, with its
288
- state and context. A session you know is running that is not listed is
289
- one sessiontools cannot see.
339
+ state and context, and SEAT names the seat a session holds: `claude`
340
+ for the default seat, or an agent's name for its hand-launched
341
+ holder. A session you know is running that is not listed is one
342
+ sessiontools cannot see.
290
343
 
291
344
  [verbs]: docs/session.md
345
+ [default-seat]: docs/session.md#the-default-seat
292
346
  [start]: docs/session-start-launcher.md
293
347
  [credentials]: docs/session-start-launcher.md#credentials-are-never-handed-down
294
348
  [account]: docs/account.md
@@ -296,11 +350,14 @@ Follow one as it grows with `tail -F`, for example
296
350
  [operator]: docs/operator-privileged-seats.md
297
351
  [reap]: docs/session-reap.md
298
352
  [completion]: docs/shell-completion.md
353
+ [state-paths]: docs/tools-state-paths.md
354
+ [state-migrate]: docs/tools-state-migrate.md
299
355
  [plugin]: docs/claude-code-plugin.md
300
356
  [settle]: docs/settle-alert-hook.md
301
357
  [clock]: docs/clock-line-hook.md
302
358
  [busy]: docs/busy-sound-hook.md
303
359
  [session-info]: docs/session-info-hook.md
360
+ [identity]: docs/identity-block.md
304
361
  [engine]: docs/sound-engine-agent.md
305
362
  [status-line]: docs/status-line-script.md
306
363
  [pane-state]: docs/pane-state-signal.md
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sessiontools"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "The harness layer: sessions, status and idle, one driver per harness - a library first"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -1,12 +1,13 @@
1
- """`session account list | status | use | new | add | remove`: the group's
2
- argparse `main(argv)`.
1
+ """`session account list | status | verify | use | new | add | remove`: the
2
+ group's argparse `main(argv)`.
3
3
 
4
- `list`, `status`, `use` and `new` live in their own modules (`status`,
5
- `use`, `new`); this module parses their flags and calls them. `add` and
6
- `remove` live here.
4
+ `list`, `status`, `verify`, `use` and `new` live in their own modules
5
+ (`status`, `verify`, `use`, `new`); this module parses their flags and
6
+ calls them. `add` and `remove` live here.
7
7
 
8
8
  session account list
9
9
  session account status [--json] [--no-resets]
10
+ session account verify [NAME] [--json]
10
11
  session account use NAME --witness WHO [--yes]
11
12
  session account new NAME --email E
12
13
  session account add NAME --email E --source BACKEND:REF [--source ...] [--no-fp]
@@ -55,12 +56,12 @@ Two deliberate calls:
55
56
 
56
57
  Exit codes: 0 done; 2 usage, unknown or duplicate name, a bad source; 3
57
58
  `remove` of the host's account; 4 (`add`) no source resolved; 5 a refused
58
- registry or config. `use` and `new` keep their own tables
59
- (`use.EXIT_CODES`, `new.EXIT_CODES`).
59
+ registry or config. `verify`, `use` and `new` keep their own tables
60
+ (`verify.EXIT_CODES`, `use.EXIT_CODES`, `new.EXIT_CODES`).
60
61
 
61
62
  Seams: every external is a keyword of `main` (the Keychain, the preflight,
62
- the seat scan, the usage fetcher, the backends, the environment, HOME, and
63
- `new`'s token store, capture and terminal), so
63
+ the seat scan, the usage fetcher, `verify`'s transport, the backends, the
64
+ environment, HOME, and `new`'s token store, capture and terminal), so
64
65
  the end-to-end test drives every verb on fakes. Left as None, each is the
65
66
  production default, chosen by the module that owns it.
66
67
  """
@@ -80,6 +81,7 @@ from sessiontools.account import keychain as kc
80
81
  from sessiontools.account import registry as reg
81
82
  from sessiontools.account import status as st
82
83
  from sessiontools.account import use as u
84
+ from sessiontools.account import verify as vf
83
85
 
84
86
  if TYPE_CHECKING:
85
87
  from sessiontools.account.setuptoken import Run
@@ -93,6 +95,17 @@ EXIT_HOST = 3 # remove: the host is on it
93
95
  EXIT_UNRESOLVED = reg.EXIT_UNRESOLVED # 4
94
96
  EXIT_REGISTRY = reg.EXIT_REGISTRY # 5
95
97
 
98
+ #: The group's `-h` leads with how it knows the accounts and which one is in
99
+ #: use (registry.default_path; status.host_of), and why `/login` moves every
100
+ #: seat on a Keychain host (keychain.SERVICE). Raw: the line breaks are kept.
101
+ DESCRIPTION = """\
102
+ Which Claude account this Mac is on, and the switch. Never prints a token.
103
+ Available: the accounts `new` or `add` put in ~/.sessionkit/tools/accounts/.
104
+ In use: the host's token, fingerprinted and looked up in that list: the
105
+ Keychain item's, or the file's when the setting account.token_file names one.
106
+ /login moves every seat on a Keychain host: all with no env token read the item.
107
+ """
108
+
96
109
  EPILOG = """\
97
110
  By default the host signs in from the Keychain item all seats read: `use`
98
111
  moves every seat with no env token within about 50 s, and each one's
@@ -108,6 +121,12 @@ file unless its agent file says inherit: [CLAUDE_CODE_OAUTH_TOKEN];
108
121
  without it, it signs in from Claude's stored credentials, and `status`
109
122
  says so.
110
123
 
124
+ `use` changes the token, never ~/.claude.json: Claude's /status and
125
+ `claude auth status` name the account from that file's oauthAccount,
126
+ so after a switch they can name the old one. `status` prints that name
127
+ on its own profile line, and `verify` makes one 1-token request to show
128
+ which account the API bills for a token.
129
+
111
130
  `new` runs `claude setup-token` in a masked copy of its screen, saves the
112
131
  token to passage at <PREFIX>/<E>, reads it back, and registers it. PREFIX
113
132
  is the `session` setting account.passage_prefix (default
@@ -123,6 +142,7 @@ backup or ledger (or `use`: a bad account.token_file or token file; or
123
142
  restored the item or file; 7 `use` could not restore it; 8 `new` did not save the
124
143
  token; 9 `new` saved it but did not register it.
125
144
  """
145
+ EPILOG += "\n" + vf.EPILOG_LINE + "\n"
126
146
 
127
147
 
128
148
  class AccountError(Exception):
@@ -261,8 +281,7 @@ def remove(name: str, *, keychain: kc.Keychain | None = None,
261
281
  def parser() -> argparse.ArgumentParser:
262
282
  p = argparse.ArgumentParser(
263
283
  prog=PROG, epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter,
264
- description="Which of the Claude accounts this Mac is on, and the switch "
265
- "between them. Never prints a token.")
284
+ description=DESCRIPTION)
266
285
  verbs = p.add_subparsers(dest="verb", metavar="VERB")
267
286
  verbs.required = True
268
287
 
@@ -272,13 +291,28 @@ def parser() -> argparse.ArgumentParser:
272
291
 
273
292
  s = verbs.add_parser("status", help="the host's account, this seat's, plan usage",
274
293
  description="Which account the host is on, this seat's route, "
275
- "and the plan usage of the live account.")
294
+ "the plan usage of the live account (a window past "
295
+ "its reset is STALE, its figure not shown), and the "
296
+ "account ~/.claude.json names (display only: the "
297
+ "token decides billing).")
276
298
  s.add_argument("--json", action="store_true", help="one JSON object")
277
299
  s.add_argument("--no-resets", action="store_true",
278
300
  help="hide when each usage window resets (shown by default)")
279
301
  s.add_argument("--resets", action="store_true",
280
302
  help="accepted and ignored: resets are shown by default")
281
303
 
304
+ v = verbs.add_parser("verify", help="ask the API which account a token bills",
305
+ description="Make ONE 1-token request to the API with a token "
306
+ "and print the account-wide usage windows it "
307
+ "reports: the 7d reset identifies the account. "
308
+ "NAME: that registry account's token. No NAME: "
309
+ "the token this seat bills (the env token, else "
310
+ "the host's token file or Keychain item). Never "
311
+ "prints a token.")
312
+ v.add_argument("name", metavar="NAME", nargs="?",
313
+ help="a registered account (default: this seat's token)")
314
+ v.add_argument("--json", action="store_true", help="one JSON object")
315
+
282
316
  # The usage line is written by hand: --witness is required to write, but
283
317
  # argparse's `required=True` would exit before `use` shows the plan.
284
318
  w = verbs.add_parser("use", help="move the HOST's sign-in to NAME",
@@ -329,6 +363,7 @@ def main(argv: Sequence[str] | None = None, *,
329
363
  preflight: u.Preflight | None = None,
330
364
  seat_scan: u.SeatScan | None = None,
331
365
  fetch: st.Fetcher | None = None,
366
+ post: vf.Post | None = None,
332
367
  backends: Mapping[str, reg.Backend] | None = None,
333
368
  env: Mapping[str, str] | None = None,
334
369
  home: str | os.PathLike | None = None,
@@ -340,7 +375,8 @@ def main(argv: Sequence[str] | None = None, *,
340
375
  out: TextIO | None = None, err: TextIO | None = None) -> int:
341
376
  """`session account VERB ...`. Returns the exit code. Every keyword is
342
377
  a seam; None is the production default. `store`, `capture` and `tty`
343
- (stdin's fd, stdout's fd) are `new`'s; so is `preflight`, as `use`'s."""
378
+ (stdin's fd, stdout's fd) are `new`'s; so is `preflight`, as `use`'s.
379
+ `post` is `verify`'s transport."""
344
380
  out = sys.stdout if out is None else out
345
381
  err = sys.stderr if err is None else err
346
382
  try:
@@ -355,6 +391,9 @@ def main(argv: Sequence[str] | None = None, *,
355
391
  if args.verb == "status":
356
392
  return st.status(as_json=args.json, resets=not args.no_resets, keychain=keychain,
357
393
  fetch=fetch, env=env, home=home, clock=clock, out=out, err=err)
394
+ if args.verb == "verify":
395
+ return vf.verify(args.name, as_json=args.json, keychain=keychain, backends=backends,
396
+ env=env, home=home, post=post, clock=clock, out=out, err=err)
358
397
  if args.verb == "use":
359
398
  return u.use(args.name, witness=args.witness, yes=args.yes, keychain=keychain,
360
399
  preflight=preflight, seat_scan=seat_scan, home=home,
@@ -26,8 +26,9 @@ What is read, and what is refused:
26
26
  - `written_at` is required: without it the file's age cannot be told, and an
27
27
  old file must never read as current. Past `STALE_AFTER` seconds the file
28
28
  is marked stale; its numbers are still shown.
29
- - A window whose `resets_at` has passed measured a window that is over. Its
30
- percent is not shown (`expired`), never guessed to be 0.
29
+ - A window whose `resets_at` has passed measured a window that is over. It
30
+ reads as the exporter's does (`status.stale_text`): STALE, when it ended,
31
+ its percent not shown (`expired`, `stale`), never guessed to be 0.
31
32
  - `account` must be absent or `"default"`, the host's own sign-in: any other
32
33
  account's window is not the host's usage.
33
34
 
@@ -204,6 +205,7 @@ def read_limits(path: str | os.PathLike, now: float) -> st.Usage:
204
205
  if age < -FUTURE_SLACK:
205
206
  return _malformed(path, "written_at is in the future")
206
207
 
208
+ age_s = max(0, int(age))
207
209
  windows = []
208
210
  for key, label in WINDOWS:
209
211
  raw = data.get(key)
@@ -217,10 +219,10 @@ def read_limits(path: str | os.PathLike, now: float) -> st.Usage:
217
219
  resets_at = _number(raw.get("resets_at"))
218
220
  expired = resets_at is not None and resets_at <= now
219
221
  windows.append(st.Window(label, None, None if expired else percent, resets_at,
220
- expired=expired))
222
+ expired=expired, age_s=age_s,
223
+ ended_s=max(0, int(now - resets_at)) if expired else None))
221
224
  if not windows:
222
225
  return st.Usage(None, unavailable=f"{path} has no usage windows")
223
- age_s = max(0, int(age))
224
226
  return st.Usage(None, tuple(windows), source=st.SOURCE_FILE, file=str(path),
225
227
  written_at=written_at, age_s=age_s, stale=age_s > STALE_AFTER)
226
228