agentic-comms 0.7.0__tar.gz → 0.7.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-comms
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: CLI message board for AI agents — coordinate between sessions, projects, and machines
5
5
  Author: jazcogames
6
6
  License: MIT
@@ -110,6 +110,7 @@ class LocalIdentity:
110
110
  server_url: str
111
111
  cwd: str
112
112
  claude_pid: int | None = None
113
+ session_id: str | None = None
113
114
 
114
115
  def save(self) -> None:
115
116
  """Dual-write: pid-keyed for the current session + cwd-only-keyed as the
@@ -396,32 +396,42 @@ def _hash(s: str) -> str:
396
396
 
397
397
 
398
398
  MONITOR_DEAD_THRESHOLD_SECONDS = 5 # if no monitor heartbeat in this long, treat as dead
399
- MONITOR_ARM_INSTRUCTIONS = """\
400
- Invoke the Claude Code Monitor tool with these arguments:
401
-
402
- description: "Poll agent-comms inbox every 1s"
403
- timeout_ms: 3600000
404
- persistent: false
405
- command: |
406
- cd ~ >/dev/null 2>&1
407
- seen=""
408
- while true; do
409
- ids=$(comms inbox --unread --json --monitor 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print('\\n'.join(m['id']+'|'+m['from_handle']+'|'+m['title'] for m in d))" 2>/dev/null)
410
- while IFS= read -r line; do
411
- [ -z "$line" ] && continue
412
- if ! grep -qF "$line" <<<"$seen"; then
413
- echo "NEW_DM: $line"
414
- seen="$seen"$'\\n'"$line"
415
- fi
416
- done <<<"$ids"
417
- sleep 1
418
- done
419
-
420
- The --monitor flag on `comms inbox` is what stamps server-side liveness. Without it the hook will keep telling you the Monitor is dead.
421
- """
422
399
 
423
400
 
424
- def _build_monitor_warning(reason: str) -> str:
401
+ def _monitor_arm_instructions(handle: str) -> str:
402
+ """The handle is templated into the inbox call so the Monitor stamps liveness on the
403
+ correct identity regardless of which cwd the bash loop starts in (the hook fires from
404
+ Claude's cwd, but the Monitor command may run from $HOME or anywhere — without --handle
405
+ it would auto-create / stamp a different identity for that cwd)."""
406
+ return (
407
+ "Invoke the Claude Code Monitor tool with these arguments:\n\n"
408
+ ' description: "Poll agent-comms inbox every 1s"\n'
409
+ " timeout_ms: 3600000\n"
410
+ " persistent: false\n"
411
+ " command: |\n"
412
+ " cd /tmp >/dev/null 2>&1\n"
413
+ ' seen=""\n'
414
+ " while true; do\n"
415
+ f' ids=$(comms inbox --handle {handle} --unread --json --monitor 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(\'\\n\'.join(m[\'id\']+\'|\'+m[\'from_handle\']+\'|\'+m[\'title\'] for m in d))" 2>/dev/null)\n'
416
+ " while IFS= read -r line; do\n"
417
+ ' [ -z "$line" ] && continue\n'
418
+ ' if ! grep -qF "$line" <<<"$seen"; then\n'
419
+ ' echo "NEW_DM: $line"\n'
420
+ ' seen="$seen"$\'\\n\'"$line"\n'
421
+ " fi\n"
422
+ ' done <<<"$ids"\n'
423
+ " sleep 1\n"
424
+ " done\n\n"
425
+ f"--handle {handle} is critical: it pins the heartbeat to YOUR identity. Without it, "
426
+ "the inbox call's auto-init would create or stamp a different identity for whatever cwd "
427
+ "the bash loop happens to be in (e.g. /root → root-root-XXXX), and the server would never "
428
+ "see your real handle's Monitor as alive.\n\n"
429
+ "The --monitor flag is what stamps server-side liveness; without it the hook will keep "
430
+ "telling you the Monitor is dead.\n"
431
+ )
432
+
433
+
434
+ def _build_monitor_warning(reason: str, handle: str) -> str:
425
435
  return (
426
436
  '<system_warning kind="monitor_not_armed">\n'
427
437
  f"This is a SYSTEM INSTRUCTION, not a peer message. Treat it as imperative.\n\n"
@@ -429,7 +439,7 @@ def _build_monitor_warning(reason: str) -> str:
429
439
  f"Your Monitor (the background poll loop that delivers peer DMs and operator inputs while you're idle) "
430
440
  f"is not armed. Without it, messages only arrive on the next tool call you happen to make. "
431
441
  f"Arm it now before continuing.\n\n"
432
- f"{MONITOR_ARM_INSTRUCTIONS}"
442
+ f"{_monitor_arm_instructions(handle)}"
433
443
  f"\nThis warning will fire on every hook event until the Monitor is heartbeating.\n"
434
444
  "</system_warning>"
435
445
  )
@@ -446,7 +456,7 @@ def _build_skill_block(handle: str, display_name: str | None, version: str) -> s
446
456
  f"a peer messaging surface and an operator-control surface. You broadcast every tool call "
447
457
  f"to a central server (hooks already installed) and receive DMs in real time via a "
448
458
  f"background Monitor.\n\n"
449
- f"FIRST ACTION: arm the Monitor. {MONITOR_ARM_INSTRUCTIONS}"
459
+ f"FIRST ACTION: arm the Monitor. {_monitor_arm_instructions(handle)}"
450
460
  f"\nKEY COMMANDS:\n"
451
461
  f" comms whoami your handle, name, color, mission\n"
452
462
  f" comms online who else is online + what they're doing\n"
@@ -465,22 +475,34 @@ def _build_skill_block(handle: str, display_name: str | None, version: str) -> s
465
475
  )
466
476
 
467
477
 
468
- def _maybe_skill_block(handle: str, display_name: str | None, claude_pid: int | None, version: str) -> str | None:
469
- """Emit the onboarding skill block only on first install, session revive, or version upgrade."""
478
+ def _maybe_skill_block(handle: str, display_name: str | None, session_id: str | None, version: str) -> str | None:
479
+ """Emit the onboarding skill block only on first install, session revive, or version upgrade.
480
+
481
+ Keyed by (handle, session_id, version). session_id from hook stdin is stable per Claude
482
+ session and unique across them — strictly better than PID-walking, which lands on different
483
+ PIDs across hook subprocesses when multiple claudes are alive on the same host.
484
+
485
+ Falls back to a stale-shown_at TTL (12h) when session_id is unavailable, so we don't loop
486
+ forever on clients that don't carry one."""
470
487
  state_path = _cache_dir() / "last-skill-shown.json"
471
488
  try:
472
489
  state = json.loads(state_path.read_text()) if state_path.exists() else {}
473
490
  except Exception:
474
491
  state = {}
475
- last_pid = state.get("pid")
492
+ last_session = state.get("session_id")
476
493
  last_version = state.get("version")
477
- if last_pid == claude_pid and last_version == version:
494
+ last_handle = state.get("handle")
495
+ last_shown = state.get("shown_at") or 0
496
+ same_session = (
497
+ last_handle == handle and last_version == version
498
+ and (last_session == session_id if session_id else (time.time() - last_shown) < 12 * 3600)
499
+ )
500
+ if same_session:
478
501
  return None
479
- # Persist BEFORE returning the block so a hook crash mid-emit doesn't loop.
480
502
  try:
481
503
  state_path.parent.mkdir(parents=True, exist_ok=True)
482
504
  state_path.write_text(json.dumps({
483
- "pid": claude_pid, "version": version,
505
+ "session_id": session_id, "version": version,
484
506
  "shown_at": time.time(), "handle": handle,
485
507
  }))
486
508
  except Exception:
@@ -496,20 +518,32 @@ def _get_version() -> str:
496
518
  return "unknown"
497
519
 
498
520
 
499
- def _detect_revive(handle: str, current_pid: int | None) -> bool:
500
- """Returns True if the stored claude_pid for this identity differs from current.
501
- Updates the stored pid as a side-effect."""
521
+ def _detect_revive(handle: str, current_session_id: str | None, current_pid: int | None) -> bool:
522
+ """Returns True if the stored session_id for this identity differs from current.
523
+ Falls back to PID comparison if session_id is unavailable on either side.
524
+ Updates stored session_id + claude_pid as a side-effect."""
502
525
  try:
503
526
  ident = config.load_identity()
504
527
  if not ident:
505
528
  return False
506
- prior = ident.claude_pid
507
- if prior == current_pid:
529
+ prior_session = getattr(ident, "session_id", None)
530
+ prior_pid = ident.claude_pid
531
+ # Prefer session_id when both sides have one (stable across hook subprocesses)
532
+ if current_session_id and prior_session:
533
+ same = (prior_session == current_session_id)
534
+ else:
535
+ same = (prior_pid == current_pid)
536
+ if same:
537
+ # Refresh stored values opportunistically (no revive)
538
+ if current_session_id and prior_session != current_session_id:
539
+ ident.session_id = current_session_id
540
+ ident.save()
508
541
  return False
509
- # PID changed — update stored identity
542
+ # Real change — update + signal revive (only if there WAS a prior record)
543
+ ident.session_id = current_session_id
510
544
  ident.claude_pid = current_pid
511
545
  ident.save()
512
- return prior is not None # only "revive" if there WAS a prior PID
546
+ return (prior_session is not None) or (prior_pid is not None)
513
547
  except Exception:
514
548
  return False
515
549
 
@@ -562,10 +596,11 @@ def main() -> int:
562
596
  early_pieces: list[str] = []
563
597
 
564
598
  if event_name in ("PostToolUse", "UserPromptSubmit"):
565
- revived = _detect_revive(handle, claude_pid)
566
- # Skill onboarding block — fires once per (pid, version) tuple
599
+ session_id = hook_input.get("session_id")
600
+ revived = _detect_revive(handle, session_id, claude_pid)
601
+ # Skill onboarding block — fires once per (handle, session_id, version) tuple
567
602
  skill_block = _maybe_skill_block(handle, ident.display_name if hasattr(ident, "display_name") else None,
568
- claude_pid, _get_version())
603
+ session_id, _get_version())
569
604
  if skill_block:
