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.
- {pocketshell-0.4.43 → pocketshell-0.4.44}/PKG-INFO +133 -10
- {pocketshell-0.4.43 → pocketshell-0.4.44}/README.md +131 -8
- {pocketshell-0.4.43 → pocketshell-0.4.44}/pyproject.toml +1 -1
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/cli.py +5 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/hooks.py +353 -95
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/logs.py +17 -8
- pocketshell-0.4.44/src/pocketshell/send.py +1351 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_hooks.py +224 -2
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_logs.py +20 -0
- pocketshell-0.4.44/tests/test_send.py +2092 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_watch_ci.py +120 -1
- {pocketshell-0.4.43 → pocketshell-0.4.44}/.gitignore +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/scheduler/README.md +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/scheduler/pocketshell-usage-capture.service +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/scheduler/pocketshell-usage-capture.timer +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/__init__.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/__main__.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/agent_card_push.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/agent_log.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/agents.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/agents_kind.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/cards.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/cgroup_agents.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/daemon.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/env.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/github.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/jobs.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/profiles.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/prune_attachments.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/push.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/qr_share.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/repos.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/resume.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/sessions.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/tree.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/usage.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/usage_capture.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/src/pocketshell/usage_reset.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/__init__.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/data/quse-0.0.11-usage.json +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/data/quse-0.0.9-usage.json +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_agent_card_push.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_agent_log.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_agents.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_agents_kind.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_cards.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_cards_push_notify.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_cgroup_agents.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_cli.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_daemon.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_env.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_github.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_jobs.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_profiles.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_prune_attachments.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_push.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_qr_share.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_repos.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_resume.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_sessions.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_tree.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_usage.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_usage_capture.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_usage_reset.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/tests/test_watch_ci_cancelled.py +0 -0
- {pocketshell-0.4.43 → pocketshell-0.4.44}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pocketshell
|
|
3
|
-
Version: 0.4.
|
|
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).
|
|
273
|
-
and
|
|
274
|
-
`$
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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).
|
|
241
|
-
and
|
|
242
|
-
`$
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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.
|
|
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")
|