pocketshell 0.4.22__tar.gz → 0.4.23__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.22 → pocketshell-0.4.23}/PKG-INFO +1 -1
- {pocketshell-0.4.22 → pocketshell-0.4.23}/pyproject.toml +1 -1
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/agent_log.py +49 -2
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_agent_log.py +106 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/.gitignore +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/README.md +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/scheduler/README.md +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/scheduler/pocketshell-usage-capture.service +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/scheduler/pocketshell-usage-capture.timer +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/__init__.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/__main__.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/agent_card_push.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/agents.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/agents_kind.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/cards.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/cgroup_agents.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/cli.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/daemon.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/env.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/github.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/hooks.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/jobs.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/logs.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/profiles.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/prune_attachments.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/push.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/qr_share.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/repos.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/resume.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/sessions.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/tree.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/usage.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/usage_capture.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/usage_reset.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/__init__.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_agent_card_push.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_agents.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_agents_kind.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_cards.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_cgroup_agents.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_cli.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_daemon.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_env.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_github.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_hooks.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_jobs.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_logs.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_profiles.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_prune_attachments.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_push.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_qr_share.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_repos.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_resume.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_sessions.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_tree.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_usage.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_usage_capture.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_usage_reset.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_watch_ci.py +0 -0
- {pocketshell-0.4.22 → pocketshell-0.4.23}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pocketshell
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.23
|
|
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
|
|
@@ -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.23"
|
|
12
12
|
description = "Unified server-side Python utility for the PocketShell Android client."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -66,6 +66,16 @@ import click
|
|
|
66
66
|
# daemon consumer can tell "session id is wrong" apart from
|
|
67
67
|
# "binary is missing".
|
|
68
68
|
_EXIT_LOG_NOT_FOUND = 66
|
|
69
|
+
# Issue #1225/#1267: sentinel emitted in place of a transcript line that
|
|
70
|
+
# exceeded ``--max-line-bytes``, immediately followed by the line's original
|
|
71
|
+
# UTF-8 byte length. MUST stay byte-identical to ``LINE_TRUNCATION_SENTINEL``
|
|
72
|
+
# in the Kotlin ``AgentConversationRepository`` so the app recognises it and
|
|
73
|
+
# renders a VISIBLE truncation marker instead of feeding the (now absent)
|
|
74
|
+
# oversized JSON to the parser. This is the Codex/OpenCode counterpart of the
|
|
75
|
+
# ``awk`` per-line byte clamp #1225 applied to the Claude flat-JSONL tail: one
|
|
76
|
+
# multi-megabyte rollout line (an inline base64 image, a huge ``tool_result``)
|
|
77
|
+
# is degraded server-side so its bytes never cross SSH into the phone's heap.
|
|
78
|
+
_LINE_TRUNCATION_SENTINEL = "@@PS_LINE_TRUNCATED@@"
|
|
69
79
|
_DEFAULT_HANDOFF_MAX_TURNS = 30
|
|
70
80
|
_DEFAULT_HANDOFF_MAX_CHARS = 20_000
|
|
71
81
|
_HANDOFF_PROMPT = (
|
|
@@ -285,6 +295,28 @@ def _tail(lines: Iterable[str], n: Optional[int]) -> List[str]:
|
|
|
285
295
|
return materialised[-n:]
|
|
286
296
|
|
|
287
297
|
|
|
298
|
+
def _clamp_line_bytes(lines: List[str], max_line_bytes: Optional[int]) -> List[str]:
|
|
299
|
+
"""Byte-bound each line, server-side, before it is emitted.
|
|
300
|
+
|
|
301
|
+
Any line whose UTF-8 byte length exceeds ``max_line_bytes`` is replaced by
|
|
302
|
+
``<_LINE_TRUNCATION_SENTINEL><byte-length>`` so a single multi-megabyte
|
|
303
|
+
rollout line (an inline base64 image, a huge ``tool_result``) never crosses
|
|
304
|
+
SSH into the phone's heap — the Codex/OpenCode counterpart of the Claude
|
|
305
|
+
``awk`` clamp (#1225/#1267). ``None`` or a non-positive cap disables the
|
|
306
|
+
clamp (the whole-file default); normal lines pass through verbatim.
|
|
307
|
+
"""
|
|
308
|
+
if not max_line_bytes or max_line_bytes <= 0:
|
|
309
|
+
return lines
|
|
310
|
+
clamped: List[str] = []
|
|
311
|
+
for line in lines:
|
|
312
|
+
byte_length = len(line.encode("utf-8"))
|
|
313
|
+
if byte_length > max_line_bytes:
|
|
314
|
+
clamped.append(f"{_LINE_TRUNCATION_SENTINEL}{byte_length}")
|
|
315
|
+
else:
|
|
316
|
+
clamped.append(line)
|
|
317
|
+
return clamped
|
|
318
|
+
|
|
319
|
+
|
|
288
320
|
def _parse_json_line(line: str) -> Optional[dict[str, Any]]:
|
|
289
321
|
"""Parse one JSONL row, returning ``None`` for malformed/partial rows."""
|
|
290
322
|
try:
|
|
@@ -631,6 +663,18 @@ def _emit_json(
|
|
|
631
663
|
is_flag=True,
|
|
632
664
|
help="Emit a JSON envelope (`{engine, session, path, count, lines}`) instead of raw JSONL.",
|
|
633
665
|
)
|
|
666
|
+
@click.option(
|
|
667
|
+
"--max-line-bytes",
|
|
668
|
+
"max_line_bytes",
|
|
669
|
+
type=click.IntRange(min=0),
|
|
670
|
+
default=None,
|
|
671
|
+
help=(
|
|
672
|
+
"Replace any single log line longer than N bytes with a compact "
|
|
673
|
+
"truncation marker, server-side, so one multi-megabyte line (a pasted "
|
|
674
|
+
"image / huge tool result) cannot balloon the read into the client's "
|
|
675
|
+
"heap. Default: no clamp (emit lines verbatim)."
|
|
676
|
+
),
|
|
677
|
+
)
|
|
634
678
|
@click.pass_context
|
|
635
679
|
def agent_log_command(
|
|
636
680
|
ctx: click.Context,
|
|
@@ -639,13 +683,16 @@ def agent_log_command(
|
|
|
639
683
|
cwd: Optional[str],
|
|
640
684
|
tail_count: Optional[int],
|
|
641
685
|
json_output: bool,
|
|
686
|
+
max_line_bytes: Optional[int],
|
|
642
687
|
) -> None:
|
|
643
688
|
"""Print an agent JSONL conversation log.
|
|
644
689
|
|
|
645
690
|
Reads the canonical per-engine path for the given session and emits
|
|
646
691
|
the JSONL content either as raw lines (default) or wrapped in a JSON
|
|
647
692
|
envelope (``--json``). ``--tail N`` bounds the output to the last N
|
|
648
|
-
events.
|
|
693
|
+
events. ``--max-line-bytes N`` degrades any single line longer than N
|
|
694
|
+
bytes to a compact truncation marker so one pathological line cannot
|
|
695
|
+
balloon the read (#1225/#1267).
|
|
649
696
|
|
|
650
697
|
Exit codes:
|
|
651
698
|
|
|
@@ -679,7 +726,7 @@ def agent_log_command(
|
|
|
679
726
|
)
|
|
680
727
|
ctx.exit(_EXIT_LOG_NOT_FOUND)
|
|
681
728
|
|
|
682
|
-
lines = _tail(_read_lines(path), tail_count)
|
|
729
|
+
lines = _clamp_line_bytes(_tail(_read_lines(path), tail_count), max_line_bytes)
|
|
683
730
|
if json_output:
|
|
684
731
|
_emit_json(engine_normalised, session, path, lines)
|
|
685
732
|
else:
|
|
@@ -375,6 +375,112 @@ def test_json_envelope_with_tail(fake_home: Path) -> None:
|
|
|
375
375
|
assert envelope["lines"] == [json.dumps(e, sort_keys=True) for e in events[-2:]]
|
|
376
376
|
|
|
377
377
|
|
|
378
|
+
# ----- --max-line-bytes clamp (#1225/#1267) --------------------------
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_max_line_bytes_degrades_an_oversized_line_to_a_marker(fake_home: Path) -> None:
|
|
382
|
+
"""A single multi-megabyte rollout line is replaced by
|
|
383
|
+
`@@PS_LINE_TRUNCATED@@<bytes>` server-side; normal lines pass through.
|
|
384
|
+
|
|
385
|
+
This is the Codex/OpenCode counterpart of the Claude `awk` byte clamp
|
|
386
|
+
(#1225): the oversized bytes never cross SSH into the phone's heap. The
|
|
387
|
+
Kotlin `AgentConversationRepository` recognises the marker and renders a
|
|
388
|
+
visible truncation note.
|
|
389
|
+
"""
|
|
390
|
+
session = "clamp-session"
|
|
391
|
+
huge = "A" * (5 * 1024 * 1024) # ~5 MB, far above a 256 KiB cap
|
|
392
|
+
events = [
|
|
393
|
+
{"type": "user", "message": "look at this"},
|
|
394
|
+
{"type": "assistant", "message": huge},
|
|
395
|
+
{"type": "assistant", "message": "seen"},
|
|
396
|
+
]
|
|
397
|
+
log_path = (
|
|
398
|
+
fake_home / ".codex" / "sessions" / "2026" / "07" / "03" / f"{session}.jsonl"
|
|
399
|
+
)
|
|
400
|
+
_write_jsonl(log_path, events)
|
|
401
|
+
|
|
402
|
+
runner = CliRunner()
|
|
403
|
+
result = runner.invoke(
|
|
404
|
+
agent_log_command,
|
|
405
|
+
[
|
|
406
|
+
"--engine",
|
|
407
|
+
"codex",
|
|
408
|
+
"--session",
|
|
409
|
+
session,
|
|
410
|
+
"--json",
|
|
411
|
+
"--max-line-bytes",
|
|
412
|
+
str(256 * 1024),
|
|
413
|
+
],
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
assert result.exit_code == 0, result.output
|
|
417
|
+
envelope = json.loads(result.output)
|
|
418
|
+
lines = envelope["lines"]
|
|
419
|
+
assert len(lines) == 3
|
|
420
|
+
# The oversized line is a marker naming its original byte length; no envelope
|
|
421
|
+
# line exceeds the cap now.
|
|
422
|
+
marker = lines[1]
|
|
423
|
+
assert marker.startswith("@@PS_LINE_TRUNCATED@@")
|
|
424
|
+
original_bytes = len(json.dumps(events[1], sort_keys=True).encode("utf-8"))
|
|
425
|
+
assert marker == f"@@PS_LINE_TRUNCATED@@{original_bytes}"
|
|
426
|
+
assert all(len(line.encode("utf-8")) <= 256 * 1024 for line in lines)
|
|
427
|
+
# The normal turns around the pathological line are untouched.
|
|
428
|
+
assert lines[0] == json.dumps(events[0], sort_keys=True)
|
|
429
|
+
assert lines[2] == json.dumps(events[2], sort_keys=True)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def test_max_line_bytes_leaves_a_normal_transcript_unchanged(fake_home: Path) -> None:
|
|
433
|
+
"""Counter-pin: every line under the cap passes through verbatim, no marker."""
|
|
434
|
+
session = "clamp-normal"
|
|
435
|
+
events = _sample_events("c", count=4)
|
|
436
|
+
log_path = (
|
|
437
|
+
fake_home / ".codex" / "sessions" / "2026" / "07" / "03" / f"{session}.jsonl"
|
|
438
|
+
)
|
|
439
|
+
_write_jsonl(log_path, events)
|
|
440
|
+
|
|
441
|
+
runner = CliRunner()
|
|
442
|
+
result = runner.invoke(
|
|
443
|
+
agent_log_command,
|
|
444
|
+
[
|
|
445
|
+
"--engine",
|
|
446
|
+
"codex",
|
|
447
|
+
"--session",
|
|
448
|
+
session,
|
|
449
|
+
"--json",
|
|
450
|
+
"--max-line-bytes",
|
|
451
|
+
str(256 * 1024),
|
|
452
|
+
],
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
assert result.exit_code == 0, result.output
|
|
456
|
+
envelope = json.loads(result.output)
|
|
457
|
+
assert envelope["lines"] == [json.dumps(e, sort_keys=True) for e in events]
|
|
458
|
+
assert not any("@@PS_LINE_TRUNCATED@@" in line for line in envelope["lines"])
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def test_no_max_line_bytes_keeps_the_oversized_line_verbatim(fake_home: Path) -> None:
|
|
462
|
+
"""Without the flag (the whole-file default) an oversized line is NOT clamped
|
|
463
|
+
— proving the clamp is what the flag opts into (the red state on base)."""
|
|
464
|
+
session = "clamp-off"
|
|
465
|
+
huge = "Z" * (300 * 1024)
|
|
466
|
+
events = [{"type": "assistant", "message": huge}]
|
|
467
|
+
log_path = (
|
|
468
|
+
fake_home / ".codex" / "sessions" / "2026" / "07" / "03" / f"{session}.jsonl"
|
|
469
|
+
)
|
|
470
|
+
_write_jsonl(log_path, events)
|
|
471
|
+
|
|
472
|
+
runner = CliRunner()
|
|
473
|
+
result = runner.invoke(
|
|
474
|
+
agent_log_command,
|
|
475
|
+
["--engine", "codex", "--session", session, "--json"],
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
assert result.exit_code == 0, result.output
|
|
479
|
+
envelope = json.loads(result.output)
|
|
480
|
+
assert envelope["lines"] == [json.dumps(events[0], sort_keys=True)]
|
|
481
|
+
assert "@@PS_LINE_TRUNCATED@@" not in result.output
|
|
482
|
+
|
|
483
|
+
|
|
378
484
|
# ----- handoff export ------------------------------------------------
|
|
379
485
|
|
|
380
486
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|