dulus 0.2.33__tar.gz → 0.2.35__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.33/dulus.egg-info → dulus-0.2.35}/PKG-INFO +1 -1
  2. {dulus-0.2.33 → dulus-0.2.35/dulus.egg-info}/PKG-INFO +1 -1
  3. {dulus-0.2.33 → dulus-0.2.35}/dulus.py +92 -9
  4. {dulus-0.2.33 → dulus-0.2.35}/gui/session_utils.py +7 -0
  5. {dulus-0.2.33 → dulus-0.2.35}/pyproject.toml +1 -1
  6. {dulus-0.2.33 → dulus-0.2.35}/tools.py +1 -1
  7. {dulus-0.2.33 → dulus-0.2.35}/LICENSE +0 -0
  8. {dulus-0.2.33 → dulus-0.2.35}/MANIFEST.in +0 -0
  9. {dulus-0.2.33 → dulus-0.2.35}/README.md +0 -0
  10. {dulus-0.2.33 → dulus-0.2.35}/agent.py +0 -0
  11. {dulus-0.2.33 → dulus-0.2.35}/backend/__init__.py +0 -0
  12. {dulus-0.2.33 → dulus-0.2.35}/backend/compressor.py +0 -0
  13. {dulus-0.2.33 → dulus-0.2.35}/backend/context.py +0 -0
  14. {dulus-0.2.33 → dulus-0.2.35}/backend/githook.py +0 -0
  15. {dulus-0.2.33 → dulus-0.2.35}/backend/marketplace.py +0 -0
  16. {dulus-0.2.33 → dulus-0.2.35}/backend/mempalace_bridge.py +0 -0
  17. {dulus-0.2.33 → dulus-0.2.35}/backend/personas.py +0 -0
  18. {dulus-0.2.33 → dulus-0.2.35}/backend/plugins.py +0 -0
  19. {dulus-0.2.33 → dulus-0.2.35}/backend/server.py +0 -0
  20. {dulus-0.2.33 → dulus-0.2.35}/backend/tasks.py +0 -0
  21. {dulus-0.2.33 → dulus-0.2.35}/batch_api.py +0 -0
  22. {dulus-0.2.33 → dulus-0.2.35}/checkpoint/__init__.py +0 -0
  23. {dulus-0.2.33 → dulus-0.2.35}/checkpoint/hooks.py +0 -0
  24. {dulus-0.2.33 → dulus-0.2.35}/checkpoint/store.py +0 -0
  25. {dulus-0.2.33 → dulus-0.2.35}/checkpoint/types.py +0 -0
  26. {dulus-0.2.33 → dulus-0.2.35}/claude_code_watcher.py +0 -0
  27. {dulus-0.2.33 → dulus-0.2.35}/clipboard_utils.py +0 -0
  28. {dulus-0.2.33 → dulus-0.2.35}/cloudsave.py +0 -0
  29. {dulus-0.2.33 → dulus-0.2.35}/common.py +0 -0
  30. {dulus-0.2.33 → dulus-0.2.35}/compaction.py +0 -0
  31. {dulus-0.2.33 → dulus-0.2.35}/config.py +0 -0
  32. {dulus-0.2.33 → dulus-0.2.35}/context.py +0 -0
  33. {dulus-0.2.33 → dulus-0.2.35}/data/__init__.py +0 -0
  34. {dulus-0.2.33 → dulus-0.2.35}/data/active_persona.json +0 -0
  35. {dulus-0.2.33 → dulus-0.2.35}/data/context.json +0 -0
  36. {dulus-0.2.33 → dulus-0.2.35}/data/marketplace.json +0 -0
  37. {dulus-0.2.33 → dulus-0.2.35}/data/personas.json +0 -0
  38. {dulus-0.2.33 → dulus-0.2.35}/data/plugins/__init__.py +0 -0
  39. {dulus-0.2.33 → dulus-0.2.35}/data/plugins/composio/__init__.py +0 -0
  40. {dulus-0.2.33 → dulus-0.2.35}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  41. {dulus-0.2.33 → dulus-0.2.35}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  42. {dulus-0.2.33 → dulus-0.2.35}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  43. {dulus-0.2.33 → dulus-0.2.35}/data/plugins/composio/plugin.json +0 -0
  44. {dulus-0.2.33 → dulus-0.2.35}/data/plugins/composio/plugin_tool.py +0 -0
  45. {dulus-0.2.33 → dulus-0.2.35}/data/tasks.json +0 -0
  46. {dulus-0.2.33 → dulus-0.2.35}/docs/README.md +0 -0
  47. {dulus-0.2.33 → dulus-0.2.35}/docs/__init__.py +0 -0
  48. {dulus-0.2.33 → dulus-0.2.35}/docs/api.html +0 -0
  49. {dulus-0.2.33 → dulus-0.2.35}/docs/architecture.md +0 -0
  50. {dulus-0.2.33 → dulus-0.2.35}/docs/azure-speech-template.json +0 -0
  51. {dulus-0.2.33 → dulus-0.2.35}/docs/dashboard/index.html +0 -0
  52. {dulus-0.2.33 → dulus-0.2.35}/docs/divider.svg +0 -0
  53. {dulus-0.2.33 → dulus-0.2.35}/docs/generate.py +0 -0
  54. {dulus-0.2.33 → dulus-0.2.35}/docs/hero.svg +0 -0
  55. {dulus-0.2.33 → dulus-0.2.35}/docs/index.html +0 -0
  56. {dulus-0.2.33 → dulus-0.2.35}/docs/news.md +0 -0
  57. {dulus-0.2.33 → dulus-0.2.35}/docs/nvidia-models.svg +0 -0
  58. {dulus-0.2.33 → dulus-0.2.35}/docs/particle-playground.html +0 -0
  59. {dulus-0.2.33 → dulus-0.2.35}/docs/personas/index.html +0 -0
  60. {dulus-0.2.33 → dulus-0.2.35}/docs/poetry-banner.png +0 -0
  61. {dulus-0.2.33 → dulus-0.2.35}/docs/preview.html +0 -0
  62. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-agents.svg +0 -0
  63. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-brainstorm.svg +0 -0
  64. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-bridges.svg +0 -0
  65. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-features.svg +0 -0
  66. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-freetier.svg +0 -0
  67. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-memory.svg +0 -0
  68. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-models.svg +0 -0
  69. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-perms.svg +0 -0
  70. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-plugins.svg +0 -0
  71. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-quickstart.svg +0 -0
  72. {dulus-0.2.33 → dulus-0.2.35}/docs/sec-ssj.svg +0 -0
  73. {dulus-0.2.33 → dulus-0.2.35}/docs/spinners.svg +0 -0
  74. {dulus-0.2.33 → dulus-0.2.35}/docs/split-pane.svg +0 -0
  75. {dulus-0.2.33 → dulus-0.2.35}/docs/terminal-boot.svg +0 -0
  76. {dulus-0.2.33 → dulus-0.2.35}/docs/uploads/particle-playground.html +0 -0
  77. {dulus-0.2.33 → dulus-0.2.35}/dulus.egg-info/SOURCES.txt +0 -0
  78. {dulus-0.2.33 → dulus-0.2.35}/dulus.egg-info/dependency_links.txt +0 -0
  79. {dulus-0.2.33 → dulus-0.2.35}/dulus.egg-info/entry_points.txt +0 -0
  80. {dulus-0.2.33 → dulus-0.2.35}/dulus.egg-info/requires.txt +0 -0
  81. {dulus-0.2.33 → dulus-0.2.35}/dulus.egg-info/top_level.txt +0 -0
  82. {dulus-0.2.33 → dulus-0.2.35}/dulus_gui.py +0 -0
  83. {dulus-0.2.33 → dulus-0.2.35}/dulus_mcp/__init__.py +0 -0
  84. {dulus-0.2.33 → dulus-0.2.35}/dulus_mcp/client.py +0 -0
  85. {dulus-0.2.33 → dulus-0.2.35}/dulus_mcp/config.py +0 -0
  86. {dulus-0.2.33 → dulus-0.2.35}/dulus_mcp/tools.py +0 -0
  87. {dulus-0.2.33 → dulus-0.2.35}/dulus_mcp/types.py +0 -0
  88. {dulus-0.2.33 → dulus-0.2.35}/gui/__init__.py +0 -0
  89. {dulus-0.2.33 → dulus-0.2.35}/gui/agent_bridge.py +0 -0
  90. {dulus-0.2.33 → dulus-0.2.35}/gui/chat_widget.py +0 -0
  91. {dulus-0.2.33 → dulus-0.2.35}/gui/main_window.py +0 -0
  92. {dulus-0.2.33 → dulus-0.2.35}/gui/personas.py +0 -0
  93. {dulus-0.2.33 → dulus-0.2.35}/gui/settings_dialog.py +0 -0
  94. {dulus-0.2.33 → dulus-0.2.35}/gui/sidebar.py +0 -0
  95. {dulus-0.2.33 → dulus-0.2.35}/gui/tasks_view.py +0 -0
  96. {dulus-0.2.33 → dulus-0.2.35}/gui/themes.py +0 -0
  97. {dulus-0.2.33 → dulus-0.2.35}/gui/tool_panel.py +0 -0
  98. {dulus-0.2.33 → dulus-0.2.35}/input.py +0 -0
  99. {dulus-0.2.33 → dulus-0.2.35}/license_manager.py +0 -0
  100. {dulus-0.2.33 → dulus-0.2.35}/memory/__init__.py +0 -0
  101. {dulus-0.2.33 → dulus-0.2.35}/memory/audit.py +0 -0
  102. {dulus-0.2.33 → dulus-0.2.35}/memory/consolidator.py +0 -0
  103. {dulus-0.2.33 → dulus-0.2.35}/memory/context.py +0 -0
  104. {dulus-0.2.33 → dulus-0.2.35}/memory/offload.py +0 -0
  105. {dulus-0.2.33 → dulus-0.2.35}/memory/palace.py +0 -0
  106. {dulus-0.2.33 → dulus-0.2.35}/memory/scan.py +0 -0
  107. {dulus-0.2.33 → dulus-0.2.35}/memory/sessions.py +0 -0
  108. {dulus-0.2.33 → dulus-0.2.35}/memory/store.py +0 -0
  109. {dulus-0.2.33 → dulus-0.2.35}/memory/tools.py +0 -0
  110. {dulus-0.2.33 → dulus-0.2.35}/memory/types.py +0 -0
  111. {dulus-0.2.33 → dulus-0.2.35}/memory/vector_search.py +0 -0
  112. {dulus-0.2.33 → dulus-0.2.35}/multi_agent/__init__.py +0 -0
  113. {dulus-0.2.33 → dulus-0.2.35}/multi_agent/subagent.py +0 -0
  114. {dulus-0.2.33 → dulus-0.2.35}/multi_agent/tools.py +0 -0
  115. {dulus-0.2.33 → dulus-0.2.35}/offload_helper.py +0 -0
  116. {dulus-0.2.33 → dulus-0.2.35}/plugin/__init__.py +0 -0
  117. {dulus-0.2.33 → dulus-0.2.35}/plugin/autoadapter.py +0 -0
  118. {dulus-0.2.33 → dulus-0.2.35}/plugin/loader.py +0 -0
  119. {dulus-0.2.33 → dulus-0.2.35}/plugin/recommend.py +0 -0
  120. {dulus-0.2.33 → dulus-0.2.35}/plugin/store.py +0 -0
  121. {dulus-0.2.33 → dulus-0.2.35}/plugin/types.py +0 -0
  122. {dulus-0.2.33 → dulus-0.2.35}/providers.py +0 -0
  123. {dulus-0.2.33 → dulus-0.2.35}/setup.cfg +0 -0
  124. {dulus-0.2.33 → dulus-0.2.35}/skill/__init__.py +0 -0
  125. {dulus-0.2.33 → dulus-0.2.35}/skill/builtin.py +0 -0
  126. {dulus-0.2.33 → dulus-0.2.35}/skill/clawhub.py +0 -0
  127. {dulus-0.2.33 → dulus-0.2.35}/skill/executor.py +0 -0
  128. {dulus-0.2.33 → dulus-0.2.35}/skill/loader.py +0 -0
  129. {dulus-0.2.33 → dulus-0.2.35}/skill/tools.py +0 -0
  130. {dulus-0.2.33 → dulus-0.2.35}/skills.py +0 -0
  131. {dulus-0.2.33 → dulus-0.2.35}/spinner.py +0 -0
  132. {dulus-0.2.33 → dulus-0.2.35}/string_utils.py +0 -0
  133. {dulus-0.2.33 → dulus-0.2.35}/subagent.py +0 -0
  134. {dulus-0.2.33 → dulus-0.2.35}/task/__init__.py +0 -0
  135. {dulus-0.2.33 → dulus-0.2.35}/task/store.py +0 -0
  136. {dulus-0.2.33 → dulus-0.2.35}/task/tools.py +0 -0
  137. {dulus-0.2.33 → dulus-0.2.35}/task/types.py +0 -0
  138. {dulus-0.2.33 → dulus-0.2.35}/tests/test_checkpoint.py +0 -0
  139. {dulus-0.2.33 → dulus-0.2.35}/tests/test_compaction.py +0 -0
  140. {dulus-0.2.33 → dulus-0.2.35}/tests/test_diff_view.py +0 -0
  141. {dulus-0.2.33 → dulus-0.2.35}/tests/test_injection_fix.py +0 -0
  142. {dulus-0.2.33 → dulus-0.2.35}/tests/test_license.py +0 -0
  143. {dulus-0.2.33 → dulus-0.2.35}/tests/test_mcp.py +0 -0
  144. {dulus-0.2.33 → dulus-0.2.35}/tests/test_memory.py +0 -0
  145. {dulus-0.2.33 → dulus-0.2.35}/tests/test_plugin.py +0 -0
  146. {dulus-0.2.33 → dulus-0.2.35}/tests/test_skills.py +0 -0
  147. {dulus-0.2.33 → dulus-0.2.35}/tests/test_subagent.py +0 -0
  148. {dulus-0.2.33 → dulus-0.2.35}/tests/test_task.py +0 -0
  149. {dulus-0.2.33 → dulus-0.2.35}/tests/test_telegram_buffer.py +0 -0
  150. {dulus-0.2.33 → dulus-0.2.35}/tests/test_tool_registry.py +0 -0
  151. {dulus-0.2.33 → dulus-0.2.35}/tests/test_voice.py +0 -0
  152. {dulus-0.2.33 → dulus-0.2.35}/tmux_offloader.py +0 -0
  153. {dulus-0.2.33 → dulus-0.2.35}/tmux_tools.py +0 -0
  154. {dulus-0.2.33 → dulus-0.2.35}/tool_registry.py +0 -0
  155. {dulus-0.2.33 → dulus-0.2.35}/ui/__init__.py +0 -0
  156. {dulus-0.2.33 → dulus-0.2.35}/ui/input.py +0 -0
  157. {dulus-0.2.33 → dulus-0.2.35}/ui/render.py +0 -0
  158. {dulus-0.2.33 → dulus-0.2.35}/voice/__init__.py +0 -0
  159. {dulus-0.2.33 → dulus-0.2.35}/voice/keyterms.py +0 -0
  160. {dulus-0.2.33 → dulus-0.2.35}/voice/recorder.py +0 -0
  161. {dulus-0.2.33 → dulus-0.2.35}/voice/stt.py +0 -0
  162. {dulus-0.2.33 → dulus-0.2.35}/voice/tts.py +0 -0
  163. {dulus-0.2.33 → dulus-0.2.35}/webchat.py +0 -0
  164. {dulus-0.2.33 → dulus-0.2.35}/webchat_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.33
