dulus 0.2.27__tar.gz → 0.2.29__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.27/dulus.egg-info → dulus-0.2.29}/PKG-INFO +2 -2
  2. {dulus-0.2.27 → dulus-0.2.29}/README.md +1 -1
  3. {dulus-0.2.27 → dulus-0.2.29}/docs/news.md +11 -0
  4. {dulus-0.2.27 → dulus-0.2.29/dulus.egg-info}/PKG-INFO +2 -2
  5. {dulus-0.2.27 → dulus-0.2.29}/dulus.py +168 -39
  6. {dulus-0.2.27 → dulus-0.2.29}/pyproject.toml +1 -1
  7. {dulus-0.2.27 → dulus-0.2.29}/LICENSE +0 -0
  8. {dulus-0.2.27 → dulus-0.2.29}/MANIFEST.in +0 -0
  9. {dulus-0.2.27 → dulus-0.2.29}/agent.py +0 -0
  10. {dulus-0.2.27 → dulus-0.2.29}/backend/__init__.py +0 -0
  11. {dulus-0.2.27 → dulus-0.2.29}/backend/compressor.py +0 -0
  12. {dulus-0.2.27 → dulus-0.2.29}/backend/context.py +0 -0
  13. {dulus-0.2.27 → dulus-0.2.29}/backend/githook.py +0 -0
  14. {dulus-0.2.27 → dulus-0.2.29}/backend/marketplace.py +0 -0
  15. {dulus-0.2.27 → dulus-0.2.29}/backend/mempalace_bridge.py +0 -0
  16. {dulus-0.2.27 → dulus-0.2.29}/backend/personas.py +0 -0
  17. {dulus-0.2.27 → dulus-0.2.29}/backend/plugins.py +0 -0
  18. {dulus-0.2.27 → dulus-0.2.29}/backend/server.py +0 -0
  19. {dulus-0.2.27 → dulus-0.2.29}/backend/tasks.py +0 -0
  20. {dulus-0.2.27 → dulus-0.2.29}/batch_api.py +0 -0
  21. {dulus-0.2.27 → dulus-0.2.29}/checkpoint/__init__.py +0 -0
  22. {dulus-0.2.27 → dulus-0.2.29}/checkpoint/hooks.py +0 -0
  23. {dulus-0.2.27 → dulus-0.2.29}/checkpoint/store.py +0 -0
  24. {dulus-0.2.27 → dulus-0.2.29}/checkpoint/types.py +0 -0
  25. {dulus-0.2.27 → dulus-0.2.29}/claude_code_watcher.py +0 -0
  26. {dulus-0.2.27 → dulus-0.2.29}/clipboard_utils.py +0 -0
  27. {dulus-0.2.27 → dulus-0.2.29}/cloudsave.py +0 -0
  28. {dulus-0.2.27 → dulus-0.2.29}/common.py +0 -0
  29. {dulus-0.2.27 → dulus-0.2.29}/compaction.py +0 -0
  30. {dulus-0.2.27 → dulus-0.2.29}/config.py +0 -0
  31. {dulus-0.2.27 → dulus-0.2.29}/context.py +0 -0
  32. {dulus-0.2.27 → dulus-0.2.29}/data/__init__.py +0 -0
  33. {dulus-0.2.27 → dulus-0.2.29}/data/active_persona.json +0 -0
  34. {dulus-0.2.27 → dulus-0.2.29}/data/context.json +0 -0
  35. {dulus-0.2.27 → dulus-0.2.29}/data/marketplace.json +0 -0
  36. {dulus-0.2.27 → dulus-0.2.29}/data/personas.json +0 -0
  37. {dulus-0.2.27 → dulus-0.2.29}/data/plugins/__init__.py +0 -0
  38. {dulus-0.2.27 → dulus-0.2.29}/data/plugins/composio/__init__.py +0 -0
  39. {dulus-0.2.27 → dulus-0.2.29}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  40. {dulus-0.2.27 → dulus-0.2.29}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  41. {dulus-0.2.27 → dulus-0.2.29}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  42. {dulus-0.2.27 → dulus-0.2.29}/data/plugins/composio/plugin.json +0 -0
  43. {dulus-0.2.27 → dulus-0.2.29}/data/plugins/composio/plugin_tool.py +0 -0
  44. {dulus-0.2.27 → dulus-0.2.29}/data/tasks.json +0 -0
  45. {dulus-0.2.27 → dulus-0.2.29}/docs/README.md +0 -0
  46. {dulus-0.2.27 → dulus-0.2.29}/docs/__init__.py +0 -0
  47. {dulus-0.2.27 → dulus-0.2.29}/docs/api.html +0 -0
  48. {dulus-0.2.27 → dulus-0.2.29}/docs/architecture.md +0 -0
  49. {dulus-0.2.27 → dulus-0.2.29}/docs/azure-speech-template.json +0 -0
  50. {dulus-0.2.27 → dulus-0.2.29}/docs/dashboard/index.html +0 -0
  51. {dulus-0.2.27 → dulus-0.2.29}/docs/divider.svg +0 -0
  52. {dulus-0.2.27 → dulus-0.2.29}/docs/generate.py +0 -0
  53. {dulus-0.2.27 → dulus-0.2.29}/docs/hero.svg +0 -0
  54. {dulus-0.2.27 → dulus-0.2.29}/docs/index.html +0 -0
  55. {dulus-0.2.27 → dulus-0.2.29}/docs/nvidia-models.svg +0 -0
  56. {dulus-0.2.27 → dulus-0.2.29}/docs/particle-playground.html +0 -0
  57. {dulus-0.2.27 → dulus-0.2.29}/docs/personas/index.html +0 -0
  58. {dulus-0.2.27 → dulus-0.2.29}/docs/poetry-banner.png +0 -0
  59. {dulus-0.2.27 → dulus-0.2.29}/docs/preview.html +0 -0
  60. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-agents.svg +0 -0
  61. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-brainstorm.svg +0 -0
  62. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-bridges.svg +0 -0
  63. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-features.svg +0 -0
  64. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-freetier.svg +0 -0
  65. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-memory.svg +0 -0
  66. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-models.svg +0 -0
  67. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-perms.svg +0 -0
  68. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-plugins.svg +0 -0
  69. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-quickstart.svg +0 -0
  70. {dulus-0.2.27 → dulus-0.2.29}/docs/sec-ssj.svg +0 -0
  71. {dulus-0.2.27 → dulus-0.2.29}/docs/spinners.svg +0 -0
  72. {dulus-0.2.27 → dulus-0.2.29}/docs/split-pane.svg +0 -0
  73. {dulus-0.2.27 → dulus-0.2.29}/docs/terminal-boot.svg +0 -0
  74. {dulus-0.2.27 → dulus-0.2.29}/docs/uploads/particle-playground.html +0 -0
  75. {dulus-0.2.27 → dulus-0.2.29}/dulus.egg-info/SOURCES.txt +0 -0
  76. {dulus-0.2.27 → dulus-0.2.29}/dulus.egg-info/dependency_links.txt +0 -0
  77. {dulus-0.2.27 → dulus-0.2.29}/dulus.egg-info/entry_points.txt +0 -0
  78. {dulus-0.2.27 → dulus-0.2.29}/dulus.egg-info/requires.txt +0 -0
  79. {dulus-0.2.27 → dulus-0.2.29}/dulus.egg-info/top_level.txt +0 -0
  80. {dulus-0.2.27 → dulus-0.2.29}/dulus_gui.py +0 -0
  81. {dulus-0.2.27 → dulus-0.2.29}/dulus_mcp/__init__.py +0 -0
  82. {dulus-0.2.27 → dulus-0.2.29}/dulus_mcp/client.py +0 -0
  83. {dulus-0.2.27 → dulus-0.2.29}/dulus_mcp/config.py +0 -0
  84. {dulus-0.2.27 → dulus-0.2.29}/dulus_mcp/tools.py +0 -0
  85. {dulus-0.2.27 → dulus-0.2.29}/dulus_mcp/types.py +0 -0
  86. {dulus-0.2.27 → dulus-0.2.29}/gui/__init__.py +0 -0
  87. {dulus-0.2.27 → dulus-0.2.29}/gui/agent_bridge.py +0 -0
  88. {dulus-0.2.27 → dulus-0.2.29}/gui/chat_widget.py +0 -0
  89. {dulus-0.2.27 → dulus-0.2.29}/gui/main_window.py +0 -0
  90. {dulus-0.2.27 → dulus-0.2.29}/gui/personas.py +0 -0
  91. {dulus-0.2.27 → dulus-0.2.29}/gui/session_utils.py +0 -0
  92. {dulus-0.2.27 → dulus-0.2.29}/gui/settings_dialog.py +0 -0
  93. {dulus-0.2.27 → dulus-0.2.29}/gui/sidebar.py +0 -0
  94. {dulus-0.2.27 → dulus-0.2.29}/gui/tasks_view.py +0 -0
  95. {dulus-0.2.27 → dulus-0.2.29}/gui/themes.py +0 -0
  96. {dulus-0.2.27 → dulus-0.2.29}/gui/tool_panel.py +0 -0
  97. {dulus-0.2.27 → dulus-0.2.29}/input.py +0 -0
  98. {dulus-0.2.27 → dulus-0.2.29}/license_manager.py +0 -0
  99. {dulus-0.2.27 → dulus-0.2.29}/memory/__init__.py +0 -0
  100. {dulus-0.2.27 → dulus-0.2.29}/memory/audit.py +0 -0
  101. {dulus-0.2.27 → dulus-0.2.29}/memory/consolidator.py +0 -0
  102. {dulus-0.2.27 → dulus-0.2.29}/memory/context.py +0 -0
  103. {dulus-0.2.27 → dulus-0.2.29}/memory/offload.py +0 -0
  104. {dulus-0.2.27 → dulus-0.2.29}/memory/palace.py +0 -0
  105. {dulus-0.2.27 → dulus-0.2.29}/memory/scan.py +0 -0
  106. {dulus-0.2.27 → dulus-0.2.29}/memory/sessions.py +0 -0
  107. {dulus-0.2.27 → dulus-0.2.29}/memory/store.py +0 -0
  108. {dulus-0.2.27 → dulus-0.2.29}/memory/tools.py +0 -0
  109. {dulus-0.2.27 → dulus-0.2.29}/memory/types.py +0 -0
  110. {dulus-0.2.27 → dulus-0.2.29}/memory/vector_search.py +0 -0
  111. {dulus-0.2.27 → dulus-0.2.29}/multi_agent/__init__.py +0 -0
  112. {dulus-0.2.27 → dulus-0.2.29}/multi_agent/subagent.py +0 -0
  113. {dulus-0.2.27 → dulus-0.2.29}/multi_agent/tools.py +0 -0
  114. {dulus-0.2.27 → dulus-0.2.29}/offload_helper.py +0 -0
  115. {dulus-0.2.27 → dulus-0.2.29}/plugin/__init__.py +0 -0
  116. {dulus-0.2.27 → dulus-0.2.29}/plugin/autoadapter.py +0 -0
  117. {dulus-0.2.27 → dulus-0.2.29}/plugin/loader.py +0 -0
  118. {dulus-0.2.27 → dulus-0.2.29}/plugin/recommend.py +0 -0
  119. {dulus-0.2.27 → dulus-0.2.29}/plugin/store.py +0 -0
  120. {dulus-0.2.27 → dulus-0.2.29}/plugin/types.py +0 -0
  121. {dulus-0.2.27 → dulus-0.2.29}/providers.py +0 -0
  122. {dulus-0.2.27 → dulus-0.2.29}/setup.cfg +0 -0
  123. {dulus-0.2.27 → dulus-0.2.29}/skill/__init__.py +0 -0
  124. {dulus-0.2.27 → dulus-0.2.29}/skill/builtin.py +0 -0
  125. {dulus-0.2.27 → dulus-0.2.29}/skill/clawhub.py +0 -0
  126. {dulus-0.2.27 → dulus-0.2.29}/skill/executor.py +0 -0
  127. {dulus-0.2.27 → dulus-0.2.29}/skill/loader.py +0 -0
  128. {dulus-0.2.27 → dulus-0.2.29}/skill/tools.py +0 -0
  129. {dulus-0.2.27 → dulus-0.2.29}/skills.py +0 -0
  130. {dulus-0.2.27 → dulus-0.2.29}/spinner.py +0 -0
  131. {dulus-0.2.27 → dulus-0.2.29}/string_utils.py +0 -0
  132. {dulus-0.2.27 → dulus-0.2.29}/subagent.py +0 -0
  133. {dulus-0.2.27 → dulus-0.2.29}/task/__init__.py +0 -0
  134. {dulus-0.2.27 → dulus-0.2.29}/task/store.py +0 -0
  135. {dulus-0.2.27 → dulus-0.2.29}/task/tools.py +0 -0
  136. {dulus-0.2.27 → dulus-0.2.29}/task/types.py +0 -0
  137. {dulus-0.2.27 → dulus-0.2.29}/tests/test_checkpoint.py +0 -0
  138. {dulus-0.2.27 → dulus-0.2.29}/tests/test_compaction.py +0 -0
  139. {dulus-0.2.27 → dulus-0.2.29}/tests/test_diff_view.py +0 -0
  140. {dulus-0.2.27 → dulus-0.2.29}/tests/test_injection_fix.py +0 -0
  141. {dulus-0.2.27 → dulus-0.2.29}/tests/test_license.py +0 -0
  142. {dulus-0.2.27 → dulus-0.2.29}/tests/test_mcp.py +0 -0
  143. {dulus-0.2.27 → dulus-0.2.29}/tests/test_memory.py +0 -0
  144. {dulus-0.2.27 → dulus-0.2.29}/tests/test_plugin.py +0 -0
  145. {dulus-0.2.27 → dulus-0.2.29}/tests/test_skills.py +0 -0
  146. {dulus-0.2.27 → dulus-0.2.29}/tests/test_subagent.py +0 -0
  147. {dulus-0.2.27 → dulus-0.2.29}/tests/test_task.py +0 -0
  148. {dulus-0.2.27 → dulus-0.2.29}/tests/test_telegram_buffer.py +0 -0
  149. {dulus-0.2.27 → dulus-0.2.29}/tests/test_tool_registry.py +0 -0
  150. {dulus-0.2.27 → dulus-0.2.29}/tests/test_voice.py +0 -0
  151. {dulus-0.2.27 → dulus-0.2.29}/tmux_offloader.py +0 -0
  152. {dulus-0.2.27 → dulus-0.2.29}/tmux_tools.py +0 -0
  153. {dulus-0.2.27 → dulus-0.2.29}/tool_registry.py +0 -0
  154. {dulus-0.2.27 → dulus-0.2.29}/tools.py +0 -0
  155. {dulus-0.2.27 → dulus-0.2.29}/ui/__init__.py +0 -0
  156. {dulus-0.2.27 → dulus-0.2.29}/ui/input.py +0 -0
  157. {dulus-0.2.27 → dulus-0.2.29}/ui/render.py +0 -0
  158. {dulus-0.2.27 → dulus-0.2.29}/voice/__init__.py +0 -0
  159. {dulus-0.2.27 → dulus-0.2.29}/voice/keyterms.py +0 -0
  160. {dulus-0.2.27 → dulus-0.2.29}/voice/recorder.py +0 -0
  161. {dulus-0.2.27 → dulus-0.2.29}/voice/stt.py +0 -0
  162. {dulus-0.2.27 → dulus-0.2.29}/voice/tts.py +0 -0
  163. {dulus-0.2.27 → dulus-0.2.29}/webchat.py +0 -0
  164. {dulus-0.2.27 → dulus-0.2.29}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.27
