dulus 0.2.30__tar.gz → 0.2.31__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 (164) hide show
  1. {dulus-0.2.30/dulus.egg-info → dulus-0.2.31}/PKG-INFO +1 -1
  2. {dulus-0.2.30 → dulus-0.2.31/dulus.egg-info}/PKG-INFO +1 -1
  3. {dulus-0.2.30 → dulus-0.2.31}/dulus.py +159 -39
  4. {dulus-0.2.30 → dulus-0.2.31}/pyproject.toml +1 -1
  5. {dulus-0.2.30 → dulus-0.2.31}/LICENSE +0 -0
  6. {dulus-0.2.30 → dulus-0.2.31}/MANIFEST.in +0 -0
  7. {dulus-0.2.30 → dulus-0.2.31}/README.md +0 -0
  8. {dulus-0.2.30 → dulus-0.2.31}/agent.py +0 -0
  9. {dulus-0.2.30 → dulus-0.2.31}/backend/__init__.py +0 -0
  10. {dulus-0.2.30 → dulus-0.2.31}/backend/compressor.py +0 -0
  11. {dulus-0.2.30 → dulus-0.2.31}/backend/context.py +0 -0
  12. {dulus-0.2.30 → dulus-0.2.31}/backend/githook.py +0 -0
  13. {dulus-0.2.30 → dulus-0.2.31}/backend/marketplace.py +0 -0
  14. {dulus-0.2.30 → dulus-0.2.31}/backend/mempalace_bridge.py +0 -0
  15. {dulus-0.2.30 → dulus-0.2.31}/backend/personas.py +0 -0
  16. {dulus-0.2.30 → dulus-0.2.31}/backend/plugins.py +0 -0
  17. {dulus-0.2.30 → dulus-0.2.31}/backend/server.py +0 -0
  18. {dulus-0.2.30 → dulus-0.2.31}/backend/tasks.py +0 -0
  19. {dulus-0.2.30 → dulus-0.2.31}/batch_api.py +0 -0
  20. {dulus-0.2.30 → dulus-0.2.31}/checkpoint/__init__.py +0 -0
  21. {dulus-0.2.30 → dulus-0.2.31}/checkpoint/hooks.py +0 -0
  22. {dulus-0.2.30 → dulus-0.2.31}/checkpoint/store.py +0 -0
  23. {dulus-0.2.30 → dulus-0.2.31}/checkpoint/types.py +0 -0
  24. {dulus-0.2.30 → dulus-0.2.31}/claude_code_watcher.py +0 -0
  25. {dulus-0.2.30 → dulus-0.2.31}/clipboard_utils.py +0 -0
  26. {dulus-0.2.30 → dulus-0.2.31}/cloudsave.py +0 -0
  27. {dulus-0.2.30 → dulus-0.2.31}/common.py +0 -0
  28. {dulus-0.2.30 → dulus-0.2.31}/compaction.py +0 -0
  29. {dulus-0.2.30 → dulus-0.2.31}/config.py +0 -0
  30. {dulus-0.2.30 → dulus-0.2.31}/context.py +0 -0
  31. {dulus-0.2.30 → dulus-0.2.31}/data/__init__.py +0 -0
  32. {dulus-0.2.30 → dulus-0.2.31}/data/active_persona.json +0 -0
  33. {dulus-0.2.30 → dulus-0.2.31}/data/context.json +0 -0
  34. {dulus-0.2.30 → dulus-0.2.31}/data/marketplace.json +0 -0
  35. {dulus-0.2.30 → dulus-0.2.31}/data/personas.json +0 -0
  36. {dulus-0.2.30 → dulus-0.2.31}/data/plugins/__init__.py +0 -0
  37. {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/__init__.py +0 -0
  38. {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  39. {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  40. {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  41. {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/plugin.json +0 -0
  42. {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/plugin_tool.py +0 -0
  43. {dulus-0.2.30 → dulus-0.2.31}/data/tasks.json +0 -0
  44. {dulus-0.2.30 → dulus-0.2.31}/docs/README.md +0 -0
  45. {dulus-0.2.30 → dulus-0.2.31}/docs/__init__.py +0 -0
  46. {dulus-0.2.30 → dulus-0.2.31}/docs/api.html +0 -0
  47. {dulus-0.2.30 → dulus-0.2.31}/docs/architecture.md +0 -0
  48. {dulus-0.2.30 → dulus-0.2.31}/docs/azure-speech-template.json +0 -0
  49. {dulus-0.2.30 → dulus-0.2.31}/docs/dashboard/index.html +0 -0
  50. {dulus-0.2.30 → dulus-0.2.31}/docs/divider.svg +0 -0
  51. {dulus-0.2.30 → dulus-0.2.31}/docs/generate.py +0 -0
  52. {dulus-0.2.30 → dulus-0.2.31}/docs/hero.svg +0 -0
  53. {dulus-0.2.30 → dulus-0.2.31}/docs/index.html +0 -0
  54. {dulus-0.2.30 → dulus-0.2.31}/docs/news.md +0 -0
  55. {dulus-0.2.30 → dulus-0.2.31}/docs/nvidia-models.svg +0 -0
  56. {dulus-0.2.30 → dulus-0.2.31}/docs/particle-playground.html +0 -0
  57. {dulus-0.2.30 → dulus-0.2.31}/docs/personas/index.html +0 -0
  58. {dulus-0.2.30 → dulus-0.2.31}/docs/poetry-banner.png +0 -0
  59. {dulus-0.2.30 → dulus-0.2.31}/docs/preview.html +0 -0
  60. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-agents.svg +0 -0
  61. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-brainstorm.svg +0 -0
  62. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-bridges.svg +0 -0
  63. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-features.svg +0 -0
  64. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-freetier.svg +0 -0
  65. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-memory.svg +0 -0
  66. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-models.svg +0 -0
  67. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-perms.svg +0 -0
  68. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-plugins.svg +0 -0
  69. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-quickstart.svg +0 -0
  70. {dulus-0.2.30 → dulus-0.2.31}/docs/sec-ssj.svg +0 -0
  71. {dulus-0.2.30 → dulus-0.2.31}/docs/spinners.svg +0 -0
  72. {dulus-0.2.30 → dulus-0.2.31}/docs/split-pane.svg +0 -0
  73. {dulus-0.2.30 → dulus-0.2.31}/docs/terminal-boot.svg +0 -0
  74. {dulus-0.2.30 → dulus-0.2.31}/docs/uploads/particle-playground.html +0 -0
  75. {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/SOURCES.txt +0 -0
  76. {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/dependency_links.txt +0 -0
  77. {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/entry_points.txt +0 -0
  78. {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/requires.txt +0 -0
  79. {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/top_level.txt +0 -0
  80. {dulus-0.2.30 → dulus-0.2.31}/dulus_gui.py +0 -0
  81. {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/__init__.py +0 -0
  82. {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/client.py +0 -0
  83. {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/config.py +0 -0
  84. {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/tools.py +0 -0
  85. {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/types.py +0 -0
  86. {dulus-0.2.30 → dulus-0.2.31}/gui/__init__.py +0 -0
  87. {dulus-0.2.30 → dulus-0.2.31}/gui/agent_bridge.py +0 -0
  88. {dulus-0.2.30 → dulus-0.2.31}/gui/chat_widget.py +0 -0
  89. {dulus-0.2.30 → dulus-0.2.31}/gui/main_window.py +0 -0
  90. {dulus-0.2.30 → dulus-0.2.31}/gui/personas.py +0 -0
  91. {dulus-0.2.30 → dulus-0.2.31}/gui/session_utils.py +0 -0
  92. {dulus-0.2.30 → dulus-0.2.31}/gui/settings_dialog.py +0 -0
  93. {dulus-0.2.30 → dulus-0.2.31}/gui/sidebar.py +0 -0
  94. {dulus-0.2.30 → dulus-0.2.31}/gui/tasks_view.py +0 -0
  95. {dulus-0.2.30 → dulus-0.2.31}/gui/themes.py +0 -0
  96. {dulus-0.2.30 → dulus-0.2.31}/gui/tool_panel.py +0 -0
  97. {dulus-0.2.30 → dulus-0.2.31}/input.py +0 -0
  98. {dulus-0.2.30 → dulus-0.2.31}/license_manager.py +0 -0
  99. {dulus-0.2.30 → dulus-0.2.31}/memory/__init__.py +0 -0
  100. {dulus-0.2.30 → dulus-0.2.31}/memory/audit.py +0 -0
  101. {dulus-0.2.30 → dulus-0.2.31}/memory/consolidator.py +0 -0
  102. {dulus-0.2.30 → dulus-0.2.31}/memory/context.py +0 -0
  103. {dulus-0.2.30 → dulus-0.2.31}/memory/offload.py +0 -0
  104. {dulus-0.2.30 → dulus-0.2.31}/memory/palace.py +0 -0
  105. {dulus-0.2.30 → dulus-0.2.31}/memory/scan.py +0 -0
  106. {dulus-0.2.30 → dulus-0.2.31}/memory/sessions.py +0 -0
  107. {dulus-0.2.30 → dulus-0.2.31}/memory/store.py +0 -0
  108. {dulus-0.2.30 → dulus-0.2.31}/memory/tools.py +0 -0
  109. {dulus-0.2.30 → dulus-0.2.31}/memory/types.py +0 -0
  110. {dulus-0.2.30 → dulus-0.2.31}/memory/vector_search.py +0 -0
  111. {dulus-0.2.30 → dulus-0.2.31}/multi_agent/__init__.py +0 -0
  112. {dulus-0.2.30 → dulus-0.2.31}/multi_agent/subagent.py +0 -0
  113. {dulus-0.2.30 → dulus-0.2.31}/multi_agent/tools.py +0 -0
  114. {dulus-0.2.30 → dulus-0.2.31}/offload_helper.py +0 -0
  115. {dulus-0.2.30 → dulus-0.2.31}/plugin/__init__.py +0 -0
  116. {dulus-0.2.30 → dulus-0.2.31}/plugin/autoadapter.py +0 -0
  117. {dulus-0.2.30 → dulus-0.2.31}/plugin/loader.py +0 -0
  118. {dulus-0.2.30 → dulus-0.2.31}/plugin/recommend.py +0 -0
  119. {dulus-0.2.30 → dulus-0.2.31}/plugin/store.py +0 -0
  120. {dulus-0.2.30 → dulus-0.2.31}/plugin/types.py +0 -0
  121. {dulus-0.2.30 → dulus-0.2.31}/providers.py +0 -0
  122. {dulus-0.2.30 → dulus-0.2.31}/setup.cfg +0 -0
  123. {dulus-0.2.30 → dulus-0.2.31}/skill/__init__.py +0 -0
  124. {dulus-0.2.30 → dulus-0.2.31}/skill/builtin.py +0 -0
  125. {dulus-0.2.30 → dulus-0.2.31}/skill/clawhub.py +0 -0
  126. {dulus-0.2.30 → dulus-0.2.31}/skill/executor.py +0 -0
  127. {dulus-0.2.30 → dulus-0.2.31}/skill/loader.py +0 -0
  128. {dulus-0.2.30 → dulus-0.2.31}/skill/tools.py +0 -0
  129. {dulus-0.2.30 → dulus-0.2.31}/skills.py +0 -0
  130. {dulus-0.2.30 → dulus-0.2.31}/spinner.py +0 -0
  131. {dulus-0.2.30 → dulus-0.2.31}/string_utils.py +0 -0
  132. {dulus-0.2.30 → dulus-0.2.31}/subagent.py +0 -0
  133. {dulus-0.2.30 → dulus-0.2.31}/task/__init__.py +0 -0
  134. {dulus-0.2.30 → dulus-0.2.31}/task/store.py +0 -0
  135. {dulus-0.2.30 → dulus-0.2.31}/task/tools.py +0 -0
  136. {dulus-0.2.30 → dulus-0.2.31}/task/types.py +0 -0
  137. {dulus-0.2.30 → dulus-0.2.31}/tests/test_checkpoint.py +0 -0
  138. {dulus-0.2.30 → dulus-0.2.31}/tests/test_compaction.py +0 -0
  139. {dulus-0.2.30 → dulus-0.2.31}/tests/test_diff_view.py +0 -0
  140. {dulus-0.2.30 → dulus-0.2.31}/tests/test_injection_fix.py +0 -0
  141. {dulus-0.2.30 → dulus-0.2.31}/tests/test_license.py +0 -0
  142. {dulus-0.2.30 → dulus-0.2.31}/tests/test_mcp.py +0 -0
  143. {dulus-0.2.30 → dulus-0.2.31}/tests/test_memory.py +0 -0
  144. {dulus-0.2.30 → dulus-0.2.31}/tests/test_plugin.py +0 -0
  145. {dulus-0.2.30 → dulus-0.2.31}/tests/test_skills.py +0 -0
  146. {dulus-0.2.30 → dulus-0.2.31}/tests/test_subagent.py +0 -0
  147. {dulus-0.2.30 → dulus-0.2.31}/tests/test_task.py +0 -0
  148. {dulus-0.2.30 → dulus-0.2.31}/tests/test_telegram_buffer.py +0 -0
  149. {dulus-0.2.30 → dulus-0.2.31}/tests/test_tool_registry.py +0 -0
  150. {dulus-0.2.30 → dulus-0.2.31}/tests/test_voice.py +0 -0
  151. {dulus-0.2.30 → dulus-0.2.31}/tmux_offloader.py +0 -0
  152. {dulus-0.2.30 → dulus-0.2.31}/tmux_tools.py +0 -0
  153. {dulus-0.2.30 → dulus-0.2.31}/tool_registry.py +0 -0
  154. {dulus-0.2.30 → dulus-0.2.31}/tools.py +0 -0
  155. {dulus-0.2.30 → dulus-0.2.31}/ui/__init__.py +0 -0
  156. {dulus-0.2.30 → dulus-0.2.31}/ui/input.py +0 -0
  157. {dulus-0.2.30 → dulus-0.2.31}/ui/render.py +0 -0
  158. {dulus-0.2.30 → dulus-0.2.31}/voice/__init__.py +0 -0
  159. {dulus-0.2.30 → dulus-0.2.31}/voice/keyterms.py +0 -0
  160. {dulus-0.2.30 → dulus-0.2.31}/voice/recorder.py +0 -0
  161. {dulus-0.2.30 → dulus-0.2.31}/voice/stt.py +0 -0
  162. {dulus-0.2.30 → dulus-0.2.31}/voice/tts.py +0 -0
  163. {dulus-0.2.30 → dulus-0.2.31}/webchat.py +0 -0
  164. {dulus-0.2.30 → dulus-0.2.31}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.30
3
+ Version: 0.2.31
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.30
3
+ Version: 0.2.31
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -1563,16 +1563,32 @@ def cmd_bg(args: str, _state, config) -> bool:
1563
1563
  def _is_alive(pid: int) -> bool:
1564
1564
  if pid <= 0:
1565
1565
  return False
1566
- try:
1567
- if _sys.platform == "win32":
1568
- # On Windows, os.kill(pid, 0) raises if the process doesn't exist.
1569
- _os.kill(pid, 0)
1566
+ if _sys.platform == "win32":
1567
+ # os.kill(pid, 0) on Windows is unreliable for GUI-subsystem
1568
+ # processes (pythonw.exe): it raises OSError(errno=22) even
1569
+ # when the process is alive. Use the native OpenProcess API.
1570
+ try:
1571
+ import ctypes
1572
+ kernel32 = ctypes.windll.kernel32
1573
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
1574
+ h = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
1575
+ if h:
1576
+ kernel32.CloseHandle(h)
1577
+ return True
1578
+ # OpenProcess returned 0 — check last error
1579
+ err = kernel32.GetLastError()
1580
+ # ERROR_INVALID_PARAMETER (87) = PID does not exist
1581
+ return err != 87
1582
+ except Exception:
1583
+ # Fallback: if the native API fails, assume alive so we
1584
+ # still attempt taskkill downstream.
1570
1585
  return True
1571
- else:
1586
+ else:
1587
+ try:
1572
1588
  _os.kill(pid, 0)
1573
1589
  return True
1574
- except (ProcessLookupError, OSError, PermissionError):
1575
- return False
1590
+ except (ProcessLookupError, OSError):
1591
+ return False
1576
1592
 
1577
1593
  def _read_pid() -> int:
1578
1594
  try:
@@ -1634,19 +1650,34 @@ def cmd_bg(args: str, _state, config) -> bool:
1634
1650
  except FileNotFoundError:
1635
1651
  pass
1636
1652
  return True
1653
+ sigterm_ok = False
1637
1654
  try:
1655
+ _os.kill(pid, _signal.SIGTERM)
1656
+ sigterm_ok = True
1657
+ ok(f"Sent SIGTERM to PID {pid}.")
1658
+ except PermissionError:
1659
+ # On Windows os.kill() to a GUI-subsystem process (pythonw.exe)
1660
+ # often raises PermissionError. Escalate to taskkill immediately.
1638
1661
  if _sys.platform == "win32":
1639
- _os.kill(pid, _signal.SIGTERM)
1662
+ try:
1663
+ _sp.run(["taskkill", "/F", "/PID", str(pid)],
1664
+ capture_output=True, timeout=5)
1665
+ ok(f"Forced stop via taskkill /F on PID {pid}.")
1666
+ except Exception as tk_e:
1667
+ err(f"Failed to kill {pid}: {tk_e}")
1668
+ return True
1640
1669
  else:
1641
- _os.kill(pid, _signal.SIGTERM)
1642
- ok(f"Sent SIGTERM to PID {pid}.")
1670
+ err(f"Failed to kill {pid}: Permission denied")
1671
+ return True
1643
1672
  except Exception as e:
1644
1673
  err(f"Failed to kill {pid}: {e}")
1645
1674
  return True
1646
- for _ in range(20):
1647
- if not _is_alive(pid):
1648
- break
1649
- _time.sleep(0.25)
1675
+
1676
+ if sigterm_ok:
1677
+ for _ in range(20):
1678
+ if not _is_alive(pid):
1679
+ break
1680
+ _time.sleep(0.25)
1650
1681
  try:
1651
1682
  BG_PID.unlink()
1652
1683
  except FileNotFoundError:
@@ -1670,19 +1701,65 @@ def cmd_bg(args: str, _state, config) -> bool:
1670
1701
  return True
1671
1702
 
1672
1703
  # ── /bg kill ──────────────────────────────────────────────────────────
1673
- # Force-stop the background DAEMON only — never the user's own REPL.
1674
- # We use the BG_PID file as the source of truth: only /bg start writes
1675
- # it, so it uniquely identifies the detached daemon. If no BG_PID file
1676
- # exists, we refuse to kill anything (port may be held by a REPL the
1677
- # user wants to keep). For SIGKILL escalation we use taskkill on Windows.
1704
+ # Force-stop whatever is holding the IPC port.
1705
+ # Priority 1: BG_PID file (fastest, most reliable).
1706
+ # Priority 2: discover the PID from the OS by scanning port 5151.
1707
+ # We NEVER kill our own REPL process (own_pid check).
1708
+ # For SIGKILL escalation we use taskkill on Windows.
1678
1709
  if sub == "kill":
1679
1710
  f_pid = _read_pid()
1680
1711
  own_pid = _os.getpid()
1681
1712
 
1713
+ def _discover_pid_from_port(port: int) -> int:
1714
+ """Ask the OS which process owns the given TCP port."""
1715
+ try:
1716
+ if _sys.platform == "win32":
1717
+ # netstat -ano → find the line with :5151 in LISTENING state
1718
+ result = _sp.run(
1719
+ ["netstat", "-ano"],
1720
+ capture_output=True, text=True, timeout=5
1721
+ )
1722
+ for line in result.stdout.splitlines():
1723
+ if f":{port}" in line and ("LISTENING" in line or "ESTABLISHED" in line):
1724
+ parts = line.strip().split()
1725
+ if parts:
1726
+ try:
1727
+ return int(parts[-1])
1728
+ except ValueError:
1729
+ continue
1730
+ else:
1731
+ # lsof -ti :port (outputs PID only)
1732
+ result = _sp.run(
1733
+ ["lsof", "-ti", f":{port}"],
1734
+ capture_output=True, text=True, timeout=5
1735
+ )
1736
+ if result.stdout.strip():
1737
+ return int(result.stdout.strip().splitlines()[0])
1738
+ # Fallback to fuser
1739
+ result = _sp.run(
1740
+ ["fuser", f"{port}/tcp"],
1741
+ capture_output=True, text=True, timeout=5
1742
+ )
1743
+ if result.stdout.strip():
1744
+ parts = result.stdout.strip().split(":")
1745
+ if len(parts) > 1:
1746
+ return int(parts[1].strip().split()[0])
1747
+ except Exception:
1748
+ pass
1749
+ return 0
1750
+
1751
+ # No PID file? Discover from the OS if the port is in use.
1752
+ if not f_pid and _ipc_alive():
1753
+ discovered = _discover_pid_from_port(DULUS_IPC_PORT)
1754
+ if discovered and discovered != own_pid:
1755
+ f_pid = discovered
1756
+ info(f"No PID file — discovered process {f_pid} holding port {DULUS_IPC_PORT}.")
1757
+ elif discovered == own_pid:
1758
+ warn("Port is held by this REPL — close it with /exit instead.")
1759
+ return True
1760
+
1682
1761
  if not f_pid:
1683
- info("No background daemon to kill (BG_PID file missing).")
1684
- info(f" If port {DULUS_IPC_PORT} is in use, it's likely your own REPL.")
1685
- info(" Close your REPL (/exit) to free the port — /bg kill won't touch it.")
1762
+ info("No background daemon to kill (port free, PID file missing).")
1686
1763
  return True
1687
1764
 
1688
1765
  if f_pid == own_pid:
@@ -1702,29 +1779,48 @@ def cmd_bg(args: str, _state, config) -> bool:
1702
1779
  return True
1703
1780
 
1704
1781
  # Send SIGTERM, give it 1s, escalate to SIGKILL/taskkill if it lingers.
1782
+ # On Windows, os.kill() to a GUI-subsystem process (pythonw.exe) often
1783
+ # raises PermissionError. We catch that immediately and escalate to
1784
+ # taskkill /F instead of giving up.
1785
+ sigterm_ok = False
1705
1786
  try:
1706
1787
  _os.kill(f_pid, _signal.SIGTERM)
1788
+ sigterm_ok = True
1707
1789
  ok(f"Sent SIGTERM to daemon PID {f_pid}.")
1708
- except (ProcessLookupError, PermissionError, OSError) as e:
1790
+ except PermissionError:
1791
+ if _sys.platform == "win32":
1792
+ warn(f"Permission denied signalling PID {f_pid} — escalating to taskkill /F.")
1793
+ try:
1794
+ _sp.run(["taskkill", "/F", "/PID", str(f_pid)],
1795
+ capture_output=True, timeout=5)
1796
+ ok(f"Forced kill via taskkill /F on PID {f_pid}.")
1797
+ except Exception as tk_e:
1798
+ err(f"taskkill failed: {tk_e}")
1799
+ return True
1800
+ else:
1801
+ err(f"Could not signal PID {f_pid}: Permission denied")
1802
+ return True
1803
+ except (ProcessLookupError, OSError) as e:
1709
1804
  err(f"Could not signal PID {f_pid}: {e}")
1710
1805
  return True
1711
1806
 
1712
- for _ in range(8):
1713
- if not _is_alive(f_pid):
1714
- break
1715
- _time.sleep(0.25)
1807
+ if sigterm_ok:
1808
+ for _ in range(8):
1809
+ if not _is_alive(f_pid):
1810
+ break
1811
+ _time.sleep(0.25)
1716
1812
 
1717
- if _is_alive(f_pid):
1718
- warn(f"PID {f_pid} did not exit on SIGTERM — escalating to SIGKILL.")
1719
- try:
1720
- if _sys.platform == "win32":
1721
- _sp.run(["taskkill", "/F", "/PID", str(f_pid)],
1722
- capture_output=True, timeout=5)
1723
- else:
1724
- _os.kill(f_pid, _signal.SIGKILL)
1725
- except Exception as e:
1726
- err(f"SIGKILL failed: {e}")
1727
- return True
1813
+ if _is_alive(f_pid):
1814
+ warn(f"PID {f_pid} did not exit on SIGTERM — escalating to SIGKILL.")
1815
+ try:
1816
+ if _sys.platform == "win32":
1817
+ _sp.run(["taskkill", "/F", "/PID", str(f_pid)],
1818
+ capture_output=True, timeout=5)
1819
+ else:
1820
+ _os.kill(f_pid, _signal.SIGKILL)
1821
+ except Exception as e:
1822
+ err(f"SIGKILL failed: {e}")
1823
+ return True
1728
1824
 
1729
1825
  try:
1730
1826
  BG_PID.unlink()
@@ -1786,6 +1882,12 @@ def cmd_bg(args: str, _state, config) -> bool:
1786
1882
  from config import save_config
1787
1883
  save_config(config)
1788
1884
 
1885
+ # Snapshot current REPL session so the daemon can resume it.
1886
+ # This ensures Telegram/Web share the SAME session_id and context.
1887
+ current_sid = config.get("_session_id", "")
1888
+ if current_sid and _state and getattr(_state, "messages", None):
1889
+ save_latest("", _state, config)
1890
+
1789
1891
  # Build the spawn command. On Windows we MUST use pythonw.exe (windowless
1790
1892
  # variant) instead of the console-subsystem python.exe / dulus shim,
1791
1893
  # otherwise Windows creates a visible console window for the daemon
@@ -1817,6 +1919,7 @@ def cmd_bg(args: str, _state, config) -> bool:
1817
1919
  env = _os.environ.copy()
1818
1920
  env["DULUS_BG_AUTO_WEBCHAT"] = "1"
1819
1921
  env["DULUS_BG_WEBCHAT_PORT"] = str(web_port)
1922
+ env["DULUS_BG_SESSION_ID"] = current_sid
1820
1923
 
1821
1924
  # Detach properly per platform.
1822
1925
  log_fp = open(BG_LOG, "ab")
@@ -5740,10 +5843,27 @@ def _run_daemon(config: dict) -> None:
5740
5843
  from checkpoint import set_session
5741
5844
  from common import ok, info, warn, err, clr
5742
5845
 
5743
- session_id = config.get("_session_id") or uuid.uuid4().hex[:8]
5846
+ import os as _os_env
5847
+ bg_session_id = _os_env.environ.get("DULUS_BG_SESSION_ID", "")
5848
+ session_id = bg_session_id or config.get("_session_id") or uuid.uuid4().hex[:8]
5744
5849
  set_session(session_id)
5745
5850
 
5746
5851
  state = AgentState()
5852
+ # If spawned from /bg start with a session ID, resume that session's state.
5853
+ if bg_session_id:
5854
+ from config import MR_SESSION_DIR
5855
+ latest_path = MR_SESSION_DIR / "session_latest.json"
5856
+ if latest_path.exists():
5857
+ try:
5858
+ import json as _json
5859
+ data = _json.loads(latest_path.read_text(encoding="utf-8", errors="replace"))
5860
+ state.messages = data.get("messages", [])
5861
+ state.turn_count = data.get("turn_count", 0)
5862
+ state.total_input_tokens = data.get("total_input_tokens", 0)
5863
+ state.total_output_tokens = data.get("total_output_tokens", 0)
5864
+ info(f"Resumed session {session_id} ({len(state.messages)} messages)")
5865
+ except Exception as _load_e:
5866
+ warn(f"Could not resume session: {_load_e}")
5747
5867
  config["_state"] = state
5748
5868
  config["_session_id"] = session_id
5749
5869
  config["_last_interaction_time"] = time.time()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.30"
7
+ version = "0.2.31"
8
8
  description = "Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
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
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
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
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
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
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
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
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
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
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
File without changes
File without changes