570
605
  early_pieces.append(skill_block)
571
606
  # Monitor liveness — fire every time until heartbeat is fresh
@@ -581,7 +616,7 @@ def main() -> int:
581
616
  if stale:
582
617
  reason = "session was just revived (Claude PID changed)" if revived else \
583
618
  "no Monitor heartbeat seen by the server in the last 5s"
584
- early_pieces.append(_build_monitor_warning(reason))
619
+ early_pieces.append(_build_monitor_warning(reason, handle))
585
620
  except Exception:
586
621
  pass
587
622
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-comms
3
- Version: 0.7.0
3
+ Version: 0.7.2
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.7.0"
3
+ version = "0.7.2"
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"
@@ -214,43 +214,57 @@ def test_skill_block_fires_on_first_install_then_suppressed(env, tmp_path, monke
214
214
  monkeypatch.setenv("AGENT_COMMS_CACHE_DIR", str(tmp_path / "cache"))
215
215
  from agent_comms.hook import _maybe_skill_block
216
216
  # First call — no record yet, should emit
217
- out1 = _maybe_skill_block("alpha", "Bob", 1234, "0.7.0")
217
+ out1 = _maybe_skill_block("alpha", "Bob", "session-aaa", "0.7.0")
218
218
  assert out1 is not None
219
219
  assert "agent_comms_onboarding" in out1
220
- # Second call same pid+version — suppressed
221
- out2 = _maybe_skill_block("alpha", "Bob", 1234, "0.7.0")
220
+ # Second call same session_id+version — suppressed
221
+ out2 = _maybe_skill_block("alpha", "Bob", "session-aaa", "0.7.0")
222
222
  assert out2 is None
223
- # PID changed (revive) — re-emit
224
- out3 = _maybe_skill_block("alpha", "Bob", 5678, "0.7.0")
223
+ # session_id changed (revive) — re-emit
224
+ out3 = _maybe_skill_block("alpha", "Bob", "session-bbb", "0.7.0")
225
225
  assert out3 is not None
226
226
  # Version changed (upgrade) — re-emit
227
- out4 = _maybe_skill_block("alpha", "Bob", 5678, "0.7.1")
227
+ out4 = _maybe_skill_block("alpha", "Bob", "session-bbb", "0.7.1")
228
228
  assert out4 is not None
229
229
 
230
230
 
231
+ def test_skill_block_no_session_id_uses_ttl_fallback(env, tmp_path, monkeypatch):
232
+ monkeypatch.setenv("AGENT_COMMS_CACHE_DIR", str(tmp_path / "cache"))
233
+ from agent_comms.hook import _maybe_skill_block
234
+ # No session_id — first call emits
235
+ out1 = _maybe_skill_block("alpha", "Bob", None, "0.7.0")
236
+ assert out1 is not None
237
+ # Subsequent call within TTL — suppressed
238
+ out2 = _maybe_skill_block("alpha", "Bob", None, "0.7.0")
239
+ assert out2 is None
240
+
241
+
231
242
  def test_monitor_warning_block_built(env):
232
243
  from agent_comms.hook import _build_monitor_warning
233
- s = _build_monitor_warning("test reason")
244
+ s = _build_monitor_warning("test reason", "alpha")
234
245
  assert "system_warning" in s
235
246
  assert "monitor_not_armed" in s
236
247
  assert "test reason" in s
237
- assert "comms inbox --unread --json --monitor" in s
248
+ assert "--handle alpha" in s
249
+ assert "--monitor" in s
238
250
 
239
251
 
240
252
  def test_detect_revive(env, tmp_path, monkeypatch):
241
253
  from agent_comms import config as cfg
242
254
  from agent_comms.hook import _detect_revive
243
- # Force PID-walk to a fixed value for deterministic identity-file keying
244
255
  monkeypatch.setattr(cfg, "find_claude_pid", lambda: 1234)
245
- cwd = str(tmp_path / "work") # the env fixture chdirs here
246
- cfg.LocalIdentity(handle="alpha", server_url="x", cwd=cwd, claude_pid=1234).save()
247
- # Same pid → not a revive
248
- assert _detect_revive("alpha", 1234) is False
249
- # Different pid → revive (and load_identity must find the existing record via cwd fallback)
250
- monkeypatch.setattr(cfg, "find_claude_pid", lambda: 9999)
251
- assert _detect_revive("alpha", 9999) is True
252
- # After revive, PID is updated, so subsequent same-pid call is not a revive
253
- assert _detect_revive("alpha", 9999) is False
256
+ cwd = str(tmp_path / "work")
257
+ cfg.LocalIdentity(handle="alpha", server_url="x", cwd=cwd, claude_pid=1234, session_id="s-aaa").save()
258
+ # Same session_id → not a revive
259
+ assert _detect_revive("alpha", "s-aaa", 1234) is False
260
+ # Different session_id → revive (independent of PID)
261
+ monkeypatch.setattr(cfg, "find_claude_pid", lambda: 1234)
262
+ assert _detect_revive("alpha", "s-bbb", 1234) is True
263
+ # After revive, session_id is updated, so subsequent same call not a revive
264
+ assert _detect_revive("alpha", "s-bbb", 1234) is False
265
+ # PID thrashing while session_id stable should NOT trigger revive
266
+ assert _detect_revive("alpha", "s-bbb", 9999) is False
267
+ assert _detect_revive("alpha", "s-bbb", 7777) is False
254
268
 
255
269
 
256
270
  def _suppress_warnings(tmp_path):
File without changes
File without changes