pocketshell 0.5.3__tar.gz → 0.5.4__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 (112) hide show
  1. {pocketshell-0.5.3 → pocketshell-0.5.4}/PKG-INFO +43 -139
  2. {pocketshell-0.5.3 → pocketshell-0.5.4}/README.md +40 -135
  3. {pocketshell-0.5.3 → pocketshell-0.5.4}/pyproject.toml +20 -33
  4. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/__init__.py +2 -3
  5. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/agent_card_push.py +1 -1
  6. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/agent_log.py +1 -1
  7. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/agents.py +26 -333
  8. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/agents_kind.py +3 -3
  9. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/aplexer.py +1 -1
  10. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/cards.py +20 -49
  11. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/cgroup_agents.py +9 -12
  12. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/cli.py +6 -12
  13. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/daemon.py +7 -69
  14. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/engines.py +2 -2
  15. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/env.py +1 -1
  16. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/hooks.py +0 -79
  17. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/memcap.py +13 -36
  18. pocketshell-0.5.4/src/pocketshell/session_enum.py +374 -0
  19. pocketshell-0.5.4/src/pocketshell/sessions.py +692 -0
  20. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/tree.py +23 -59
  21. pocketshell-0.5.4/src/pocketshell/workspaces.py +224 -0
  22. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/conftest.py +0 -5
  23. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_agent_card_push.py +1 -4
  24. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_agents.py +1 -606
  25. pocketshell-0.5.4/tests/test_agents_fixture_aplexer.py +158 -0
  26. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_agents_kind.py +17 -17
  27. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_aplexer_resolution.py +7 -4
  28. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_cards.py +17 -17
  29. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_cards_push_notify.py +0 -1
  30. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_cgroup_agents.py +18 -18
  31. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_daemon.py +3 -47
  32. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_daemon_fallback.py +3 -7
  33. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_engines.py +3 -3
  34. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_hooks.py +7 -84
  35. pocketshell-0.5.3/tests/test_sessions_mem_cap.py → pocketshell-0.5.4/tests/test_session_cgroup_kernel_proof.py +186 -609
  36. pocketshell-0.5.4/tests/test_session_enum.py +93 -0
  37. pocketshell-0.5.4/tests/test_sessions.py +54 -0
  38. pocketshell-0.5.4/tests/test_sessions_attach.py +55 -0
  39. pocketshell-0.5.4/tests/test_sessions_create_routing.py +90 -0
  40. pocketshell-0.5.4/tests/test_sessions_kill.py +57 -0
  41. pocketshell-0.5.4/tests/test_sessions_mem_cap.py +38 -0
  42. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_ssh_fixture_penalties.py +18 -61
  43. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_tree.py +13 -38
  44. pocketshell-0.5.4/tests/test_workspaces.py +98 -0
  45. {pocketshell-0.5.3 → pocketshell-0.5.4}/uv.lock +8 -90
  46. pocketshell-0.5.3/src/pocketshell/config.py +0 -163
  47. pocketshell-0.5.3/src/pocketshell/jobs.py +0 -829
  48. pocketshell-0.5.3/src/pocketshell/resume.py +0 -698
  49. pocketshell-0.5.3/src/pocketshell/send.py +0 -1495
  50. pocketshell-0.5.3/src/pocketshell/session_enum.py +0 -905
  51. pocketshell-0.5.3/src/pocketshell/sessions.py +0 -1991
  52. pocketshell-0.5.3/tests/test_agents_fixture_aplexer.py +0 -714
  53. pocketshell-0.5.3/tests/test_agents_kind_target.py +0 -280
  54. pocketshell-0.5.3/tests/test_agents_source_target.py +0 -426
  55. pocketshell-0.5.3/tests/test_jobs.py +0 -896
  56. pocketshell-0.5.3/tests/test_resume.py +0 -558
  57. pocketshell-0.5.3/tests/test_send.py +0 -2715
  58. pocketshell-0.5.3/tests/test_session_enum.py +0 -144
  59. pocketshell-0.5.3/tests/test_session_enum_schema2.py +0 -1226
  60. pocketshell-0.5.3/tests/test_sessions.py +0 -448
  61. pocketshell-0.5.3/tests/test_sessions_attach.py +0 -652
  62. pocketshell-0.5.3/tests/test_sessions_create_routing.py +0 -1284
  63. pocketshell-0.5.3/tests/test_sessions_kill.py +0 -748
  64. {pocketshell-0.5.3 → pocketshell-0.5.4}/.gitignore +0 -0
  65. {pocketshell-0.5.3 → pocketshell-0.5.4}/scheduler/README.md +0 -0
  66. {pocketshell-0.5.3 → pocketshell-0.5.4}/scheduler/pocketshell-usage-capture.service +0 -0
  67. {pocketshell-0.5.3 → pocketshell-0.5.4}/scheduler/pocketshell-usage-capture.timer +0 -0
  68. {pocketshell-0.5.3 → pocketshell-0.5.4}/scripts/sync-readme-exit-codes.py +0 -0
  69. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/__main__.py +0 -0
  70. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/github.py +0 -0
  71. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/logs.py +0 -0
  72. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/profiles.py +0 -0
  73. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/prune_attachments.py +0 -0
  74. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/push.py +0 -0
  75. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/qr_share.py +0 -0
  76. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/repos.py +0 -0
  77. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/serve.py +0 -0
  78. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/usage.py +0 -0
  79. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/usage_capture.py +0 -0
  80. {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/usage_reset.py +0 -0
  81. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/__init__.py +0 -0
  82. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.11-usage.json +0 -0
  83. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.13-usage.json +0 -0
  84. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.15-usage.json +0 -0
  85. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.15-usage.ndjson +0 -0
  86. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.15-usage.provenance.txt +0 -0
  87. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.9-usage.json +0 -0
  88. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/sshd_anchor/Dockerfile.from-unanchored-base +0 -0
  89. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-agent.json +0 -0
  90. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-crashed-start.json +0 -0
  91. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-liveness-mix.json +0 -0
  92. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-mid-create.json +0 -0
  93. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-post-kill-window.json +0 -0
  94. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-reported-state.json +0 -0
  95. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot.json +0 -0
  96. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_agent_log.py +0 -0
  97. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_aplexer_contract.py +0 -0
  98. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_cli.py +0 -0
  99. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_env.py +0 -0
  100. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_github.py +0 -0
  101. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_logs.py +0 -0
  102. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_profiles.py +0 -0
  103. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_prune_attachments.py +0 -0
  104. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_push.py +0 -0
  105. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_qr_share.py +0 -0
  106. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_repos.py +0 -0
  107. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_serve.py +0 -0
  108. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_usage.py +0 -0
  109. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_usage_capture.py +0 -0
  110. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_usage_reset.py +0 -0
  111. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_watch_ci.py +0 -0
  112. {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_watch_ci_cancelled.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pocketshell
3
- Version: 0.5.3
3
+ Version: 0.5.4
4
4
  Summary: Unified server-side Python utility for the PocketShell Android client.
5
5
  Project-URL: Homepage, https://github.com/alexeygrigorev/pocketshell
6
6
  Project-URL: Issues, https://github.com/alexeygrigorev/pocketshell/issues
7
7
  Author: Alexey Grigorev
8
8
  License: MIT
9
- Keywords: agents,pocketshell,ssh,tmux,usage
9
+ Keywords: agents,pocketshell,ssh,usage
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
@@ -18,12 +18,11 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Topic :: Software Development
19
19
  Classifier: Topic :: System :: Monitoring
20
20
  Requires-Python: >=3.11
21
- Requires-Dist: aplexer==0.1.4; sys_platform == 'linux'
21
+ Requires-Dist: aplexer==0.1.5; sys_platform == 'linux'
22
22
  Requires-Dist: click>=8.2.0
23
23
  Requires-Dist: google-auth>=2.0.0
24
24
  Requires-Dist: pyyaml>=6.0
25
25
  Requires-Dist: quse==0.0.15
26
- Requires-Dist: tmuxctl>=0.3.5
27
26
  Provides-Extra: dev
28
27
  Requires-Dist: pytest>=8.4.0; extra == 'dev'
29
28
  Requires-Dist: ruff>=0.15.0; extra == 'dev'
@@ -35,9 +34,25 @@ Description-Content-Type: text/markdown
35
34
 
36
35
  Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
37
36
  Android client. The app probes for this single helper on each remote
38
- host and uses its subcommands for usage, tmux session/job metadata,
39
- agent conversations, QR host setup, repository discovery, environment
40
- files, hooks, logs, and daemon lifecycle checks.
37
+ host and uses its subcommands for usage, aplexer session lifecycle, agent
38
+ conversations, QR host setup, repository discovery, environment files, hooks,
39
+ logs, and daemon lifecycle checks.
40
+
41
+ ## Durable workspaces
42
+
43
+ The Quiet workspace-first client uses the host-side workspace membership
44
+ contract before it has any live session to enumerate:
45
+
46
+ ```text
47
+ pocketshell workspaces list --host <host> --json
48
+ pocketshell workspaces add <path> --host <host> --json
49
+ pocketshell workspaces remove <path> --host <host> --json
50
+ ```
51
+
52
+ Membership is stored in the existing private tree registry. Each entry has a
53
+ canonical absolute `path` for identity and a separate `display_path` for the
54
+ path spelling shown in the UI. Adding or removing the same path repeatedly is
55
+ safe, and `list` retains empty workspaces.
41
56
 
42
57
  ## Install
43
58
 
@@ -82,9 +97,10 @@ Top-level commands in the current helper:
82
97
 
83
98
  ```text
84
99
  pocketshell usage [provider] [--json] # provider quota / usage
85
- pocketshell send --pane %3 --token <id> # acknowledged pane delivery
86
- pocketshell sessions list [--by activity] # tmux session summaries
87
- pocketshell jobs ... # tmux recurring jobs
100
+ pocketshell sessions list --json # schema-3 aplexer session rows
101
+ pocketshell sessions create NAME --json # create or reuse a session
102
+ pocketshell sessions attach NAME # attach to a live session
103
+ pocketshell sessions kill NAME --json # stop and reap a session
88
104
  pocketshell agent-log ... # agent conversation logs
89
105
  pocketshell repos list ... # local / GitHub repositories
90
106
  pocketshell github status [--json] # gh install / auth state
@@ -101,136 +117,25 @@ flag set. Some parity subcommands still proxy through the existing host
101
117
  tools internally so their output remains byte-identical to what the app
102
118
  already parses.
103
119
 
104
- ### `pocketshell send`
120
+ ### `pocketshell sessions`
105
121
 
106
- Deliver a payload into an exact tmux pane, **exactly once per token**. The
107
- exit status IS the acknowledgement — the client no longer has to read the
108
- terminal screen and guess whether its prompt landed (issue #2122, epic
109
- #2121).
122
+ The session group is deliberately aplexer-only. `list`, `create`, `attach`, and
123
+ `kill` all use the bundled `a` executable resolved next to the installed
124
+ PocketShell interpreter; a missing or unusable aplexer is reported as an error
125
+ instead of an empty session list.
110
126
 
111
127
  ```bash
112
- printf 'summarise the diff' | pocketshell send --pane %3 --token <row-id> --enter
113
- pocketshell send --prune-older-than 30d
128
+ pocketshell sessions list --json
129
+ pocketshell sessions create my-session --cwd ~/git/project --mem none --json
130
+ pocketshell sessions attach my-session
131
+ pocketshell sessions kill my-session --json
114
132
  ```
115
133
 
116
- The payload is read from **stdin as raw bytes** and delivered byte-exact
117
- (`load-buffer -` → `paste-buffer -d -r`, never argv). This command does not
118
- add bracketed-paste markers: the client already frames its payload, and
119
- framing twice put the inner markers into the receiving program as literal
120
- text (issue #1854). Callers that want bracketed paste write the framed bytes
121
- to stdin.
122
-
123
- Exit codes are stable. Both renderings below — this table and `--help` — are
124
- generated from the one `EXIT_CODE_TABLE` the code exits with, so neither can
125
- drift from the other (issue #2153; regenerate with
126
- `tools/pocketshell/scripts/sync-readme-exit-codes.py`, pinned by a test in the
127
- `Python utility tests (pocketshell)` check):
128
-
129
- <!-- BEGIN GENERATED: send exit codes (source: EXIT_CODE_TABLE in pocketshell/send.py) -->
130
-
131
- | Exit | stdout reason | Meaning |
132
- | ---- | ------------- | ------- |
133
- | 0 | `delivered` \| `already-delivered` \| `pruned` | Success. 'delivered' = injected by THIS call and journaled. 'already-delivered' = the token was already journaled, nothing was injected. 'pruned' = --prune-older-than removed N records. |
134
- | 2 | `bad-usage` | Invalid or missing arguments. Nothing was injected or journaled. |
135
- | 3 | `pane-not-found` | The pane id does not exist on the tmux server, or it is dead. Nothing was injected; the token is NOT journaled and stays retryable. |
136
- | 4 | `tmux-failed` | tmux is missing, no server is running, or a tmux command returned a definitive failure. This call put NOTHING into the pane and recorded no delivery, and it left the journal exactly as it found it: a claim this call took is released, and a pre-existing unresolved record it overwrote under --resend-interrupted is restored byte-for-byte. A retry therefore cannot duplicate — but 'unchanged' is not 'absent': if the token was already journaled-unresolved it still is, and the next plain call answers 'send-interrupted' rather than injecting. |
137
- | 5 | `send-interrupted` \| `journal-corrupt` | Delivery is genuinely UNKNOWN and the token is left journaled-unresolved, so no plain call will ever inject it again. Two ways in. Either a PREVIOUS attempt died without an answer (or left an unreadable record) and its owning process is gone, in which case this call injected nothing; or THIS call got past the point of no return — tmux accepted the paste and then the Enter failed, or the delivery could not be journaled — in which case the payload may ALREADY be in the pane. Never auto-retry either reading. Re-run with --resend-interrupted only to accept a possible duplicate. |
138
- | 6 | `timeout` | A tmux invocation exceeded --timeout. If the timeout hit at or after the commit the token is left in the unknown state above and the payload may ALREADY be in the pane; a retry then reports 'send-interrupted' rather than injecting again. |
139
- | 7 | `journal-failed` | The durable token journal could not be read or written (permissions, disk). Nothing was injected — the journal is written BEFORE the pane is touched precisely so this failure is safe. |
140
- | 8 | `send-in-progress` | Another send for this token is STILL RUNNING (its process is alive on this host). Nothing was injected by this call and nothing is unknown: the outcome is owned by that call. Retry shortly to read the answer — it will be 'delivered'/'already-delivered' or, if that process dies, 'send-interrupted'. --resend-interrupted does not override this: there is no unknown to resolve while the owner is alive, and forcing one would duplicate the payload. |
141
-
142
- <!-- END GENERATED: send exit codes -->
143
-
144
- (`pruned` prints the record count after the reason word: `pruned <n>`.)
145
-
146
- **The paste is the point of no return, and that is the boundary between exit 4
147
- and exit 5** (issue #2136). A client that branches on this table to decide
148
- whether to auto-retry (#2124) needs one property, and it is exactly what exit 4
149
- now means:
150
-
151
- > **Exit 4 ⇒ this call put nothing into the pane, and left the journal exactly
152
- > as it found it.**
153
-
154
- Every exit-4 site satisfies it. Failures at the pane lookup or while filling
155
- the paste buffer happen before the journal is written at all — nothing injected,
156
- token unclaimed, cleanly retryable; this is the ordinary case. A definitive
157
- `paste-buffer` failure (tmux answered "no", or tmux became unexecutable before
158
- the paste) rolls this call's claim back: for a plain call that returns the token
159
- to absent, and under `--resend-interrupted` the *pre-existing* unresolved record
160
- is restored byte-for-byte rather than erased.
161
-
162
- Note what "unchanged" does **not** mean. It does not mean "absent": a token that
163
- was already journaled-unresolved still is, so the next plain call answers exit 5
164
- rather than injecting. A retry after exit 4 can therefore never duplicate, but
165
- it is not guaranteed to inject.
166
-
167
- **Exit 5 in detail** — it carries two different facts, and a client must treat
168
- both the same way (do not auto-retry; surface the choice):
169
-
170
- - A **previous** attempt for this token died without an answer, or left an
171
- unreadable record, and its owning process is gone. This call injected nothing.
172
- - **This** call got past the paste. tmux accepted the payload, so it **is** in
173
- the pane, and then either the `send-keys Enter` failed or the delivery could
174
- not be journaled. The claim is deliberately kept — rolling it back is what
175
- would let a plain retry paste the payload a second time.
176
-
177
- Both readings leave the token journaled-unresolved, so the invariant is the same
178
- for both: no plain call will ever inject it again, and `--resend-interrupted` is
179
- the explicit opt-in that accepts a possible duplicate.
180
-
181
- The `Enter` step has two failure shapes — tmux answers non-zero, or tmux stops
182
- being executable between the paste and the `send-keys` — and they leave
183
- byte-identical state. They report one outcome (exit 5). Until #2136 the second
184
- reported exit 4, which told a client the pane was untouched while the payload
185
- was sitting in it.
186
-
187
- stdout is machine-readable: the first whitespace-delimited token is one of
188
- the reasons above; human detail goes to stderr. Every retry path drains stdin
189
- before exiting, so a caller piping a payload never takes SIGPIPE on a
190
- successful acknowledgement. (Argument validation runs *before* stdin is read,
191
- deliberately: a caller with an open-but-idle stdin gets `bad-usage`
192
- immediately instead of blocking on a payload that will never arrive.)
193
-
194
- **Durability invariant: at-most-once, except on an explicit opt-in.** A token
195
- is never injected a second time unless the caller passes
196
- `--resend-interrupted` on the injecting call itself; no sequence of failures,
197
- kills, races or automatic housekeeping can turn an injected token back into a
198
- state a plain call will inject. The journal under
199
- `${XDG_STATE_HOME:-~/.local/state}/pocketshell/sends/` is two-phase — a
200
- `pending` record is written (atomically, fsync'd) immediately before the one
201
- command that can put bytes into the pane, then promoted to `delivered` once
202
- tmux answers. A definitive tmux failure rolls that claim back, so ordinary
203
- errors stay cleanly retryable; rolling back means undoing *this* call, so a
204
- record this call created is removed and a pre-existing unresolved record it
205
- overwrote is restored byte-for-byte rather than erased.
206
-
207
- An unresolved record is then read against its owner process: gone ⇒ a previous
208
- attempt died and delivery is genuinely unknown (exit 5, resolvable with
209
- `--resend-interrupted`, so the state is never absorbing); still running ⇒
210
- nothing is unknown and the outcome belongs to that call (exit 8, retryable).
211
- `--resend-interrupted` does not override a live owner — there is no unknown to
212
- resolve, and forcing one would simply duplicate the payload.
213
-
214
- The invariant's honest edges: a definitive non-zero from `paste-buffer` is
215
- taken as proof nothing reached the pane; the journal directory must survive
216
- (delete it and the memory is gone); `--prune-older-than` is an operator
217
- action that *can* clear unresolved records; and exit 8 is bounded by the owner
218
- process's **liveness**, not by the owner's `--timeout`. A suspended owner
219
- (reproduced with `SIGSTOP`) holds its token in `send-in-progress` for as long
220
- as it stays stopped, because the liveness probe asks whether the process still
221
- exists, not whether it is making progress, and `--timeout` bounds the tmux
222
- calls of the process that passed it rather than some other process's lifetime.
223
- This fails safe — the payload is never duplicated and the token never becomes
224
- absorbing once the owner dies — and a client cannot reach it through its own
225
- use, since it would have to suspend its own in-flight send. Nothing reaps a
226
- suspended owner.
227
-
228
- Records carry a timestamp and are pruned two ways: explicitly with
229
- `--prune-older-than <30d|12h|90m|3600s>`, and automatically on delivery at a
230
- **30-day default retention** (throttled to at most once every 6 h), so the
231
- directory cannot grow without bound even if pruning is never invoked. The
232
- automatic sweep only removes **resolved** records — ageing an unknown out of
233
- the journal would silently make the token injectable again.
134
+ The list and lifecycle responses use schema 3. Rows carry the aplexer id,
135
+ workspace, tag, phase, attachment state, and agent metadata; there is no
136
+ backend discriminator or legacy session socket. Create is idempotent for the
137
+ same workspace and tag. Kill stops the workload and reaps the aplexer record
138
+ before returning its JSON result.
234
139
 
235
140
  ### `pocketshell usage`
236
141
 
@@ -247,7 +152,7 @@ absent/unavailable daemon or explicitly supported method skew falls through
247
152
  to the one-shot subprocess path. Timeout, malformed-response, and
248
153
  daemon-internal failures are surfaced instead of being retried locally.
249
154
 
250
- All daemon-backed wrappers (`usage`, `repos`, `tree`, `jobs`, `sessions`, and
155
+ All daemon-backed wrappers (`usage`, `repos`, `tree`, `sessions`, and
251
156
  `agents kind`) use one typed fallback boundary. It emits the safe
252
157
  `pocketshell.daemon_call` event with `reason`, `method`, `phase`, RPC code, and
253
158
  available CLI/daemon versions. It never logs RPC parameters or command output.
@@ -496,9 +401,8 @@ uv sync --group dev
496
401
  uv run pytest
497
402
  ```
498
403
 
499
- The tests stub `pocketshell.usage.subprocess.run` (and the `quse`/`tmuxctl`
500
- binary resolvers) so they run in seconds without invoking any real binary or
501
- hitting a provider API.
404
+ The tests stub subprocess boundaries and the bundled aplexer resolver so
405
+ they run in seconds without invoking a real host session.
502
406
 
503
407
  ## Release flow
504
408
 
@@ -2,9 +2,25 @@
2
2
 
3
3
  Unified server-side Python utility for the [PocketShell](https://github.com/alexeygrigorev/pocketshell)
4
4
  Android client. The app probes for this single helper on each remote
5
- host and uses its subcommands for usage, tmux session/job metadata,
6
- agent conversations, QR host setup, repository discovery, environment
7
- files, hooks, logs, and daemon lifecycle checks.
5
+ host and uses its subcommands for usage, aplexer session lifecycle, agent
6
+ conversations, QR host setup, repository discovery, environment files, hooks,
7
+ logs, and daemon lifecycle checks.
8
+
9
+ ## Durable workspaces
10
+
11
+ The Quiet workspace-first client uses the host-side workspace membership
12
+ contract before it has any live session to enumerate:
13
+
14
+ ```text
15
+ pocketshell workspaces list --host <host> --json
16
+ pocketshell workspaces add <path> --host <host> --json
17
+ pocketshell workspaces remove <path> --host <host> --json
18
+ ```
19
+
20
+ Membership is stored in the existing private tree registry. Each entry has a
21
+ canonical absolute `path` for identity and a separate `display_path` for the
22
+ path spelling shown in the UI. Adding or removing the same path repeatedly is
23
+ safe, and `list` retains empty workspaces.
8
24
 
9
25
  ## Install
10
26
 
@@ -49,9 +65,10 @@ Top-level commands in the current helper:
49
65
 
50
66
  ```text
51
67
  pocketshell usage [provider] [--json] # provider quota / usage
52
- pocketshell send --pane %3 --token <id> # acknowledged pane delivery
53
- pocketshell sessions list [--by activity] # tmux session summaries
54
- pocketshell jobs ... # tmux recurring jobs
68
+ pocketshell sessions list --json # schema-3 aplexer session rows
69
+ pocketshell sessions create NAME --json # create or reuse a session
70
+ pocketshell sessions attach NAME # attach to a live session
71
+ pocketshell sessions kill NAME --json # stop and reap a session
55
72
  pocketshell agent-log ... # agent conversation logs
56
73
  pocketshell repos list ... # local / GitHub repositories
57
74
  pocketshell github status [--json] # gh install / auth state
@@ -68,136 +85,25 @@ flag set. Some parity subcommands still proxy through the existing host
68
85
  tools internally so their output remains byte-identical to what the app
69
86
  already parses.
70
87
 
71
- ### `pocketshell send`
88
+ ### `pocketshell sessions`
72
89
 
73
- Deliver a payload into an exact tmux pane, **exactly once per token**. The
74
- exit status IS the acknowledgement — the client no longer has to read the
75
- terminal screen and guess whether its prompt landed (issue #2122, epic
76
- #2121).
90
+ The session group is deliberately aplexer-only. `list`, `create`, `attach`, and
91
+ `kill` all use the bundled `a` executable resolved next to the installed
92
+ PocketShell interpreter; a missing or unusable aplexer is reported as an error
93
+ instead of an empty session list.
77
94
 
78
95
  ```bash
79
- printf 'summarise the diff' | pocketshell send --pane %3 --token <row-id> --enter
80
- pocketshell send --prune-older-than 30d
96
+ pocketshell sessions list --json
97
+ pocketshell sessions create my-session --cwd ~/git/project --mem none --json
98
+ pocketshell sessions attach my-session
99
+ pocketshell sessions kill my-session --json
81
100
  ```
82
101
 
83
- The payload is read from **stdin as raw bytes** and delivered byte-exact
84
- (`load-buffer -` → `paste-buffer -d -r`, never argv). This command does not
85
- add bracketed-paste markers: the client already frames its payload, and
86
- framing twice put the inner markers into the receiving program as literal
87
- text (issue #1854). Callers that want bracketed paste write the framed bytes
88
- to stdin.
89
-
90
- Exit codes are stable. Both renderings below — this table and `--help` — are
91
- generated from the one `EXIT_CODE_TABLE` the code exits with, so neither can
92
- drift from the other (issue #2153; regenerate with
93
- `tools/pocketshell/scripts/sync-readme-exit-codes.py`, pinned by a test in the
94
- `Python utility tests (pocketshell)` check):
95
-
96
- <!-- BEGIN GENERATED: send exit codes (source: EXIT_CODE_TABLE in pocketshell/send.py) -->
97
-
98
- | Exit | stdout reason | Meaning |
99
- | ---- | ------------- | ------- |
100
- | 0 | `delivered` \| `already-delivered` \| `pruned` | Success. 'delivered' = injected by THIS call and journaled. 'already-delivered' = the token was already journaled, nothing was injected. 'pruned' = --prune-older-than removed N records. |
101
- | 2 | `bad-usage` | Invalid or missing arguments. Nothing was injected or journaled. |
102
- | 3 | `pane-not-found` | The pane id does not exist on the tmux server, or it is dead. Nothing was injected; the token is NOT journaled and stays retryable. |
103
- | 4 | `tmux-failed` | tmux is missing, no server is running, or a tmux command returned a definitive failure. This call put NOTHING into the pane and recorded no delivery, and it left the journal exactly as it found it: a claim this call took is released, and a pre-existing unresolved record it overwrote under --resend-interrupted is restored byte-for-byte. A retry therefore cannot duplicate — but 'unchanged' is not 'absent': if the token was already journaled-unresolved it still is, and the next plain call answers 'send-interrupted' rather than injecting. |
104
- | 5 | `send-interrupted` \| `journal-corrupt` | Delivery is genuinely UNKNOWN and the token is left journaled-unresolved, so no plain call will ever inject it again. Two ways in. Either a PREVIOUS attempt died without an answer (or left an unreadable record) and its owning process is gone, in which case this call injected nothing; or THIS call got past the point of no return — tmux accepted the paste and then the Enter failed, or the delivery could not be journaled — in which case the payload may ALREADY be in the pane. Never auto-retry either reading. Re-run with --resend-interrupted only to accept a possible duplicate. |
105
- | 6 | `timeout` | A tmux invocation exceeded --timeout. If the timeout hit at or after the commit the token is left in the unknown state above and the payload may ALREADY be in the pane; a retry then reports 'send-interrupted' rather than injecting again. |
106
- | 7 | `journal-failed` | The durable token journal could not be read or written (permissions, disk). Nothing was injected — the journal is written BEFORE the pane is touched precisely so this failure is safe. |
107
- | 8 | `send-in-progress` | Another send for this token is STILL RUNNING (its process is alive on this host). Nothing was injected by this call and nothing is unknown: the outcome is owned by that call. Retry shortly to read the answer — it will be 'delivered'/'already-delivered' or, if that process dies, 'send-interrupted'. --resend-interrupted does not override this: there is no unknown to resolve while the owner is alive, and forcing one would duplicate the payload. |
108
-
109
- <!-- END GENERATED: send exit codes -->
110
-
111
- (`pruned` prints the record count after the reason word: `pruned <n>`.)
112
-
113
- **The paste is the point of no return, and that is the boundary between exit 4
114
- and exit 5** (issue #2136). A client that branches on this table to decide
115
- whether to auto-retry (#2124) needs one property, and it is exactly what exit 4
116
- now means:
117
-
118
- > **Exit 4 ⇒ this call put nothing into the pane, and left the journal exactly
119
- > as it found it.**
120
-
121
- Every exit-4 site satisfies it. Failures at the pane lookup or while filling
122
- the paste buffer happen before the journal is written at all — nothing injected,
123
- token unclaimed, cleanly retryable; this is the ordinary case. A definitive
124
- `paste-buffer` failure (tmux answered "no", or tmux became unexecutable before
125
- the paste) rolls this call's claim back: for a plain call that returns the token
126
- to absent, and under `--resend-interrupted` the *pre-existing* unresolved record
127
- is restored byte-for-byte rather than erased.
128
-
129
- Note what "unchanged" does **not** mean. It does not mean "absent": a token that
130
- was already journaled-unresolved still is, so the next plain call answers exit 5
131
- rather than injecting. A retry after exit 4 can therefore never duplicate, but
132
- it is not guaranteed to inject.
133
-
134
- **Exit 5 in detail** — it carries two different facts, and a client must treat
135
- both the same way (do not auto-retry; surface the choice):
136
-
137
- - A **previous** attempt for this token died without an answer, or left an
138
- unreadable record, and its owning process is gone. This call injected nothing.
139
- - **This** call got past the paste. tmux accepted the payload, so it **is** in
140
- the pane, and then either the `send-keys Enter` failed or the delivery could
141
- not be journaled. The claim is deliberately kept — rolling it back is what
142
- would let a plain retry paste the payload a second time.
143
-
144
- Both readings leave the token journaled-unresolved, so the invariant is the same
145
- for both: no plain call will ever inject it again, and `--resend-interrupted` is
146
- the explicit opt-in that accepts a possible duplicate.
147
-
148
- The `Enter` step has two failure shapes — tmux answers non-zero, or tmux stops
149
- being executable between the paste and the `send-keys` — and they leave
150
- byte-identical state. They report one outcome (exit 5). Until #2136 the second
151
- reported exit 4, which told a client the pane was untouched while the payload
152
- was sitting in it.
153
-
154
- stdout is machine-readable: the first whitespace-delimited token is one of
155
- the reasons above; human detail goes to stderr. Every retry path drains stdin
156
- before exiting, so a caller piping a payload never takes SIGPIPE on a
157
- successful acknowledgement. (Argument validation runs *before* stdin is read,
158
- deliberately: a caller with an open-but-idle stdin gets `bad-usage`
159
- immediately instead of blocking on a payload that will never arrive.)
160
-
161
- **Durability invariant: at-most-once, except on an explicit opt-in.** A token
162
- is never injected a second time unless the caller passes
163
- `--resend-interrupted` on the injecting call itself; no sequence of failures,
164
- kills, races or automatic housekeeping can turn an injected token back into a
165
- state a plain call will inject. The journal under
166
- `${XDG_STATE_HOME:-~/.local/state}/pocketshell/sends/` is two-phase — a
167
- `pending` record is written (atomically, fsync'd) immediately before the one
168
- command that can put bytes into the pane, then promoted to `delivered` once
169
- tmux answers. A definitive tmux failure rolls that claim back, so ordinary
170
- errors stay cleanly retryable; rolling back means undoing *this* call, so a
171
- record this call created is removed and a pre-existing unresolved record it
172
- overwrote is restored byte-for-byte rather than erased.
173
-
174
- An unresolved record is then read against its owner process: gone ⇒ a previous
175
- attempt died and delivery is genuinely unknown (exit 5, resolvable with
176
- `--resend-interrupted`, so the state is never absorbing); still running ⇒
177
- nothing is unknown and the outcome belongs to that call (exit 8, retryable).
178
- `--resend-interrupted` does not override a live owner — there is no unknown to
179
- resolve, and forcing one would simply duplicate the payload.
180
-
181
- The invariant's honest edges: a definitive non-zero from `paste-buffer` is
182
- taken as proof nothing reached the pane; the journal directory must survive
183
- (delete it and the memory is gone); `--prune-older-than` is an operator
184
- action that *can* clear unresolved records; and exit 8 is bounded by the owner
185
- process's **liveness**, not by the owner's `--timeout`. A suspended owner
186
- (reproduced with `SIGSTOP`) holds its token in `send-in-progress` for as long
187
- as it stays stopped, because the liveness probe asks whether the process still
188
- exists, not whether it is making progress, and `--timeout` bounds the tmux
189
- calls of the process that passed it rather than some other process's lifetime.
190
- This fails safe — the payload is never duplicated and the token never becomes
191
- absorbing once the owner dies — and a client cannot reach it through its own
192
- use, since it would have to suspend its own in-flight send. Nothing reaps a
193
- suspended owner.
194
-
195
- Records carry a timestamp and are pruned two ways: explicitly with
196
- `--prune-older-than <30d|12h|90m|3600s>`, and automatically on delivery at a
197
- **30-day default retention** (throttled to at most once every 6 h), so the
198
- directory cannot grow without bound even if pruning is never invoked. The
199
- automatic sweep only removes **resolved** records — ageing an unknown out of
200
- the journal would silently make the token injectable again.
102
+ The list and lifecycle responses use schema 3. Rows carry the aplexer id,
103
+ workspace, tag, phase, attachment state, and agent metadata; there is no
104
+ backend discriminator or legacy session socket. Create is idempotent for the
105
+ same workspace and tag. Kill stops the workload and reaps the aplexer record
106
+ before returning its JSON result.
201
107
 
202
108
  ### `pocketshell usage`
203
109
 
@@ -214,7 +120,7 @@ absent/unavailable daemon or explicitly supported method skew falls through
214
120
  to the one-shot subprocess path. Timeout, malformed-response, and
215
121
  daemon-internal failures are surfaced instead of being retried locally.
216
122
 
217
- All daemon-backed wrappers (`usage`, `repos`, `tree`, `jobs`, `sessions`, and
123
+ All daemon-backed wrappers (`usage`, `repos`, `tree`, `sessions`, and
218
124
  `agents kind`) use one typed fallback boundary. It emits the safe
219
125
  `pocketshell.daemon_call` event with `reason`, `method`, `phase`, RPC code, and
220
126
  available CLI/daemon versions. It never logs RPC parameters or command output.
@@ -463,9 +369,8 @@ uv sync --group dev
463
369
  uv run pytest
464
370
  ```
465
371
 
466
- The tests stub `pocketshell.usage.subprocess.run` (and the `quse`/`tmuxctl`
467
- binary resolvers) so they run in seconds without invoking any real binary or
468
- hitting a provider API.
372
+ The tests stub subprocess boundaries and the bundled aplexer resolver so
373
+ they run in seconds without invoking a real host session.
469
374
 
470
375
  ## Release flow
471
376
 
@@ -14,13 +14,13 @@ name = "pocketshell"
14
14
  # scripts/check-pypi-version.sh enforces the stamped value matches the tag;
15
15
  # scripts/check-version-coupling.sh enforces the derivation itself is
16
16
  # single-sourced. See tools/pocketshell/README.md ("Version coupling").
17
- version = "0.5.3"
17
+ version = "0.5.4"
18
18
  description = "Unified server-side Python utility for the PocketShell Android client."
19
19
  readme = "README.md"
20
20
  requires-python = ">=3.11"
21
21
  authors = [{ name = "Alexey Grigorev" }]
22
22
  license = { text = "MIT" }
23
- keywords = ["pocketshell", "ssh", "tmux", "agents", "usage"]
23
+ keywords = ["pocketshell", "ssh", "agents", "usage"]
24
24
  classifiers = [
25
25
  "Development Status :: 3 - Alpha",
26
26
  "Environment :: Console",
@@ -65,25 +65,8 @@ dependencies = [
65
65
  # human/agent-readable default output format (#714). The Android client
66
66
  # consumes the `--json` flag instead; the host-facing default is YAML.
67
67
  "pyyaml>=6.0",
68
- # Session create/resume delegates to the `tmuxctl` binary (see
69
- # `pocketshell/resume.py`: `tmuxctl create-detached` / `create-or-attach`),
70
- # so installing pocketshell pulls a tmuxctl that has the server-cgroup fix.
71
- # >=0.3.3 is required: earlier tmuxctl started the shared tmux server inside
72
- # the SSH login's `session-*.scope`, so a logout (or a per-session OOM) tore
73
- # down every session at once; 0.3.3 starts the server in its own
74
- # login-independent unit under robust.slice (tmuxctl#4). 0.3.0 first shipped
75
- # the `create-detached` verb this CLI calls.
76
- # >=0.3.5 is required as of 2026-08-26: 0.3.4 still resolves/creates
77
- # sessions on the legacy shared socket only. A pinned-old pocketshell host
78
- # CLI running alongside a newer interactive `tmuxctl`/`t` checkout (which
79
- # does per-session servers) raced session creation for the same name onto
80
- # two different sockets, so the app/desktop and an interactive `t` session
81
- # silently disagreed about which sessions existed. 0.3.5 ships tmuxctl's
82
- # per-session-server migration so every caller resolves sessions the same
83
- # way.
84
- "tmuxctl>=0.3.5",
85
68
  # aplexer ships WITH the pocketshell CLI (issue #2543). `pocketshell
86
- # sessions create --backend aplexer` needs BOTH the `a` CLI and its
69
+ # sessions create` needs BOTH the `a` CLI and its
87
70
  # `aplexer` worker binary (aplexer/src/lib.rs::worker_executable resolves
88
71
  # the worker as a sibling of current_exe); the published wheel bundles
89
72
  # both native binaries and exposes them as the `a` / `aplexer`
@@ -98,8 +81,12 @@ dependencies = [
98
81
  # so the pinned wheel is the ONLY aplexer this CLI can run. Bumping the
99
82
  # pin is a deliberate, reviewed change.
100
83
  #
101
- # 0.1.4 is required for the session tree to say WHICH agent is running.
102
- # Through 0.1.3 a snapshot row's only agent-ish field was `engine`, and
84
+ # 0.1.5 is required for PocketShell's plain attach path. `a attach
85
+ # --no-status` removes aplexer's status chrome and reserved row while
86
+ # forwarding raw input and every PTY resize, so the Android terminal gets
87
+ # the full screen without a second terminal UI. 0.1.4 is also required
88
+ # for the session tree to say WHICH agent is running. Through 0.1.3 a
89
+ # snapshot row's only agent-ish field was `engine`, and
103
90
  # `engine` cannot answer that question: every session PocketShell creates
104
91
  # is `engine: "shell"` with the agent launched by hand inside it, so a
105
92
  # tree full of claude/codex/opencode sessions is indistinguishable from a
@@ -112,7 +99,7 @@ dependencies = [
112
99
  # running) and never persisted, so it cannot go stale.
113
100
  #
114
101
  # `session_enum._aplexer_rows` reads that key straight onto
115
- # `LiveSession.agent` and schema 2 emits it on every row. Against 0.1.3
102
+ # `LiveSession.agent` and schema 3 emits it on every row. Against 0.1.3
116
103
  # the key is simply absent and every row reports `agent: null` — the
117
104
  # listing still works, it just cannot name an agent, which is the whole
118
105
  # point of the bump. tests/test_aplexer_contract.py pins the behaviour
@@ -143,7 +130,7 @@ dependencies = [
143
130
  # session tree silently lost EVERY aplexer row. Worse, a record-less
144
131
  # directory that outlives the window (a killed/crashed `a start`) bricks
145
132
  # 0.1.2's registry permanently — including `a start` itself, which runs
146
- # the same scan under the lock, so `sessions create --backend aplexer`
133
+ # the same scan under the lock, so `sessions create`
147
134
  # stops working until someone manually `rmdir`s it.
148
135
  # tests/test_aplexer_contract.py drives the bundled binary through both
149
136
  # production probes with exactly that on-disk shape; verified red on
@@ -159,7 +146,7 @@ dependencies = [
159
146
  # * aplexer ee4b957 — profile `executable` override (argv[0]) for Codex
160
147
  # variants. Without it, `[profiles.zcodex] engine="codex",
161
148
  # executable="zcodex"` resolves argv[0] to `codex`, and `pocketshell
162
- # sessions create --backend aplexer --engine codex --profile zcodex`
149
+ # sessions create --engine codex --profile zcodex`
163
150
  # dies with "a: command is not executable or was not found in PATH:
164
151
  # codex".
165
152
  # * aplexer d13ecb2 — preserve the provider environment for plain
@@ -172,7 +159,7 @@ dependencies = [
172
159
  #
173
160
  # There is no separate release-time guard for the aplexer version itself
174
161
  # (scripts/check-pypi-version.sh only couples pocketshell's OWN version to
175
- # the release tag and says nothing about quse or tmuxctl either), so the
162
+ # the release tag and says nothing about quse either), so the
176
163
  # exact pin + the committed uv.lock + that contract test are the
177
164
  # enforcement.
178
165
  #
@@ -180,16 +167,15 @@ dependencies = [
180
167
  # manylinux wheels only (x86_64 + aarch64) and NO sdist, so an unmarked
181
168
  # hard dependency would make `pip install pocketshell` unresolvable on
182
169
  # macOS/Windows. PocketShell hosts are Linux; a non-Linux host simply gets
183
- # no bundled aplexer at all, so `sessions create --backend aplexer` there
184
- # fails loud (there is no PATH fallback — D22) while tmux, the default
185
- # backend, keeps working.
170
+ # no bundled aplexer at all, so `sessions create` there fails loud (there
171
+ # is no PATH fallback — D22).
186
172
  #
187
- # Install-surface note (unchanged in 0.1.4): as of 0.1.2 the wheels moved
173
+ # Install-surface note (unchanged in 0.1.5): as of 0.1.2 the wheels moved
188
174
  # from manylinux_2_17 to manylinux_2_28, raising the minimum glibc from
189
175
  # 2.17 to 2.28. Linux hosts older than that (CentOS 7, Ubuntu 18.04) can
190
176
  # no longer install pocketshell at all now that aplexer is a hard
191
177
  # dependency. Flagged, not solved here — see docs/aplexer-integration.md.
192
- "aplexer==0.1.4; sys_platform == 'linux'",
178
+ "aplexer==0.1.5; sys_platform == 'linux'",
193
179
  ]
194
180
 
195
181
  [project.scripts]
@@ -243,7 +229,7 @@ exclude = [
243
229
  required-version = "==0.10.11"
244
230
  # Keep the lockfile's reproducibility cutoff project-local. The host may have
245
231
  # a rolling global `exclude-newer` policy that predates deliberately pinned
246
- # releases (quse 0.0.15 and tmuxctl 0.3.5); this exact value is also the
232
+ # release (quse 0.0.15); this exact value is also the
247
233
  # `uv.lock` options cutoff and must be used when checking this lock. Moved
248
234
  # past quse 0.0.15's 2026-08-28T12:09:07Z PyPI upload for issue #2293, then
249
235
  # past aplexer 0.1.2's 2026-09-05T22:38:08Z PyPI upload (its `aplexer-client`
@@ -255,7 +241,8 @@ required-version = "==0.10.11"
255
241
  # runtime dep landed at 00:32:04Z) for the session-tree regression fix above,
256
242
  # and now past aplexer 0.1.4's 2026-09-06T16:14:52Z PyPI upload (its
257
243
  # `aplexer-client` runtime dep landed at 16:14:48Z) for the `agent` field.
258
- exclude-newer = "2026-09-06T17:00:00Z"
244
+ # 0.1.5 uploaded at 21:06:11Z (client at 21:06:06Z) for plain attach.
245
+ exclude-newer = "2026-09-08T22:00:00Z"
259
246
 
260
247
  [tool.pytest.ini_options]
261
248
  testpaths = ["tests"]
@@ -1,8 +1,7 @@
1
1
  """Unified server-side Python utility for the PocketShell Android client.
2
2
 
3
- This package replaces the separately-installed `quse` and `tmuxctl`
4
- utilities the PocketShell app used to probe for. The app now probes a
5
- single `pocketshell` binary, which exposes the `usage`, `jobs`,
3
+ This package replaces the separately-installed `quse` utility the PocketShell
4
+ app used to probe for. The app now probes a single `pocketshell` binary, which exposes the `usage`,
6
5
  `agent-log`, `sessions`, `repos`, `serve`, and daemon subcommands.
7
6
 
8
7
  See https://github.com/alexeygrigorev/pocketshell/issues/170.
@@ -156,7 +156,7 @@ def card_to_data(
156
156
  Keys (mirrors the app's ``AgentCardPushPayload`` contract):
157
157
 
158
158
  - ``type`` = ``agent_card``
159
- - ``session`` = the tmux session the card belongs to (deep-link target)
159
+ - ``session`` = the session the card belongs to (deep-link target)
160
160
  - ``host`` = best-effort host hostname for host resolution (may be empty)
161
161
  - ``card_id`` = the card id
162
162
  - ``card_type`` = e.g. ``checklist``
@@ -31,7 +31,7 @@ Why direct file read instead of a subprocess delegation:
31
31
 
32
32
  - There is no upstream CLI for these reads. The Android app reads them
33
33
  itself via ``ssh exec 'tail -n N <path>'``. There is no ``quse``- or
34
- ``tmuxctl``-shaped binary on the host to wrap; reimplementing the
34
+ host-side session binary on the host to wrap; reimplementing the
35
35
  ``tail -n N`` step in Python is the smallest reasonable parity layer.
36
36
  - The JSONL files are append-only, plain text, one event per line. The
37
37
  default read path emits raw lines verbatim. The ``handoff`` path only