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.
- {dulus-0.2.30/dulus.egg-info → dulus-0.2.31}/PKG-INFO +1 -1
- {dulus-0.2.30 → dulus-0.2.31/dulus.egg-info}/PKG-INFO +1 -1
- {dulus-0.2.30 → dulus-0.2.31}/dulus.py +159 -39
- {dulus-0.2.30 → dulus-0.2.31}/pyproject.toml +1 -1
- {dulus-0.2.30 → dulus-0.2.31}/LICENSE +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/MANIFEST.in +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/README.md +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/agent.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/compressor.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/context.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/githook.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/marketplace.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/personas.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/plugins.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/server.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/backend/tasks.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/batch_api.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/checkpoint/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/checkpoint/hooks.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/checkpoint/store.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/checkpoint/types.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/claude_code_watcher.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/clipboard_utils.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/cloudsave.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/common.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/compaction.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/config.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/context.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/active_persona.json +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/context.json +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/marketplace.json +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/personas.json +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/plugins/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/plugin.json +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/data/tasks.json +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/README.md +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/api.html +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/architecture.md +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/dashboard/index.html +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/divider.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/generate.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/hero.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/index.html +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/news.md +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/particle-playground.html +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/personas/index.html +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/poetry-banner.png +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/preview.html +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-agents.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-features.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-memory.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-models.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-perms.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/spinners.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/split-pane.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/SOURCES.txt +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus_gui.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/client.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/config.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/dulus_mcp/types.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/agent_bridge.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/chat_widget.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/main_window.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/personas.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/session_utils.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/settings_dialog.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/sidebar.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/tasks_view.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/themes.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/gui/tool_panel.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/input.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/license_manager.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/audit.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/consolidator.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/context.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/offload.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/palace.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/scan.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/sessions.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/store.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/tools.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/types.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/memory/vector_search.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/multi_agent/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/multi_agent/subagent.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/multi_agent/tools.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/offload_helper.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/plugin/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/plugin/autoadapter.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/plugin/loader.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/plugin/recommend.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/plugin/store.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/plugin/types.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/providers.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/setup.cfg +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/skill/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/skill/builtin.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/skill/clawhub.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/skill/executor.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/skill/loader.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/skill/tools.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/skills.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/spinner.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/string_utils.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/subagent.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/task/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/task/store.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/task/tools.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/task/types.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_compaction.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_diff_view.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_license.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_mcp.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_memory.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_plugin.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_skills.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_subagent.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_task.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tests/test_voice.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tmux_offloader.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tmux_tools.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tool_registry.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/tools.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/ui/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/ui/input.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/ui/render.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/voice/__init__.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/voice/keyterms.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/voice/recorder.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/voice/stt.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/voice/tts.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/webchat.py +0 -0
- {dulus-0.2.30 → dulus-0.2.31}/webchat_server.py +0 -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
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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
|
-
|
|
1586
|
+
else:
|
|
1587
|
+
try:
|
|
1572
1588
|
_os.kill(pid, 0)
|
|
1573
1589
|
return True
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1642
|
-
|
|
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
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
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
|
|
1674
|
-
#
|
|
1675
|
-
#
|
|
1676
|
-
#
|
|
1677
|
-
#
|
|
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 (
|
|
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
|
|
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
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|