3
+ Version: 0.2.35
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.33
3
+ Version: 0.2.35
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
@@ -154,11 +154,11 @@ if str(DULUS_CODE_ROOT) not in sys.path:
154
154
  from tools import ask_input_interactive, _tg_thread_local, _is_in_tg_turn
155
155
  import input as dulus_input
156
156
  try:
157
- import paste_placeholders as _paste_ph
157
+ import paste_placeholders as _paste_ph # type: ignore
158
158
  except ImportError:
159
159
  _paste_ph = None # type: ignore[assignment]
160
160
  try:
161
- import git_prompt as _git_prompt
161
+ import git_prompt as _git_prompt # type: ignore
162
162
  except ImportError:
163
163
  _git_prompt = None # type: ignore[assignment]
164
164
  try:
@@ -1187,7 +1187,9 @@ def save_latest(args: str, state, config=None) -> bool:
1187
1187
 
1188
1188
  import uuid
1189
1189
  now = datetime.now()
1190
- sid = uuid.uuid4().hex[:8]
1190
+ sid = cfg.get("_session_id") or uuid.uuid4().hex[:8]
1191
+ # Ensure config has it for consistency
1192
+ cfg["_session_id"] = sid
1191
1193
  ts = now.strftime("%H%M%S")
1192
1194
  date_str = now.strftime("%Y-%m-%d")