3
+ Version: 0.2.29
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
@@ -69,7 +69,7 @@ SET /sticky_input ON since the first run for the best experience!
69
69
  <a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
70
70
  <img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
71
71
  <img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
72
- <img src="https://img.shields.io/badge/version-v0.2.27-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
72
+ <img src="https://img.shields.io/badge/version-v0.2.29-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
73
73
  <img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
74
74
  <img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
75
75
  <img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
@@ -22,7 +22,7 @@ SET /sticky_input ON since the first run for the best experience!
22
22
  <a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
23
23
  <img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
24
24
  <img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
25
- <img src="https://img.shields.io/badge/version-v0.2.27-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
25
+ <img src="https://img.shields.io/badge/version-v0.2.29-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
26
26
  <img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
27
27
  <img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
28
28
  <img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
@@ -3,6 +3,17 @@
3
3
  ## 🔥🔥🔥 News (Pacific Time)
4
4
 
5
5
 
6
+ - May 09, 2026 (**v0.2.29**): **`/bg start` actually works from inside a REPL + daemon-mode webchat default-on + tested end-to-end this time**
7
+ - **The whole point of `/bg start` was broken from day one.** A REPL itself binds `127.0.0.1:5151` to serve `dulus "..."` shell calls, so the moment you typed `/bg start` from inside that REPL, the duplicate-detection check saw "port in use" and refused — by the very REPL invoking the command. `/bg kill` then killed the only thing on the port: your own REPL. Pure logic flaw on me.
8
+ - **Now `/bg start` releases the REPL's own IPC first.** When invoked from inside a REPL, the command stops the REPL's IPC thread, force-closes the socket with `SO_LINGER {1, 0}` (skips TIME_WAIT), waits ~600ms for the OS to free the port, and only then spawns the daemon. The REPL keeps running — it just becomes a normal client whose `dulus "..."` dispatches now go to the daemon.
9
+ - **Daemon mode auto-starts WebChat by default.** Previously WebChat only fired when `/bg start` injected an env var; running `dulus --daemon` directly gave you IPC + Telegram but no browser endpoint. Now `--daemon` always starts WebChat on `127.0.0.1:5000` (loopback), opt-out with `webchat_disabled: true` in config or `DULUS_DAEMON_NO_WEB=1`.
10
+ - **Daemon callback was calling `agent.run()` with the wrong signature.** Earlier I passed `agent_run(state, msg, config, is_background=True)` but the real signature is `(user_message, state, config, system_prompt, ...)`. Every Telegram/IPC turn raised `TypeError` silently, daemon looked alive but never answered. Fixed and verified by `inspect.signature`.
11
+ - **`/bg status` now distinguishes REPL from daemon.** Source of truth is the `BG_PID` file (only `/bg start` writes it). If port is in use but no PID file, status says "likely your own Dulus REPL — for a true headless daemon, run `dulus --daemon` from a fresh shell".
12
+ - **`/bg kill` only targets the real daemon now.** Reads `BG_PID`, kills only that PID (refuses if PID matches the calling process). Will not nuke the REPL you're typing in. Falls through to `taskkill /F` on Windows if SIGTERM doesn't work.
13
+ - **End-to-end smoke tested.** A throwaway harness boots a fake REPL on 5151, sends a JSON request and gets `{"response": "REPL echoes: hello"}`, releases the port, spawns a fake daemon on the same port, sends another request and gets `{"response": "DAEMON echoes: klk papi"}`. Handoff works without TIME_WAIT bind failures.
14
+
15
+ - May 09, 2026 (**v0.2.28**): **IPC server thread no longer crashes on idle / dropped connections** — `conn.recv()` was hitting its 60s read timeout and raising `TimeoutError` out of the worker, killing the IPC thread silently. After that, the daemon was still running but `dulus "..."` from any shell would hang forever. Caught socket.timeout / ConnectionReset / BrokenPipe / OSError around the request-handling block so a single bad client can't take down the server.
16
+
6
17
  - May 09, 2026 (**v0.2.27**): **`/bg` no longer leaves stale state** — when something else (a REPL, an old daemon) is already on `127.0.0.1:5151`, `/bg start` now refuses to spawn a duplicate that would just fail to bind, explains what's happening, and clears the stale PID file. `/bg status` self-heals: when it sees a stale PID it auto-removes it instead of complaining on every call. The previous "Some Dulus is listening on IPC, but our PID file is stale" warning is gone for good.
