sessiontools 0.3.0__tar.gz → 0.3.1__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.
- {sessiontools-0.3.0 → sessiontools-0.3.1}/PKG-INFO +14 -1
- {sessiontools-0.3.0 → sessiontools-0.3.1}/README.md +13 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/pyproject.toml +1 -1
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/claude_code.py +1 -1
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/cli.py +58 -2
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/hooks.py +1 -1
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/park.py +1 -1
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/procs.py +59 -2
- sessiontools-0.3.1/src/sessiontools/start/__init__.py +557 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/agentfile.py +7 -2
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/agents.py +57 -10
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/args.py +19 -9
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/context.py +15 -5
- sessiontools-0.3.1/src/sessiontools/start/default_seat.md +3 -0
- sessiontools-0.3.1/src/sessiontools/start/default_seat.py +534 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/environment.py +27 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/errors.py +3 -0
- sessiontools-0.3.1/src/sessiontools/start/holders.py +418 -0
- sessiontools-0.3.1/src/sessiontools/start/homefolder.py +220 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/identity.py +4 -2
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/placement.py +29 -4
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/plan.py +60 -22
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/vendor.py +13 -1
- sessiontools-0.3.0/src/sessiontools/start/__init__.py +0 -151
- sessiontools-0.3.0/src/sessiontools/start/holders.py +0 -193
- {sessiontools-0.3.0 → sessiontools-0.3.1}/LICENSE +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/__init__.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/__init__.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/cli.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/keychain.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/leakscan.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/limits.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/movehome.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/new.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/registry.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/setuptoken.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/status.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/tokenfile.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/tokenstore.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/use.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/band.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/busy.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/clockline.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/completion.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/__init__.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/boot.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/breakdown.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/nnls.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/render.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/slash_context.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/tools.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/transcript.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/default_model.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/default_model_cli.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/default_model_watch.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/get.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/__init__.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/_tiktoken_child.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/count.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/render.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/transcript.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/listing.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/operator.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/pane_state.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/pane_state_agent.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/py.typed +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/reap.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/send.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/session_info.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/settings.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/settle.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/snapshot.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/sound/__init__.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/sound/engine.swift +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/sound/event.swift +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/sound/service.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/claude_settings.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/policy.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/record.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/resume.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/run.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/secrets.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/statusline/__init__.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/statusline/sample.json +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/statusline/statusline.sh +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/tmux.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/turn.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/verbs.py +0 -0
- {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/window.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sessiontools
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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>
|
|
@@ -68,6 +68,16 @@ eval "$(session completion bash)"
|
|
|
68
68
|
whether a pin keeps it there. Off until you set `model.default`.
|
|
69
69
|
- `session --help`: every verb.
|
|
70
70
|
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
- [x] macOS (tested on macOS 27, Apple silicon)
|
|
74
|
+
- [ ] Linux
|
|
75
|
+
- [ ] WSL
|
|
76
|
+
- [ ] Windows
|
|
77
|
+
|
|
78
|
+
What sessiontools depends on, what needs each one, and how to upgrade
|
|
79
|
+
it: [dependencies][dependencies].
|
|
80
|
+
|
|
71
81
|
## Switching accounts
|
|
72
82
|
|
|
73
83
|
`session account use NAME` moves this Mac to another Claude account from
|
|
@@ -243,6 +253,8 @@ and the kit's other packages keep their files in it too.
|
|
|
243
253
|
- [The default model][default-model]: `session default-model`, opt-in,
|
|
244
254
|
puts settings.json's `"model"` back when something rewrites it
|
|
245
255
|
- [The Python library][library]: `from sessiontools import sessions`
|
|
256
|
+
- [Dependencies][dependencies]: what sessiontools needs, what needs each
|
|
257
|
+
one, and how to upgrade it
|
|
246
258
|
- [Building and testing][building]: make targets, specs, releases
|
|
247
259
|
- [CHANGELOG][changelog]; licence AGPL-3.0-or-later ([LICENSE][license])
|
|
248
260
|
|
|
@@ -311,6 +323,7 @@ Follow one as it grows with `tail -F`, for example
|
|
|
311
323
|
[default-model]: docs/default-model.md
|
|
312
324
|
[library]: docs/python-library-api.md
|
|
313
325
|
[building]: docs/building-and-testing.md
|
|
326
|
+
[dependencies]: docs/dependencies.md
|
|
314
327
|
[changelog]: CHANGELOG.md
|
|
315
328
|
[license]: LICENSE
|
|
316
329
|
[configuration]: docs/configuration.md
|
|
@@ -52,6 +52,16 @@ eval "$(session completion bash)"
|
|
|
52
52
|
whether a pin keeps it there. Off until you set `model.default`.
|
|
53
53
|
- `session --help`: every verb.
|
|
54
54
|
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- [x] macOS (tested on macOS 27, Apple silicon)
|
|
58
|
+
- [ ] Linux
|
|
59
|
+
- [ ] WSL
|
|
60
|
+
- [ ] Windows
|
|
61
|
+
|
|
62
|
+
What sessiontools depends on, what needs each one, and how to upgrade
|
|
63
|
+
it: [dependencies][dependencies].
|
|
64
|
+
|
|
55
65
|
## Switching accounts
|
|
56
66
|
|
|
57
67
|
`session account use NAME` moves this Mac to another Claude account from
|
|
@@ -227,6 +237,8 @@ and the kit's other packages keep their files in it too.
|
|
|
227
237
|
- [The default model][default-model]: `session default-model`, opt-in,
|
|
228
238
|
puts settings.json's `"model"` back when something rewrites it
|
|
229
239
|
- [The Python library][library]: `from sessiontools import sessions`
|
|
240
|
+
- [Dependencies][dependencies]: what sessiontools needs, what needs each
|
|
241
|
+
one, and how to upgrade it
|
|
230
242
|
- [Building and testing][building]: make targets, specs, releases
|
|
231
243
|
- [CHANGELOG][changelog]; licence AGPL-3.0-or-later ([LICENSE][license])
|
|
232
244
|
|
|
@@ -295,6 +307,7 @@ Follow one as it grows with `tail -F`, for example
|
|
|
295
307
|
[default-model]: docs/default-model.md
|
|
296
308
|
[library]: docs/python-library-api.md
|
|
297
309
|
[building]: docs/building-and-testing.md
|
|
310
|
+
[dependencies]: docs/dependencies.md
|
|
298
311
|
[changelog]: CHANGELOG.md
|
|
299
312
|
[license]: LICENSE
|
|
300
313
|
[configuration]: docs/configuration.md
|
|
@@ -72,7 +72,7 @@ class Session:
|
|
|
72
72
|
agent: str | None = None
|
|
73
73
|
raw_status: str | None = None # the harness's own word, for the curious
|
|
74
74
|
path: Path | None = None # the sidecar it came from
|
|
75
|
-
proc_start: str | None = None # the process's start
|
|
75
|
+
proc_start: str | None = None # the process's start as written - liveness (procs.parse_start)
|
|
76
76
|
started_at: float | None = None # epoch seconds, when the session began
|
|
77
77
|
version: str | None = None # the harness's version
|
|
78
78
|
#: Where the harness says it runs: `<tmux session>:@window.%pane`, e.g.
|
|
@@ -8,6 +8,7 @@ package code, well structured, in the right place - not copied.
|
|
|
8
8
|
session get [PROP [SEAT]] [--json]
|
|
9
9
|
session context [SEAT] [--peak] [--max N] [--parts [--tools] | --startup | --boot | --hooks] [--json]
|
|
10
10
|
session start ... launch a session from an agent file
|
|
11
|
+
session agent default the default seat's name (the vendor's word), one line
|
|
11
12
|
session sound landed|given --session SID [--ref ID] [--json]
|
|
12
13
|
session sound engine build|on|off|status the sound engine
|
|
13
14
|
session operator the operator and the privileged seats, as JSON
|
|
@@ -49,6 +50,7 @@ VERBS = (
|
|
|
49
50
|
("get", "read one property of a live seat"),
|
|
50
51
|
("context", "how full a seat's context window is"),
|
|
51
52
|
("start", "launch a session from an agent file"),
|
|
53
|
+
("agent", "the default seat's name, for scripts (default)"),
|
|
52
54
|
("settings", "a session's settings, each key with the file that set it"),
|
|
53
55
|
("sound", "signal the busy sound (landed|given); `sound engine` switches the engine"),
|
|
54
56
|
("operator", "who seats ask, and who is privileged (one line of JSON)"),
|
|
@@ -708,6 +710,58 @@ def _park(argv: list[str]) -> int:
|
|
|
708
710
|
return park.main(argv)
|
|
709
711
|
|
|
710
712
|
|
|
713
|
+
AGENT_USAGE = """Usage: session agent default
|
|
714
|
+
|
|
715
|
+
default print the default seat's name, the vendor's own word, and nothing
|
|
716
|
+
else: one line on stdout, nothing on stderr, exit 0. Safe in a
|
|
717
|
+
shell substitution: message setup --box "$(session agent default)"
|
|
718
|
+
"""
|
|
719
|
+
|
|
720
|
+
AGENT_DEFAULT_EPILOG = """\
|
|
721
|
+
Reads no host state: no ps, no box, no lock, no settings. The word comes
|
|
722
|
+
from the launcher's vendor table, so it always agrees with the name a bare
|
|
723
|
+
`session start` gives its seat. Exit 0, or 2 for a usage error."""
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _agent_default_parser() -> argparse.ArgumentParser:
|
|
727
|
+
"""`session agent default`'s parser: no options."""
|
|
728
|
+
return argparse.ArgumentParser(prog="session agent default", epilog=AGENT_DEFAULT_EPILOG,
|
|
729
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
730
|
+
description="Print the default seat's name (the vendor's "
|
|
731
|
+
"own word) and a newline, and nothing else.")
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
#: `session agent`: its one sub-verb is dispatched by hand, as statusline's
|
|
735
|
+
#: are, so completion and the dispatcher read the same table.
|
|
736
|
+
AGENT = Group(subs={"default": "sessiontools.cli:_agent_default_parser"})
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _agent(argv: list[str]) -> int:
|
|
740
|
+
"""`session agent default`: the default seat's word, for the readers that
|
|
741
|
+
cannot import sessiontools (phone, the kit's setup, the wake word). The
|
|
742
|
+
word is imported here and nowhere else: `sessiontools.start` loads
|
|
743
|
+
PyYAML, and `import sessiontools.cli` never may (tests/test_cli.py)."""
|
|
744
|
+
if argv[:1] and argv[0] in AGENT.subs:
|
|
745
|
+
_agent_default_parser().parse_args(argv[1:])
|
|
746
|
+
try:
|
|
747
|
+
from sessiontools.start.default_seat import word
|
|
748
|
+
except ImportError as e:
|
|
749
|
+
missing = "PyYAML" if e.name == "yaml" else (e.name or "a module")
|
|
750
|
+
print(f"session agent: broken install - {missing} will not import ({e}); "
|
|
751
|
+
"reinstall sessiontools with its dependencies: "
|
|
752
|
+
"uv tool install --editable --reinstall CHECKOUT", file=sys.stderr)
|
|
753
|
+
return 1
|
|
754
|
+
sys.stdout.write(f"{word()}\n")
|
|
755
|
+
return 0
|
|
756
|
+
if argv[:1] in (["-h"], ["--help"]):
|
|
757
|
+
sys.stdout.write(AGENT_USAGE)
|
|
758
|
+
return 0
|
|
759
|
+
if argv:
|
|
760
|
+
print(f"session agent: unknown subcommand {argv[0]!r}", file=sys.stderr)
|
|
761
|
+
sys.stderr.write(AGENT_USAGE)
|
|
762
|
+
return 2
|
|
763
|
+
|
|
764
|
+
|
|
711
765
|
COMPLETION_EPILOG = """install, one line per shell:
|
|
712
766
|
zsh, in ~/.zshrc after compinit:
|
|
713
767
|
eval "$(session completion zsh)"
|
|
@@ -753,11 +807,12 @@ def _completion(argv: list[str]) -> int:
|
|
|
753
807
|
return 0
|
|
754
808
|
|
|
755
809
|
|
|
756
|
-
HANDLERS = {"list": _list, "get": _get, "context": _context, "start": _start,
|
|
810
|
+
HANDLERS = {"list": _list, "get": _get, "context": _context, "start": _start, "agent": _agent,
|
|
757
811
|
"settings": _settings, "sound": _sound, "operator": _operator,
|
|
758
812
|
"statusline": _statusline, "pane-state": _pane_state,
|
|
759
813
|
"default-model": _default_model, "ledger": _ledger,
|
|
760
|
-
"account": _account, "reap": _reap, "park": _park,
|
|
814
|
+
"account": _account, "reap": _reap, "park": _park,
|
|
815
|
+
"completion": _completion}
|
|
761
816
|
|
|
762
817
|
#: Each verb's parser, as a `sessiontools.verbs` reference: a builder, or a
|
|
763
818
|
#: `Group` for a verb whose first word is dispatched by hand. Each handler
|
|
@@ -768,6 +823,7 @@ PARSERS = {"list": "sessiontools.cli:_list_parser",
|
|
|
768
823
|
"get": "sessiontools.cli:_get_parser",
|
|
769
824
|
"context": "sessiontools.cli:_context_parser",
|
|
770
825
|
"start": "sessiontools.start.args:build_parser",
|
|
826
|
+
"agent": "sessiontools.cli:AGENT",
|
|
771
827
|
"settings": "sessiontools.cli:_settings_parser",
|
|
772
828
|
"sound": "sessiontools.cli:SOUND",
|
|
773
829
|
"operator": "sessiontools.cli:_operator_parser",
|
|
@@ -13,7 +13,7 @@ carried context, else the hook's event name. Per command:
|
|
|
13
13
|
window holds it again:
|
|
14
14
|
- EXACT: the same text;
|
|
15
15
|
- TEMPLATED: the same once every digit run is one `#` (DIGITS), and not
|
|
16
|
-
exact: `[clock] 13:07
|
|
16
|
+
exact: `[clock] 13:07 UTC` then `[clock] 13:08 UTC`.
|
|
17
17
|
The first copy of a line is never a repeat. IN-BATCH is the part of those
|
|
18
18
|
repeats whose earlier copy came in the SAME batch: a PostToolUse hook
|
|
19
19
|
firing once per tool call of one batch (the prototype's `dup` + `templ`).
|
|
@@ -1065,7 +1065,7 @@ again with no work since keeps it, so parking buys no time. Nothing here
|
|
|
1065
1065
|
computes a deadline, kills, mails or writes config.
|
|
1066
1066
|
|
|
1067
1067
|
--keep-until T --why R records a request to be kept until T, an absolute
|
|
1068
|
-
ISO 8601 time with an offset (2030-06-09T17:00:00+
|
|
1068
|
+
ISO 8601 time with an offset (2030-06-09T17:00:00+00:00, or Z). Nothing
|
|
1069
1069
|
acts on it here. A park without it clears it.
|
|
1070
1070
|
|
|
1071
1071
|
--check [SEAT] writes nothing. It reads the record and runs tree, work,
|
|
@@ -4,7 +4,10 @@ Liveness is one rule, the one `session list` always used (and sessionmail's
|
|
|
4
4
|
doorbell uses since 2030-06-09): a sidecar's session is live when its pid is
|
|
5
5
|
running AND that process started within SAME_PROCESS_SECONDS of the
|
|
6
6
|
sidecar's `procStart`. A pid the OS has handed to something else is not the
|
|
7
|
-
session. `
|
|
7
|
+
session. `procStart` comes in two encodings (`parse_start`): on macOS it is
|
|
8
|
+
`ps lstart` in UTC ctime, so `ps` is asked in UTC; on Linux it is the
|
|
9
|
+
process's start in clock ticks since boot, which becomes a wall-clock time
|
|
10
|
+
by the rule `ps` itself uses for `lstart`.
|
|
8
11
|
|
|
9
12
|
`table(commands=True)` adds each process's command line: the pane
|
|
10
13
|
signal sorts a seat's tool shells by what runs under them. Callers that do
|
|
@@ -35,6 +38,60 @@ def parse_ctime(text: str | None) -> float | None:
|
|
|
35
38
|
return when.timestamp()
|
|
36
39
|
|
|
37
40
|
|
|
41
|
+
#: Linux's kernel statistics, whose `btime` line is the boot time in epoch
|
|
42
|
+
#: seconds. A module constant so a test can point it at a file of its own.
|
|
43
|
+
PROC_STAT = "/proc/stat"
|
|
44
|
+
|
|
45
|
+
#: Clock ticks per second when the system cannot say. Linux reports process
|
|
46
|
+
#: start times in USER_HZ, 100 on every mainstream architecture.
|
|
47
|
+
DEFAULT_CLOCK_TICKS = 100
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def boot_time() -> int | None:
|
|
51
|
+
"""The `btime` line of PROC_STAT: when the system booted, in epoch
|
|
52
|
+
seconds. None where there is no such file (macOS) or no such line."""
|
|
53
|
+
try:
|
|
54
|
+
with open(PROC_STAT, encoding="ascii", errors="replace") as f:
|
|
55
|
+
for line in f:
|
|
56
|
+
name, _, value = line.partition(" ")
|
|
57
|
+
if name == "btime":
|
|
58
|
+
return int(value) if value.strip().isdigit() else None
|
|
59
|
+
except OSError:
|
|
60
|
+
return None
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def clock_ticks() -> int:
|
|
65
|
+
"""Clock ticks per second (`SC_CLK_TCK`), DEFAULT_CLOCK_TICKS when the
|
|
66
|
+
system cannot say."""
|
|
67
|
+
try:
|
|
68
|
+
hz = os.sysconf("SC_CLK_TCK")
|
|
69
|
+
except (AttributeError, ValueError, OSError):
|
|
70
|
+
return DEFAULT_CLOCK_TICKS
|
|
71
|
+
return hz if hz > 0 else DEFAULT_CLOCK_TICKS
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_start(text: str | None) -> float | None:
|
|
75
|
+
"""A sidecar's `procStart` -> epoch seconds; None when it is not one.
|
|
76
|
+
|
|
77
|
+
Two encodings, told apart by their shape, never by the platform:
|
|
78
|
+
- UTC ctime (`Sun Jun 9 17:00:00 2030`), what Claude Code writes on
|
|
79
|
+
macOS: `parse_ctime`.
|
|
80
|
+
- Bare digits (`88300`), what it writes on Linux: the process's start
|
|
81
|
+
in clock ticks since boot (field 22 of `/proc/<pid>/stat`). It
|
|
82
|
+
becomes `btime + ticks // HZ`, the rule procps uses to print
|
|
83
|
+
`lstart`, so it lands on the second the process table shows. With no
|
|
84
|
+
boot time to add (no PROC_STAT) it is None: not provably live.
|
|
85
|
+
The kernel prints starttime as an unsigned 64-bit number, at most 20
|
|
86
|
+
digits; a longer run of digits is not one, and is None too."""
|
|
87
|
+
if text and text.isascii() and text.isdigit():
|
|
88
|
+
if len(text) > 20:
|
|
89
|
+
return None
|
|
90
|
+
boot = boot_time()
|
|
91
|
+
return None if boot is None else float(boot + int(text) // clock_ticks())
|
|
92
|
+
return parse_ctime(text)
|
|
93
|
+
|
|
94
|
+
|
|
38
95
|
@dataclass(frozen=True)
|
|
39
96
|
class Proc:
|
|
40
97
|
pid: int
|
|
@@ -103,7 +160,7 @@ def same_process(pid: int | None, proc_start: str | None, procs: dict[int, Proc]
|
|
|
103
160
|
proc = procs.get(pid) if pid is not None else None
|
|
104
161
|
if proc is None or proc.started is None:
|
|
105
162
|
return False
|
|
106
|
-
claimed =
|
|
163
|
+
claimed = parse_start(proc_start)
|
|
107
164
|
return claimed is not None and abs(proc.started - claimed) <= SAME_PROCESS_SECONDS
|
|
108
165
|
|
|
109
166
|
|