agentic-comms 0.9.2__tar.gz → 0.9.4__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 (20) hide show
  1. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/PKG-INFO +1 -1
  2. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agent_comms/cli.py +70 -7
  3. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agent_comms/hook.py +43 -22
  4. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agent_comms/monitor.py +13 -0
  5. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agentic_comms.egg-info/PKG-INFO +1 -1
  6. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/pyproject.toml +1 -1
  7. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/README.md +0 -0
  8. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agent_comms/__init__.py +0 -0
  9. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agent_comms/__main__.py +0 -0
  10. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agent_comms/api.py +0 -0
  11. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agent_comms/config.py +0 -0
  12. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agent_comms/install.py +0 -0
  13. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agentic_comms.egg-info/SOURCES.txt +0 -0
  14. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agentic_comms.egg-info/dependency_links.txt +0 -0
  15. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agentic_comms.egg-info/entry_points.txt +0 -0
  16. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agentic_comms.egg-info/requires.txt +0 -0
  17. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/agentic_comms.egg-info/top_level.txt +0 -0
  18. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/setup.cfg +0 -0
  19. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/tests/test_cli.py +0 -0
  20. {agentic_comms-0.9.2 → agentic_comms-0.9.4}/tests/test_install_codex.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-comms
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: CLI message board for AI agents — coordinate between sessions, projects, and machines
5
5
  Author: jazcogames
6
6
  License: MIT
@@ -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"),
@@ -991,7 +1045,9 @@ def doctor(
991
1045
  except Exception as e:
992
1046
  report["monitor"] = {"error": str(e)[:200]}
993
1047
 
994
- # Wrong-handle Monitor detection (local files)
1048
+ # Wrong-handle Monitor detection (local files).
1049
+ # Includes kill -0 liveness check (Phil's 0.9.3 follow-up) so ghost armed-files
1050
+ # from killed-but-not-cleaned-up monitors don't appear in the listing.
995
1051
  try:
996
1052
  cache = Path(os.environ.get("AGENT_COMMS_CACHE_DIR", str(Path.home() / ".cache" / "agent-comms")))
997
1053
  live_monitors = []
@@ -1004,12 +1060,19 @@ def doctor(
1004
1060
  except Exception:
1005
1061
  continue
1006
1062
  last_beat = float(data.get("last_beat") or 0)
1007
- if now - last_beat <= 60:
1008
- live_monitors.append({
1009
- "pid": data.get("pid"), "handle": data.get("handle"),
1010
- "version": data.get("version"),
1011
- "last_beat_age_seconds": int(now - last_beat),
1012
- })
1063
+ if now - last_beat > 60:
1064
+ continue
1065
+ pid = int(data.get("pid") or 0)
1066
+ if pid > 0:
1067
+ try:
1068
+ os.kill(pid, 0)
1069
+ except (ProcessLookupError, PermissionError, OSError):
1070
+ continue
1071
+ live_monitors.append({
1072
+ "pid": data.get("pid"), "handle": data.get("handle"),
1073
+ "version": data.get("version"),
1074
+ "last_beat_age_seconds": int(now - last_beat),
1075
+ })
1013
1076
  report["live_monitors"] = live_monitors
1014
1077
  if canonical:
1015
1078
  wrong = [m for m in live_monitors if m.get("handle") != canonical.handle]
@@ -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: false\n"
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 _check_wrong_handle_monitors(current_handle: str, claude_pid: int | None) -> list[tuple[str, int]]:
681
- """Scan ~/.cache/agent-comms/monitor-armed-* for any live Monitor (last_beat
682
- within 60s) whose handle != current_handle AND whose process is a child of
683
- THIS Claude session.
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
- A file present but stale (>60s since last_beat) means the Monitor crashed
691
- without atexit cleanup — ignored here, the regular `monitor_not_armed`
692
- warning path handles that case."""
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
- # Ancestry filter: walk armed_pid's PPID chain. Only flag as "wrong"
711
- # if our claude_pid appears in that ancestry — meaning this Monitor
712
- # was spawned by THIS Claude session but somehow got the wrong handle.
713
- # If claude_pid isn't found, the Monitor belongs to a sibling Claude
714
- # session (legitimate, leave it alone).
715
- if claude_pid and armed_pid:
716
- chain = config._ppid_chain(armed_pid)
717
- if claude_pid not in chain:
718
- continue
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(),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-comms
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: CLI message board for AI agents — coordinate between sessions, projects, and machines
5
5
  Author: jazcogames
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentic-comms"
3
- version = "0.9.2"
3
+ version = "0.9.4"
4
4
  description = "CLI message board for AI agents — coordinate between sessions, projects, and machines"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
File without changes
File without changes