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.
- {pocketshell-0.5.3 → pocketshell-0.5.4}/PKG-INFO +43 -139
- {pocketshell-0.5.3 → pocketshell-0.5.4}/README.md +40 -135
- {pocketshell-0.5.3 → pocketshell-0.5.4}/pyproject.toml +20 -33
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/__init__.py +2 -3
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/agent_card_push.py +1 -1
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/agent_log.py +1 -1
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/agents.py +26 -333
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/agents_kind.py +3 -3
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/aplexer.py +1 -1
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/cards.py +20 -49
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/cgroup_agents.py +9 -12
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/cli.py +6 -12
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/daemon.py +7 -69
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/engines.py +2 -2
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/env.py +1 -1
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/hooks.py +0 -79
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/memcap.py +13 -36
- pocketshell-0.5.4/src/pocketshell/session_enum.py +374 -0
- pocketshell-0.5.4/src/pocketshell/sessions.py +692 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/tree.py +23 -59
- pocketshell-0.5.4/src/pocketshell/workspaces.py +224 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/conftest.py +0 -5
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_agent_card_push.py +1 -4
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_agents.py +1 -606
- pocketshell-0.5.4/tests/test_agents_fixture_aplexer.py +158 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_agents_kind.py +17 -17
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_aplexer_resolution.py +7 -4
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_cards.py +17 -17
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_cards_push_notify.py +0 -1
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_cgroup_agents.py +18 -18
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_daemon.py +3 -47
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_daemon_fallback.py +3 -7
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_engines.py +3 -3
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_hooks.py +7 -84
- pocketshell-0.5.3/tests/test_sessions_mem_cap.py → pocketshell-0.5.4/tests/test_session_cgroup_kernel_proof.py +186 -609
- pocketshell-0.5.4/tests/test_session_enum.py +93 -0
- pocketshell-0.5.4/tests/test_sessions.py +54 -0
- pocketshell-0.5.4/tests/test_sessions_attach.py +55 -0
- pocketshell-0.5.4/tests/test_sessions_create_routing.py +90 -0
- pocketshell-0.5.4/tests/test_sessions_kill.py +57 -0
- pocketshell-0.5.4/tests/test_sessions_mem_cap.py +38 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_ssh_fixture_penalties.py +18 -61
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_tree.py +13 -38
- pocketshell-0.5.4/tests/test_workspaces.py +98 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/uv.lock +8 -90
- pocketshell-0.5.3/src/pocketshell/config.py +0 -163
- pocketshell-0.5.3/src/pocketshell/jobs.py +0 -829
- pocketshell-0.5.3/src/pocketshell/resume.py +0 -698
- pocketshell-0.5.3/src/pocketshell/send.py +0 -1495
- pocketshell-0.5.3/src/pocketshell/session_enum.py +0 -905
- pocketshell-0.5.3/src/pocketshell/sessions.py +0 -1991
- pocketshell-0.5.3/tests/test_agents_fixture_aplexer.py +0 -714
- pocketshell-0.5.3/tests/test_agents_kind_target.py +0 -280
- pocketshell-0.5.3/tests/test_agents_source_target.py +0 -426
- pocketshell-0.5.3/tests/test_jobs.py +0 -896
- pocketshell-0.5.3/tests/test_resume.py +0 -558
- pocketshell-0.5.3/tests/test_send.py +0 -2715
- pocketshell-0.5.3/tests/test_session_enum.py +0 -144
- pocketshell-0.5.3/tests/test_session_enum_schema2.py +0 -1226
- pocketshell-0.5.3/tests/test_sessions.py +0 -448
- pocketshell-0.5.3/tests/test_sessions_attach.py +0 -652
- pocketshell-0.5.3/tests/test_sessions_create_routing.py +0 -1284
- pocketshell-0.5.3/tests/test_sessions_kill.py +0 -748
- {pocketshell-0.5.3 → pocketshell-0.5.4}/.gitignore +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/scheduler/README.md +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/scheduler/pocketshell-usage-capture.service +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/scheduler/pocketshell-usage-capture.timer +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/scripts/sync-readme-exit-codes.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/__main__.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/github.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/logs.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/profiles.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/prune_attachments.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/push.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/qr_share.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/repos.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/serve.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/usage.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/usage_capture.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/src/pocketshell/usage_reset.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/__init__.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.11-usage.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.13-usage.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.15-usage.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.15-usage.ndjson +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.15-usage.provenance.txt +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/quse-0.0.9-usage.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/data/sshd_anchor/Dockerfile.from-unanchored-base +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-agent.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-crashed-start.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-liveness-mix.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-mid-create.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-post-kill-window.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot-reported-state.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/fixtures/aplexer/snapshot.json +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_agent_log.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_aplexer_contract.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_cli.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_env.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_github.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_logs.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_profiles.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_prune_attachments.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_push.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_qr_share.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_repos.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_serve.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_usage.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_usage_capture.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_usage_reset.py +0 -0
- {pocketshell-0.5.3 → pocketshell-0.5.4}/tests/test_watch_ci.py +0 -0
- {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
|
+
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,
|
|
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.
|
|
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,
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
86
|
-
pocketshell sessions
|
|
87
|
-
pocketshell
|
|
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
|
|
120
|
+
### `pocketshell sessions`
|
|
105
121
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
pocketshell
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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`, `
|
|
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
|
|
500
|
-
|
|
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,
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
53
|
-
pocketshell sessions
|
|
54
|
-
pocketshell
|
|
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
|
|
88
|
+
### `pocketshell sessions`
|
|
72
89
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
pocketshell
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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`, `
|
|
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
|
|
467
|
-
|
|
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.
|
|
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", "
|
|
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
|
|
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.
|
|
102
|
-
#
|
|
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
|
|
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
|
|
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 --
|
|
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
|
|
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
|
|
184
|
-
#
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
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`
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
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
|