7
18
 
8
19
  - May 09, 2026 (**v0.2.26**): **`/bg start` daemon crash fix + defensive `clr()` + composio fallback**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.27
3
+ Version: 0.2.29
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
@@ -69,7 +69,7 @@ SET /sticky_input ON since the first run for the best experience!
69
69
  <a href="https://pypi.org/project/dulus/"><img src="https://static.pepy.tech/badge/dulus?style=flat-square" alt="downloads"/></a>
70
70
  <img src="https://img.shields.io/badge/python-3.11+-ff6b1f?style=flat-square&labelColor=07070a" alt="python"/>
71
71
  <img src="https://img.shields.io/badge/license-GPLv3-ff6b1f?style=flat-square&labelColor=07070a" alt="license"/>
72
- <img src="https://img.shields.io/badge/version-v0.2.27-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
72
+ <img src="https://img.shields.io/badge/version-v0.2.29-ff6b1f?style=flat-square&labelColor=07070a" alt="version"/>
73
73
  <img src="https://img.shields.io/badge/providers-11-ff6b1f?style=flat-square&labelColor=07070a" alt="providers"/>
74
74
  <img src="https://img.shields.io/badge/tools-27-ff6b1f?style=flat-square&labelColor=07070a" alt="tools"/>
75
75
  <img src="https://img.shields.io/badge/tests-263+-ff6b1f?style=flat-square&labelColor=07070a" alt="tests"/>