1193
1195
  data = _build_session_data(state, session_id=sid)
@@ -1201,6 +1203,14 @@ def save_latest(args: str, state, config=None) -> bool:
1201
1203
  # 2. daily/YYYY-MM-DD/session_HHMMSS_sid.json
1202
1204
  day_dir = DAILY_DIR / date_str
1203
1205
  day_dir.mkdir(parents=True, exist_ok=True)
1206
+
1207
+ # Delete older copies of this same session ID to prevent duplication
1208
+ for old_copy in day_dir.glob(f"session_*_{sid}.json"):
1209
+ try:
1210
+ old_copy.unlink()
1211
+ except Exception:
1212
+ pass
1213
+
1204
1214
  daily_path = day_dir / f"session_{ts}_{sid}.json"
1205
1215
  daily_path.write_text(payload)
1206
1216
 
@@ -3856,7 +3866,7 @@ def cmd_exit(_args: str, _state, config) -> bool:
3856
3866
  session_data = _build_session_data(_state)
3857
3867
  gist_id, err_msg = upload_session(
3858
3868
  session_data, config["gist_token"],
3859
- existing_gist_id=config.get("cloudsave_last_gist_id"),
3869
+ gist_id=config.get("cloudsave_last_gist_id"),
3860
3870
  )
3861
3871
  if err_msg:
