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.
Files changed (89) hide show
  1. {sessiontools-0.3.0 → sessiontools-0.3.1}/PKG-INFO +14 -1
  2. {sessiontools-0.3.0 → sessiontools-0.3.1}/README.md +13 -0
  3. {sessiontools-0.3.0 → sessiontools-0.3.1}/pyproject.toml +1 -1
  4. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/claude_code.py +1 -1
  5. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/cli.py +58 -2
  6. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/hooks.py +1 -1
  7. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/park.py +1 -1
  8. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/procs.py +59 -2
  9. sessiontools-0.3.1/src/sessiontools/start/__init__.py +557 -0
  10. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/agentfile.py +7 -2
  11. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/agents.py +57 -10
  12. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/args.py +19 -9
  13. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/context.py +15 -5
  14. sessiontools-0.3.1/src/sessiontools/start/default_seat.md +3 -0
  15. sessiontools-0.3.1/src/sessiontools/start/default_seat.py +534 -0
  16. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/environment.py +27 -0
  17. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/errors.py +3 -0
  18. sessiontools-0.3.1/src/sessiontools/start/holders.py +418 -0
  19. sessiontools-0.3.1/src/sessiontools/start/homefolder.py +220 -0
  20. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/identity.py +4 -2
  21. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/placement.py +29 -4
  22. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/plan.py +60 -22
  23. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/vendor.py +13 -1
  24. sessiontools-0.3.0/src/sessiontools/start/__init__.py +0 -151
  25. sessiontools-0.3.0/src/sessiontools/start/holders.py +0 -193
  26. {sessiontools-0.3.0 → sessiontools-0.3.1}/LICENSE +0 -0
  27. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/__init__.py +0 -0
  28. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/__init__.py +0 -0
  29. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/cli.py +0 -0
  30. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/keychain.py +0 -0
  31. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/leakscan.py +0 -0
  32. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/limits.py +0 -0
  33. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/movehome.py +0 -0
  34. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/new.py +0 -0
  35. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/registry.py +0 -0
  36. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/setuptoken.py +0 -0
  37. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/status.py +0 -0
  38. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/tokenfile.py +0 -0
  39. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/tokenstore.py +0 -0
  40. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/account/use.py +0 -0
  41. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/band.py +0 -0
  42. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/busy.py +0 -0
  43. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/clockline.py +0 -0
  44. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/completion.py +0 -0
  45. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/__init__.py +0 -0
  46. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/boot.py +0 -0
  47. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/breakdown.py +0 -0
  48. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/nnls.py +0 -0
  49. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/render.py +0 -0
  50. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/slash_context.py +0 -0
  51. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/tools.py +0 -0
  52. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/context_parts/transcript.py +0 -0
  53. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/default_model.py +0 -0
  54. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/default_model_cli.py +0 -0
  55. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/default_model_watch.py +0 -0
  56. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/get.py +0 -0
  57. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/__init__.py +0 -0
  58. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/_tiktoken_child.py +0 -0
  59. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/count.py +0 -0
  60. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/render.py +0 -0
  61. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/ledger/transcript.py +0 -0
  62. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/listing.py +0 -0
  63. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/operator.py +0 -0
  64. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/pane_state.py +0 -0
  65. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/pane_state_agent.py +0 -0
  66. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/py.typed +0 -0
  67. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/reap.py +0 -0
  68. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/send.py +0 -0
  69. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/session_info.py +0 -0
  70. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/settings.py +0 -0
  71. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/settle.py +0 -0
  72. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/snapshot.py +0 -0
  73. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/sound/__init__.py +0 -0
  74. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/sound/engine.swift +0 -0
  75. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/sound/event.swift +0 -0
  76. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/sound/service.py +0 -0
  77. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/claude_settings.py +0 -0
  78. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/policy.py +0 -0
  79. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/record.py +0 -0
  80. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/resume.py +0 -0
  81. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/run.py +0 -0
  82. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/start/secrets.py +0 -0
  83. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/statusline/__init__.py +0 -0
  84. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/statusline/sample.json +0 -0
  85. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/statusline/statusline.sh +0 -0
  86. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/tmux.py +0 -0
  87. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/turn.py +0 -0
  88. {sessiontools-0.3.0 → sessiontools-0.3.1}/src/sessiontools/verbs.py +0 -0
  89. {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.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sessiontools"
3
- version = "0.3.0"
3
+ version = "0.3.1"
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"
@@ -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, UTC ctime - liveness
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, "completion": _completion}
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 AEDT` then `[clock] 13:08 AEDT`.
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+11:00, or Z). Nothing
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. `ps` is asked in UTC because `procStart` is UTC ctime.
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 = parse_ctime(proc_start)
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