@@ -218,7 +218,7 @@ try:
218
218
  from importlib.metadata import version as _pkg_version
219
219
  VERSION = _pkg_version("dulus")
220
220
  except Exception:
221
- VERSION = "0.2.27" # dev fallback — keep in sync with pyproject.toml
221
+ VERSION = "0.2.29" # dev fallback — keep in sync with pyproject.toml
222
222
 
223
223
  # ── ANSI helpers (used even with rich for non-markdown output) ─────────────
224
224
  from common import C, clr, info, ok, warn, err, stream_thinking, print_tool_start, print_tool_end, sanitize_text
@@ -1514,7 +1514,8 @@ def cmd_bg(args: str, _state, config) -> bool:
1514
1514
  and Telegram simultaneously.
1515
1515
 
1516
1516
  /bg start [--web-port PORT] — spawn detached daemon + webchat
1517
- /bg stop — kill the background daemon
1517
+ /bg stop — kill the background daemon (uses PID file)
1518
+ /bg kill — nuke whatever's on port 5151 (no PID file needed)
1518
1519
  /bg status — is it alive? on which ports?
1519
1520
  /bg attach — print how to attach (tmux on unix, URL on win)
1520
1521
 
@@ -1568,31 +1569,35 @@ def cmd_bg(args: str, _state, config) -> bool:
1568
1569
  return False
