agentic-comms 0.9.2__tar.gz → 0.9.3__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.
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/PKG-INFO +1 -1
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agent_comms/cli.py +54 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agent_comms/hook.py +43 -22
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agent_comms/monitor.py +13 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agentic_comms.egg-info/PKG-INFO +1 -1
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/pyproject.toml +1 -1
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/README.md +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agent_comms/__init__.py +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agent_comms/__main__.py +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agent_comms/api.py +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agent_comms/config.py +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agent_comms/install.py +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agentic_comms.egg-info/SOURCES.txt +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agentic_comms.egg-info/dependency_links.txt +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agentic_comms.egg-info/entry_points.txt +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agentic_comms.egg-info/requires.txt +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/agentic_comms.egg-info/top_level.txt +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/setup.cfg +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/tests/test_cli.py +0 -0
- {agentic_comms-0.9.2 → agentic_comms-0.9.3}/tests/test_install_codex.py +0 -0
|
@@ -883,6 +883,60 @@ def _detect_control_mode(project_scope: bool = False) -> bool:
|
|
|
883
883
|
return False
|
|
884
884
|
|
|
885
885
|
|
|
886
|
+
@app.command("repair-monitor")
|
|
887
|
+
def repair_monitor():
|
|
888
|
+
"""One-shot recovery for a stuck Monitor state. Per Craig's UX report:
|
|
889
|
+
instead of 5 manual steps + guesswork to fix wrong-handle / stale-file
|
|
890
|
+
issues, run this.
|
|
891
|
+
|
|
892
|
+
Does:
|
|
893
|
+
1. Identifies THIS session's canonical handle via the resolver.
|
|
894
|
+
2. Deletes any monitor-armed-* file owned by a dead pid (kill -0 check).
|
|
895
|
+
3. Deletes any monitor-armed-* file from a sibling Claude session that
|
|
896
|
+
matches session_id mismatch (so the next hook fire stops complaining).
|
|
897
|
+
4. Prints the exact Monitor tool invocation to arm a fresh one.
|
|
898
|
+
"""
|
|
899
|
+
import os as _os
|
|
900
|
+
ident = config.resolve_identity(allow_create=False)
|
|
901
|
+
if not ident:
|
|
902
|
+
_die("no identity resolved for this session — can't repair without one. Run `comms whoami`.")
|
|
903
|
+
cache = Path(_os.environ.get("AGENT_COMMS_CACHE_DIR", str(Path.home() / ".cache" / "agent-comms")))
|
|
904
|
+
removed_dead = 0
|
|
905
|
+
removed_foreign = 0
|
|
906
|
+
kept_mine = 0
|
|
907
|
+
if cache.exists():
|
|
908
|
+
for p in cache.glob("monitor-armed-*"):
|
|
909
|
+
try:
|
|
910
|
+
data = json.loads(p.read_text())
|
|
911
|
+
except Exception:
|
|
912
|
+
p.unlink(missing_ok=True); removed_dead += 1; continue
|
|
913
|
+
pid = int(data.get("pid") or 0)
|
|
914
|
+
armed_sid = data.get("session_id")
|
|
915
|
+
# kill -0 liveness
|
|
916
|
+
alive = False
|
|
917
|
+
if pid > 0:
|
|
918
|
+
try:
|
|
919
|
+
_os.kill(pid, 0); alive = True
|
|
920
|
+
except Exception:
|
|
921
|
+
alive = False
|
|
922
|
+
if not alive:
|
|
923
|
+
p.unlink(missing_ok=True); removed_dead += 1; continue
|
|
924
|
+
# foreign-session pruning (only safe with both sides knowing session_id)
|
|
925
|
+
if ident.session_id and armed_sid and armed_sid != ident.session_id:
|
|
926
|
+
p.unlink(missing_ok=True); removed_foreign += 1; continue
|
|
927
|
+
kept_mine += 1
|
|
928
|
+
print(f"repair-monitor for handle={ident.handle} session_id={ident.session_id or '(none)'}")
|
|
929
|
+
print(f" removed_dead_files: {removed_dead}")
|
|
930
|
+
print(f" removed_foreign_files: {removed_foreign}")
|
|
931
|
+
print(f" kept_local_files: {kept_mine}")
|
|
932
|
+
print()
|
|
933
|
+
print("To arm a fresh Monitor in Claude Code, invoke the Monitor tool with:")
|
|
934
|
+
print(f' description: "Poll agent-comms inbox + operator_input every 1s"')
|
|
935
|
+
print(f" timeout_ms: 3600000")
|
|
936
|
+
print(f" persistent: true")
|
|
937
|
+
print(f" command: python3 -m agent_comms.monitor --handle {ident.handle}")
|
|
938
|
+
|
|
939
|
+
|
|
886
940
|
@app.command()
|
|
887
941
|
def doctor(
|
|
888
942
|
json_out: bool = typer.Option(False, "--json"),
|
|
@@ -413,7 +413,7 @@ def _monitor_arm_instructions(handle: str) -> str:
|
|
|
413
413
|
"Invoke the Claude Code Monitor tool with these arguments:\n\n"
|
|
414
414
|
' description: "Poll agent-comms inbox + operator_input every 1s"\n'
|
|
415
415
|
" timeout_ms: 3600000\n"
|
|
416
|
-
" persistent:
|
|
416
|
+
" persistent: true\n"
|
|
417
417
|
" command: |\n"
|
|
418
418
|
f" python3 -m agent_comms.monitor --handle {handle}\n\n"
|
|
419
419
|
f"--handle {handle} is critical: it pins the heartbeat to YOUR identity. Without "
|
|
@@ -677,19 +677,35 @@ def _build_wrong_handle_warning(armed_handle: str, armed_pid: int, current_handl
|
|
|
677
677
|
)
|
|
678
678
|
|
|
679
679
|
|
|
680
|
-
def
|
|
681
|
-
"""
|
|
682
|
-
|
|
683
|
-
|
|
680
|
+
def _pid_alive(pid: int) -> bool:
|
|
681
|
+
"""`kill -0` semantics — true if the pid exists and we can signal it.
|
|
682
|
+
Craig's (b) ask: don't trip the wrong-handle warning on a file whose pid
|
|
683
|
+
is already dead. The 60s last_beat staleness check covers the worst case,
|
|
684
|
+
but within that window a kill -9'd Monitor still has a fresh-looking file."""
|
|
685
|
+
if pid <= 0:
|
|
686
|
+
return False
|
|
687
|
+
try:
|
|
688
|
+
os.kill(pid, 0)
|
|
689
|
+
return True
|
|
690
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
691
|
+
return False
|
|
684
692
|
|
|
685
|
-
The claude_pid ancestry filter eliminates the false-positive where two
|
|
686
|
-
Claude sessions share a host: each session has its own legitimate Monitor
|
|
687
|
-
with its own (correct-for-that-session) handle. Without the filter we'd
|
|
688
|
-
flag the OTHER session's perfectly-valid Monitor as wrong.
|
|
689
693
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
694
|
+
def _check_wrong_handle_monitors(
|
|
695
|
+
current_handle: str,
|
|
696
|
+
claude_pid: int | None,
|
|
697
|
+
session_id: str | None,
|
|
698
|
+
) -> list[tuple[str, int]]:
|
|
699
|
+
"""Return monitors that belong to THIS Claude session but have the wrong
|
|
700
|
+
handle. Filters out:
|
|
701
|
+
- stale files (last_beat >60s)
|
|
702
|
+
- dead processes (kill -0 fails)
|
|
703
|
+
- sibling Claude sessions (different session_id OR different claude_pid
|
|
704
|
+
ancestry)
|
|
705
|
+
|
|
706
|
+
The session_id match is the primary filter — robust across PPID detection
|
|
707
|
+
quirks on macOS and across the (rare) PID reuse case. PPID ancestry is a
|
|
708
|
+
fallback when armed file lacks session_id (pre-0.9.3 monitors)."""
|
|
693
709
|
cache = _cache_dir()
|
|
694
710
|
if not cache.exists():
|
|
695
711
|
return []
|
|
@@ -707,15 +723,20 @@ def _check_wrong_handle_monitors(current_handle: str, claude_pid: int | None) ->
|
|
|
707
723
|
armed_pid = int(data.get("pid") or 0)
|
|
708
724
|
if not armed_handle or armed_handle == current_handle:
|
|
709
725
|
continue
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
#
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
726
|
+
if not _pid_alive(armed_pid):
|
|
727
|
+
continue
|
|
728
|
+
# Primary filter: session_id stored in the armed file (0.9.3+).
|
|
729
|
+
armed_sid = data.get("session_id")
|
|
730
|
+
if armed_sid is not None and session_id is not None:
|
|
731
|
+
if armed_sid != session_id:
|
|
732
|
+
continue # sibling Claude session, legitimate Monitor
|
|
733
|
+
else:
|
|
734
|
+
# Fallback for pre-0.9.3 armed files (no session_id): walk PPID
|
|
735
|
+
# ancestry. If our claude_pid isn't an ancestor of the armed pid,
|
|
736
|
+
# the Monitor belongs to a sibling session.
|
|
737
|
+
if claude_pid:
|
|
738
|
+
if claude_pid not in config._ppid_chain(armed_pid):
|
|
739
|
+
continue
|
|
719
740
|
wrong.append((armed_handle, armed_pid))
|
|
720
741
|
return wrong
|
|
721
742
|
|
|
@@ -877,7 +898,7 @@ def main() -> int:
|
|
|
877
898
|
# then loop forever as the operator re-armed-with-stale-handle and
|
|
878
899
|
# re-armed-with-stale-handle.
|
|
879
900
|
try:
|
|
880
|
-
wrong_monitors = _check_wrong_handle_monitors(handle, claude_pid)
|
|
901
|
+
wrong_monitors = _check_wrong_handle_monitors(handle, claude_pid, session_id)
|
|
881
902
|
for armed_handle, armed_pid in wrong_monitors:
|
|
882
903
|
early_pieces.append(_build_wrong_handle_warning(armed_handle, armed_pid, handle))
|
|
883
904
|
except Exception:
|
|
@@ -57,15 +57,28 @@ def _armed_file(pid: int) -> Path:
|
|
|
57
57
|
def _write_armed(handle: str) -> None:
|
|
58
58
|
"""Write self-identification file so the hook can detect wrong-handle races.
|
|
59
59
|
|
|
60
|
+
Includes `claude_pid` and `session_id` (from `find_claude_pid` and the
|
|
61
|
+
AGENT_COMMS_SESSION_ID env var if set) so the hook can scope its wrong-
|
|
62
|
+
handle check to ONLY monitors that belong to its own session, not sibling
|
|
63
|
+
Claude sessions sharing the host. Without this, Craig's complaint (host-
|
|
64
|
+
wide cross-talk surfacing other sessions' Monitors as "wrong for me")
|
|
65
|
+
persists even after the PPID ancestry filter, since PPID detection is
|
|
66
|
+
sometimes flaky on macOS.
|
|
67
|
+
|
|
60
68
|
Refreshed every poll iteration — `last_beat` doubles as a process-liveness
|
|
61
69
|
timestamp (stale files older than ~60s mean the Monitor died without atexit
|
|
62
70
|
cleanup, and the hook should treat as not-armed)."""
|
|
63
71
|
try:
|
|
72
|
+
from . import config as _config
|
|
73
|
+
cl_pid = _config.find_claude_pid()
|
|
74
|
+
sess = os.environ.get("AGENT_COMMS_SESSION_ID") or _config.read_active_session()
|
|
64
75
|
p = _armed_file(os.getpid())
|
|
65
76
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
66
77
|
p.write_text(json.dumps({
|
|
67
78
|
"handle": handle,
|
|
68
79
|
"pid": os.getpid(),
|
|
80
|
+
"claude_pid": cl_pid,
|
|
81
|
+
"session_id": sess,
|
|
69
82
|
"started_at": _STARTED_AT,
|
|
70
83
|
"last_beat": time.time(),
|
|
71
84
|
"version": _installed_version(),
|
|
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
|