From 1f7b00e1a5ddfda0a60c0fe512eda7fff49d6f94 Mon Sep 17 00:00:00 2001 From: Paul Vasiletz Date: Wed, 21 Oct 2026 13:37:38 +0300 Subject: [PATCH 1/5] plugins, skills | support custom skills on the omega-memory volume with overrides for built-in skills --- Autotests/mock/test_workflow_plugin_mock.py | 2 +- config/config.yaml | 16 ++++--- plugins/workflow/README.md | 25 ++++++++--- plugins/workflow/workflow.metta | 46 ++++++++++++++++----- src/utils.metta | 5 +++ 5 files changed, 71 insertions(+), 23 deletions(-) diff --git a/Autotests/mock/test_workflow_plugin_mock.py b/Autotests/mock/test_workflow_plugin_mock.py index ba0575108..dddfb419a 100644 --- a/Autotests/mock/test_workflow_plugin_mock.py +++ b/Autotests/mock/test_workflow_plugin_mock.py @@ -32,7 +32,7 @@ DEMO_MESSAGE = "This is a test workflow demonstration" RESEARCH_WORKFLOW = "research-workflow" -RESEARCH_DIR = "/PeTTa/repos/Omega/memory/workflow_space/research" +RESEARCH_DIR = "/PeTTa/repos/Omega/memory/workflow/space/research" RESEARCH_NAME = "qa-research-autotest" diff --git a/config/config.yaml b/config/config.yaml index 131e6cd0d..729c3b6c7 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -155,13 +155,17 @@ asione_model: "asi1-ultra" # Plugins # Workflow -# Path to the directory which contains available workflows. Default value is -# /plugins/workflow/instructions. See plugins/workflow/README.md -# for details. -#pluginWorkflowInstructionsDir: ./plugins/workflow/instructions +# Path to the directory which contains built-in workflows shipped with Omega. +# Default value is /plugins/workflow/instructions. See +# plugins/workflow/README.md for details. +pluginWorkflowInstructionsDir: ./repos/Omega/plugins/workflow/instructions +# Path to the directory which contains custom workflows. A custom workflow +# overrides the built-in one with the same name. Default value is +# /workflow/skills +pluginWorkflowCustomInstructionsDir: ./repos/Omega/memory/workflow/skills # Path to the directory to keep workflow working files when workflow is active. -# Default value is /workflow_space -#pluginWorkflowMemoryDir: ./memory/workflow_space +# Default value is /workflow/space +pluginWorkflowMemoryDir: ./repos/Omega/memory/workflow/space # OpenClaw openClawEnabled: disabled diff --git a/plugins/workflow/README.md b/plugins/workflow/README.md index dd5558176..76a461369 100644 --- a/plugins/workflow/README.md +++ b/plugins/workflow/README.md @@ -60,7 +60,19 @@ For example: Start an agent and ask it to use the workflow. For the test workflow above send: `Demonstrate workflow plugin` -New workflows put into `pluginWorkflowInstructionsDir` while the agent is +## Built-in and custom workflows + +Workflows are looked up in two directories: +- built-in workflows shipped with Omega are kept in + `pluginWorkflowInstructionsDir` and are updated together with Omega; +- custom workflows are kept in `pluginWorkflowCustomInstructionsDir` which is + placed in the memory directory by default, so they survive container + restarts and image updates. The directory is created on start if missing. + +A custom workflow overrides the built-in one with the same name. To change a +built-in workflow copy it into the custom directory and edit the copy. + +New workflows put into `pluginWorkflowCustomInstructionsDir` while the agent is running become available after the agent calls the `workflow-refresh-list` skill. Ask the agent to refresh the list of workflows, or just ask it to load the new workflow by name. @@ -69,11 +81,14 @@ the new workflow by name. Wofkflow plugin Omega configuration parameters: - `pluginWorkflowInstructionsDir` - path to the directory which contains - available workflows. Default value is `/plugins/workflow/instructions` + built-in workflows. Default value is `/plugins/workflow/instructions` +- `pluginWorkflowCustomInstructionsDir` - path to the directory which contains + custom workflows. Default value is `/workflow/skills` - `pluginWorkflowMemoryDir` - path to the directory to keep workflow working - files when workflow is active. Default value is `/memory/workflow_space` + files when workflow is active. Default value is `/workflow/space` + +Custom and working directories are created on start together with missing +parent directories. One can set this parameters passing them as a command line arguments. For example: diff --git a/plugins/workflow/workflow.metta b/plugins/workflow/workflow.metta index 3058c12d3..1a868cf1d 100644 --- a/plugins/workflow/workflow.metta +++ b/plugins/workflow/workflow.metta @@ -1,6 +1,7 @@ !(import! &self (library lib_spaces)) (= (pluginWorkflowInstructionsDir) (empty)) +(= (pluginWorkflowCustomInstructionsDir) (empty)) (= (pluginWorkflowMemoryDir) (empty)) (= (loadOmegaPlugin) @@ -9,10 +10,15 @@ (configure pluginWorkflowInstructionsDir (joinPath ((projectRootDirectory) "plugins" "workflow" "instructions"))) (if (not (exists-directory (pluginWorkflowInstructionsDir))) (Error (_loadOmegaPlugin) (strings-concat ("Instructions directory: " (pluginWorkflowInstructionsDir) " doesn't exist"))) - (progn - (configure pluginWorkflowMemoryDir (joinPath ((memoryDirectory) workflow_space))) + (progn + (configure pluginWorkflowCustomInstructionsDir (joinPath ((memoryDirectory) "workflow" "skills"))) + (let $customrc (make-directory-path (pluginWorkflowCustomInstructionsDir)) ()) + (if (not (exists-directory (pluginWorkflowCustomInstructionsDir))) + (log ERROR "workflow-plugin" (strings-concat ("Could not create custom instructions directory: " (pluginWorkflowCustomInstructionsDir)", only built-in workflows are available"))) + ()) + (configure pluginWorkflowMemoryDir (joinPath ((memoryDirectory) "workflow" "space"))) (log INFO "workflow-plugin" (strings-concat ("Creating working directory: " (pluginWorkflowMemoryDir)))) - (let $rc (make-directory (pluginWorkflowMemoryDir)) ()) + (let $rc (make-directory-path (pluginWorkflowMemoryDir)) ()) (if (exists-directory (pluginWorkflowMemoryDir)) (progn (log INFO "workflow-plugin" "Adding plugin's skills") @@ -45,12 +51,19 @@ ; False)) (= (instructionsDir) (get-existing-dir (pluginWorkflowInstructionsDir))) + +;; custom workflow overrides built-in one with the same name +(= (workflow-dir $name) + (if (exists-file (joinPath ((pluginWorkflowCustomInstructionsDir) $name "SKILL.md"))) + (pluginWorkflowCustomInstructionsDir) + (instructionsDir))) + (= (workflow-load-instructions $name) (let $active (get-state &workflow_active_name) (if (== $active "") (if (not (valid-file-or-folder-name $name)) (Error (quote (workflow-load-instructions $name)) (strings-concat ("ERROR: invalid workflow name: " $name))) - (let $path (joinPath ((instructionsDir) $name "SKILL.md")) + (let $path (joinPath ((workflow-dir $name) $name "SKILL.md")) (if (not (exists-file $path)) (Error (quote (workflow-load-instructions $name)) (strings-concat ("ERROR: skill file not found: " $path ". Try to call workflow-refresh-list"))) (let $content (read-file $path) @@ -83,7 +96,7 @@ ; to load skills descriptions which are defined only for current workflow (= (add-workflow-skills $name) - (let $path (joinPath ((instructionsDir) $name "skill")) + (let $path (joinPath ((workflow-dir $name) $name "skill")) (progn (import! &workflow_skills $path) (collapse (match &workflow_skills (skill $func $desc $args) (add-skill $func $desc $args))) @@ -102,11 +115,22 @@ (add-prompt-extension workflow_active_instructions $content))) ;;========================== Functions to load workflow description ======================= -;; returns workflow name and its description +;; returns workflow name and its description, custom workflows override built-in ones with the same name (= (workflow-list-available) (let* ( ($base (instructionsDir)) - ($dirs (directory-dirs $base))) - (collect-description $dirs $base))) + ($builtin (collect-description (filter-shadowed (directory-dirs $base)) $base ())) + ($custom (pluginWorkflowCustomInstructionsDir))) + (if (exists-directory $custom) + (collect-description (directory-dirs $custom) $custom $builtin) + $builtin))) + +;; drops built-in workflows overridden by custom ones with the same name +(= (filter-shadowed ()) ()) +(= (filter-shadowed (cons $first $tail)) + (let $rest (filter-shadowed $tail) + (if (exists-file (joinPath ((pluginWorkflowCustomInstructionsDir) $first "SKILL.md"))) + $rest + (cons-atom $first $rest)))) ;; re-reads available workflows into the prompt (= (workflow-refresh-list) @@ -116,10 +140,10 @@ (add-prompt-extension workflows-available $text)))) ;; collects workflow description from description.txt -(= (collect-description () $base) ()) -(= (collect-description (cons $first $tail) $base) +(= (collect-description () $base $acc) $acc) +(= (collect-description (cons $first $tail) $base $acc) (let* (($path (joinPath ($base $first "description.txt"))) - ($rest (collect-description $tail $base))) + ($rest (collect-description $tail $base $acc))) (if (exists-file $path) (cons-atom (strings-concat ($first ": " (read-file $path))) $rest) $rest))) diff --git a/src/utils.metta b/src/utils.metta index 3aa2e0f62..714b3e58d 100644 --- a/src/utils.metta +++ b/src/utils.metta @@ -8,6 +8,7 @@ !(import_prolog_function sread) !(import_prolog_function string_concat) !(import_prolog_function make_directory) +!(import_prolog_function make_directory_path) (= (sleep $t) (progn (translatePredicate (sleep $t)) @@ -29,6 +30,10 @@ (let $rc (catch (((translatePredicate (make_directory $path))))) (return-on-error $rc True))) +(= (make-directory-path $path) + (let $rc (catch (((translatePredicate (make_directory_path $path))))) + (return-on-error $rc True))) + (= (string-replace $str $a $b) (atomic_list_concat (split_string $str $a "") $b)) From ad9e49acbcdac8fedba077795c83e89c7fc47c9f Mon Sep 17 00:00:00 2001 From: Paul Vasiletz Date: Thu, 22 Oct 2026 14:21:54 +0300 Subject: [PATCH 2/5] memory export | add skills export support (import-kb 0.2.6) --- entrypoint.sh | 1 + scripts/omega | 22 +++++++++++++++------- src/memory_export.py | 10 ++++++++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 97d7397ab..f5cb0501f 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -82,6 +82,7 @@ elif operation == "import": mode=os.environ.get("MEMORY_IMPORT_MODE", "overwrite"), include_history=os.environ.get("MEMORY_IMPORT_NO_HISTORY") != "1", include_vectors=os.environ.get("MEMORY_IMPORT_NO_VECTOR") != "1", + include_skills=os.environ.get("MEMORY_IMPORT_NO_SKILLS") != "1", ) else: raise ValueError(f"Unsupported memory portability operation: {operation!r}")' diff --git a/scripts/omega b/scripts/omega index 15954614a..b4c567431 100755 --- a/scripts/omega +++ b/scripts/omega @@ -669,6 +669,7 @@ help() { echo -e "\t--memory-mode overwrite|append import mode (default: overwrite)" echo -e "\t--only-history restore history only" echo -e "\t--only-vector restore long-term memory only" + echo -e "\t--only-skills restore custom skills and workflow space only" } require_option_value() { @@ -700,6 +701,7 @@ options() { memory_import_mode="overwrite" memory_import_only_history=0 memory_import_only_vector=0 + memory_import_only_skills=0 memory_export_enabled=0 if [[ "$#" -eq 0 ]]; then @@ -763,6 +765,10 @@ options() { memory_import_only_vector=1 shift ;; + --only-skills) + memory_import_only_skills=1 + shift + ;; --version|-v) version; exit 0;; --help|-h) help; exit 0;; *) help @@ -833,13 +839,12 @@ options() { return 1 fi - if [[ "${memory_import_only_history}" == "1" && "${memory_import_only_vector}" == "1" ]]; then - echo "--only-history and --only-vector cannot be combined" >&2 + if (( memory_import_only_history + memory_import_only_vector + memory_import_only_skills > 1 )); then + echo "--only-history, --only-vector and --only-skills cannot be combined" >&2 return 1 fi - if [[ -z "${memory_import_file}" && - ( "${memory_import_only_history}" == "1" || - "${memory_import_only_vector}" == "1" ) ]]; then + if [[ -z "${memory_import_file}" ]] && + (( memory_import_only_history + memory_import_only_vector + memory_import_only_skills > 0 )); then echo "Import component flags require --memory-import" >&2 return 1 fi @@ -980,10 +985,13 @@ start() { -e "MEMORY_IMPORT_MODE=${memory_import_mode}" ) if [[ "${memory_import_only_history}" == "1" ]]; then - memory_import_env+=(-e MEMORY_IMPORT_NO_VECTOR=1) + memory_import_env+=(-e MEMORY_IMPORT_NO_VECTOR=1 -e MEMORY_IMPORT_NO_SKILLS=1) fi if [[ "${memory_import_only_vector}" == "1" ]]; then - memory_import_env+=(-e MEMORY_IMPORT_NO_HISTORY=1) + memory_import_env+=(-e MEMORY_IMPORT_NO_HISTORY=1 -e MEMORY_IMPORT_NO_SKILLS=1) + fi + if [[ "${memory_import_only_skills}" == "1" ]]; then + memory_import_env+=(-e MEMORY_IMPORT_NO_HISTORY=1 -e MEMORY_IMPORT_NO_VECTOR=1) fi fi diff --git a/src/memory_export.py b/src/memory_export.py index 651778755..5e90507d8 100644 --- a/src/memory_export.py +++ b/src/memory_export.py @@ -34,6 +34,10 @@ def _resolve_chroma_path() -> Path: return Path(str(configured)).expanduser().resolve() +def _resolve_config_path(key: str) -> Path: + return Path(str(config_get_by_key(key))).expanduser().resolve() + + def _cloud_embedder(provider, model): def embed(texts): from import_knowledge.import_knowledge import embed_batch, init_embeddings @@ -67,6 +71,8 @@ def create_memory_store(): memory_dir=_resolve_memory_dir(), chroma_path=_resolve_chroma_path(), collection_name="memories", + skills_dir=_resolve_config_path("pluginWorkflowCustomInstructionsDir"), + space_dir=_resolve_config_path("pluginWorkflowMemoryDir"), **_embedding_options(), ) @@ -89,7 +95,7 @@ def _get_transfer(): return _transfer -_VALID_COMPONENTS = ("history", "ltm", "both") +_VALID_COMPONENTS = ("history", "ltm", "skills", "all") def is_export_enabled() -> bool: @@ -135,7 +141,7 @@ def handle_export_command( return ( "Unknown /memory-export command. " - "Use: /memory-export history|ltm|both" + "Use: /memory-export history|ltm|skills|all" ) From b5d73fab72163d31c9a45af50b4b6e107e20a4cd Mon Sep 17 00:00:00 2001 From: Paul Vasiletz Date: Thu, 22 Oct 2026 14:26:33 +0300 Subject: [PATCH 3/5] memory export | update docs --- docs/reference-channels.md | 2 +- docs/reference-memory-portability.md | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/reference-channels.md b/docs/reference-channels.md index 03513ef18..affd15aef 100644 --- a/docs/reference-channels.md +++ b/docs/reference-channels.md @@ -79,7 +79,7 @@ Minimal JSON chat adapter over a WebSocket connection. Selected with `commchanne - `stop_websocket()` — stop the listener thread and close the socket. - Requires the `websockets` Python package. - When `WS_TOKEN` is set it is sent as an `Authorization: Bearer ` header. Unlike the IRC/Telegram/Slack adapters there is no one-time `auth ` gate — trust is established by the endpoint URL and bearer token. -- Supports immediate `/memory-export history|ltm|both` commands when memory export is enabled and `WS_TOKEN` is configured. The export handler uses a SHA-256-derived connection principal and never exposes the bearer token. Protect the endpoint with `wss://` and server-side access controls because WebSocket does not have the per-user ownership gate used by the other channels. +- Supports immediate `/memory-export history|ltm|skills|all` commands when memory export is enabled and `WS_TOKEN` is configured. The export handler uses a SHA-256-derived connection principal and never exposes the bearer token. Protect the endpoint with `wss://` and server-side access controls because WebSocket does not have the per-user ownership gate used by the other channels. - Reconnects automatically with exponential backoff (1s → 30s, ±20% jitter) and is safe to start once at process startup. ### Frame protocol diff --git a/docs/reference-memory-portability.md b/docs/reference-memory-portability.md index aad6a8651..37bab361f 100644 --- a/docs/reference-memory-portability.md +++ b/docs/reference-memory-portability.md @@ -52,7 +52,8 @@ token so the credential itself is never used as an identifier: ```text /memory-export history /memory-export ltm -/memory-export both +/memory-export skills +/memory-export all ``` Completion is delivered through the active channel and includes the filename, @@ -65,10 +66,15 @@ manifest.json history/history.metta vector/collections.json vector/records.jsonl +skills/... +space/... ``` History is the conversation trace. LTM is logical user-memory records from -ChromaDB. Prompts, credentials, logs, skills, and other operational state are +ChromaDB. Skills are custom workflows from `pluginWorkflowCustomInstructionsDir` +together with workflow working files from `pluginWorkflowMemoryDir`; only +regular files are exported, symbolic links and empty directories are skipped. +Built-in workflows, prompts, credentials, logs, and other operational state are not exported. SHA-256 detects corruption, not archive authorship. ## Import @@ -85,9 +91,12 @@ scripts/omega start -d singularitynet/omega: -p OpenAI -t telegram \ ``` `overwrite` replaces the selected components after validation and rollback -preparation. `append` preserves existing history and adds imported LTM records -under new IDs. Select a single component with `--only-history` or -`--only-vector`. Without either option, both components are imported. +preparation. For skills it replaces both the custom skills and the workflow +space directories, even when the archive contains no skills. `append` preserves +existing history and adds imported LTM records under new IDs, and copies imported +skills and space files over the existing ones with the same names. Select a +single component with `--only-history`, `--only-vector`, or `--only-skills`. +Without these options, all components present in the archive are imported. The importer validates archive paths, checksums, manifest metadata, record counts, and embedding compatibility before changing live memory. It runs before From 4919e562c7fb84b60dd21d21906496ce8da268d1 Mon Sep 17 00:00:00 2001 From: Paul Vasiletz Date: Fri, 23 Oct 2026 13:13:24 +0300 Subject: [PATCH 4/5] tests, memory export | update memory export test suite --- tests/test_memory_export.py | 67 ++++++++++++++++++++++---------- tests/test_omegaclaw_launcher.py | 37 +++++++++++++++--- 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/tests/test_memory_export.py b/tests/test_memory_export.py index 214a75e86..67b23bc26 100644 --- a/tests/test_memory_export.py +++ b/tests/test_memory_export.py @@ -40,15 +40,15 @@ def test_export_command_requires_policy(handler): ) assert "Memory export complete" in handler.handle_export_command( - "/memory-export both", "authenticated-user" + "/memory-export all", "authenticated-user" ) - assert exported == ["both"] + assert exported == ["all"] handler.is_export_enabled = lambda: False assert handler.handle_export_command( - "/memory-export both", "authenticated-user" + "/memory-export all", "authenticated-user" ) is None - assert exported == ["both"] + assert exported == ["all"] def test_export_requires_authenticated_user(handler): @@ -56,13 +56,13 @@ def test_export_requires_authenticated_user(handler): "an unauthenticated command must not start an export" ) - assert handler.handle_export_command("/memory-export both") == ( + assert handler.handle_export_command("/memory-export all") == ( "Memory export denied: an authenticated user is required." ) def test_module_import_does_not_require_memory_portability(handler): assert "memory_portability" not in sys.modules - assert handler.is_export_command("/memory-export both") + assert handler.is_export_command("/memory-export all") def test_entrypoint_configures_memory_transfer_directory(): @@ -70,7 +70,7 @@ def test_entrypoint_configures_memory_transfer_directory(): assert 'transfer_dir="/memory-transfer"' in entrypoint -@pytest.mark.parametrize("component", ["history", "ltm", "both"]) +@pytest.mark.parametrize("component", ["history", "ltm", "skills", "all"]) def test_export_runs_immediately(handler, component): exported = [] handler._get_transfer = lambda: types.SimpleNamespace( @@ -100,7 +100,7 @@ def test_confirmation_command_is_no_longer_supported(handler): assert reply == ( "Unknown /memory-export command. " - "Use: /memory-export history|ltm|both" + "Use: /memory-export history|ltm|skills|all" ) def test_transfer_uses_effective_runtime_embedding_provider(handler, monkeypatch): @@ -183,12 +183,17 @@ def __init__(self, **kwargs): memory_dir = tmp_path / "custom-memory" chroma_path = tmp_path / "custom-chroma" + settings = { + "embeddingprovider": "Local", + "pluginWorkflowCustomInstructionsDir": str(tmp_path / "custom-skills"), + "pluginWorkflowMemoryDir": str(tmp_path / "custom-space"), + } monkeypatch.setattr(handler, "_resolve_memory_dir", lambda: memory_dir) monkeypatch.setattr(handler, "_resolve_chroma_path", lambda: chroma_path) monkeypatch.setattr( handler, "config_get_by_key", - lambda key, default=None: "Local" if key == "embeddingprovider" else default, + lambda key, default=None: settings.get(key, default), ) store = handler.create_memory_store() @@ -199,6 +204,8 @@ def __init__(self, **kwargs): "memory_dir": memory_dir, "chroma_path": chroma_path, "collection_name": "memories", + "skills_dir": tmp_path / "custom-skills", + "space_dir": tmp_path / "custom-space", } ] @@ -239,7 +246,12 @@ def configure(monkeypatch, handler, tmp_path, provider, config_model="", env_provider="Local", env_model="env-only-model"): monkeypatch.setenv("EMBEDDING_PROVIDER", env_provider) monkeypatch.setenv("EMBEDDING_MODEL", env_model) - settings = {"embeddingprovider": provider, "embeddingModel": config_model} + settings = { + "embeddingprovider": provider, + "embeddingModel": config_model, + "pluginWorkflowCustomInstructionsDir": str(tmp_path / "skills"), + "pluginWorkflowMemoryDir": str(tmp_path / "space"), + } monkeypatch.setattr( handler, "config_get_by_key", @@ -331,6 +343,8 @@ def test_memory_store_ignores_embedding_environment_variables( "memory_dir": tmp_path / "memory", "chroma_path": tmp_path / "chroma", "collection_name": "memories", + "skills_dir": tmp_path / "skills", + "space_dir": tmp_path / "space", } ] @@ -340,6 +354,9 @@ def test_entrypoint_passes_container_arguments_to_memory_portability(): assert "init_config(sys.argv[1:])" in entrypoint assert "init_config([])" not in entrypoint + assert ( + 'include_skills=os.environ.get("MEMORY_IMPORT_NO_SKILLS") != "1"' in entrypoint + ) assert entrypoint.count( '''"${agent_su[@]}" -s /bin/sh -c 'exec python3 -c "$MEMORY_PORTABILITY_PYTHON" "$@"' sh "$@"''' ) == 2 @@ -388,6 +405,8 @@ def test_storage_paths_are_resolved_from_omega_config( configured = { "memoryDirectory": str(tmp_path / "configured-memory"), "chromaDbPath": str(tmp_path / "configured-chroma"), + "pluginWorkflowCustomInstructionsDir": str(tmp_path / "configured-skills"), + "pluginWorkflowMemoryDir": str(tmp_path / "configured-space"), } monkeypatch.delenv("CHROMA_DB_PATH", raising=False) monkeypatch.setattr( @@ -398,6 +417,12 @@ def test_storage_paths_are_resolved_from_omega_config( assert handler._resolve_memory_dir() == tmp_path / "configured-memory" assert handler._resolve_chroma_path() == tmp_path / "configured-chroma" + assert handler._resolve_config_path( + "pluginWorkflowCustomInstructionsDir" + ) == tmp_path / "configured-skills" + assert handler._resolve_config_path( + "pluginWorkflowMemoryDir" + ) == tmp_path / "configured-space" def test_websocket_defers_memory_export_to_core_dispatch(monkeypatch): @@ -423,9 +448,9 @@ def test_websocket_defers_memory_export_to_core_dispatch(monkeypatch): module.send_message = replies.append module._handle_frame(json.dumps({ - "type": "user_message", "seq": 1, "text": "/memory-export both" + "type": "user_message", "seq": 1, "text": "/memory-export all" })) - assert received == [(1, "/memory-export both")] + assert received == [(1, "/memory-export all")] assert replies == [] @@ -440,7 +465,7 @@ def test_commchannel_receive_dispatches_control_commands(monkeypatch): principals: list[str] = [] control = types.ModuleType("src.memory_export") - control.is_export_command = lambda text: text == "/memory-export both" + control.is_export_command = lambda text: text == "/memory-export all" control.handle_export_command = lambda text, principal: ( principals.append(principal) or "Memory export complete" ) @@ -451,7 +476,7 @@ def test_commchannel_receive_dispatches_control_commands(monkeypatch): replies: list[str] = [] channels._commchannel = types.SimpleNamespace( - receive=lambda: "alice: /memory-export both | alice: hello", + receive=lambda: "alice: /memory-export all | alice: hello", send=replies.append, ) channels._commchannel_id = "telegram" @@ -470,7 +495,7 @@ def test_commchannel_receive_denies_export_without_authenticated_user(monkeypatc monkeypatch.setitem(sys.modules, "auth", auth) control = types.ModuleType("src.memory_export") - control.is_export_command = lambda text: text == "/memory-export both" + control.is_export_command = lambda text: text == "/memory-export all" control.handle_export_command = lambda text, principal: ( "Memory export denied: an authenticated user is required." if principal is None @@ -483,7 +508,7 @@ def test_commchannel_receive_denies_export_without_authenticated_user(monkeypatc replies: list[str] = [] channels._commchannel = types.SimpleNamespace( - receive=lambda: "alice: /memory-export both", + receive=lambda: "alice: /memory-export all", send=replies.append, ) channels._commchannel_id = "telegram" @@ -503,7 +528,7 @@ def test_commchannel_receive_dispatches_websocket_export(monkeypatch): commands: list[str] = [] principals: list[str] = [] control = types.ModuleType("src.memory_export") - control.is_export_command = lambda text: text == "/memory-export both" + control.is_export_command = lambda text: text == "/memory-export all" control.handle_export_command = lambda text, principal: ( commands.append(text) or principals.append(principal) @@ -516,13 +541,13 @@ def test_commchannel_receive_dispatches_websocket_export(monkeypatch): replies: list[str] = [] channels._commchannel = types.SimpleNamespace( - receive=lambda: "/memory-export both", + receive=lambda: "/memory-export all", send=replies.append, ) channels._commchannel_id = "websocket" assert channels.commChannelReceive() == "" - assert commands == ["/memory-export both"] + assert commands == ["/memory-export all"] assert principals == [ f"websocket:{hashlib.sha256(websocket_token.encode('utf-8')).hexdigest()}" ] @@ -544,7 +569,7 @@ def test_websocket_export_requires_bearer_token(monkeypatch): def test_commchannel_receive_does_not_consume_command_mentions(monkeypatch): control = types.ModuleType("src.memory_export") - control.is_export_command = lambda text: text == "/memory-export both" + control.is_export_command = lambda text: text == "/memory-export all" control.handle_export_command = lambda *_: pytest.fail( "a command mentioned in normal text must not execute" ) @@ -553,7 +578,7 @@ def test_commchannel_receive_does_not_consume_command_mentions(monkeypatch): monkeypatch.delitem(sys.modules, "channels", raising=False) channels = importlib.import_module("channels") - message = "alice: please use /memory-export both" + message = "alice: please use /memory-export all" channels._commchannel = types.SimpleNamespace( receive=lambda: message, send=lambda *_: pytest.fail("normal messages must not generate replies"), diff --git a/tests/test_omegaclaw_launcher.py b/tests/test_omegaclaw_launcher.py index 27c0a6b91..bf4dee19a 100644 --- a/tests/test_omegaclaw_launcher.py +++ b/tests/test_omegaclaw_launcher.py @@ -333,8 +333,21 @@ def test_container_user_can_write_group_protected_transfer_directory(tmp_path): @pytest.mark.parametrize( ("option", "included_environment", "excluded_environment"), [ - ("--only-history", "MEMORY_IMPORT_NO_VECTOR=1", "MEMORY_IMPORT_NO_HISTORY=1"), - ("--only-vector", "MEMORY_IMPORT_NO_HISTORY=1", "MEMORY_IMPORT_NO_VECTOR=1"), + ( + "--only-history", + ("MEMORY_IMPORT_NO_VECTOR=1", "MEMORY_IMPORT_NO_SKILLS=1"), + "MEMORY_IMPORT_NO_HISTORY=1", + ), + ( + "--only-vector", + ("MEMORY_IMPORT_NO_HISTORY=1", "MEMORY_IMPORT_NO_SKILLS=1"), + "MEMORY_IMPORT_NO_VECTOR=1", + ), + ( + "--only-skills", + ("MEMORY_IMPORT_NO_HISTORY=1", "MEMORY_IMPORT_NO_VECTOR=1"), + "MEMORY_IMPORT_NO_SKILLS=1", + ), ], ) def test_only_component_options_select_one_import_component( @@ -346,15 +359,27 @@ def test_only_component_options_select_one_import_component( result = _run_launcher(tmp_path, option) assert result.returncode == 0, result.stderr - assert included_environment in result.stdout + for environment in included_environment: + assert environment in result.stdout assert excluded_environment not in result.stdout -def test_only_component_options_are_mutually_exclusive(tmp_path): - result = _run_launcher(tmp_path, "--only-history", "--only-vector") +@pytest.mark.parametrize( + "options", + [ + ("--only-history", "--only-vector"), + ("--only-history", "--only-skills"), + ("--only-vector", "--only-skills"), + ], +) +def test_only_component_options_are_mutually_exclusive(tmp_path, options): + result = _run_launcher(tmp_path, *options) assert result.returncode != 0 - assert "--only-history and --only-vector cannot be combined" in result.stderr + assert ( + "--only-history, --only-vector and --only-skills cannot be combined" + in result.stderr + ) @pytest.mark.parametrize("command", ["start", "stop", "clean"]) From 2bc9f80a84581ab80624831521f9930fedfd9eef Mon Sep 17 00:00:00 2001 From: Paul Vasiletz Date: Fri, 23 Oct 2026 13:14:20 +0300 Subject: [PATCH 5/5] deps | bump the import-kb package version to 0.2.6 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 19f8bd16c..b06c9448b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ chromadb==1.5.9 openai==2.38.0 transformers==5.8.0 sentence-transformers==5.5.1 -import-kb==0.2.4 +import-kb==0.2.6 py-landlock==0.1.1 pyyaml==6.0.3 ddgs==9.14.4