dulus 0.2.28__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.28/dulus.egg-info → dulus-0.2.29}/PKG-INFO +2 -2
  2. {dulus-0.2.28 → dulus-0.2.29}/README.md +1 -1
  3. {dulus-0.2.28 → dulus-0.2.29}/docs/news.md +9 -0
  4. {dulus-0.2.28 → dulus-0.2.29/dulus.egg-info}/PKG-INFO +2 -2
  5. {dulus-0.2.28 → dulus-0.2.29}/dulus.py +159 -39
  6. {dulus-0.2.28 → dulus-0.2.29}/pyproject.toml +1 -1
  7. {dulus-0.2.28 → dulus-0.2.29}/LICENSE +0 -0
  8. {dulus-0.2.28 → dulus-0.2.29}/MANIFEST.in +0 -0
  9. {dulus-0.2.28 → dulus-0.2.29}/agent.py +0 -0
  10. {dulus-0.2.28 → dulus-0.2.29}/backend/__init__.py +0 -0
  11. {dulus-0.2.28 → dulus-0.2.29}/backend/compressor.py +0 -0
  12. {dulus-0.2.28 → dulus-0.2.29}/backend/context.py +0 -0
  13. {dulus-0.2.28 → dulus-0.2.29}/backend/githook.py +0 -0
  14. {dulus-0.2.28 → dulus-0.2.29}/backend/marketplace.py +0 -0
  15. {dulus-0.2.28 → dulus-0.2.29}/backend/mempalace_bridge.py +0 -0
  16. {dulus-0.2.28 → dulus-0.2.29}/backend/personas.py +0 -0
  17. {dulus-0.2.28 → dulus-0.2.29}/backend/plugins.py +0 -0
  18. {dulus-0.2.28 → dulus-0.2.29}/backend/server.py +0 -0
  19. {dulus-0.2.28 → dulus-0.2.29}/backend/tasks.py +0 -0
  20. {dulus-0.2.28 → dulus-0.2.29}/batch_api.py +0 -0
  21. {dulus-0.2.28 → dulus-0.2.29}/checkpoint/__init__.py +0 -0
  22. {dulus-0.2.28 → dulus-0.2.29}/checkpoint/hooks.py +0 -0
  23. {dulus-0.2.28 → dulus-0.2.29}/checkpoint/store.py +0 -0
  24. {dulus-0.2.28 → dulus-0.2.29}/checkpoint/types.py +0 -0
  25. {dulus-0.2.28 → dulus-0.2.29}/claude_code_watcher.py +0 -0
  26. {dulus-0.2.28 → dulus-0.2.29}/clipboard_utils.py +0 -0
  27. {dulus-0.2.28 → dulus-0.2.29}/cloudsave.py +0 -0
  28. {dulus-0.2.28 → dulus-0.2.29}/common.py +0 -0
  29. {dulus-0.2.28 → dulus-0.2.29}/compaction.py +0 -0
  30. {dulus-0.2.28 → dulus-0.2.29}/config.py +0 -0
  31. {dulus-0.2.28 → dulus-0.2.29}/context.py +0 -0
  32. {dulus-0.2.28 → dulus-0.2.29}/data/__init__.py +0 -0
  33. {dulus-0.2.28 → dulus-0.2.29}/data/active_persona.json +0 -0
  34. {dulus-0.2.28 → dulus-0.2.29}/data/context.json +0 -0
  35. {dulus-0.2.28 → dulus-0.2.29}/data/marketplace.json +0 -0
  36. {dulus-0.2.28 → dulus-0.2.29}/data/personas.json +0 -0
  37. {dulus-0.2.28 → dulus-0.2.29}/data/plugins/__init__.py +0 -0
  38. {dulus-0.2.28 → dulus-0.2.29}/data/plugins/composio/__init__.py +0 -0
  39. {dulus-0.2.28 → dulus-0.2.29}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  40. {dulus-0.2.28 → dulus-0.2.29}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  41. {dulus-0.2.28 → dulus-0.2.29}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  42. {dulus-0.2.28 → dulus-0.2.29}/data/plugins/composio/plugin.json +0 -0
  43. {dulus-0.2.28 → dulus-0.2.29}/data/plugins/composio/plugin_tool.py +0 -0
  44. {dulus-0.2.28 → dulus-0.2.29}/data/tasks.json +0 -0
  45. {dulus-0.2.28 → dulus-0.2.29}/docs/README.md +0 -0
  46. {dulus-0.2.28 → dulus-0.2.29}/docs/__init__.py +0 -0
  47. {dulus-0.2.28 → dulus-0.2.29}/docs/api.html +0 -0
  48. {dulus-0.2.28 → dulus-0.2.29}/docs/architecture.md +0 -0
  49. {dulus-0.2.28 → dulus-0.2.29}/docs/azure-speech-template.json +0 -0
  50. {dulus-0.2.28 → dulus-0.2.29}/docs/dashboard/index.html +0 -0
  51. {dulus-0.2.28 → dulus-0.2.29}/docs/divider.svg +0 -0
  52. {dulus-0.2.28 → dulus-0.2.29}/docs/generate.py +0 -0
  53. {dulus-0.2.28 → dulus-0.2.29}/docs/hero.svg +0 -0
  54. {dulus-0.2.28 → dulus-0.2.29}/docs/index.html +0 -0
  55. {dulus-0.2.28 → dulus-0.2.29}/docs/nvidia-models.svg +0 -0
  56. {dulus-0.2.28 → dulus-0.2.29}/docs/particle-playground.html +0 -0
  57. {dulus-0.2.28 → dulus-0.2.29}/docs/personas/index.html +0 -0
  58. {dulus-0.2.28 → dulus-0.2.29}/docs/poetry-banner.png +0 -0
  59. {dulus-0.2.28 → dulus-0.2.29}/docs/preview.html +0 -0
  60. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-agents.svg +0 -0
  61. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-brainstorm.svg +0 -0
  62. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-bridges.svg +0 -0
  63. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-features.svg +0 -0
  64. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-freetier.svg +0 -0
  65. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-memory.svg +0 -0
  66. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-models.svg +0 -0
  67. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-perms.svg +0 -0
  68. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-plugins.svg +0 -0
  69. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-quickstart.svg +0 -0
  70. {dulus-0.2.28 → dulus-0.2.29}/docs/sec-ssj.svg +0 -0
  71. {dulus-0.2.28 → dulus-0.2.29}/docs/spinners.svg +0 -0
  72. {dulus-0.2.28 → dulus-0.2.29}/docs/split-pane.svg +0 -0
  73. {dulus-0.2.28 → dulus-0.2.29}/docs/terminal-boot.svg +0 -0
  74. {dulus-0.2.28 → dulus-0.2.29}/docs/uploads/particle-playground.html +0 -0
  75. {dulus-0.2.28 → dulus-0.2.29}/dulus.egg-info/SOURCES.txt +0 -0
  76. {dulus-0.2.28 → dulus-0.2.29}/dulus.egg-info/dependency_links.txt +0 -0
  77. {dulus-0.2.28 → dulus-0.2.29}/dulus.egg-info/entry_points.txt +0 -0
  78. {dulus-0.2.28 → dulus-0.2.29}/dulus.egg-info/requires.txt +0 -0
  79. {dulus-0.2.28 → dulus-0.2.29}/dulus.egg-info/top_level.txt +0 -0
  80. {dulus-0.2.28 → dulus-0.2.29}/dulus_gui.py +0 -0
  81. {dulus-0.2.28 → dulus-0.2.29}/dulus_mcp/__init__.py +0 -0
  82. {dulus-0.2.28 → dulus-0.2.29}/dulus_mcp/client.py +0 -0
  83. {dulus-0.2.28 → dulus-0.2.29}/dulus_mcp/config.py +0 -0
  84. {dulus-0.2.28 → dulus-0.2.29}/dulus_mcp/tools.py +0 -0
  85. {dulus-0.2.28 → dulus-0.2.29}/dulus_mcp/types.py +0 -0
  86. {dulus-0.2.28 → dulus-0.2.29}/gui/__init__.py +0 -0
  87. {dulus-0.2.28 → dulus-0.2.29}/gui/agent_bridge.py +0 -0
  88. {dulus-0.2.28 → dulus-0.2.29}/gui/chat_widget.py +0 -0
  89. {dulus-0.2.28 → dulus-0.2.29}/gui/main_window.py +0 -0
  90. {dulus-0.2.28 → dulus-0.2.29}/gui/personas.py +0 -0
  91. {dulus-0.2.28 → dulus-0.2.29}/gui/session_utils.py +0 -0
  92. {dulus-0.2.28 → dulus-0.2.29}/gui/settings_dialog.py +0 -0
  93. {dulus-0.2.28 → dulus-0.2.29}/gui/sidebar.py +0 -0
  94. {dulus-0.2.28 → dulus-0.2.29}/gui/tasks_view.py +0 -0
  95. {dulus-0.2.28 → dulus-0.2.29}/gui/themes.py +0 -0
  96. {dulus-0.2.28 → dulus-0.2.29}/gui/tool_panel.py +0 -0
  97. {dulus-0.2.28 → dulus-0.2.29}/input.py +0 -0
  98. {dulus-0.2.28 → dulus-0.2.29}/license_manager.py +0 -0
  99. {dulus-0.2.28 → dulus-0.2.29}/memory/__init__.py +0 -0
  100. {dulus-0.2.28 → dulus-0.2.29}/memory/audit.py +0 -0
  101. {dulus-0.2.28 → dulus-0.2.29}/memory/consolidator.py +0 -0
  102. {dulus-0.2.28 → dulus-0.2.29}/memory/context.py +0 -0
  103. {dulus-0.2.28 → dulus-0.2.29}/memory/offload.py +0 -0
  104. {dulus-0.2.28 → dulus-0.2.29}/memory/palace.py +0 -0
  105. {dulus-0.2.28 → dulus-0.2.29}/memory/scan.py +0 -0
  106. {dulus-0.2.28 → dulus-0.2.29}/memory/sessions.py +0 -0
  107. {dulus-0.2.28 → dulus-0.2.29}/memory/store.py +0 -0
  108. {dulus-0.2.28 → dulus-0.2.29}/memory/tools.py +0 -0
  109. {dulus-0.2.28 → dulus-0.2.29}/memory/types.py +0 -0
  110. {dulus-0.2.28 → dulus-0.2.29}/memory/vector_search.py +0 -0
  111. {dulus-0.2.28 → dulus-0.2.29}/multi_agent/__init__.py +0 -0
  112. {dulus-0.2.28 → dulus-0.2.29}/multi_agent/subagent.py +0 -0
  113. {dulus-0.2.28 → dulus-0.2.29}/multi_agent/tools.py +0 -0
  114. {dulus-0.2.28 → dulus-0.2.29}/offload_helper.py +0 -0
  115. {dulus-0.2.28 → dulus-0.2.29}/plugin/__init__.py +0 -0
  116. {dulus-0.2.28 → dulus-0.2.29}/plugin/autoadapter.py +0 -0
  117. {dulus-0.2.28 → dulus-0.2.29}/plugin/loader.py +0 -0
  118. {dulus-0.2.28 → dulus-0.2.29}/plugin/recommend.py +0 -0
  119. {dulus-0.2.28 → dulus-0.2.29}/plugin/store.py +0 -0
  120. {dulus-0.2.28 → dulus-0.2.29}/plugin/types.py +0 -0
  121. {dulus-0.2.28 → dulus-0.2.29}/providers.py +0 -0
  122. {dulus-0.2.28 → dulus-0.2.29}/setup.cfg +0 -0
  123. {dulus-0.2.28 → dulus-0.2.29}/skill/__init__.py +0 -0
  124. {dulus-0.2.28 → dulus-0.2.29}/skill/builtin.py +0 -0
  125. {dulus-0.2.28 → dulus-0.2.29}/skill/clawhub.py +0 -0
  126. {dulus-0.2.28 → dulus-0.2.29}/skill/executor.py +0 -0
  127. {dulus-0.2.28 → dulus-0.2.29}/skill/loader.py +0 -0
  128. {dulus-0.2.28 → dulus-0.2.29}/skill/tools.py +0 -0
  129. {dulus-0.2.28 → dulus-0.2.29}/skills.py +0 -0
  130. {dulus-0.2.28 → dulus-0.2.29}/spinner.py +0 -0
  131. {dulus-0.2.28 → dulus-0.2.29}/string_utils.py +0 -0
  132. {dulus-0.2.28 → dulus-0.2.29}/subagent.py +0 -0
  133. {dulus-0.2.28 → dulus-0.2.29}/task/__init__.py +0 -0
  134. {dulus-0.2.28 → dulus-0.2.29}/task/store.py +0 -0
  135. {dulus-0.2.28 → dulus-0.2.29}/task/tools.py +0 -0
  136. {dulus-0.2.28 → dulus-0.2.29}/task/types.py +0 -0
  137. {dulus-0.2.28 → dulus-0.2.29}/tests/test_checkpoint.py +0 -0
  138. {dulus-0.2.28 → dulus-0.2.29}/tests/test_compaction.py +0 -0
  139. {dulus-0.2.28 → dulus-0.2.29}/tests/test_diff_view.py +0 -0
  140. {dulus-0.2.28 → dulus-0.2.29}/tests/test_injection_fix.py +0 -0
  141. {dulus-0.2.28 → dulus-0.2.29}/tests/test_license.py +0 -0
  142. {dulus-0.2.28 → dulus-0.2.29}/tests/test_mcp.py +0 -0
  143. {dulus-0.2.28 → dulus-0.2.29}/tests/test_memory.py +0 -0
  144. {dulus-0.2.28 → dulus-0.2.29}/tests/test_plugin.py +0 -0
  145. {dulus-0.2.28 → dulus-0.2.29}/tests/test_skills.py +0 -0
  146. {dulus-0.2.28 → dulus-0.2.29}/tests/test_subagent.py +0 -0
  147. {dulus-0.2.28 → dulus-0.2.29}/tests/test_task.py +0 -0
  148. {dulus-0.2.28 → dulus-0.2.29}/tests/test_telegram_buffer.py +0 -0
  149. {dulus-0.2.28 → dulus-0.2.29}/tests/test_tool_registry.py +0 -0
  150. {dulus-0.2.28 → dulus-0.2.29}/tests/test_voice.py +0 -0
  151. {dulus-0.2.28 → dulus-0.2.29}/tmux_offloader.py +0 -0
  152. {dulus-0.2.28 → dulus-0.2.29}/tmux_tools.py +0 -0
  153. {dulus-0.2.28 → dulus-0.2.29}/tool_registry.py +0 -0
  154. {dulus-0.2.28 → dulus-0.2.29}/tools.py +0 -0
  155. {dulus-0.2.28 → dulus-0.2.29}/ui/__init__.py +0 -0
  156. {dulus-0.2.28 → dulus-0.2.29}/ui/input.py +0 -0
  157. {dulus-0.2.28 → dulus-0.2.29}/ui/render.py +0 -0
  158. {dulus-0.2.28 → dulus-0.2.29}/voice/__init__.py +0 -0
  159. {dulus-0.2.28 → dulus-0.2.29}/voice/keyterms.py +0 -0
  160. {dulus-0.2.28 → dulus-0.2.29}/voice/recorder.py +0 -0
  161. {dulus-0.2.28 → dulus-0.2.29}/voice/stt.py +0 -0
  162. {dulus-0.2.28 → dulus-0.2.29}/voice/tts.py +0 -0
  163. {dulus-0.2.28 → dulus-0.2.29}/webchat.py +0 -0
  164. {dulus-0.2.28 → 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.28
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.28-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.28-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,15 @@
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
+
6
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.
7
16
 
8
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.28
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.28-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.28" # 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:
@@ -4095,10 +4182,26 @@ def _ipc_server_loop(config, state):
4095
4182
  except Exception:
4096
4183
  pass
4097
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
4098
4200
  try:
4099
4201
  sock.close()
4100
4202
  except Exception:
4101
4203
  pass
4204
+ config["_ipc_listening"] = False
4102
4205
 
4103
4206
 
4104
4207
  def _try_ipc_dispatch(prompt: str, timeout: float = 0.4) -> bool:
@@ -5608,29 +5711,46 @@ def _run_daemon(config: dict) -> None:
5608
5711
  config["_session_id"] = session_id
5609
5712
  config["_last_interaction_time"] = time.time()
5610
5713
 
5611
- # Same callback used by the REPL so Telegram can trigger runs
5612
- # NB: the run_query referenced here lives on the global namespace once the
5613
- # daemon is running with --daemon (we provide a thin wrapper below).
5614
- 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):
5615
5720
  try:
5616
5721
  from agent import run as agent_run
5617
- for _ in agent_run(state, msg, config, is_background=is_background):
5618
- 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
5619
5731
  except Exception as _e:
5620
5732
  err(f"daemon run_query error: {type(_e).__name__}: {_e}")
5621
- config["_run_query_callback"] = lambda msg: _daemon_run_query(msg, is_background=True)
5622
-
5623
- # Auto-start the webchat server alongside the daemon when /bg start asked
5624
- # for it same session, same state. One Dulus, three entry points (CLI
5625
- # 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).
5626
5741
  import os as _os_d
5627
- if _os_d.environ.get("DULUS_BG_AUTO_WEBCHAT") == "1":
5628
- config["_bg_start_webchat"] = True
5629
- try:
5630
- config["_webchat_port"] = int(_os_d.environ.get("DULUS_BG_WEBCHAT_PORT", 5000))
5631
- except ValueError:
5632
- pass
5633
- 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
5634
5754
  try:
5635
5755
  import webchat_server as _wc
5636
5756
  _wc_port = int(config.get("_webchat_port", 5000))
@@ -7135,7 +7255,7 @@ _CMD_META: dict[str, tuple[str, list[str]]] = {
7135
7255
  "todo", "in-progress", "done", "blocked"]),
7136
7256
  "proactive": ("Manage proactive background watcher", ["off"]),
7137
7257
  "daemon": ("Toggle daemon — allow external triggers (Telegram) to spawn Dulus", ["on", "off"]),
7138
- "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"]),
7139
7259
  "lite": ("Toggle lite mode (reduce system prompt)", ["on", "off"]),
7140
7260
  "rtk": ("Toggle RTK token-optimized shell rewriting", ["on", "off"]),
7141
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.28"
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