3862
3872
  err(f"Cloud sync failed: {err_msg}")
@@ -5932,19 +5942,85 @@ def _run_daemon(config: dict) -> None:
5932
5942
  # `is_background` kwarg, which made every Telegram/IPC turn raise
5933
5943
  # silently and never actually answer the user. Fixed now.
5934
5944
  def _daemon_run_query(msg):
5945
+ qlock = config.get("_query_lock")
5946
+ if qlock:
5947
+ qlock.acquire()
5935
5948
  try:
5949
+ import sys
5950
+ import checkpoint as ckpt
5936
5951
  from agent import run as agent_run
5937
5952
  from context import build_system_prompt
5953
+ from dulus import save_latest, _tg_get_chat_ids, _telegram_stop, _tg_send
5954
+
5938
5955
  sys_prompt = build_system_prompt(config)
5939
- # Append the user message to state so build_system_prompt-aware
5940
- # turns and history work correctly.
5956
+ is_telegram_turn = config.get("_telegram_incoming", False)
5957
+ # Basic heuristic: if the message starts with System Automated Event, it's a background event
5958
+ is_background = msg.startswith("(System Automated Event)")
5959
+
5960
+ if is_background and not is_telegram_turn:
5961
+ ttok = config.get("telegram_token")
5962
+ _tids = _tg_get_chat_ids(config)
5963
+ tchat = config.get("_active_tg_chat_id") or (_tids[0] if _tids else 0)
5964
+ if ttok and tchat and _telegram_stop and not _telegram_stop.is_set():
5965
+ import threading as _tg_thread
5966
+ from dulus import _tg_send
5967
+ _tg_thread.Thread(target=_tg_send, args=(ttok, tchat, f"⚙ {msg}"), daemon=True).start()
5968
+
5941
5969
  for ev in agent_run(msg, state, config, sys_prompt):
