claude-code-tools 1.26.5__tar.gz → 1.27.0__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.
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/PKG-INFO +1 -1
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/__init__.py +1 -1
- claude_code_tools-1.27.0/claude_code_tools/amux/activity.py +260 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/amux/cli.py +52 -7
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/amux/detect.py +8 -2
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/amux/model.py +48 -1
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/amux/render.py +14 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/amux/scan.py +2 -1
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_process.py +5 -3
- claude_code_tools-1.27.0/claude_code_tools/codex_server_wait.py +59 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/pyproject.toml +2 -2
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/.gitignore +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/LICENSE +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/README.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/action_rpc.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/__init__.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/backends.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/cli.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/config.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/convert.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/locking.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/paths.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/registry.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/store.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/tmux.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/trust.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/aichat.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/amux/__init__.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/amux/__main__.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/amux/cache.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/claude_continue.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/cli_passthrough.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_app_server_rpc.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_continue.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_callback.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_cli.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_fingerprint.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_generation.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_legacy.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_models.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_reservation.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_resume.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_retry.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_reuse.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_state.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_supervisor.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_worker.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/config.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/csv2gsheet.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/delete_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/dotenv_vault.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/env_safe.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/export_all.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/export_claude_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/export_codex_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/export_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_claude_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_codex_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_original_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_trimmed_sessions.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/fix_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/gdoc2docx.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/gdoc2md.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/github_wake.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/github_watch_daemon.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/github_watch_store.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/gsheet2csv.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/helper_sessions.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/issue_reply_delivery.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/legacy_session_wrappers.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/md2gdoc.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/move_account.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/__init__.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/cli.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/hooks.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/migrations.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/models.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/prompt_detect.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/store.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/watcher.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/node_menu_ui.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_claude_noise.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_claude_to_codex.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_codex_flatten.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_codex_to_claude.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_render.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_service.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/resolve_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/resolve_session_names.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/resolve_session_render.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/search_index.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_cli_resolution.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_lineage.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_menu.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_menu_cli.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_resolution.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_selection.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_utils.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/smart_trim.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/smart_trim_core.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/tmux_cli_controller.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/tmux_execution_helpers.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/tmux_remote_controller.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/trim_in_place.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/trim_session.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/trim_session_claude.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/trim_session_codex.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_contract.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_formatting.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_manifest.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_projection.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_rendering.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_snapshots.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_store_backends.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_processes.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_runs.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_store_io.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_validation.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/agent-tunnel-spec.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/claude-code-tmux-tutorials.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/dot-zshrc.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/find-claude-session.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/intercom-spec.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/lmsh.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/local-llm-setup.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/tmux-cli-instructions.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/docs/vault-documentation.md +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/hatch_build.py +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/node_ui/action_config.js +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/node_ui/menu.js +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/node_ui/package-lock.json +0 -0
- {claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/node_ui/package.json +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Read bounded, session-scoped evidence of submitted agent input.
|
|
2
|
+
|
|
3
|
+
History timestamps are not pane activity: another pane can resume the same
|
|
4
|
+
conversation. Missing or ambiguous evidence stays unknown. No transcript mtimes,
|
|
5
|
+
launch arguments, or terminal output are used as input timestamps.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import sqlite3
|
|
15
|
+
import subprocess
|
|
16
|
+
import time
|
|
17
|
+
from collections import Counter
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .model import Agent
|
|
22
|
+
|
|
23
|
+
_HISTORY_LIMIT = 16 * 1024 * 1024
|
|
24
|
+
_UUID = r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}"
|
|
25
|
+
_ROLLOUT = re.compile(rf"rollout-.*-({_UUID})\.jsonl$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _homes(kind: str) -> list[Path]:
|
|
29
|
+
"""Discover immediate profile directories and the explicit current home."""
|
|
30
|
+
root = Path.home()
|
|
31
|
+
paths = [root / f".{kind}"]
|
|
32
|
+
try:
|
|
33
|
+
paths.extend(p for p in root.iterdir() if p.name.startswith(f".{kind}"))
|
|
34
|
+
except OSError:
|
|
35
|
+
pass
|
|
36
|
+
variable = "CLAUDE_CONFIG_DIR" if kind == "claude" else "CODEX_HOME"
|
|
37
|
+
configured = os.environ.get(variable)
|
|
38
|
+
if configured:
|
|
39
|
+
paths.append(Path(configured).expanduser())
|
|
40
|
+
result: list[Path] = []
|
|
41
|
+
for path in paths:
|
|
42
|
+
try:
|
|
43
|
+
resolved = path.resolve()
|
|
44
|
+
if path.is_dir() and resolved not in result:
|
|
45
|
+
result.append(resolved)
|
|
46
|
+
except OSError:
|
|
47
|
+
continue
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _history(home: Path, kind: str) -> tuple[dict[str, float], str]:
|
|
52
|
+
"""Read a bounded history tail, returning timestamps and read status."""
|
|
53
|
+
try:
|
|
54
|
+
with (home / "history.jsonl").open("rb") as stream:
|
|
55
|
+
size = stream.seek(0, 2)
|
|
56
|
+
start = max(0, size - _HISTORY_LIMIT)
|
|
57
|
+
stream.seek(start)
|
|
58
|
+
data = stream.read(_HISTORY_LIMIT)
|
|
59
|
+
if start:
|
|
60
|
+
data = data.partition(b"\n")[2]
|
|
61
|
+
except OSError:
|
|
62
|
+
return {}, "unavailable"
|
|
63
|
+
times: dict[str, float] = {}
|
|
64
|
+
for line in data.splitlines():
|
|
65
|
+
try:
|
|
66
|
+
record = json.loads(line)
|
|
67
|
+
except (ValueError, UnicodeDecodeError):
|
|
68
|
+
continue
|
|
69
|
+
if not isinstance(record, dict):
|
|
70
|
+
continue
|
|
71
|
+
submitted = record.get("display" if kind == "claude" else "text", "")
|
|
72
|
+
if isinstance(submitted, str) and submitted.lstrip().startswith(
|
|
73
|
+
("<hook_prompt", "<task-notification", "<subagent", "<system-reminder")
|
|
74
|
+
):
|
|
75
|
+
continue
|
|
76
|
+
sid = record.get("sessionId" if kind == "claude" else "session_id")
|
|
77
|
+
timestamp = record.get("timestamp" if kind == "claude" else "ts")
|
|
78
|
+
if not isinstance(sid, str) or not sid:
|
|
79
|
+
continue
|
|
80
|
+
if isinstance(timestamp, bool) or not isinstance(timestamp, (int, float)):
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
value = float(timestamp) / (1000 if kind == "claude" else 1)
|
|
84
|
+
except OverflowError:
|
|
85
|
+
continue
|
|
86
|
+
if math.isfinite(value) and 0 < value <= time.time():
|
|
87
|
+
times[sid] = max(times.get(sid, 0), value)
|
|
88
|
+
return times, "tail" if start else "complete"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _run(args: list[str]) -> str | None:
|
|
92
|
+
"""Run a bounded read-only subprocess; failure is distinct from no records."""
|
|
93
|
+
try:
|
|
94
|
+
result = subprocess.run(
|
|
95
|
+
args, capture_output=True, text=True, timeout=5,
|
|
96
|
+
env={**os.environ, "TZ": "UTC"},
|
|
97
|
+
)
|
|
98
|
+
except (OSError, subprocess.SubprocessError):
|
|
99
|
+
return None
|
|
100
|
+
return result.stdout if result.returncode == 0 else None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _starts(pids: set[int]) -> dict[int, str]:
|
|
104
|
+
"""Get process start identities to reject stale PID metadata."""
|
|
105
|
+
if not pids:
|
|
106
|
+
return {}
|
|
107
|
+
output = _run(["ps", "-p", ",".join(map(str, sorted(pids))), "-o", "pid=,lstart="])
|
|
108
|
+
result: dict[int, str] = {}
|
|
109
|
+
for line in (output or "").splitlines():
|
|
110
|
+
parts = line.strip().split(None, 1)
|
|
111
|
+
if len(parts) == 2 and parts[0].isdigit():
|
|
112
|
+
result[int(parts[0])] = " ".join(parts[1].split())
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _claude_record(home: Path, pid: int, start: str) -> dict[str, Any] | None:
|
|
117
|
+
"""Accept only runtime metadata matching a currently observed process."""
|
|
118
|
+
try:
|
|
119
|
+
with (home / "sessions" / f"{pid}.json").open("rb") as stream:
|
|
120
|
+
record = json.loads(stream.read(64 * 1024))
|
|
121
|
+
except (OSError, ValueError, UnicodeDecodeError):
|
|
122
|
+
return None
|
|
123
|
+
if not isinstance(record, dict):
|
|
124
|
+
return None
|
|
125
|
+
recorded_start = record.get("procStart")
|
|
126
|
+
sid = record.get("sessionId")
|
|
127
|
+
if (
|
|
128
|
+
record.get("pid") != pid
|
|
129
|
+
or not isinstance(recorded_start, str)
|
|
130
|
+
or " ".join(recorded_start.split()) != start
|
|
131
|
+
or not start
|
|
132
|
+
or not isinstance(sid, str)
|
|
133
|
+
or not sid
|
|
134
|
+
):
|
|
135
|
+
return None
|
|
136
|
+
return record
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _codex_pids(agent: Agent, tree: dict[int, list[tuple[int, str]]]) -> set[int]:
|
|
140
|
+
"""Include native Codex children of the selected launcher, bounded in size."""
|
|
141
|
+
pids = {agent.pid} if agent.pid > 0 else set()
|
|
142
|
+
queue = list(pids)
|
|
143
|
+
while queue and len(pids) < 32:
|
|
144
|
+
for pid, command in tree.get(queue.pop(0), []):
|
|
145
|
+
# Follow only the launcher/native chain, never arbitrary tool workers.
|
|
146
|
+
executable = command.split()[0] if command.split() else ""
|
|
147
|
+
if pid not in pids and Path(executable).name == "codex":
|
|
148
|
+
pids.add(pid)
|
|
149
|
+
queue.append(pid)
|
|
150
|
+
return pids
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _rollouts(pids: set[int]) -> tuple[dict[int, set[Path]], bool]:
|
|
154
|
+
"""Identify open rollout descriptors in one lsof query, without reading them."""
|
|
155
|
+
if not pids:
|
|
156
|
+
return {}, True
|
|
157
|
+
output = _run(["lsof", "-nP", "-a", "-p", ",".join(map(str, sorted(pids))), "-Fn"])
|
|
158
|
+
if output is None:
|
|
159
|
+
return {}, False
|
|
160
|
+
found: dict[int, set[Path]] = {}
|
|
161
|
+
pid = 0
|
|
162
|
+
for line in output.splitlines():
|
|
163
|
+
if line.startswith("p") and line[1:].isdigit():
|
|
164
|
+
pid = int(line[1:])
|
|
165
|
+
elif pid in pids and line.startswith("n"):
|
|
166
|
+
path = Path(line[1:])
|
|
167
|
+
if _ROLLOUT.fullmatch(path.name):
|
|
168
|
+
found.setdefault(pid, set()).add(path)
|
|
169
|
+
return found, True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _goal_status(home: Path, sid: str) -> str:
|
|
173
|
+
"""Read only the matched thread's goal status, never its objective."""
|
|
174
|
+
try:
|
|
175
|
+
uri = (home / "goals_1.sqlite").as_uri() + "?mode=ro"
|
|
176
|
+
connection = sqlite3.connect(uri, uri=True, timeout=0.1)
|
|
177
|
+
try:
|
|
178
|
+
row = connection.execute(
|
|
179
|
+
"SELECT status FROM thread_goals WHERE thread_id = ?", (sid,)
|
|
180
|
+
).fetchone()
|
|
181
|
+
finally:
|
|
182
|
+
connection.close()
|
|
183
|
+
except (sqlite3.Error, ValueError):
|
|
184
|
+
return "unknown"
|
|
185
|
+
return str(row[0]) if row else "none"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def enrich(agents: list[Agent], child_map: dict[int, list[tuple[int, str]]]) -> None:
|
|
189
|
+
"""Add last submitted-input evidence without modifying running sessions.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
agents: Live pane records to enrich in place.
|
|
193
|
+
child_map: Process snapshot containing launcher/native parentage.
|
|
194
|
+
"""
|
|
195
|
+
homes = {kind: _homes(kind) for kind in {a.kind for a in agents}}
|
|
196
|
+
histories: dict[tuple[Path, str], tuple[dict[str, float], str]] = {}
|
|
197
|
+
starts = _starts({a.pid for a in agents if a.kind == "claude" and a.pid > 0})
|
|
198
|
+
pid_sets = {
|
|
199
|
+
a.pane: _codex_pids(a, child_map) for a in agents if a.kind == "codex"
|
|
200
|
+
}
|
|
201
|
+
paths, lsof_ok = _rollouts(set().union(*pid_sets.values()) if pid_sets else set())
|
|
202
|
+
identities: list[tuple[Agent, Path, str]] = []
|
|
203
|
+
for agent in agents:
|
|
204
|
+
agent.session_id = ""
|
|
205
|
+
agent.last_input_at = None
|
|
206
|
+
agent.input_source = "unknown"
|
|
207
|
+
agent.input_scope = "unknown"
|
|
208
|
+
matches: list[tuple[Path, str]] = []
|
|
209
|
+
runtime_records: list[dict[str, Any]] = []
|
|
210
|
+
if agent.kind == "claude":
|
|
211
|
+
for home in homes["claude"]:
|
|
212
|
+
record = _claude_record(home, agent.pid, starts.get(agent.pid, ""))
|
|
213
|
+
if record:
|
|
214
|
+
matches.append((home, record["sessionId"]))
|
|
215
|
+
runtime_records.append(record)
|
|
216
|
+
method = "claude-pid"
|
|
217
|
+
else:
|
|
218
|
+
open_paths = set().union(
|
|
219
|
+
*(paths.get(p, set()) for p in pid_sets[agent.pane])
|
|
220
|
+
)
|
|
221
|
+
for path in open_paths:
|
|
222
|
+
match = _ROLLOUT.fullmatch(path.name)
|
|
223
|
+
for home in homes["codex"]:
|
|
224
|
+
if match and path.is_relative_to(home / "sessions"):
|
|
225
|
+
matches.append((home, match.group(1)))
|
|
226
|
+
method = "codex-open-rollout"
|
|
227
|
+
agent.extra["activity_probe_ok"] = lsof_ok
|
|
228
|
+
matches = list(dict.fromkeys(matches))
|
|
229
|
+
if len(matches) != 1:
|
|
230
|
+
agent.extra["input_evidence"] = "ambiguous" if matches else "unavailable"
|
|
231
|
+
continue
|
|
232
|
+
home, sid = matches[0]
|
|
233
|
+
agent.session_id = sid
|
|
234
|
+
if agent.kind == "claude":
|
|
235
|
+
record = runtime_records[0]
|
|
236
|
+
status = record.get("status", "unknown")
|
|
237
|
+
agent.extra["runtime_status"] = status
|
|
238
|
+
if isinstance(record.get("name"), str) and record["name"]:
|
|
239
|
+
agent.name = record["name"]
|
|
240
|
+
if status in ("shell", "working", "running") and agent.state == "idle":
|
|
241
|
+
agent.state = "bg"
|
|
242
|
+
else:
|
|
243
|
+
goal = _goal_status(home, sid)
|
|
244
|
+
agent.extra["goal_status"] = goal
|
|
245
|
+
if goal == "active" and agent.state == "idle":
|
|
246
|
+
agent.state = "bg"
|
|
247
|
+
agent.input_scope = "session"
|
|
248
|
+
key = (home, agent.kind)
|
|
249
|
+
if key not in histories:
|
|
250
|
+
histories[key] = _history(home, agent.kind)
|
|
251
|
+
timestamps, status = histories[key]
|
|
252
|
+
agent.last_input_at = timestamps.get(sid)
|
|
253
|
+
agent.input_source = f"{method}/history-{status}"
|
|
254
|
+
agent.extra["activity_home"] = str(home)
|
|
255
|
+
agent.extra["input_evidence"] = "found" if sid in timestamps else "unavailable"
|
|
256
|
+
identities.append((agent, home, sid))
|
|
257
|
+
counts = Counter((home, sid) for _, home, sid in identities)
|
|
258
|
+
for agent, home, sid in identities:
|
|
259
|
+
if counts[home, sid] > 1:
|
|
260
|
+
agent.input_scope = "shared-session"
|
|
@@ -24,7 +24,7 @@ from .model import Agent
|
|
|
24
24
|
|
|
25
25
|
_FZF_HEADER = (
|
|
26
26
|
"enter=jump ctrl-r=refresh esc=quit "
|
|
27
|
-
"input=asking you busy=working bg=
|
|
27
|
+
"input=asking you busy=working bg=background age=last submitted input"
|
|
28
28
|
)
|
|
29
29
|
|
|
30
30
|
|
|
@@ -44,11 +44,27 @@ def _agents_for_display(max_age: float) -> tuple[list[Agent], bool]:
|
|
|
44
44
|
return _fresh(), False
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _display(agents: list[Agent], args: argparse.Namespace) -> list[Agent]:
|
|
48
|
+
"""Refresh ages even for cached rows, then apply the requested view."""
|
|
49
|
+
for agent in agents:
|
|
50
|
+
agent.classify_inactivity(args.dormant_hours)
|
|
51
|
+
if args.dormant:
|
|
52
|
+
agents = [a for a in agents if a.inactivity == "dormant"]
|
|
53
|
+
if args.sort == "oldest":
|
|
54
|
+
agents = sorted(agents, key=lambda a: (
|
|
55
|
+
a.last_input_at is None,
|
|
56
|
+
a.last_input_at if a.last_input_at is not None else 0,
|
|
57
|
+
a.pane,
|
|
58
|
+
))
|
|
59
|
+
return agents
|
|
60
|
+
|
|
61
|
+
|
|
47
62
|
def cmd_list(args: argparse.Namespace) -> int:
|
|
48
63
|
"""Print every live agent as a table or JSON."""
|
|
49
64
|
agents = cache.read(max_age=args.max_age)[0] if args.cached else _fresh()
|
|
50
65
|
if not agents and args.cached:
|
|
51
66
|
agents = _fresh()
|
|
67
|
+
agents = _display(agents, args)
|
|
52
68
|
if args.json:
|
|
53
69
|
print(render.as_json(agents))
|
|
54
70
|
else:
|
|
@@ -67,7 +83,7 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
|
|
67
83
|
def cmd_rows(args: argparse.Namespace) -> int:
|
|
68
84
|
"""Emit picker rows (internal: used by fzf's reload binding)."""
|
|
69
85
|
agents = _fresh() if args.refresh else _agents_for_display(args.max_age)[0]
|
|
70
|
-
print(render.picker_lines(agents, colour=True))
|
|
86
|
+
print(render.picker_lines(_display(agents, args), colour=True))
|
|
71
87
|
return 0
|
|
72
88
|
|
|
73
89
|
|
|
@@ -81,18 +97,29 @@ def cmd_pick(args: argparse.Namespace) -> int:
|
|
|
81
97
|
return 1
|
|
82
98
|
|
|
83
99
|
agents, from_cache = _agents_for_display(args.max_age)
|
|
100
|
+
agents = _display(agents, args)
|
|
101
|
+
if not agents and from_cache:
|
|
102
|
+
agents = _display(_fresh(), args)
|
|
103
|
+
from_cache = False
|
|
84
104
|
if not agents:
|
|
85
|
-
print("no agents
|
|
105
|
+
print("no matching agents")
|
|
86
106
|
return 0
|
|
87
107
|
|
|
88
108
|
# fzf runs bind commands through a shell, so the interpreter path must be
|
|
89
109
|
# quoted: a venv at "/tmp/my venv/bin/python" would otherwise run "/tmp/my".
|
|
90
110
|
self_cmd = f"{shlex.quote(sys.executable)} -m claude_code_tools.amux"
|
|
111
|
+
view_flags = (
|
|
112
|
+
f" --dormant-hours {args.dormant_hours} --sort {args.sort}"
|
|
113
|
+
+ (" --dormant" if args.dormant else "")
|
|
114
|
+
)
|
|
91
115
|
binds = [
|
|
92
|
-
f"ctrl-r:reload({self_cmd} rows --refresh)",
|
|
116
|
+
f"ctrl-r:reload({self_cmd} rows --refresh{view_flags})",
|
|
93
117
|
# Opening on cached rows is what makes this instant; refresh the
|
|
94
118
|
# moment the list is up so stale states self-correct without a keypress.
|
|
95
|
-
|
|
119
|
+
(
|
|
120
|
+
f"load:reload-sync({self_cmd} rows --refresh{view_flags})"
|
|
121
|
+
if from_cache else ""
|
|
122
|
+
),
|
|
96
123
|
]
|
|
97
124
|
cmd = [
|
|
98
125
|
"fzf",
|
|
@@ -182,6 +209,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
182
209
|
rows.add_argument("--refresh", action="store_true")
|
|
183
210
|
rows.set_defaults(func=cmd_rows)
|
|
184
211
|
|
|
212
|
+
for command in (pick, lst, rows, scan_cmd):
|
|
213
|
+
command.add_argument(
|
|
214
|
+
"--max-age", type=_finite_seconds, default=argparse.SUPPRESS,
|
|
215
|
+
help="seconds a cached scan stays usable (default: 30)",
|
|
216
|
+
)
|
|
217
|
+
for command in (pick, lst, rows):
|
|
218
|
+
command.add_argument(
|
|
219
|
+
"--dormant", action="store_true",
|
|
220
|
+
help="show waiting agents with old known submitted input only",
|
|
221
|
+
)
|
|
222
|
+
command.add_argument(
|
|
223
|
+
"--dormant-hours", type=_finite_seconds, default=72.0,
|
|
224
|
+
help="input age in hours above which waiting agents are dormant (72)",
|
|
225
|
+
)
|
|
226
|
+
command.add_argument(
|
|
227
|
+
"--sort", choices=("state", "oldest"), default="state",
|
|
228
|
+
help="sort by current state or oldest known submitted input",
|
|
229
|
+
)
|
|
185
230
|
return parser
|
|
186
231
|
|
|
187
232
|
|
|
@@ -193,8 +238,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
193
238
|
# only ["pick"] -- reparsing dropped global options, so `amux --max-age 0`
|
|
194
239
|
# silently used the 30s default.
|
|
195
240
|
known = {"pick", "list", "scan", "rows"}
|
|
196
|
-
if not any(tok in known for tok in raw):
|
|
197
|
-
raw.
|
|
241
|
+
if not any(tok in known | {"-h", "--help"} for tok in raw):
|
|
242
|
+
raw.insert(0, "pick")
|
|
198
243
|
args = parser.parse_args(raw)
|
|
199
244
|
if not scan.tmux_available():
|
|
200
245
|
print("amux: no tmux server running", file=sys.stderr)
|
|
@@ -93,7 +93,9 @@ _BUSY = re.compile(
|
|
|
93
93
|
#: those can be used). Without this a pane with a working subagent but a free
|
|
94
94
|
#: main prompt reported as idle.
|
|
95
95
|
_BG = re.compile(
|
|
96
|
-
r"\b\d
|
|
96
|
+
r"\b[1-9]\d*\s+monitors?\b|"
|
|
97
|
+
r"\b[1-9]\d*\s+shells? still running\b|"
|
|
98
|
+
r"^\s*⏵⏵.*·\s*[1-9]\d*\s+shells?\b|^\s*◯\s+\S",
|
|
97
99
|
re.MULTILINE,
|
|
98
100
|
)
|
|
99
101
|
|
|
@@ -136,7 +138,11 @@ def detect_state(screen: str, kind: Kind) -> State:
|
|
|
136
138
|
return "busy"
|
|
137
139
|
if kind == "codex" and _codex_awaiting_answer(tail):
|
|
138
140
|
return "input"
|
|
139
|
-
|
|
141
|
+
background_lines = "\n".join(
|
|
142
|
+
line.rstrip() for line in screen.splitlines()
|
|
143
|
+
if not re.search(r"^\s*◯.*\bidle\s*$", line)
|
|
144
|
+
)
|
|
145
|
+
if _BG.search(background_lines):
|
|
140
146
|
return "bg"
|
|
141
147
|
return "idle"
|
|
142
148
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import math
|
|
6
|
+
import time
|
|
5
7
|
from dataclasses import asdict, dataclass, field
|
|
6
8
|
from typing import Any, Literal
|
|
7
9
|
|
|
@@ -37,6 +39,12 @@ class Agent:
|
|
|
37
39
|
model: Model string parsed from the harness footer, e.g. ``opus-5``.
|
|
38
40
|
info: One-line context lifted from the pane's footer.
|
|
39
41
|
pid: PID of the agent process itself (not the pane's shell).
|
|
42
|
+
session_id: Current conversation ID, only when reliably matched.
|
|
43
|
+
last_input_at: Last recorded submitted user input, Unix seconds.
|
|
44
|
+
input_source: Identity method and history read status, or unknown.
|
|
45
|
+
input_scope: Session-level evidence; shared-session for duplicate panes.
|
|
46
|
+
inactivity: Working, recent, dormant, or unknown input-age classification.
|
|
47
|
+
input_age_seconds: Age calculated at display time, including cached rows.
|
|
40
48
|
"""
|
|
41
49
|
|
|
42
50
|
pane: str
|
|
@@ -50,8 +58,37 @@ class Agent:
|
|
|
50
58
|
model: str = ""
|
|
51
59
|
info: str = ""
|
|
52
60
|
pid: int = 0
|
|
61
|
+
session_id: str = ""
|
|
62
|
+
last_input_at: float | None = None
|
|
63
|
+
input_source: str = "unknown"
|
|
64
|
+
input_scope: str = "unknown"
|
|
65
|
+
inactivity: str = "unknown"
|
|
66
|
+
input_age_seconds: float | None = None
|
|
53
67
|
extra: dict[str, Any] = field(default_factory=dict)
|
|
54
68
|
|
|
69
|
+
def classify_inactivity(
|
|
70
|
+
self, threshold_hours: float = 72, now: float | None = None
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Classify old submitted input independently of current activity."""
|
|
73
|
+
now = time.time() if now is None else now
|
|
74
|
+
self.input_age_seconds = (
|
|
75
|
+
max(0, now - self.last_input_at)
|
|
76
|
+
if self.last_input_at is not None else None
|
|
77
|
+
)
|
|
78
|
+
working = (
|
|
79
|
+
self.state in ("busy", "bg")
|
|
80
|
+
or self.extra.get("runtime_status") in ("shell", "working", "running")
|
|
81
|
+
or self.extra.get("goal_status") == "active"
|
|
82
|
+
)
|
|
83
|
+
if working:
|
|
84
|
+
self.inactivity = "working"
|
|
85
|
+
elif self.input_age_seconds is None:
|
|
86
|
+
self.inactivity = "unknown"
|
|
87
|
+
elif self.input_age_seconds > threshold_hours * 3600:
|
|
88
|
+
self.inactivity = "dormant"
|
|
89
|
+
else:
|
|
90
|
+
self.inactivity = "recent"
|
|
91
|
+
|
|
55
92
|
@property
|
|
56
93
|
def rank(self) -> int:
|
|
57
94
|
"""Urgency rank used for sorting (lower sorts first)."""
|
|
@@ -74,13 +111,23 @@ class Agent:
|
|
|
74
111
|
value = known.get(field_name)
|
|
75
112
|
if value is not None and not isinstance(value, str):
|
|
76
113
|
return None
|
|
77
|
-
for field_name in ("name", "cwd", "repo", "branch", "model", "info"
|
|
114
|
+
for field_name in ("name", "cwd", "repo", "branch", "model", "info",
|
|
115
|
+
"session_id", "input_source", "input_scope", "inactivity"):
|
|
78
116
|
if not isinstance(known.get(field_name, ""), str):
|
|
79
117
|
return None
|
|
80
118
|
if known.get("pane") is None or known.get("session") is None:
|
|
81
119
|
return None
|
|
82
120
|
if not isinstance(known.get("pid", 0), int):
|
|
83
121
|
return None
|
|
122
|
+
for key in ("last_input_at", "input_age_seconds"):
|
|
123
|
+
value = known.get(key)
|
|
124
|
+
if value is not None and (
|
|
125
|
+
isinstance(value, bool) or not isinstance(value, (int, float))
|
|
126
|
+
or not math.isfinite(value) or value < 0
|
|
127
|
+
):
|
|
128
|
+
return None
|
|
129
|
+
if not isinstance(known.get("extra", {}), dict):
|
|
130
|
+
return None
|
|
84
131
|
# Literal[...] is a type-checker hint only; a hand-edited cache can
|
|
85
132
|
# still carry kind="vim" or state="waiting" and render as an agent.
|
|
86
133
|
if known.get("kind") not in ("claude", "codex"):
|
|
@@ -29,6 +29,18 @@ def _colour(state: str, text: str, colour: bool) -> str:
|
|
|
29
29
|
return f"\033[{STATE_COLOR.get(state, '0')}m{text}\033[0m"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def input_age(agent: Agent) -> str:
|
|
33
|
+
"""Compact age of the last submitted input, never terminal output age."""
|
|
34
|
+
age = agent.input_age_seconds
|
|
35
|
+
if age is None:
|
|
36
|
+
return "unknown"
|
|
37
|
+
if age >= 86400:
|
|
38
|
+
return f"{age / 86400:.1f}d"
|
|
39
|
+
if age >= 3600:
|
|
40
|
+
return f"{age / 3600:.1f}h"
|
|
41
|
+
return f"{int(age // 60)}m"
|
|
42
|
+
|
|
43
|
+
|
|
32
44
|
def picker_row(agent: Agent, colour: bool = True) -> str:
|
|
33
45
|
"""The visible part of an fzf row (no key field); see :func:`picker_lines`."""
|
|
34
46
|
state = _colour(agent.state, f"{agent.state:<{_W_STATE}}", colour)
|
|
@@ -36,6 +48,7 @@ def picker_row(agent: Agent, colour: bool = True) -> str:
|
|
|
36
48
|
where = _clip(f"{agent.repo}{branch}", _W_REPO)
|
|
37
49
|
return (
|
|
38
50
|
f"{_clip(agent.pane, _W_PANE):<{_W_PANE}} {state} {agent.kind:<{_W_KIND}} "
|
|
51
|
+
f"{input_age(agent):>9} {agent.inactivity:<8} "
|
|
39
52
|
f"{_clip(agent.name or '-', _W_NAME):<{_W_NAME}} "
|
|
40
53
|
f"{where:<{_W_REPO}} {agent.info}"
|
|
41
54
|
)
|
|
@@ -74,6 +87,7 @@ def table(agents: list[Agent], colour: bool = True) -> str:
|
|
|
74
87
|
|
|
75
88
|
header = (
|
|
76
89
|
f"{'PANE':<{_W_PANE}} {'STATE':<{_W_STATE}} {'KIND':<{_W_KIND}} "
|
|
90
|
+
f"{'INPUT AGE':>9} {'ACTIVITY':<8} "
|
|
77
91
|
f"{'NAME':<{_W_NAME}} {'REPO@BRANCH':<{_W_REPO}} CONTEXT"
|
|
78
92
|
)
|
|
79
93
|
rule = "─" * min(len(header) + 20, 120)
|
|
@@ -19,7 +19,7 @@ import subprocess
|
|
|
19
19
|
from concurrent.futures import ThreadPoolExecutor
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
|
-
from . import detect
|
|
22
|
+
from . import activity, detect
|
|
23
23
|
from .model import Agent
|
|
24
24
|
|
|
25
25
|
#: Field separator for tmux -F output; chosen to not occur in paths or titles.
|
|
@@ -238,6 +238,7 @@ def scan(workers: int = 16) -> list[Agent]:
|
|
|
238
238
|
)
|
|
239
239
|
)
|
|
240
240
|
|
|
241
|
+
activity.enrich(agents, child_map)
|
|
241
242
|
agents.sort(key=lambda a: (a.rank, a.pane))
|
|
242
243
|
return agents
|
|
243
244
|
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_process.py
RENAMED
|
@@ -33,6 +33,9 @@ from claude_code_tools.codex_server_models import (
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
from claude_code_tools.codex_server_wait import child_exited_without_reaping
|
|
37
|
+
|
|
38
|
+
|
|
36
39
|
DIAGNOSTIC_OUTPUT_MAX_BYTES = 64 * 1024
|
|
37
40
|
_LINUX_BOOT_ID = "/proc/sys/kernel/random/boot_id"
|
|
38
41
|
_PROC_PIDTBSDINFO = 3
|
|
@@ -203,16 +206,15 @@ def _wait_process_exit_without_reaping(
|
|
|
203
206
|
deadline: float,
|
|
204
207
|
) -> bool:
|
|
205
208
|
"""Observe child exit while preserving its PID through group cleanup."""
|
|
206
|
-
wait_flags = os.WEXITED | os.WNOHANG | os.WNOWAIT
|
|
207
209
|
while True:
|
|
208
210
|
try:
|
|
209
|
-
|
|
211
|
+
exited = child_exited_without_reaping(process.pid)
|
|
210
212
|
except ChildProcessError:
|
|
211
213
|
process.poll()
|
|
212
214
|
return False
|
|
213
215
|
except InterruptedError:
|
|
214
216
|
continue
|
|
215
|
-
if
|
|
217
|
+
if exited:
|
|
216
218
|
return True
|
|
217
219
|
remaining = deadline - time.monotonic()
|
|
218
220
|
if remaining <= 0:
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Non-reaping child-exit observation across supported POSIX platforms."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _DarwinSiginfo(ctypes.Structure):
|
|
11
|
+
"""Darwin siginfo_t from sys/signal.h (including reserved ABI fields)."""
|
|
12
|
+
|
|
13
|
+
_fields_ = [
|
|
14
|
+
("si_signo", ctypes.c_int),
|
|
15
|
+
("si_errno", ctypes.c_int),
|
|
16
|
+
("si_code", ctypes.c_int),
|
|
17
|
+
("si_pid", ctypes.c_int),
|
|
18
|
+
("si_uid", ctypes.c_uint),
|
|
19
|
+
("si_status", ctypes.c_int),
|
|
20
|
+
("si_addr", ctypes.c_void_p),
|
|
21
|
+
("si_value", ctypes.c_void_p),
|
|
22
|
+
("si_band", ctypes.c_long),
|
|
23
|
+
("padding", ctypes.c_ulong * 7),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def child_exited_without_reaping(pid: int) -> bool:
|
|
28
|
+
"""Observe an exited child while keeping its PID reserved for cleanup.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
pid: PID of a direct child that this caller has not reaped.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Whether the child has exited. A running child returns False.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
OSError: The OS cannot observe this child, including ECHILD or EINTR.
|
|
38
|
+
NotImplementedError: No supported non-reaping API is available.
|
|
39
|
+
"""
|
|
40
|
+
flags = os.WEXITED | os.WNOHANG | os.WNOWAIT
|
|
41
|
+
waitid = getattr(os, "waitid", None)
|
|
42
|
+
if waitid is not None:
|
|
43
|
+
return waitid(os.P_PID, pid, flags) is not None
|
|
44
|
+
if sys.platform != "darwin":
|
|
45
|
+
raise NotImplementedError("non-reaping child observation needs waitid")
|
|
46
|
+
|
|
47
|
+
# Some macOS Python builds omit os.waitid although libSystem provides it.
|
|
48
|
+
# P_PID is 1 in Darwin's sys/wait.h; Python may omit that constant too.
|
|
49
|
+
library = ctypes.CDLL(None, use_errno=True)
|
|
50
|
+
native_waitid = library.waitid
|
|
51
|
+
native_waitid.argtypes = [
|
|
52
|
+
ctypes.c_int, ctypes.c_uint, ctypes.POINTER(_DarwinSiginfo), ctypes.c_int
|
|
53
|
+
]
|
|
54
|
+
native_waitid.restype = ctypes.c_int
|
|
55
|
+
info = _DarwinSiginfo()
|
|
56
|
+
if native_waitid(1, pid, ctypes.byref(info), flags) == -1:
|
|
57
|
+
error = ctypes.get_errno()
|
|
58
|
+
raise OSError(error, os.strerror(error))
|
|
59
|
+
return info.si_pid == pid
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-code-tools"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.27.0"
|
|
4
4
|
description = "Collection of tools for working with Claude Code"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -113,7 +113,7 @@ markers = [
|
|
|
113
113
|
|
|
114
114
|
[tool.commitizen]
|
|
115
115
|
name = "cz_conventional_commits"
|
|
116
|
-
version = "1.
|
|
116
|
+
version = "1.27.0"
|
|
117
117
|
tag_format = "v$version"
|
|
118
118
|
version_files = [
|
|
119
119
|
"pyproject.toml:version",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/__init__.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/backends.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/config.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/convert.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/discord_bot.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/locking.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/paths.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/registry.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/session.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/store.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/tmux.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/agent_tunnel/trust.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_app_server_rpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_callback.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_fingerprint.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_generation.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_legacy.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_models.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_reservation.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_resume.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_retry.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_reuse.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_state.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_supervisor.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/codex_server_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/export_claude_session.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/export_codex_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_claude_session.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_codex_session.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_original_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/find_trimmed_sessions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/github_watch_daemon.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/github_watch_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/issue_reply_delivery.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/legacy_session_wrappers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/msg/prompt_detect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_claude_noise.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_claude_to_codex.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_codex_flatten.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/port_codex_to_claude.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/resolve_session_names.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/resolve_session_render.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_cli_resolution.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_resolution.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/session_selection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/tmux_cli_controller.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/tmux_execution_helpers.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/tmux_remote_controller.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/trim_session_claude.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/trim_session_codex.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_contract.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_formatting.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_manifest.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_projection.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_rendering.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_cli_snapshots.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_processes.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_store_io.py
RENAMED
|
File without changes
|
{claude_code_tools-1.26.5 → claude_code_tools-1.27.0}/claude_code_tools/workflow_validation.py
RENAMED
|
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
|