dulus 3.13.4__tar.gz → 3.13.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dulus-3.13.4/dulus.egg-info → dulus-3.13.6}/PKG-INFO +1 -1
- {dulus-3.13.4 → dulus-3.13.6}/agent.py +13 -0
- {dulus-3.13.4 → dulus-3.13.6}/compaction.py +30 -0
- {dulus-3.13.4 → dulus-3.13.6}/context.py +22 -2
- {dulus-3.13.4 → dulus-3.13.6}/docs/CHANGELOG.md +21 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/news.md +4 -0
- {dulus-3.13.4 → dulus-3.13.6/dulus.egg-info}/PKG-INFO +1 -1
- {dulus-3.13.4 → dulus-3.13.6}/dulus.egg-info/SOURCES.txt +1 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus.py +29 -12
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/background_tasks.py +17 -9
- {dulus-3.13.4 → dulus-3.13.6}/lookback.py +36 -3
- {dulus-3.13.4 → dulus-3.13.6}/memory/__init__.py +12 -1
- {dulus-3.13.4 → dulus-3.13.6}/memory/palace.py +153 -0
- {dulus-3.13.4 → dulus-3.13.6}/pyproject.toml +1 -1
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_background_tasks.py +30 -13
- dulus-3.13.6/tests/test_gold_memory_preload.py +151 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_lookback_compact.py +53 -0
- {dulus-3.13.4 → dulus-3.13.6}/webchat_server.py +55 -0
- {dulus-3.13.4 → dulus-3.13.6}/LICENSE +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/MANIFEST.in +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/README.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/algolia_search.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/analytics.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/agents_bridge.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/compressor.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/context.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/githook.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/marketplace.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/mempalace_bridge.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/paths.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/personas.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/plugins.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/server.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/backend/tasks.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/banners/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/banners/banner_rotator.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/banners/banners_dulus_ai.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/batch_api.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/checkpoint/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/checkpoint/hooks.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/checkpoint/store.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/checkpoint/types.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/__main__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/ansi.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/assets/ascii/dulus_logo.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/assets/ascii/eagle.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/assets/ascii/terminal_art.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/banners.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/demo.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/effects.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/effort_slider.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/progress.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/py.typed +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/spinners.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cli_animations/status.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/clipboard_utils.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/cloudsave.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/common.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/config.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/active_persona.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/marketplace.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/personas.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/plugins/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/plugins/composio/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/plugins/composio/plugin.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/data/tasks.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/display_blocks.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/API.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/BRAND_GUIDE.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/CONTRIBUTING.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/DEPLOYMENT.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/FAQ.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/GETTING_STARTED.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_AR.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_EN.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_ES.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_FR.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_JA.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_KO.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_PT.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_RU.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/README_ZH.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/SECURITY.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/api.html +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/architecture.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/azure-speech-template.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/dashboard/index.html +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/divider.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/dulus-bird.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/generate.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/index.html +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/nvidia-models.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/particle-playground.html +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/personas/index.html +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/poetry-banner.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/preview.html +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/python-console.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/auto-adapter.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/dulus-os.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/hero.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/lookback_two_windows.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/pyc_heap.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/pyc_receipts.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/pyc_trap.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/roundtable.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/task-board.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/readme/webchat.png +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-agents.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-brainstorm.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-bridges.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-features.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-freetier.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-memory.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-models.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-perms.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-plugins.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-quickstart.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/sec-ssj.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/spinners.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/split-pane.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/terminal-boot.svg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/docs/termux-edge.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus.egg-info/requires.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus.egg-info/top_level.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_account.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_bar_client.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_gui.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_mcp/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_mcp/client.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_mcp/commands.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_mcp/config.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_mcp/hub.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_mcp/tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_mcp/types.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_quick_menu.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/add_dir_manager.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/afk_mode.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/approval_runtime.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/clipboard_utils.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/diff_visualizer.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/export_import.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/hook_engine.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/notification_manager.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/session_fork.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/shell_mode.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/todo_visualizer.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/tree_ls.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/wire_events.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/dulus_tools/yolo_mode.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/file_filter.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/fs_compass.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/governance.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/agent_bridge.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/chat_widget.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/main_window.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/personas.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/sandbox_server.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/session_utils.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/settings_dialog.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/sidebar.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/tasks_view.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/themes.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/gui/tool_panel.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/input.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/license_manager.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/audit.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/consolidator.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/context.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/mempalace_bridge.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/offload.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/scan.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/adaptation_guides.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/askuserquestion_after_numbered_list.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/background_jobs_no_extra_timers.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/cleverness_over_resources.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/corporate_tls_breaks_curl_cffi.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/hardened_rules.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/investigator_workflow_heuristic.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/native_code_over_sketchy_plugins.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/no_personal_info_in_code.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/searches_include_modified_date.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/tmux_send_keys_power_patterns.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/token_optimization_technique.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/tool_chaining_capability.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/insights/webbridge_evaluate_first_pattern.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/seeds/short_memory.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/sessions.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/store.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/types.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/memory/vector_search.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/model_params.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/multi_agent/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/multi_agent/subagent.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/multi_agent/tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/paste_placeholders.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/personality_config.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/plugin/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/plugin/autoadapter.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/plugin/loader.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/plugin/recommend.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/plugin/store.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/plugin/types.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/profiles.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/providers.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/pyconsole.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/README.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/dist/index.html +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/dist/wallpaper-default.jpg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/dist/wallpapers/default.jpeg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/dist/wallpapers/light.jpeg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/dist/wallpapers/nature.jpeg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/dist/wallpapers/tech.jpeg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox/info.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/sandbox_bootstrap.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/setup.cfg +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/builtin.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/LICENSE-obsidian-skills +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/README.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/defuddle/SKILL.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/json-canvas/SKILL.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/json-canvas/references/EXAMPLES.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/obsidian-bases/SKILL.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/obsidian-bases/references/FUNCTIONS_REFERENCE.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/obsidian-cli/SKILL.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/obsidian-markdown/SKILL.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/obsidian-markdown/references/CALLOUTS.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/obsidian-markdown/references/EMBEDS.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/obsidian-markdown/references/PROPERTIES.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/README.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/SKILL.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/addresses.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/advancedbackgroundchecks.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/beenverified.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/clustal.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/clustrmaps.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/cyberbackgroundchecks.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/familytreenow.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/fastpeoplesearch.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/intelius.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/mylife.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/nuwber.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/peekyou.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/peoplefinders.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/radaris.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/rehold.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/searchpeoplefree.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/socialcatfish.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/spokeo.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/thatsthem.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/truepeoplesearch.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/usphonebook.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/brokers/whitepages.json +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/legal/ccpa.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/legal/drop.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/legal/gdpr.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/methods.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/site-playbooks.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/references/state-machine.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/autopilot.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/badbool.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/brokers.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/cdp.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/config.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/crypto.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/dossier.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/email_modes.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/emailer.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/ledger.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/legal.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/paths.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/pdd.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/registry.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/report.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/scan.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/storage.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/tiers.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/scripts/vectors.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/templates/consent/authorization.md +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/templates/emails/ccpa-authorized-agent.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/templates/emails/ccpa-deletion.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/templates/emails/ccpa-indirect-deletion.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/templates/emails/gdpr-erasure.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/bundled/unbroker/templates/emails/generic-optout.txt +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/clawhub.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/executor.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/loader.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skill/tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/skills.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/soul.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/spinner.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/steer_input.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/subagent.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/task/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/task/store.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/task/tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/task/types.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_afk_yolo.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_approval_runtime.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_background_task_tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_chatgpt_oauth.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_checkpoint.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_cli_animations_integration.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_clipboard_utils.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_compaction.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_composio_tool_generator.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_diff_view.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_diff_visualization.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_display_blocks.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_dulus_account.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_export_import.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_grok_usage.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_hook_engine.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_injection_fix.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_json_protocol.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_license.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_lookback_cache.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_mcp.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_mcp_algolia.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_memory.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_memory_home_resolution.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_notification_manager.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_plugin.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_profile_isolation.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_profile_plugin_scope.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_provider_errors.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_session_fork.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_shell_detection.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_shell_mode.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_skills.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_skillssh_algolia.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_steer_input.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_subagent.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_task.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_telegram_buffer.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_think_tool.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_todo_tool.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_todo_visualization.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_tool_registry.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_tree_ls.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_voice.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_webbridge_tabs.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tests/test_wire_events.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tmux_tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/token_budget.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tool_registry.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tools_background.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tools_think.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/tools_todo.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/ui/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/ui/input.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/ui/render.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/updater.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/voice/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/voice/audio_utils.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/voice/keyterms.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/voice/recorder.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/voice/stt.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/voice/tts.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/voice/wake_word.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/webbridge/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/webbridge/core.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/webbridge/tools.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/webchat.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/webchat_ui/__init__.py +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/webchat_ui/app.js +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/webchat_ui/index.html +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/webchat_ui/style.css +0 -0
- {dulus-3.13.4 → dulus-3.13.6}/welcome.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 3.13.
|
|
3
|
+
Version: 3.13.6
|
|
4
4
|
Summary: Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS.
|
|
5
5
|
Author-email: KevRojo <dev@dulus.ai>
|
|
6
6
|
Maintainer-email: KevRojo <dev@dulus.ai>
|
|
@@ -206,6 +206,19 @@ def run(
|
|
|
206
206
|
except Exception:
|
|
207
207
|
_api_source, _lb_meta = state.messages, {"enabled": False, "truncated": False}
|
|
208
208
|
|
|
209
|
+
# Strip GUI/REPL baseline display blobs (soul/gold/welcome). The model
|
|
210
|
+
# already has them via system prompt; leaving them as role:assistant
|
|
211
|
+
# made web-history consolidate drop everything before the last
|
|
212
|
+
# assistant (= the gold blob itself) on first turn.
|
|
213
|
+
try:
|
|
214
|
+
from memory import is_baseline_display_message
|
|
215
|
+
_api_source = [
|
|
216
|
+
m for m in _api_source
|
|
217
|
+
if not is_baseline_display_message(m)
|
|
218
|
+
]
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
209
222
|
# Sanitize message contents before sending to API (surrogate safety)
|
|
210
223
|
_safe_messages = []
|
|
211
224
|
for m in _api_source:
|
|
@@ -678,6 +678,13 @@ def save_loopback_archive(messages: list, state=None, config: dict | None = None
|
|
|
678
678
|
Writes ~/.dulus/loopback_archives/archive_<session>.json and binds
|
|
679
679
|
config["_loopback_archive_path"] + config["_loopback_archive"] (in-memory
|
|
680
680
|
copy) for the Loopback tool. Returns the path or None on failure.
|
|
681
|
+
|
|
682
|
+
CRITICAL — second-/Nth-compact guard:
|
|
683
|
+
A later ``/compact`` runs against an *already slimmed* ``state.messages``.
|
|
684
|
+
Naively rewriting the archive would overwrite the original full history
|
|
685
|
+
with the post-compact crumbs. We NEVER shrink a durable archive: if an
|
|
686
|
+
in-memory or on-disk copy is longer than ``messages``, keep the longer one
|
|
687
|
+
and only refresh the binding.
|
|
681
688
|
"""
|
|
682
689
|
if not messages:
|
|
683
690
|
return None
|
|
@@ -685,6 +692,29 @@ def save_loopback_archive(messages: list, state=None, config: dict | None = None
|
|
|
685
692
|
LOOPBACK_ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)
|
|
686
693
|
sid = _session_key(state, config)
|
|
687
694
|
path = LOOPBACK_ARCHIVE_DIR / f"archive_{sid}.json"
|
|
695
|
+
|
|
696
|
+
# Resolve the longest known full archive for this session.
|
|
697
|
+
prev: list = []
|
|
698
|
+
if isinstance(config, dict):
|
|
699
|
+
live = config.get("_loopback_archive")
|
|
700
|
+
if isinstance(live, list) and live:
|
|
701
|
+
prev = live
|
|
702
|
+
if not prev and path.exists():
|
|
703
|
+
try:
|
|
704
|
+
disk = json.loads(path.read_text(encoding="utf-8"))
|
|
705
|
+
disk_msgs = disk.get("messages") if isinstance(disk, dict) else None
|
|
706
|
+
if isinstance(disk_msgs, list) and disk_msgs:
|
|
707
|
+
prev = disk_msgs
|
|
708
|
+
except Exception:
|
|
709
|
+
prev = []
|
|
710
|
+
|
|
711
|
+
if prev and len(prev) > len(messages):
|
|
712
|
+
# Refuse to shrink. Keep the fuller archive as the durable source.
|
|
713
|
+
if isinstance(config, dict):
|
|
714
|
+
config["_loopback_archive"] = prev
|
|
715
|
+
config["_loopback_archive_path"] = str(path)
|
|
716
|
+
return path
|
|
717
|
+
|
|
688
718
|
payload = {
|
|
689
719
|
"session_id": sid,
|
|
690
720
|
"timestamp": time.strftime("%Y%m%d_%H%M%S"),
|
|
@@ -442,12 +442,28 @@ def _resolve_reply_language(config: dict | None) -> str:
|
|
|
442
442
|
return raw
|
|
443
443
|
|
|
444
444
|
|
|
445
|
+
|
|
446
|
+
def _append_baseline_fragments(prompt: str) -> str:
|
|
447
|
+
"""Attach soul + gold system fragments (always-on baseline)."""
|
|
448
|
+
try:
|
|
449
|
+
from memory import soul_system_fragment, gold_system_fragment
|
|
450
|
+
_soul = soul_system_fragment()
|
|
451
|
+
if _soul:
|
|
452
|
+
prompt += "\n\n" + _soul
|
|
453
|
+
_gold = gold_system_fragment()
|
|
454
|
+
if _gold:
|
|
455
|
+
prompt += "\n\n" + _gold
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
return prompt
|
|
459
|
+
|
|
460
|
+
|
|
445
461
|
def build_system_prompt(config: dict | None = None) -> str:
|
|
446
462
|
import platform
|
|
447
463
|
model_lower = (config.get("model", "") if config else "").lower()
|
|
448
464
|
is_deepseek_r1 = "deepseek-r1" in model_lower or "deepseek-reasoner" in model_lower
|
|
449
465
|
if is_deepseek_r1 and config and config.get("deep_override", False):
|
|
450
|
-
return _build_ollama_system_prompt(config)
|
|
466
|
+
return _append_baseline_fragments(_build_ollama_system_prompt(config))
|
|
451
467
|
|
|
452
468
|
auto_show = "ON" if (not config or config.get("auto_show", True)) else "OFF"
|
|
453
469
|
lite = bool(config and config.get("lite_mode"))
|
|
@@ -470,8 +486,12 @@ def build_system_prompt(config: dict | None = None) -> str:
|
|
|
470
486
|
dulus_md="" if lite else get_dulus_md(),
|
|
471
487
|
)
|
|
472
488
|
|
|
489
|
+
# Soul + Gold baseline ALWAYS (even in lite) — model source of truth.
|
|
490
|
+
# Display copies in chat are GUI-only and get stripped before the API call.
|
|
491
|
+
prompt = _append_baseline_fragments(prompt)
|
|
492
|
+
|
|
473
493
|
if lite:
|
|
474
|
-
# Bail early — minimal prompt only.
|
|
494
|
+
# Bail early — minimal prompt only (baseline already attached).
|
|
475
495
|
return prompt
|
|
476
496
|
|
|
477
497
|
try:
|
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.13.6] - 2026-08-31
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **WebChat CONFIG None guard**: `CONFIG` is checked before `.pop(...)` so pyright/static quality stays clean when config is unset
|
|
14
|
+
- **Background tasks on Windows**: worker launch no longer goes through bash-quoted `python -c`; Windows-safe `_worker.py` path keeps win-3.13 CI stable
|
|
15
|
+
|
|
16
|
+
### Tests
|
|
17
|
+
- Quality CI green on Ubuntu + Windows · Python 3.11 + 3.13 · Types and package green
|
|
18
|
+
|
|
19
|
+
## [3.13.5] - 2026-08-31
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Gold / short_memory baseline**: curated gold memories (incl. `short_memory`) and soul now ride in `build_system_prompt` every turn — always-on, **not** gated by `/mem_palace`
|
|
23
|
+
- **First-turn amnesia**: agent + lookback strip GUI/REPL gold/soul/welcome *display* blobs before the provider call, so web-history consolidate no longer treats gold as “last assistant reply” and drops real context
|
|
24
|
+
- **MemPalace dual-path noise**: per-turn inject (CLI + WebChat) skips `short_memory` / soul — already in the system prompt
|
|
25
|
+
- **Second `/compact` archive wipe**: `save_loopback_archive` refuses to shrink; Loopback keeps the full pre-compact history across N compacts
|
|
26
|
+
- **WebChat gold preload**: session seeds gold *display* copies for the transcript; model source of truth is still the system prompt (public GUI stays as-is — this is server/engine, not an Interant GUI port)
|
|
27
|
+
|
|
28
|
+
### Tests
|
|
29
|
+
- `test_gold_memory_preload` + `test_lookback_compact` (11 passed)
|
|
30
|
+
|
|
10
31
|
## [3.13.4] - 2026-08-31
|
|
11
32
|
|
|
12
33
|
### Fixed
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## 🔥🔥🔥 News (Pacific Time)
|
|
4
4
|
|
|
5
|
+
- August 31, 2026 (**v3.13.6**): **CI quality locked green.** WebChat guards `CONFIG` before pop (pyright clean) and background workers launch Windows-safe without bash-quoted `python -c` — win-3.13 flake gone. Hotfix patch on top of 3.13.5. `pip install -U dulus`. 🦅
|
|
6
|
+
|
|
7
|
+
- August 31, 2026 (**v3.13.5**): **🏆 short_memory stopped vanishing.** Gold memories + soul now live in the **system prompt** every turn (always-on — `/mem_palace off` no longer starves them). WebChat seeds gold for the transcript; the agent/lookback strip those display blobs before the provider call so first-turn consolidate can’t eat your context. Second `/compact` no longer wipes the Loopback archive. Engine + WebChat fix — not a desktop-GUI rewrite. `pip install -U dulus`. 🦅
|
|
8
|
+
|
|
5
9
|
- August 31, 2026 (**v3.13.4**): **Context windows are honest again.** `token_budget` ships so compaction knows Claude is 200k, Gemini is 1M, DeepSeek is 64k — not a flat 128k for everyone. Quality CI: **925 passed**. `pip install -U dulus`. 🦅
|
|
6
10
|
|
|
7
11
|
- August 31, 2026 (**v3.13.3**): **`/compact` got serious.** Full archive → disk for Loopback, live context → ~0 with a dense hint card, and **lookback is forced ON** for that session even if it was off — so quality does not fall off a cliff after compact. `pip install -U dulus`. 🦅
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: dulus
|
|
3
|
-
Version: 3.13.
|
|
3
|
+
Version: 3.13.6
|
|
4
4
|
Summary: Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS.
|
|
5
5
|
Author-email: KevRojo <dev@dulus.ai>
|
|
6
6
|
Maintainer-email: KevRojo <dev@dulus.ai>
|
|
@@ -332,6 +332,7 @@ tests/test_diff_visualization.py
|
|
|
332
332
|
tests/test_display_blocks.py
|
|
333
333
|
tests/test_dulus_account.py
|
|
334
334
|
tests/test_export_import.py
|
|
335
|
+
tests/test_gold_memory_preload.py
|
|
335
336
|
tests/test_grok_usage.py
|
|
336
337
|
tests/test_hook_engine.py
|
|
337
338
|
tests/test_injection_fix.py
|
|
@@ -395,7 +395,7 @@ try:
|
|
|
395
395
|
from importlib.metadata import version as _pkg_version
|
|
396
396
|
VERSION = _pkg_version("dulus")
|
|
397
397
|
except Exception:
|
|
398
|
-
VERSION = "3.13.
|
|
398
|
+
VERSION = "3.13.6" # dev fallback — keep in sync with pyproject.toml
|
|
399
399
|
|
|
400
400
|
# ── Machine-readable protocol output (`--output json`) ─────────────────────
|
|
401
401
|
# Dulus is increasingly run as a child process by another agent runtime (an
|
|
@@ -3570,6 +3570,8 @@ def cmd_mem_palace(args: str, _state, config) -> bool:
|
|
|
3570
3570
|
config["mem_palace"] = not config.get("mem_palace", True)
|
|
3571
3571
|
state_str = "ON" if config["mem_palace"] else "OFF"
|
|
3572
3572
|
ok(f"MemPalace auto-injection: {state_str}")
|
|
3573
|
+
if not config["mem_palace"]:
|
|
3574
|
+
info("Gold memories (incl. short_memory) still load at session start.")
|
|
3573
3575
|
save_config(config)
|
|
3574
3576
|
return True
|
|
3575
3577
|
|
|
@@ -12227,18 +12229,21 @@ def repl(config: dict, initial_prompt: str | None = None):
|
|
|
12227
12229
|
except Exception as e:
|
|
12228
12230
|
startup_status_msgs.append(clr(f" ⚠ Schema inject skip: {e}", "yellow"))
|
|
12229
12231
|
|
|
12230
|
-
# ── Gold Memories Auto-Load
|
|
12231
|
-
#
|
|
12232
|
-
#
|
|
12232
|
+
# ── Gold Memories Auto-Load (GUI/REPL display copies) ─────────────────────
|
|
12233
|
+
# Model source of truth is build_system_prompt → gold_system_fragment().
|
|
12234
|
+
# These assistant-role copies are for the transcript/GUI only and get
|
|
12235
|
+
# stripped before the provider call (agent.py + lookback). Never gated
|
|
12236
|
+
# by /mem_palace (that toggle is only for per-turn semantic search).
|
|
12233
12237
|
try:
|
|
12234
|
-
from memory import
|
|
12235
|
-
|
|
12236
|
-
for
|
|
12237
|
-
state.messages.append(
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12238
|
+
from memory import gold_context_messages, gold_system_fragment
|
|
12239
|
+
_gold_in_system = bool(gold_system_fragment())
|
|
12240
|
+
for _msg in gold_context_messages():
|
|
12241
|
+
state.messages.append(_msg)
|
|
12242
|
+
_gold_name = _msg["content"].split("]", 1)[0].removeprefix("[Golden Memory Loaded: ")
|
|
12243
|
+
_where = "system+GUI" if _gold_in_system else "GUI only"
|
|
12244
|
+
startup_status_msgs.append(
|
|
12245
|
+
clr(f" 🏆 Gold memory loaded ({_where}): {_gold_name}", "yellow", "bold")
|
|
12246
|
+
)
|
|
12242
12247
|
except Exception:
|
|
12243
12248
|
pass
|
|
12244
12249
|
|
|
@@ -12735,6 +12740,18 @@ def repl(config: dict, initial_prompt: str | None = None):
|
|
|
12735
12740
|
for _h in _raw_hits:
|
|
12736
12741
|
_mp_log(f" hit: score={float(_h.get('keyword_score', 0.0)):.3f} {_h.get('name','?')}", "dim")
|
|
12737
12742
|
_kept = [h for h in _raw_hits if float(h.get("keyword_score", 0.0)) >= _MIN_SCORE]
|
|
12743
|
+
# Skip short_memory/soul — already in system-prompt baseline.
|
|
12744
|
+
try:
|
|
12745
|
+
from memory import is_baseline_memory_name
|
|
12746
|
+
_kept = [
|
|
12747
|
+
h for h in _kept
|
|
12748
|
+
if not is_baseline_memory_name(h.get("name"))
|
|
12749
|
+
and not is_baseline_memory_name(
|
|
12750
|
+
(h.get("description") or "").split()[:1][0] if h.get("description") else None
|
|
12751
|
+
)
|
|
12752
|
+
]
|
|
12753
|
+
except Exception:
|
|
12754
|
+
pass
|
|
12738
12755
|
# ── Dedup: skip memories already injected earlier in this session.
|
|
12739
12756
|
# Key by content hash (not name) because mempalace often returns
|
|
12740
12757
|
# generic names like "palace" for every hit in a wing — name-based
|
|
@@ -513,13 +513,21 @@ class BackgroundTaskManager:
|
|
|
513
513
|
env["DULUS_BG_TASK"] = "1"
|
|
514
514
|
env["DULUS_BG_TIMEOUT"] = str(timeout_s)
|
|
515
515
|
|
|
516
|
-
#
|
|
517
|
-
|
|
518
|
-
|
|
516
|
+
# Write a tiny runner script so cwd/command never pass through shell
|
|
517
|
+
# quoting (bash `'"'"'` tricks + Python -c single quotes break hard on
|
|
518
|
+
# Windows paths like C:\Users\... and on echo 'HELLO').
|
|
519
|
+
runner = task_dir / "_worker.py"
|
|
520
|
+
runner.write_text(
|
|
521
|
+
"import os, subprocess, sys\n"
|
|
522
|
+
f"os.chdir({cwd!r})\n"
|
|
523
|
+
f"raise SystemExit(subprocess.call({command!r}, shell=True))\n",
|
|
524
|
+
encoding="utf-8",
|
|
525
|
+
)
|
|
519
526
|
|
|
527
|
+
log_fh = open(task_dir / "output.log", "w", encoding="utf-8")
|
|
520
528
|
kwargs: dict[str, Any] = {
|
|
521
529
|
"stdin": subprocess.DEVNULL,
|
|
522
|
-
"stdout":
|
|
530
|
+
"stdout": log_fh,
|
|
523
531
|
"stderr": subprocess.STDOUT,
|
|
524
532
|
"cwd": cwd,
|
|
525
533
|
"env": env,
|
|
@@ -527,13 +535,13 @@ class BackgroundTaskManager:
|
|
|
527
535
|
|
|
528
536
|
if os.name != "nt":
|
|
529
537
|
kwargs["start_new_session"] = True
|
|
538
|
+
else:
|
|
539
|
+
# Detach from the console so the parent can exit cleanly on CI.
|
|
540
|
+
create_new = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
541
|
+
kwargs["creationflags"] = create_new
|
|
530
542
|
|
|
531
543
|
process = subprocess.Popen(
|
|
532
|
-
[
|
|
533
|
-
sys.executable,
|
|
534
|
-
"-c",
|
|
535
|
-
f"import subprocess, sys; sys.exit(subprocess.call('{script}', shell=True))",
|
|
536
|
-
],
|
|
544
|
+
[sys.executable, str(runner)],
|
|
537
545
|
**kwargs,
|
|
538
546
|
)
|
|
539
547
|
return process.pid
|
|
@@ -155,6 +155,25 @@ def find_lookback_start(messages: list, turns: int) -> int:
|
|
|
155
155
|
return start
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
|
|
159
|
+
def _strip_baseline_display(messages: list) -> list:
|
|
160
|
+
"""Drop soul/gold/welcome GUI display blobs from an API window.
|
|
161
|
+
|
|
162
|
+
Those ride in the system prompt now (see memory.gold_system_fragment /
|
|
163
|
+
soul_system_fragment). Keeping them as role:assistant in the API payload
|
|
164
|
+
made web-history consolidate treat gold as "last assistant reply".
|
|
165
|
+
"""
|
|
166
|
+
if not messages:
|
|
167
|
+
return messages
|
|
168
|
+
try:
|
|
169
|
+
from memory import is_baseline_display_message
|
|
170
|
+
except Exception:
|
|
171
|
+
return messages
|
|
172
|
+
out = [m for m in messages if not is_baseline_display_message(m)]
|
|
173
|
+
# Preserve identity when nothing was stripped (callers/tests rely on it).
|
|
174
|
+
return messages if len(out) == len(messages) else out
|
|
175
|
+
|
|
176
|
+
|
|
158
177
|
def apply_lookback_window(
|
|
159
178
|
messages: list,
|
|
160
179
|
config: dict | None = None,
|
|
@@ -166,6 +185,9 @@ def apply_lookback_window(
|
|
|
166
185
|
|
|
167
186
|
Full ``messages`` is never mutated. When disabled or small enough, the
|
|
168
187
|
original list is returned (same object) with meta.truncated=False.
|
|
188
|
+
|
|
189
|
+
Baseline display blobs (soul/gold/welcome) are always stripped from the
|
|
190
|
+
returned API window — they live in the system prompt, not chat turns.
|
|
169
191
|
"""
|
|
170
192
|
msgs = messages or []
|
|
171
193
|
on = lookback_enabled(config) if enabled is None else bool(enabled)
|
|
@@ -186,9 +208,20 @@ def apply_lookback_window(
|
|
|
186
208
|
"start_index": 0,
|
|
187
209
|
"hidden_messages": 0,
|
|
188
210
|
}
|
|
211
|
+
def _api_window(window: list, meta: dict[str, Any]) -> tuple[list, dict[str, Any]]:
|
|
212
|
+
"""Strip baseline display blobs and refresh window counts."""
|
|
213
|
+
cleaned = _strip_baseline_display(window)
|
|
214
|
+
if cleaned is window:
|
|
215
|
+
return window, meta
|
|
216
|
+
meta = dict(meta)
|
|
217
|
+
meta["window_messages"] = len(cleaned)
|
|
218
|
+
meta["window_user_turns"] = count_user_turns(cleaned)
|
|
219
|
+
meta["baseline_stripped"] = len(window) - len(cleaned)
|
|
220
|
+
return cleaned, meta
|
|
221
|
+
|
|
189
222
|
if not on or total == 0:
|
|
190
223
|
_clear_anchor(config)
|
|
191
|
-
return msgs, meta
|
|
224
|
+
return _api_window(msgs, meta)
|
|
192
225
|
|
|
193
226
|
# Hysteresis: reuse the previous start (anchor) while its window still
|
|
194
227
|
# holds n..n+slack user turns. The API prefix then stays append-only
|
|
@@ -211,7 +244,7 @@ def apply_lookback_window(
|
|
|
211
244
|
|
|
212
245
|
if start <= 0:
|
|
213
246
|
_clear_anchor(config)
|
|
214
|
-
return msgs, meta
|
|
247
|
+
return _api_window(msgs, meta)
|
|
215
248
|
|
|
216
249
|
# Cache-aware gate: only truncate when the head we'd hide is meaningfully
|
|
217
250
|
# bigger than the window we'd keep. Otherwise dropping the front busts the
|
|
@@ -242,7 +275,7 @@ def apply_lookback_window(
|
|
|
242
275
|
"window_user_turns": count_user_turns(window),
|
|
243
276
|
"hidden_messages": start,
|
|
244
277
|
})
|
|
245
|
-
return window, meta
|
|
278
|
+
return _api_window(window, meta)
|
|
246
279
|
|
|
247
280
|
|
|
248
281
|
def lookback_system_note(meta: dict[str, Any]) -> str:
|
|
@@ -57,7 +57,18 @@ from .types import ( # noqa: F401
|
|
|
57
57
|
WHAT_NOT_TO_SAVE,
|
|
58
58
|
)
|
|
59
59
|
from .consolidator import consolidate_session, mine_files, snapshot_memory_files, new_memory_files # noqa: F401
|
|
60
|
-
from .palace import
|
|
60
|
+
from .palace import ( # noqa: F401
|
|
61
|
+
ensure_memory_palace,
|
|
62
|
+
ensure_short_memory,
|
|
63
|
+
gold_context_messages,
|
|
64
|
+
gold_system_fragment,
|
|
65
|
+
soul_system_fragment,
|
|
66
|
+
is_baseline_display_message,
|
|
67
|
+
is_baseline_memory_name,
|
|
68
|
+
GOLD_MARKER,
|
|
69
|
+
SOUL_MARKER,
|
|
70
|
+
BASELINE_DISPLAY_MARKERS,
|
|
71
|
+
)
|
|
61
72
|
from .mempalace_bridge import ( # noqa: F401
|
|
62
73
|
schedule_mempalace_mine,
|
|
63
74
|
wait_pending_mines,
|
|
@@ -13,6 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
|
|
14
14
|
from .store import (
|
|
15
15
|
MemoryEntry,
|
|
16
|
+
USER_MEMORY_DIR,
|
|
16
17
|
get_memory_dir,
|
|
17
18
|
has_stacked_frontmatter,
|
|
18
19
|
parse_frontmatter,
|
|
@@ -267,6 +268,158 @@ def seed_insight_memories() -> bool:
|
|
|
267
268
|
return changed
|
|
268
269
|
|
|
269
270
|
|
|
271
|
+
|
|
272
|
+
# ── Baseline display markers (GUI/REPL transcript only) ─────────────────────
|
|
273
|
+
# These role:assistant blobs are for the human to see in the chat UI. The model
|
|
274
|
+
# source of truth is gold_system_fragment() / soul_system_fragment() in the
|
|
275
|
+
# system prompt. Agent + lookback strip these markers from the API payload.
|
|
276
|
+
GOLD_MARKER = "[Golden Memory Loaded:"
|
|
277
|
+
SOUL_MARKER = "[Identity Essence Loaded:"
|
|
278
|
+
SOUL_RELOAD_MARKER = "[Identity Essence Reloaded:"
|
|
279
|
+
WELCOME_MARKER = "<!-- dulus:welcome -->"
|
|
280
|
+
BASELINE_DISPLAY_MARKERS = (
|
|
281
|
+
GOLD_MARKER,
|
|
282
|
+
SOUL_MARKER,
|
|
283
|
+
SOUL_RELOAD_MARKER,
|
|
284
|
+
WELCOME_MARKER,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def is_baseline_display_message(msg: dict | None) -> bool:
|
|
289
|
+
"""True if *msg* is a GUI/REPL display blob for soul/gold/welcome."""
|
|
290
|
+
if not isinstance(msg, dict):
|
|
291
|
+
return False
|
|
292
|
+
content = msg.get("content") or ""
|
|
293
|
+
if not isinstance(content, str):
|
|
294
|
+
return False
|
|
295
|
+
s = content.lstrip()
|
|
296
|
+
return any(s.startswith(m) for m in BASELINE_DISPLAY_MARKERS)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def is_baseline_memory_name(name: str | None) -> bool:
|
|
300
|
+
"""True for memories that already ride in the system-prompt baseline.
|
|
301
|
+
|
|
302
|
+
MemPalace per-turn inject must skip these — re-injecting a truncated
|
|
303
|
+
``short_memory`` on top of the full system copy is pure noise.
|
|
304
|
+
"""
|
|
305
|
+
if not name:
|
|
306
|
+
return False
|
|
307
|
+
key = str(name).strip().lower().replace("-", "_").replace(" ", "_")
|
|
308
|
+
key = key.rsplit("/", 1)[-1].rsplit("\\", 1)[-1].rsplit(".", 1)[0]
|
|
309
|
+
if key in {"short_memory", "shortmemory", "soul"}:
|
|
310
|
+
return True
|
|
311
|
+
if key.startswith("golden_memory") or "short_memory" in key:
|
|
312
|
+
return True
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _clamp_body(body: str, max_chars: int) -> str:
|
|
317
|
+
body = (body or "").strip()
|
|
318
|
+
if not body or len(body) <= max_chars:
|
|
319
|
+
return body
|
|
320
|
+
head = max_chars * 3 // 4
|
|
321
|
+
return (
|
|
322
|
+
body[:head]
|
|
323
|
+
+ "\n\n[... gold memory clipped — file is oversized; "
|
|
324
|
+
"re-curate it with MemorySave ...]\n\n"
|
|
325
|
+
+ body[-(max_chars - head):]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _iter_gold_entries(max_chars: int = 8000):
|
|
330
|
+
"""Yield (name, body) for every gold memory, short_memory first."""
|
|
331
|
+
try:
|
|
332
|
+
ensure_short_memory(force_gold=True)
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
try:
|
|
336
|
+
from .store import load_index
|
|
337
|
+
entries = [e for e in load_index("all") if getattr(e, "gold", False)]
|
|
338
|
+
except Exception:
|
|
339
|
+
return
|
|
340
|
+
def _sort_key(e):
|
|
341
|
+
name = (getattr(e, "name", "") or "").lower()
|
|
342
|
+
return (0 if name == "short_memory" else 1, name)
|
|
343
|
+
|
|
344
|
+
for entry in sorted(entries, key=_sort_key):
|
|
345
|
+
body = _clamp_body(getattr(entry, "content", "") or "", max_chars)
|
|
346
|
+
if not body:
|
|
347
|
+
continue
|
|
348
|
+
yield (getattr(entry, "name", "gold") or "gold", body)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def gold_system_fragment(max_chars: int = 8000, max_total: int = 12000) -> str:
|
|
352
|
+
"""Gold memories as a stable system-prompt block (model source of truth).
|
|
353
|
+
|
|
354
|
+
This is how the model actually *receives* short_memory and other gold
|
|
355
|
+
entries — not as ``role:assistant`` chat turns (those break web-history
|
|
356
|
+
consolidate, Anthropic role alternation, and lookback).
|
|
357
|
+
|
|
358
|
+
Deliberately NOT gated by ``mem_palace``. Called every turn from
|
|
359
|
+
``build_system_prompt`` so edits to short_memory.md show up immediately.
|
|
360
|
+
"""
|
|
361
|
+
parts: list[str] = []
|
|
362
|
+
total = 0
|
|
363
|
+
for name, body in _iter_gold_entries(max_chars=max_chars):
|
|
364
|
+
block = f"[Golden Memory Loaded: {name}]\n\n{body}"
|
|
365
|
+
if total and total + len(block) > max_total:
|
|
366
|
+
break
|
|
367
|
+
parts.append(block)
|
|
368
|
+
total += len(block) + 2
|
|
369
|
+
if not parts:
|
|
370
|
+
return ""
|
|
371
|
+
return (
|
|
372
|
+
"# ── Golden Memory (always-on baseline — NOT gated by /mem_palace) ──\n"
|
|
373
|
+
"These are curated essentials. Treat short_memory as the live scratchpad "
|
|
374
|
+
"of where we left off; do not ask the user to re-explain it.\n\n"
|
|
375
|
+
+ "\n\n".join(parts)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def soul_system_fragment(max_chars: int = 8000) -> str:
|
|
380
|
+
"""Soul identity as a system-prompt block (same rationale as gold)."""
|
|
381
|
+
try:
|
|
382
|
+
path = USER_MEMORY_DIR / "soul.md"
|
|
383
|
+
if not path.exists():
|
|
384
|
+
return ""
|
|
385
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
386
|
+
body = raw
|
|
387
|
+
if raw.lstrip().startswith("---"):
|
|
388
|
+
try:
|
|
389
|
+
from .store import parse_frontmatter
|
|
390
|
+
_, body = parse_frontmatter(raw)
|
|
391
|
+
except Exception:
|
|
392
|
+
body = raw
|
|
393
|
+
body = _clamp_body(body, max_chars)
|
|
394
|
+
if not body:
|
|
395
|
+
return ""
|
|
396
|
+
return (
|
|
397
|
+
"# ── Identity Essence (soul) ──\n"
|
|
398
|
+
f"[Identity Essence Loaded: soul]\n\n{body}"
|
|
399
|
+
)
|
|
400
|
+
except Exception:
|
|
401
|
+
return ""
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def gold_context_messages(max_chars: int = 8000) -> list[dict[str, str]]:
|
|
405
|
+
"""GUI/REPL *display* copies of gold memories (NOT the model source of truth).
|
|
406
|
+
|
|
407
|
+
Kept as ``role:assistant`` so `/api/chat/history` and the desktop GUI can
|
|
408
|
+
render "🏆 Gold memory loaded" in the transcript. The agent loop strips
|
|
409
|
+
these markers before the provider call; the real baseline rides in the
|
|
410
|
+
system prompt via ``gold_system_fragment()``.
|
|
411
|
+
|
|
412
|
+
Deliberately NOT gated by the ``mem_palace`` config toggle.
|
|
413
|
+
"""
|
|
414
|
+
messages: list[dict[str, str]] = []
|
|
415
|
+
for name, body in _iter_gold_entries(max_chars=max_chars):
|
|
416
|
+
messages.append({
|
|
417
|
+
"role": "assistant",
|
|
418
|
+
"content": f"[Golden Memory Loaded: {name}]\n\n{body}",
|
|
419
|
+
})
|
|
420
|
+
return messages
|
|
421
|
+
|
|
422
|
+
|
|
270
423
|
def ensure_memory_palace() -> bool:
|
|
271
424
|
"""Initialize missing core buckets + always ensure gold short_memory.
|
|
272
425
|
|
|
@@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "dulus"
|
|
13
|
-
version = "3.13.
|
|
13
|
+
version = "3.13.6"
|
|
14
14
|
description = "Provider-independent agent runtime for the terminal — cloud and local models, real tools, MCP, plugins, memory, sub-agents, and Dulus OS."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.10"
|
|
@@ -513,27 +513,44 @@ class TestBackgroundTaskManager:
|
|
|
513
513
|
assert path.exists()
|
|
514
514
|
|
|
515
515
|
def test_launch_worker_creates_subprocess(self, tmp_manager: BackgroundTaskManager) -> None:
|
|
516
|
-
# Actually test subprocess creation (will create a real process)
|
|
517
|
-
|
|
516
|
+
# Actually test subprocess creation (will create a real process).
|
|
517
|
+
# No single quotes — Windows cmd treats them literally; the worker
|
|
518
|
+
# runner now uses repr()-safe Python, but keep the command portable.
|
|
519
|
+
view = tmp_manager.create_bash_task("echo HELLO_FROM_TEST", "test echo")
|
|
518
520
|
assert view.runtime.worker_pid is not None
|
|
519
521
|
assert view.runtime.worker_pid > 0
|
|
520
522
|
|
|
521
|
-
#
|
|
522
|
-
time.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
523
|
+
# Poll — CI Windows runners are slower than a fixed 0.5s sleep.
|
|
524
|
+
deadline = time.time() + 10.0
|
|
525
|
+
text = ""
|
|
526
|
+
while time.time() < deadline:
|
|
527
|
+
chunk = tmp_manager.read_output(view.spec.id)
|
|
528
|
+
text = chunk.text or ""
|
|
529
|
+
if "HELLO_FROM_TEST" in text:
|
|
530
|
+
break
|
|
531
|
+
time.sleep(0.1)
|
|
532
|
+
assert "HELLO_FROM_TEST" in text, f"worker output empty/missing marker: {text!r}"
|
|
527
533
|
|
|
528
534
|
def test_launch_worker_with_cwd(self, tmp_manager: BackgroundTaskManager, tmp_path: Path) -> None:
|
|
529
535
|
subdir = tmp_path / "subdir"
|
|
530
536
|
subdir.mkdir()
|
|
531
537
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
538
|
+
# Portable cwd probe: Python prints the path on every OS (pwd is Unix-only).
|
|
539
|
+
view = tmp_manager.create_bash_task(
|
|
540
|
+
f'"{sys.executable}" -c "import os; print(os.getcwd())"',
|
|
541
|
+
"test cwd",
|
|
542
|
+
cwd=str(subdir),
|
|
543
|
+
)
|
|
544
|
+
deadline = time.time() + 10.0
|
|
545
|
+
text = ""
|
|
546
|
+
while time.time() < deadline:
|
|
547
|
+
chunk = tmp_manager.read_output(view.spec.id)
|
|
548
|
+
text = chunk.text or ""
|
|
549
|
+
if str(subdir) in text or subdir.name in text:
|
|
550
|
+
break
|
|
551
|
+
time.sleep(0.1)
|
|
552
|
+
# Normalize separators so Win paths still match.
|
|
553
|
+
assert str(subdir) in text or str(subdir).replace("\\", "/") in text.replace("\\", "/")
|
|
537
554
|
|
|
538
555
|
def test_recovery_marks_stale_tasks(self, tmp_manager: BackgroundTaskManager) -> None:
|
|
539
556
|
with patch.object(tmp_manager, "_launch_worker", return_value=12345):
|