1569
1570
 
1570
1571
  # ── /bg status ────────────────────────────────────────────────────────
1572
+ # Source of truth for "is a real detached daemon running": the BG_PID
1573
+ # file. A REPL also binds 5151 but doesn't write the PID file, so we
1574
+ # can distinguish "the user's own REPL" from "a true headless daemon".
1571
1575
  if sub == "status":
1572
1576
  pid = _read_pid()
1573
1577
  alive = _is_alive(pid)
1574
1578
  ipc = _ipc_alive()
1575
1579
  if alive and ipc:
1576
- ok(f"Dulus background: RUNNING")
1580
+ ok(f"Dulus background daemon: RUNNING")
1577
1581
  info(f" PID: {pid}")
1578
1582
  info(f" IPC: 127.0.0.1:{DULUS_IPC_PORT} (responding)")
1579
1583
  info(f" Web: http://127.0.0.1:{config.get('_webchat_port', 5000)}/")
1580
1584
  info(f" Log: {BG_LOG}")
1581
1585
  elif alive and not ipc:
1582
- warn(f"PID {pid} alive but IPC port not responding (still booting?)")
1586
+ warn(f"Daemon PID {pid} alive but IPC not responding (still booting?)")
1583
1587
  elif ipc:
1584
- warn("Another Dulus (REPL or older daemon) is on the IPC port.")
1585
- info(f" IPC: 127.0.0.1:{DULUS_IPC_PORT} (responding, but not our daemon)")
1586
- info(" You can still reach it via `dulus \"...\"` from any shell.")
1587
- info(" /bg start won't spawn a duplicatekill the other one first if needed.")
1588
- # Clear the stale PID file so this warning self-heals next time.
1588
+ # No PID file (or stale) but port is in use this is almost
1589
+ # certainly the user's own REPL serving IPC, not a daemon.
1590
+ info("No background daemon running.")
1591
+ info(f" But port {DULUS_IPC_PORT} is in uselikely your own Dulus REPL.")
1592
+ info(" `dulus \"...\"` from any shell still works (it'll hit your REPL).")
1593
+ info(" For a TRUE headless daemon (Telegram surviving close), start it from")
1594
+ info(" a fresh shell with no REPL open: `dulus --daemon` or `/bg start`.")
1589
1595
  try:
1590
1596
  BG_PID.unlink()
1591
1597
  except FileNotFoundError:
1592
1598
  pass
1593
1599
  else:
1594
- info("Dulus background: NOT RUNNING")
1595
- # Clean up any stale PID file just in case.
1600
+ info("Dulus background daemon: NOT RUNNING")
1596
1601
  try:
1597
1602
  BG_PID.unlink()
1598
1603
  except FileNotFoundError:
@@ -1644,23 +1649,105 @@ def cmd_bg(args: str, _state, config) -> bool:
1644
1649
  info(f" • Tail log: tail -f {BG_LOG}")
1645
1650
  return True
1646
1651
 
1652
+ # ── /bg kill ──────────────────────────────────────────────────────────
1653
+ # Force-stop the background DAEMON only — never the user's own REPL.
1654
+ # We use the BG_PID file as the source of truth: only /bg start writes
1655
+ # it, so it uniquely identifies the detached daemon. If no BG_PID file
1656
+ # exists, we refuse to kill anything (port may be held by a REPL the
1657
+ # user wants to keep). For SIGKILL escalation we use taskkill on Windows.
1658
+ if sub == "kill":
1659
+ f_pid = _read_pid()
1660
+ own_pid = _os.getpid()
1661
+
1662
+ if not f_pid:
1663
+ info("No background daemon to kill (BG_PID file missing).")
1664
+ info(f" If port {DULUS_IPC_PORT} is in use, it's likely your own REPL.")
1665
+ info(" Close your REPL (/exit) to free the port — /bg kill won't touch it.")
1666
+ return True
1667
+
1668
+ if f_pid == own_pid:
1669
+ warn("Refusing to kill the current process (that's me, the REPL you're in).")
1670
+ try:
1671
+ BG_PID.unlink()
1672
+ except FileNotFoundError:
1673
+ pass
1674
+ return True
1675
+
1676
+ if not _is_alive(f_pid):
1677
+ info(f"Daemon PID {f_pid} is already dead. Cleaning up PID file.")
1678
+ try:
1679
+ BG_PID.unlink()
1680
+ except FileNotFoundError:
1681
+ pass
1682
+ return True
1683
+
1684
+ # Send SIGTERM, give it 1s, escalate to SIGKILL/taskkill if it lingers.
1685
+ try:
1686
+ _os.kill(f_pid, _signal.SIGTERM)
1687
+ ok(f"Sent SIGTERM to daemon PID {f_pid}.")
1688
+ except (ProcessLookupError, PermissionError, OSError) as e:
1689
+ err(f"Could not signal PID {f_pid}: {e}")
1690
+ return True
1691
+
1692
+ for _ in range(8):
1693
+ if not _is_alive(f_pid):
1694
+ break
1695
+ _time.sleep(0.25)
1696
+
1697
+ if _is_alive(f_pid):
1698
+ warn(f"PID {f_pid} did not exit on SIGTERM — escalating to SIGKILL.")
1699
+ try:
1700
+ if _sys.platform == "win32":
1701
+ _sp.run(["taskkill", "/F", "/PID", str(f_pid)],
1702
+ capture_output=True, timeout=5)
1703
+ else:
1704
+ _os.kill(f_pid, _signal.SIGKILL)
1705
+ except Exception as e:
1706
+ err(f"SIGKILL failed: {e}")
1707
+ return True
1708
+
1709
+ try:
1710
+ BG_PID.unlink()
1711
+ except FileNotFoundError:
1712
+ pass
1713
+ ok(f"Daemon (PID {f_pid}) stopped.")
1714
+ return True
1715
+
1647
1716
  # ── /bg start ─────────────────────────────────────────────────────────
