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.
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/PKG-INFO +1 -1
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agent_comms/config.py +1 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agent_comms/hook.py +78 -43
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agentic_comms.egg-info/PKG-INFO +1 -1
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/pyproject.toml +1 -1
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/tests/test_cli.py +32 -18
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/README.md +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agent_comms/__init__.py +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agent_comms/__main__.py +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agent_comms/api.py +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agent_comms/cli.py +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agent_comms/install.py +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agentic_comms.egg-info/SOURCES.txt +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agentic_comms.egg-info/dependency_links.txt +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agentic_comms.egg-info/entry_points.txt +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agentic_comms.egg-info/requires.txt +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/agentic_comms.egg-info/top_level.txt +0 -0
- {agentic_comms-0.7.0 → agentic_comms-0.7.2}/setup.cfg +0 -0
|
@@ -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
|
|
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"{
|
|
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. {
|
|
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,
|
|
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
|
-
|
|
492
|
+
last_session = state.get("session_id")
|
|
476
493
|
last_version = state.get("version")
|
|
477
|
-
|
|
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
|
-
"
|
|
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
|
|
501
|
-
|
|
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
|
-
|
|
507
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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",
|
|
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
|
|
221
|
-
out2 = _maybe_skill_block("alpha", "Bob",
|
|
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
|
-
#
|
|
224
|
-
out3 = _maybe_skill_block("alpha", "Bob",
|
|
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",
|
|
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 "
|
|
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")
|
|
246
|
-
cfg.LocalIdentity(handle="alpha", server_url="x", cwd=cwd, claude_pid=1234).save()
|
|
247
|
-
# Same
|
|
248
|
-
assert _detect_revive("alpha", 1234) is False
|
|
249
|
-
# Different
|
|
250
|
-
monkeypatch.setattr(cfg, "find_claude_pid", lambda:
|
|
251
|
-
assert _detect_revive("alpha",
|
|
252
|
-
# After revive,
|
|
253
|
-
assert _detect_revive("alpha",
|
|
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
|
|
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
|