pocketshell 0.4.43__tar.gz → 0.4.44__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 (66) hide show
  1. {pocketshell-0.4.43 → pocketshell-0.4.44}/PKG-INFO +133 -10
  2. {pocketshell-0.4.43 → pocketshell-0.4.44}/README.md +131 -8
  3. {pocketshell-0.4.43 → pocketshell-0.4.44}/pyproject.toml +1 -1
  4. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/cli.py +5 -0
  5. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/hooks.py +353 -95
  6. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/logs.py +17 -8
  7. pocketshell-0.4.44/src/pocketshell/send.py +1351 -0
  8. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_hooks.py +224 -2
  9. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_logs.py +20 -0
  10. pocketshell-0.4.44/tests/test_send.py +2092 -0
  11. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_watch_ci.py +120 -1
  12. {pocketshell-0.4.43 → pocketshell-0.4.44}/.gitignore +0 -0
  13. {pocketshell-0.4.43 → pocketshell-0.4.44}/scheduler/README.md +0 -0
  14. {pocketshell-0.4.43 → pocketshell-0.4.44}/scheduler/pocketshell-usage-capture.service +0 -0
  15. {pocketshell-0.4.43 → pocketshell-0.4.44}/scheduler/pocketshell-usage-capture.timer +0 -0
  16. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/__init__.py +0 -0
  17. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/__main__.py +0 -0
  18. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/agent_card_push.py +0 -0
  19. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/agent_log.py +0 -0
  20. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/agents.py +0 -0
  21. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/agents_kind.py +0 -0
  22. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/cards.py +0 -0
  23. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/cgroup_agents.py +0 -0
  24. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/daemon.py +0 -0
  25. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/env.py +0 -0
  26. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/github.py +0 -0
  27. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/jobs.py +0 -0
  28. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/profiles.py +0 -0
  29. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/prune_attachments.py +0 -0
  30. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/push.py +0 -0
  31. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/qr_share.py +0 -0
  32. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/repos.py +0 -0
  33. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/resume.py +0 -0
  34. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/sessions.py +0 -0
  35. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/tree.py +0 -0
  36. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/usage.py +0 -0
  37. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/usage_capture.py +0 -0
  38. {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/usage_reset.py +0 -0
  39. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/__init__.py +0 -0
  40. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/data/quse-0.0.11-usage.json +0 -0
  41. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/data/quse-0.0.9-usage.json +0 -0
  42. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_agent_card_push.py +0 -0
  43. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_agent_log.py +0 -0
  44. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_agents.py +0 -0
  45. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_agents_kind.py +0 -0
  46. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_cards.py +0 -0
  47. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_cards_push_notify.py +0 -0
  48. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_cgroup_agents.py +0 -0
  49. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_cli.py +0 -0
  50. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_daemon.py +0 -0
  51. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_env.py +0 -0
  52. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_github.py +0 -0
  53. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_jobs.py +0 -0
  54. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_profiles.py +0 -0
  55. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_prune_attachments.py +0 -0
  56. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_push.py +0 -0
  57. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_qr_share.py +0 -0
  58. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_repos.py +0 -0
  59. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_resume.py +0 -0
  60. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_sessions.py +0 -0
  61. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_tree.py +0 -0
  62. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_usage.py +0 -0
  63. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_usage_capture.py +0 -0
  64. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_usage_reset.py +0 -0
  65. {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_watch_ci_cancelled.py +0 -0
  66. {pocketshell-0.4.43 → pocketshell-0.4.44}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pocketshell
3
- Version: 0.4.43
3
+ Version: 0.4.44
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
@@ -81,6 +81,7 @@ Top-level commands in the current helper:
81
81
 
82
82
  ```text
83
83
  pocketshell usage [provider] [--json] # provider quota / usage
84
+ pocketshell send --pane %3 --token <id> # acknowledged pane delivery
84
85
  pocketshell sessions list [--by activity] # tmux session summaries
85
86
  pocketshell jobs ... # tmux recurring jobs
86
87
  pocketshell agent-log ... # agent conversation logs
@@ -98,6 +99,110 @@ flag set. Some parity subcommands still proxy through the existing host
98
99
  tools internally so their output remains byte-identical to what the app
99
100
  already parses.
100
101
 
102
+ ### `pocketshell send`
103
+
104
+ Deliver a payload into an exact tmux pane, **exactly once per token**. The
105
+ exit status IS the acknowledgement — the client no longer has to read the
106
+ terminal screen and guess whether its prompt landed (issue #2122, epic
107
+ #2121).
108
+
109
+ ```bash
110
+ printf 'summarise the diff' | pocketshell send --pane %3 --token <row-id> --enter
111
+ pocketshell send --prune-older-than 30d
112
+ ```
113
+
114
+ The payload is read from **stdin as raw bytes** and delivered byte-exact
115
+ (`load-buffer -` → `paste-buffer -d -r`, never argv). This command does not
116
+ add bracketed-paste markers: the client already frames its payload, and
117
+ framing twice put the inner markers into the receiving program as literal
118
+ text (issue #1854). Callers that want bracketed paste write the framed bytes
119
+ to stdin.
120
+
121
+ Exit codes are stable, and `--help` documents them (rendered from the same
122
+ table the code exits with, so they cannot drift):
123
+
124
+ | Exit | stdout reason | Meaning |
125
+ | ---- | ------------- | ------- |
126
+ | 0 | `delivered` | Injected by this call and journaled. |
127
+ | 0 | `already-delivered` | The token was already journaled; nothing injected. |
128
+ | 0 | `pruned <n>` | `--prune-older-than` removed `n` records. |
129
+ | 2 | `bad-usage` | Invalid/missing arguments. Nothing injected or journaled. |
130
+ | 3 | `pane-not-found` | Pane missing or dead. Not journaled; stays retryable. |
131
+ | 4 | `tmux-failed` | tmux missing / no server / a definitive tmux failure. This call never recorded a delivery. Usually the pane was never touched and the token is left unclaimed, but not always — see the note below before auto-retrying. |
132
+ | 5 | `send-interrupted` or `journal-corrupt` | A previous attempt for this token died without an answer (its process is gone). Delivery is genuinely UNKNOWN and nothing was injected now. |
133
+ | 6 | `timeout` | A tmux call exceeded `--timeout`. |
134
+ | 7 | `journal-failed` | The journal could not be read/written. Nothing injected. |
135
+ | 8 | `send-in-progress` | Another send for this token is STILL RUNNING. Nothing injected, nothing unknown — retry shortly to read that call's answer. |
136
+
137
+ **Exit 4 in detail** — a client that branches on this table to decide whether
138
+ to auto-retry (#2124) must not read `tmux-failed` as "clean slate". It
139
+ guarantees only that this call never recorded a *delivery*. It does **not**
140
+ guarantee the token is unjournaled, and in one narrow case it does not
141
+ guarantee the pane is untouched:
142
+
143
+ - Failure at the pane lookup, or while filling the paste buffer — both happen
144
+ before the journal is written at all: nothing injected, token unclaimed,
145
+ cleanly retryable. This is the ordinary case.
146
+ - A definitive `paste-buffer` failure rolls this call's claim back. For a plain
147
+ call that returns the token to absent (cleanly retryable). Under
148
+ `--resend-interrupted` the *pre-existing* unresolved record is restored
149
+ byte-for-byte rather than erased, so the token stays journaled-unresolved and
150
+ the next plain call answers exit 5, not a fresh injection.
151
+ - tmux disappearing between a successful paste and the `Enter`: the payload
152
+ **is** in the pane, the pending record is deliberately kept, and the next
153
+ plain call answers exit 5.
154
+
155
+ A plain retry after exit 4 is therefore always *safe* — it re-reads the journal
156
+ and answers exit 5 rather than duplicating — but it is not guaranteed to inject.
157
+
158
+ stdout is machine-readable: the first whitespace-delimited token is one of
159
+ the reasons above; human detail goes to stderr. Every retry path drains stdin
160
+ before exiting, so a caller piping a payload never takes SIGPIPE on a
161
+ successful acknowledgement. (Argument validation runs *before* stdin is read,
162
+ deliberately: a caller with an open-but-idle stdin gets `bad-usage`
163
+ immediately instead of blocking on a payload that will never arrive.)
164
+
165
+ **Durability invariant: at-most-once, except on an explicit opt-in.** A token
166
+ is never injected a second time unless the caller passes
167
+ `--resend-interrupted` on the injecting call itself; no sequence of failures,
168
+ kills, races or automatic housekeeping can turn an injected token back into a
169
+ state a plain call will inject. The journal under
170
+ `${XDG_STATE_HOME:-~/.local/state}/pocketshell/sends/` is two-phase — a
171
+ `pending` record is written (atomically, fsync'd) immediately before the one
172
+ command that can put bytes into the pane, then promoted to `delivered` once
173
+ tmux answers. A definitive tmux failure rolls that claim back, so ordinary
174
+ errors stay cleanly retryable; rolling back means undoing *this* call, so a
175
+ record this call created is removed and a pre-existing unresolved record it
176
+ overwrote is restored byte-for-byte rather than erased.
177
+
178
+ An unresolved record is then read against its owner process: gone ⇒ a previous
179
+ attempt died and delivery is genuinely unknown (exit 5, resolvable with
180
+ `--resend-interrupted`, so the state is never absorbing); still running ⇒
181
+ nothing is unknown and the outcome belongs to that call (exit 8, retryable).
182
+ `--resend-interrupted` does not override a live owner — there is no unknown to
183
+ resolve, and forcing one would simply duplicate the payload.
184
+
185
+ The invariant's honest edges: a definitive non-zero from `paste-buffer` is
186
+ taken as proof nothing reached the pane; the journal directory must survive
187
+ (delete it and the memory is gone); `--prune-older-than` is an operator
188
+ action that *can* clear unresolved records; and exit 8 is bounded by the owner
189
+ process's **liveness**, not by the owner's `--timeout`. A suspended owner
190
+ (reproduced with `SIGSTOP`) holds its token in `send-in-progress` for as long
191
+ as it stays stopped, because the liveness probe asks whether the process still
192
+ exists, not whether it is making progress, and `--timeout` bounds the tmux
193
+ calls of the process that passed it rather than some other process's lifetime.
194
+ This fails safe — the payload is never duplicated and the token never becomes
195
+ absorbing once the owner dies — and a client cannot reach it through its own
196
+ use, since it would have to suspend its own in-flight send. Nothing reaps a
197
+ suspended owner.
198
+
199
+ Records carry a timestamp and are pruned two ways: explicitly with
200
+ `--prune-older-than <30d|12h|90m|3600s>`, and automatically on delivery at a
201
+ **30-day default retention** (throttled to at most once every 6 h), so the
202
+ directory cannot grow without bound even if pruning is never invoked. The
203
+ automatic sweep only removes **resolved** records — ageing an unknown out of
204
+ the journal would silently make the token injectable again.
205
+
101
206
  ### `pocketshell usage`
102
207
 
103
208
  ```text
@@ -269,11 +374,29 @@ pocketshell hooks uninstall [--engine ...]
269
374
  - **OpenCode** — drops a `pocketshell-idle-signal.js` plugin into
270
375
  `~/.config/opencode/plugin/` without disturbing other plugins.
271
376
 
272
- `install` is idempotent (running twice adds nothing new). Handler scripts
273
- and the event bus live under `~/.cache/pocketshell/hooks/` (override with
274
- `$POCKETSHELL_HOOKS_DIR`); each handler appends a normalized record
275
- `{ts, engine, state, source, session_id, cwd, ...}` to
276
- `events.jsonl`.
377
+ `install` is idempotent (running twice adds nothing new). Generated handler
378
+ scripts and `.installed` ownership metadata are durable data under
379
+ `$XDG_DATA_HOME/pocketshell/hooks/` (default
380
+ `~/.local/share/pocketshell/hooks/`). The volatile event bus stays at
381
+ `$XDG_CACHE_HOME/pocketshell/hooks/events.jsonl` (default
382
+ `~/.cache/pocketshell/hooks/events.jsonl`). A routine cache cleanup therefore
383
+ starts a fresh bus without breaking the absolute commands retained by Claude or
384
+ Codex; the next event recreates the cache directory and bus.
385
+
386
+ Path overrides are intentionally separate:
387
+
388
+ - `$POCKETSHELL_HOOKS_HANDLER_DIR` overrides the durable generated-handler dir.
389
+ - `$POCKETSHELL_HOOKS_EVENTS_FILE` overrides the event bus file.
390
+ - The historical `$POCKETSHELL_HOOKS_DIR` remains an alias for the **handler
391
+ directory only** when the new handler variable is unset. It no longer moves
392
+ the bus. Use both new variables and rerun `hooks install` when both paths need
393
+ customization.
394
+
395
+ Each generated handler embeds the resolved bus path and appends a normalized
396
+ record `{ts, engine, state, source, session_id, cwd, ...}` there. `install`
397
+ also migrates PocketShell-owned Claude/Codex commands from the old cache path to
398
+ the durable path even when cache cleanup already removed the old scripts;
399
+ foreign hooks and foreign Codex `notify` programs remain untouched.
277
400
 
278
401
  **Per-engine uninstall** (`pocketshell hooks uninstall`) removes only what
279
402
  we added and is idempotent:
@@ -289,9 +412,9 @@ we added and is idempotent:
289
412
  - **OpenCode** — deletes our plugin file; other plugins and the dir
290
413
  itself are left in place.
291
414
 
292
- The event bus (`events.jsonl`) is preserved on uninstall so
293
- already-emitted records stay readable; only the generated handler
294
- scripts are cleaned up.
415
+ The event bus (`events.jsonl`) is preserved on uninstall so already-emitted
416
+ records stay readable; only PocketShell-owned current/legacy config entries,
417
+ generated executables, and durable ownership metadata are cleaned up.
295
418
 
296
419
  ## Development
297
420
 
@@ -49,6 +49,7 @@ Top-level commands in the current helper:
49
49
 
50
50
  ```text
51
51
  pocketshell usage [provider] [--json] # provider quota / usage
52
+ pocketshell send --pane %3 --token <id> # acknowledged pane delivery
52
53
  pocketshell sessions list [--by activity] # tmux session summaries
53
54
  pocketshell jobs ... # tmux recurring jobs
54
55
  pocketshell agent-log ... # agent conversation logs
@@ -66,6 +67,110 @@ flag set. Some parity subcommands still proxy through the existing host
66
67
  tools internally so their output remains byte-identical to what the app
67
68
  already parses.
68
69
 
70
+ ### `pocketshell send`
71
+
72
+ Deliver a payload into an exact tmux pane, **exactly once per token**. The
73
+ exit status IS the acknowledgement — the client no longer has to read the
74
+ terminal screen and guess whether its prompt landed (issue #2122, epic
75
+ #2121).
76
+
77
+ ```bash
78
+ printf 'summarise the diff' | pocketshell send --pane %3 --token <row-id> --enter
79
+ pocketshell send --prune-older-than 30d
80
+ ```
81
+
82
+ The payload is read from **stdin as raw bytes** and delivered byte-exact
83
+ (`load-buffer -` → `paste-buffer -d -r`, never argv). This command does not
84
+ add bracketed-paste markers: the client already frames its payload, and
85
+ framing twice put the inner markers into the receiving program as literal
86
+ text (issue #1854). Callers that want bracketed paste write the framed bytes
87
+ to stdin.
88
+
89
+ Exit codes are stable, and `--help` documents them (rendered from the same
90
+ table the code exits with, so they cannot drift):
91
+
92
+ | Exit | stdout reason | Meaning |
93
+ | ---- | ------------- | ------- |
94
+ | 0 | `delivered` | Injected by this call and journaled. |
95
+ | 0 | `already-delivered` | The token was already journaled; nothing injected. |
96
+ | 0 | `pruned <n>` | `--prune-older-than` removed `n` records. |
97
+ | 2 | `bad-usage` | Invalid/missing arguments. Nothing injected or journaled. |
98
+ | 3 | `pane-not-found` | Pane missing or dead. Not journaled; stays retryable. |
99
+ | 4 | `tmux-failed` | tmux missing / no server / a definitive tmux failure. This call never recorded a delivery. Usually the pane was never touched and the token is left unclaimed, but not always — see the note below before auto-retrying. |
100
+ | 5 | `send-interrupted` or `journal-corrupt` | A previous attempt for this token died without an answer (its process is gone). Delivery is genuinely UNKNOWN and nothing was injected now. |
101
+ | 6 | `timeout` | A tmux call exceeded `--timeout`. |
102
+ | 7 | `journal-failed` | The journal could not be read/written. Nothing injected. |
103
+ | 8 | `send-in-progress` | Another send for this token is STILL RUNNING. Nothing injected, nothing unknown — retry shortly to read that call's answer. |
104
+
105
+ **Exit 4 in detail** — a client that branches on this table to decide whether
106
+ to auto-retry (#2124) must not read `tmux-failed` as "clean slate". It
107
+ guarantees only that this call never recorded a *delivery*. It does **not**
108
+ guarantee the token is unjournaled, and in one narrow case it does not
109
+ guarantee the pane is untouched:
110
+
111
+ - Failure at the pane lookup, or while filling the paste buffer — both happen
112
+ before the journal is written at all: nothing injected, token unclaimed,
113
+ cleanly retryable. This is the ordinary case.
114
+ - A definitive `paste-buffer` failure rolls this call's claim back. For a plain
115
+ call that returns the token to absent (cleanly retryable). Under
116
+ `--resend-interrupted` the *pre-existing* unresolved record is restored
117
+ byte-for-byte rather than erased, so the token stays journaled-unresolved and
118
+ the next plain call answers exit 5, not a fresh injection.
119
+ - tmux disappearing between a successful paste and the `Enter`: the payload
120
+ **is** in the pane, the pending record is deliberately kept, and the next
121
+ plain call answers exit 5.
122
+
123
+ A plain retry after exit 4 is therefore always *safe* — it re-reads the journal
124
+ and answers exit 5 rather than duplicating — but it is not guaranteed to inject.
125
+
126
+ stdout is machine-readable: the first whitespace-delimited token is one of
127
+ the reasons above; human detail goes to stderr. Every retry path drains stdin
128
+ before exiting, so a caller piping a payload never takes SIGPIPE on a
129
+ successful acknowledgement. (Argument validation runs *before* stdin is read,
130
+ deliberately: a caller with an open-but-idle stdin gets `bad-usage`
131
+ immediately instead of blocking on a payload that will never arrive.)
132
+
133
+ **Durability invariant: at-most-once, except on an explicit opt-in.** A token
134
+ is never injected a second time unless the caller passes
135
+ `--resend-interrupted` on the injecting call itself; no sequence of failures,
136
+ kills, races or automatic housekeeping can turn an injected token back into a
137
+ state a plain call will inject. The journal under
138
+ `${XDG_STATE_HOME:-~/.local/state}/pocketshell/sends/` is two-phase — a
139
+ `pending` record is written (atomically, fsync'd) immediately before the one
140
+ command that can put bytes into the pane, then promoted to `delivered` once
141
+ tmux answers. A definitive tmux failure rolls that claim back, so ordinary
142
+ errors stay cleanly retryable; rolling back means undoing *this* call, so a
143
+ record this call created is removed and a pre-existing unresolved record it
144
+ overwrote is restored byte-for-byte rather than erased.
145
+
146
+ An unresolved record is then read against its owner process: gone ⇒ a previous
147
+ attempt died and delivery is genuinely unknown (exit 5, resolvable with
148
+ `--resend-interrupted`, so the state is never absorbing); still running ⇒
149
+ nothing is unknown and the outcome belongs to that call (exit 8, retryable).
150
+ `--resend-interrupted` does not override a live owner — there is no unknown to
151
+ resolve, and forcing one would simply duplicate the payload.
152
+
153
+ The invariant's honest edges: a definitive non-zero from `paste-buffer` is
154
+ taken as proof nothing reached the pane; the journal directory must survive
155
+ (delete it and the memory is gone); `--prune-older-than` is an operator
156
+ action that *can* clear unresolved records; and exit 8 is bounded by the owner
157
+ process's **liveness**, not by the owner's `--timeout`. A suspended owner
158
+ (reproduced with `SIGSTOP`) holds its token in `send-in-progress` for as long
159
+ as it stays stopped, because the liveness probe asks whether the process still
160
+ exists, not whether it is making progress, and `--timeout` bounds the tmux
161
+ calls of the process that passed it rather than some other process's lifetime.
162
+ This fails safe — the payload is never duplicated and the token never becomes
163
+ absorbing once the owner dies — and a client cannot reach it through its own
164
+ use, since it would have to suspend its own in-flight send. Nothing reaps a
165
+ suspended owner.
166
+
167
+ Records carry a timestamp and are pruned two ways: explicitly with
168
+ `--prune-older-than <30d|12h|90m|3600s>`, and automatically on delivery at a
169
+ **30-day default retention** (throttled to at most once every 6 h), so the
170
+ directory cannot grow without bound even if pruning is never invoked. The
171
+ automatic sweep only removes **resolved** records — ageing an unknown out of
172
+ the journal would silently make the token injectable again.
173
+
69
174
  ### `pocketshell usage`
70
175
 
71
176
  ```text
@@ -237,11 +342,29 @@ pocketshell hooks uninstall [--engine ...]
237
342
  - **OpenCode** — drops a `pocketshell-idle-signal.js` plugin into
238
343
  `~/.config/opencode/plugin/` without disturbing other plugins.
239
344
 
240
- `install` is idempotent (running twice adds nothing new). Handler scripts
241
- and the event bus live under `~/.cache/pocketshell/hooks/` (override with
242
- `$POCKETSHELL_HOOKS_DIR`); each handler appends a normalized record
243
- `{ts, engine, state, source, session_id, cwd, ...}` to
244
- `events.jsonl`.
345
+ `install` is idempotent (running twice adds nothing new). Generated handler
346
+ scripts and `.installed` ownership metadata are durable data under
347
+ `$XDG_DATA_HOME/pocketshell/hooks/` (default
348
+ `~/.local/share/pocketshell/hooks/`). The volatile event bus stays at
349
+ `$XDG_CACHE_HOME/pocketshell/hooks/events.jsonl` (default
350
+ `~/.cache/pocketshell/hooks/events.jsonl`). A routine cache cleanup therefore
351
+ starts a fresh bus without breaking the absolute commands retained by Claude or
352
+ Codex; the next event recreates the cache directory and bus.
353
+
354
+ Path overrides are intentionally separate:
355
+
356
+ - `$POCKETSHELL_HOOKS_HANDLER_DIR` overrides the durable generated-handler dir.
357
+ - `$POCKETSHELL_HOOKS_EVENTS_FILE` overrides the event bus file.
358
+ - The historical `$POCKETSHELL_HOOKS_DIR` remains an alias for the **handler
359
+ directory only** when the new handler variable is unset. It no longer moves
360
+ the bus. Use both new variables and rerun `hooks install` when both paths need
361
+ customization.
362
+
363
+ Each generated handler embeds the resolved bus path and appends a normalized
364
+ record `{ts, engine, state, source, session_id, cwd, ...}` there. `install`
365
+ also migrates PocketShell-owned Claude/Codex commands from the old cache path to
366
+ the durable path even when cache cleanup already removed the old scripts;
367
+ foreign hooks and foreign Codex `notify` programs remain untouched.
245
368
 
246
369
  **Per-engine uninstall** (`pocketshell hooks uninstall`) removes only what
247
370
  we added and is idempotent:
@@ -257,9 +380,9 @@ we added and is idempotent:
257
380
  - **OpenCode** — deletes our plugin file; other plugins and the dir
258
381
  itself are left in place.
259
382
 
260
- The event bus (`events.jsonl`) is preserved on uninstall so
261
- already-emitted records stay readable; only the generated handler
262
- scripts are cleaned up.
383
+ The event bus (`events.jsonl`) is preserved on uninstall so already-emitted
384
+ records stay readable; only PocketShell-owned current/legacy config entries,
385
+ generated executables, and durable ownership metadata are cleaned up.
263
386
 
264
387
  ## Development
265
388
 
@@ -8,7 +8,7 @@ name = "pocketshell"
8
8
  # scripts/check-pypi-version.sh enforces this; .github/workflows/build.yml
9
9
  # runs that check before publishing to PyPI. See
10
10
  # tools/pocketshell/README.md ("Release flow") for the bump procedure.
11
- version = "0.4.43"
11
+ version = "0.4.44"
12
12
  description = "Unified server-side Python utility for the PocketShell Android client."
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.11"
@@ -36,6 +36,7 @@ from pocketshell.prune_attachments import prune_attachments_command
36
36
  from pocketshell.push import push_group
37
37
  from pocketshell.qr_share import qr_share_command
38
38
  from pocketshell.repos import repos_group
39
+ from pocketshell.send import send_command
39
40
  from pocketshell.sessions import sessions_group
40
41
  from pocketshell.tree import tree_group
41
42
  from pocketshell.usage import usage_command
@@ -62,6 +63,10 @@ cli.add_command(agents_group, name="agents")
62
63
  cli.add_command(profiles_group, name="profiles")
63
64
  cli.add_command(jobs_group, name="jobs")
64
65
  cli.add_command(sessions_group, name="sessions")
66
+ # Issue #2122 (epic #2121): the acknowledged outbound delivery primitive. The
67
+ # exec's exit status IS the delivery acknowledgement, replacing the client's
68
+ # bounded terminal-observation guess.
69
+ cli.add_command(send_command, name="send")
65
70
  cli.add_command(tree_group, name="tree")
66
71
  cli.add_command(agent_log_command, name="agent-log")
67
72
  cli.add_command(repos_group, name="repos")