1648
1717
  if sub == "start":
1649
1718
  # Already running?
1650
1719
  existing_pid = _read_pid()
1651
- if _is_alive(existing_pid) and _ipc_alive():
1720
+ if _is_alive(existing_pid) and _ipc_alive() and existing_pid != _os.getpid():
1652
1721
  info(f"Already running (PID {existing_pid}). Use `/bg status` for details.")
1653
1722
  return True
1654
1723
 
1655
- # Some other Dulus (a REPL on this machine, an old daemon, etc.) is
1656
- # holding the IPC port. Spawning a new daemon would just fail to bind
1657
- # and leave a stale PID. Better: tell the user what's up.
1724
+ # If THIS REPL owns the IPC port, release it first so the spawned
1725
+ # daemon can bind. Without this, /bg start from inside a REPL would
1726
+ # always fail because the very REPL invoking it is holding 5151.
1727
+ # We stop our own IPC server thread, give the OS a moment to free
1728
+ # the socket, and then proceed to spawn the daemon. The REPL keeps
1729
+ # running fine — it just becomes a normal client (its `dulus "..."`
1730
+ # dispatches still work, they just go to the daemon now).
1731
+ if _ipc_alive() and config.get("_ipc_thread") is not None:
1732
+ info("Releasing this REPL's IPC port so the daemon can take over...")
1733
+ config["_ipc_stop"] = True
1734
+ ipc_thread = config.get("_ipc_thread")
1735
+ try:
1736
+ ipc_thread.join(timeout=2.5)
1737
+ except Exception:
1738
+ pass
1739
+ # Clear the marker so a future /bg stop or restart doesn't reuse it.
1740
+ config["_ipc_thread"] = None
1741
+ config.pop("_ipc_listening", None)
1742
+ # Brief sleep to let the OS reclaim the port (TIME_WAIT etc.).
1743
+ _time.sleep(0.6)
1744
+
1745
+ # If something *else* still holds the port (an external Dulus, a
1746
+ # stale daemon from a crash, etc.), refuse cleanly so we don't leave
1747
+ # a stale PID file.
1658
1748
  if _ipc_alive():
1659
- warn(f"Port {DULUS_IPC_PORT} is already in use by another Dulus process.")
1660
- info("If that's a REPL you're running, this Dulus is already reachable")
1661
- info(f" via `dulus \"...\"` from any shell — no /bg start needed.")
1662
- info("If it's a stale daemon, find and kill it manually, then retry.")
1663
- # Clean up the stale PID file so /bg status stops complaining.
1749
+ warn(f"Port {DULUS_IPC_PORT} is in use by another process I don't own.")
1750
+ info("Run `/bg kill` first if it's a stale daemon, or close the other Dulus.")
1664
1751
  try:
1665
1752
  BG_PID.unlink()
1666
1753
  except FileNotFoundError:
@@ -4080,16 +4167,41 @@ def _ipc_server_loop(config, state):
4080
4167
  conn.sendall(payload)
4081
4168
  except Exception:
4082
4169
  pass
4170
+ except (_socket.timeout, TimeoutError, ConnectionResetError, BrokenPipeError, OSError):
4171
+ # Common transient socket errors: client opened conn and walked
4172
+ # away (recv timeout), client killed mid-write, etc. Drop this
4173
+ # connection but keep the server thread running.
4174
+ pass
4175
+ except Exception:
4176
+ # Catch-all so a single bad request never takes down the IPC
4177
+ # server thread (which would silently break /bg start's promise).
4178
+ pass
4083
4179
  finally:
4084
4180
  try:
4085
4181
  conn.close()
4086
4182
  except Exception:
4087
4183
  pass
4088
4184
 
4185
+ # Release the port immediately on shutdown so a daemon spawned right
4186
+ # after `/bg start` can bind without waiting for TIME_WAIT to expire.
4187
+ # SO_LINGER {onoff:1, linger:0} forces an RST close that bypasses
4188
+ # the TIME_WAIT state (cost: any in-flight bytes are dropped, which is
4189
+ # fine — we're not sending anything when we shut down).
4190
+ try:
4191
+ import struct as _struct
4192
+ sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_LINGER,
4193
+ _struct.pack("ii", 1, 0))
4194
+ except Exception:
4195
+ pass
4196
+ try:
4197
+ sock.shutdown(_socket.SHUT_RDWR)
4198
+ except Exception:
4199
+ pass
4089
4200
  try:
4090
4201
  sock.close()
4091
4202
  except Exception:
4092
4203
  pass
4204
+ config["_ipc_listening"] = False
4093
4205
 
4094
4206
 
4095
4207
  def _try_ipc_dispatch(prompt: str, timeout: float = 0.4) -> bool:
@@ -5599,29 +5711,46 @@ def _run_daemon(config: dict) -> None:
5599
5711
  config["_session_id"] = session_id
5600
5712
  config["_last_interaction_time"] = time.time()
5601
5713
 
5602
- # Same callback used by the REPL so Telegram can trigger runs
5603
- # NB: the run_query referenced here lives on the global namespace once the
5604
- # daemon is running with --daemon (we provide a thin wrapper below).
5605
- def _daemon_run_query(msg, is_background=True):
5714
+ # Same callback used by the REPL so Telegram / IPC can trigger runs.
5715
+ # The `agent.run()` signature is (user_message, state, config, system_prompt, ...)
5716
+ # earlier I called it with the wrong arg order + a non-existent
5717
+ # `is_background` kwarg, which made every Telegram/IPC turn raise
5718
+ # silently and never actually answer the user. Fixed now.
5719
+ def _daemon_run_query(msg):
5606
5720
  try:
5607
5721
  from agent import run as agent_run
5608
- for _ in agent_run(state, msg, config, is_background=is_background):
5609
- pass
5722
+ from context import build_system_prompt
5723
+ sys_prompt = build_system_prompt(config)
5724
+ # Append the user message to state so build_system_prompt-aware
5725
+ # turns and history work correctly.
5726
+ for ev in agent_run(msg, state, config, sys_prompt):
5727
+ # Drain the generator — we don't need to render in daemon mode,
5728
+ # the Telegram bridge / IPC server reads the final assistant
5729
+ # message off `state.messages` after this returns.
5730
+ _ = ev
5610
5731
  except Exception as _e:
5611
5732
  err(f"daemon run_query error: {type(_e).__name__}: {_e}")
5612
- config["_run_query_callback"] = lambda msg: _daemon_run_query(msg, is_background=True)
5613
-
5614
- # Auto-start the webchat server alongside the daemon when /bg start asked
5615
- # for it same session, same state. One Dulus, three entry points (CLI
5616
- # via IPC, browser via webchat, Telegram via bridge).
5733
+ config["_run_query_callback"] = _daemon_run_query
5734
+
5735
+ # Auto-start the webchat server alongside the daemon always, by default.
5736
+ # The whole point of daemon mode is "headless Dulus serving every entry
5737
+ # point at once" (CLI via IPC, browser via WebChat, Telegram via bridge).
5738
+ # Skip only if config["webchat_disabled"] is true OR env var
5739
+ # DULUS_DAEMON_NO_WEB=1 is set (escape hatch for users who explicitly
5740
+ # don't want a browser endpoint exposed even on loopback).
5617
5741
  import os as _os_d
5618
- if _os_d.environ.get("DULUS_BG_AUTO_WEBCHAT") == "1":
5619
- config["_bg_start_webchat"] = True
5620
- try:
5621
- config["_webchat_port"] = int(_os_d.environ.get("DULUS_BG_WEBCHAT_PORT", 5000))
5622
- except ValueError:
5623
- pass
5624
- if config.get("_bg_start_webchat"):
5742
+ _no_web = (
5743
+ config.get("webchat_disabled")
5744
+ or _os_d.environ.get("DULUS_DAEMON_NO_WEB") == "1"
5745
+ )
5746
+ if not _no_web:
5747
+ # If /bg start passed an explicit port through env, honor it.
5748
+ env_port = _os_d.environ.get("DULUS_BG_WEBCHAT_PORT")
5749
+ if env_port:
5750
+ try:
5751
+ config["_webchat_port"] = int(env_port)
5752
+ except ValueError:
5753
+ pass
5625
5754
  try:
5626
5755
  import webchat_server as _wc
5627
5756
  _wc_port = int(config.get("_webchat_port", 5000))
@@ -7126,7 +7255,7 @@ _CMD_META: dict[str, tuple[str, list[str]]] = {
7126
7255
  "todo", "in-progress", "done", "blocked"]),
7127
7256
  "proactive": ("Manage proactive background watcher", ["off"]),
7128
7257
  "daemon": ("Toggle daemon — allow external triggers (Telegram) to spawn Dulus", ["on", "off"]),
7129
- "bg": ("Background Dulus — one detached daemon for CLI + Web + Telegram", ["start", "stop", "status", "attach"]),
7258
+ "bg": ("Background Dulus — one detached daemon for CLI + Web + Telegram", ["start", "stop", "kill", "status", "attach"]),
7130
7259
  "lite": ("Toggle lite mode (reduce system prompt)", ["on", "off"]),
7131
7260
  "rtk": ("Toggle RTK token-optimized shell rewriting", ["on", "off"]),
7132
7261
  "cloudsave": ("Cloud-sync sessions to GitHub Gist", ["setup", "auto", "list", "load", "push"]),
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.27"
7
+ version = "0.2.29"
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