agent-manager-cli 0.1.8__tar.gz → 0.1.9__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: agent-manager-cli
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: CLI для удалённого управления AI-агентами — Node Agent, daemon, сканер сессий
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -656,8 +656,39 @@ async def run_daemon_bidirectional(ws_url: str, ws_token: str, command: list[str
656
656
 
657
657
  stdout_task = asyncio.create_task(_read_stdout())
658
658
  ws_task = asyncio.create_task(_read_ws())
659
- await stdout_task
660
- ws_task.cancel()
659
+
660
+ # Run both in parallel. Normally stdout_task finishes first
661
+ # (Claude exits naturally at end of task). If ws_task finishes
662
+ # first it means the backend connection died — in that case we
663
+ # have no way to recover the session, so we terminate Claude
664
+ # so the daemon subprocess exits cleanly. The node agent will
665
+ # detect the dead subprocess and spawn a fresh one on the next
666
+ # attach command (with --continue --resume so history is kept).
667
+ done, _pending = await asyncio.wait(
668
+ [stdout_task, ws_task],
669
+ return_when=asyncio.FIRST_COMPLETED,
670
+ )
671
+
672
+ if ws_task in done and proc.returncode is None:
673
+ _log(
674
+ "daemon",
675
+ "WS closed while Claude is still running — terminating CLI "
676
+ "(node agent will respawn on next attach)",
677
+ )
678
+ try:
679
+ proc.terminate()
680
+ except ProcessLookupError:
681
+ pass
682
+
683
+ # Let remaining tasks clean up (stdout_task should exit once
684
+ # claude's stdout closes after terminate).
685
+ for task in (stdout_task, ws_task):
686
+ if not task.done():
687
+ task.cancel()
688
+ try:
689
+ await task
690
+ except (asyncio.CancelledError, Exception):
691
+ pass
661
692
 
662
693
  proc.wait()
663
694
  _log("daemon", f"Process exited with code {proc.returncode}")
@@ -888,8 +919,34 @@ async def _run_node_agent(
888
919
  async def _heartbeat():
889
920
  while True:
890
921
  await asyncio.sleep(25)
922
+ # Reap any daemon subprocesses that have exited so we
923
+ # don't report them as alive.
924
+ dead = [
925
+ aid for aid, p in daemon_procs.items()
926
+ if p.returncode is not None
927
+ ]
928
+ for aid in dead:
929
+ daemon_procs.pop(aid, None)
930
+
931
+ # Report live daemons to the backend so it can refresh
932
+ # `daemon:live:{id}` heartbeats. This is the source of
933
+ # truth for the UI's `is_daemon_alive` check — without
934
+ # it the backend has no way to know when a daemon
935
+ # subprocess is alive but its own ws to the backend is
936
+ # dead (orphaned after backend restart, etc.).
937
+ alive_ids = [
938
+ aid for aid, p in daemon_procs.items()
939
+ if p.returncode is None
940
+ ]
891
941
  try:
892
- await ws.send(json.dumps({"type": "heartbeat"}))
942
+ await ws.send(
943
+ json.dumps(
944
+ {
945
+ "type": "heartbeat",
946
+ "daemons": alive_ids,
947
+ }
948
+ )
949
+ )
893
950
  except websockets.exceptions.ConnectionClosed as e:
894
951
  _log("node", f"heartbeat: connection closed ({e.code} {e.reason or '-'})")
895
952
  break
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-manager-cli"
3
- version = "0.1.8"
3
+ version = "0.1.9"
4
4
  description = "CLI для удалённого управления AI-агентами — Node Agent, daemon, сканер сессий"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"