5942
- # Drain the generator we don't need to render in daemon mode,
5943
- # the Telegram bridge / IPC server reads the final assistant
5944
- # message off `state.messages` after this returns.
5970
+ if "webchat_server" in sys.modules and sys.modules["webchat_server"].is_running():
5971
+ try:
5972
+ import webchat_server as _wcs
5973
+ r = _wcs._event_to_dict(ev)
5974
+ if r:
5975
+ if isinstance(r, tuple):
5976
+ payload, wait_event = r
5977
+ _wcs.broadcast_event("chunk", payload)
5978
+ wait_event.wait(timeout=2.0)
5979
+ else:
5980
+ _wcs.broadcast_event("chunk", r)
5981
+ except Exception:
5982
+ pass
5945
5983
  _ = ev
5984
+
5985
+ try:
5986
+ tracked = ckpt.get_tracked_edits()
5987
+ last_snaps = ckpt.list_snapshots(session_id)
5988
+ skip = False
5989
+ if not tracked and last_snaps:
5990
+ if len(state.messages) == last_snaps[-1].get("message_index", -1):
5991
+ skip = True
5992
+ if not skip:
5993
+ ckpt.make_snapshot(session_id, state, config, msg, tracked_edits=tracked)
5994
+ ckpt.reset_tracked()
5995
+ except Exception:
5996
+ pass
5997
+
5998
+ try:
5999
+ save_latest("", state, config)
6000
+ except Exception:
6001
+ pass
6002
+
6003
+ # Broadcast background notifications to Telegram to maintain parity with REPL
6004
+ if is_background and not is_telegram_turn:
6005
+ ttok = config.get("telegram_token")
6006
+ _tids = _tg_get_chat_ids(config)
6007
+ tchat = config.get("_active_tg_chat_id") or (_tids[0] if _tids else 0)
6008
+
6009
+ if ttok and tchat and _telegram_stop and not _telegram_stop.is_set():
6010
+ if state.messages and state.messages[-1].get("role") == "assistant":
6011
+ ans_content = state.messages[-1].get("content", "")
6012
+ if isinstance(ans_content, list):
6013
+ parts = [b["text"] if isinstance(b, dict) else str(b) for b in ans_content if (isinstance(b, dict) and b.get("type") == "text") or isinstance(b, str)]
6014
+ ans_content = "\n".join(parts)
6015
+ if ans_content:
6016
+ import threading as _tg_thread
6017
+ _tg_thread.Thread(target=_tg_send, args=(ttok, tchat, ans_content), daemon=True).start()
6018
+
5946
6019
  except Exception as _e:
5947
6020
  err(f"daemon run_query error: {type(_e).__name__}: {_e}")
6021
+ finally:
6022
+ if qlock:
6023
+ qlock.release()
5948
6024
  config["_run_query_callback"] = _daemon_run_query
5949
6025
 
5950
6026
  # Register slash-command callback so Telegram and WebChat can run
@@ -6016,6 +6092,12 @@ def _run_daemon(config: dict) -> None:
6016
6092
  config["_ipc_thread"] = ti
6017
6093
  ti.start()
6018
6094
 
6095
+ # Job Sentinel: Detect background completions and wake up the agent
6096
+ if config.get("_job_sentinel_thread") is None:
6097
+ tj = threading.Thread(target=_job_sentinel_loop, args=(config, state), daemon=True)
6098
+ config["_job_sentinel_thread"] = tj
6099
+ tj.start()
6100
+
6019
6101
  # 'accent' / 'orange' are only present in some custom themes; default
6020
6102
  # palette is {blue, cyan, gray, green, magenta, red, white, yellow}.
6021
6103
  # KeyError here would crash the daemon before the user ever sees a prompt.
@@ -6413,6 +6495,7 @@ def cmd_voice(args: str, state, config) -> bool:
6413
6495
  name = next(d["name"] for d in devices if d["index"] == idx)
6414
6496
  ok(f"Microphone set to: [{idx}] {name}")
6415
6497
  try:
6498
+ from config import save_config
6416
6499
  save_config(config)
6417
6500
  except Exception:
6418
6501
  pass
@@ -107,6 +107,13 @@ def save_session(state, config: dict, session_id: str | None = None) -> str:
107
107
  # 3. Save to daily folder
108
108
  day_dir = DAILY_DIR / date_str
109
109
  day_dir.mkdir(parents=True, exist_ok=True)
110
+
111
+ # Prune old copies for this session ID
112
+ for old_copy in day_dir.glob(f"session_*_{sid}.json"):
113
+ try:
114
+ old_copy.unlink()
115
+ except: pass
116
+
110
117
  daily_path = day_dir / f"session_{ts}_{sid}.json"
111
118
  daily_path.write_text(payload, encoding="utf-8")
112
119
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dulus"
7
- version = "0.2.33"
7
+ version = "0.2.35"
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"
@@ -1893,7 +1893,7 @@ def _sleeptimer(seconds: int, config: dict) -> str:
1893
1893
 
1894
1894
  t = threading.Thread(target=worker, daemon=True)
1895
1895
  t.start()
1896
- return f"Timer successfully scheduled for {seconds} seconds. You can output your final thoughts and end your turn. You will be automatically awakened."
1896
+ return f"Timer successfully scheduled for {seconds} seconds. Do NOT output anything. End your turn silently and wait for the system to wake you up."
1897
1897
 
1898
1898
 
1899
1899
  def _print_to_console(content: str = "", style: str = "normal", prefix: str = "", from_line: int = None, to_line: int = None, file_path: str = None, config: dict = None) -> str:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes