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.
Files changed (60) hide show
  1. {pocketshell-0.4.22 → pocketshell-0.4.23}/PKG-INFO +1 -1
  2. {pocketshell-0.4.22 → pocketshell-0.4.23}/pyproject.toml +1 -1
  3. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/agent_log.py +49 -2
  4. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_agent_log.py +106 -0
  5. {pocketshell-0.4.22 → pocketshell-0.4.23}/.gitignore +0 -0
  6. {pocketshell-0.4.22 → pocketshell-0.4.23}/README.md +0 -0
  7. {pocketshell-0.4.22 → pocketshell-0.4.23}/scheduler/README.md +0 -0
  8. {pocketshell-0.4.22 → pocketshell-0.4.23}/scheduler/pocketshell-usage-capture.service +0 -0
  9. {pocketshell-0.4.22 → pocketshell-0.4.23}/scheduler/pocketshell-usage-capture.timer +0 -0
  10. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/__init__.py +0 -0
  11. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/__main__.py +0 -0
  12. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/agent_card_push.py +0 -0
  13. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/agents.py +0 -0
  14. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/agents_kind.py +0 -0
  15. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/cards.py +0 -0
  16. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/cgroup_agents.py +0 -0
  17. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/cli.py +0 -0
  18. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/daemon.py +0 -0
  19. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/env.py +0 -0
  20. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/github.py +0 -0
  21. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/hooks.py +0 -0
  22. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/jobs.py +0 -0
  23. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/logs.py +0 -0
  24. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/profiles.py +0 -0
  25. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/prune_attachments.py +0 -0
  26. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/push.py +0 -0
  27. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/qr_share.py +0 -0
  28. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/repos.py +0 -0
  29. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/resume.py +0 -0
  30. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/sessions.py +0 -0
  31. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/tree.py +0 -0
  32. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/usage.py +0 -0
  33. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/usage_capture.py +0 -0
  34. {pocketshell-0.4.22 → pocketshell-0.4.23}/src/pocketshell/usage_reset.py +0 -0
  35. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/__init__.py +0 -0
  36. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_agent_card_push.py +0 -0
  37. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_agents.py +0 -0
  38. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_agents_kind.py +0 -0
  39. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_cards.py +0 -0
  40. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_cgroup_agents.py +0 -0
  41. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_cli.py +0 -0
  42. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_daemon.py +0 -0
  43. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_env.py +0 -0
  44. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_github.py +0 -0
  45. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_hooks.py +0 -0
  46. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_jobs.py +0 -0
  47. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_logs.py +0 -0
  48. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_profiles.py +0 -0
  49. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_prune_attachments.py +0 -0
  50. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_push.py +0 -0
  51. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_qr_share.py +0 -0
  52. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_repos.py +0 -0
  53. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_resume.py +0 -0
  54. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_sessions.py +0 -0
  55. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_tree.py +0 -0
  56. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_usage.py +0 -0
  57. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_usage_capture.py +0 -0
  58. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_usage_reset.py +0 -0
  59. {pocketshell-0.4.22 → pocketshell-0.4.23}/tests/test_watch_ci.py +0 -0
  60. {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.22
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.22"
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