The `plan` subagent was **incomplete** — it hit the sync tool-round limit before finishing and did not produce a plan.

What happened:
- The subagent used 2 tools: `list_directory` (on `/home/matrix-lite/app`) and `glob_files` (for `**/*.py`)
- It was still requesting more tools when it reached its round limit
- No plan was produced

I will not claim the plan was created.

I can create the plan directly myself using my own tools, or you could re-run with a narrower prompt (e.g., focusing only on the subagent scheduler code). Want me to draft it directly?

Native identity and behavior

Generic smoke/native worker profile

Native tool use contract

Persistent-chat prompt port placeholder

MCP proxy capability index

Agent skill lifecycle placeholder

Pinned state

Decision state

Curated memory

Session packet candidate

Session-start/restore packet candidate

Current task

Longer-term ideas / experiments

Runtime hints and notices

Operational notices

Recent failures

Persistent context manifest health

Native scoped grants

Provider capability probes

Native session summary

Native hot tail

Bounded keyword retrieval

Vector search / RAG placeholder

Turn packet candidate

Matrix attachment/reply/batch context candidate

Native skill: prompt-governance

Native skill: senior-architect

Current Matrix event context

Output:
The `plan` subagent was **incomplete** — it hit the sync tool-round limit before finishing and did not produce a plan.

What happened:
- The subagent used 2 tools: `list_directory` (on `/home/matrix-lite/app`) and `glob_files` (for `**/*.py`)
- It was still requesting more tools when it reached its round limit
- No plan was produced

I will not claim the plan was created.

I can create the plan directly myself using my own tools, or you could re-run with a narrower prompt (e.g., focusing only on the subagent scheduler code). Want me to draft it directly?

Output: Incomplete: native subagent reached its sync tool-round limit and still requested more tools during finalization. Inspect the job log for pending tool calls and rerun with a narrower prompt or async mode.

Native identity and behavior

Generic smoke/native worker profile

Native tool use contract

Persistent-chat prompt port placeholder

MCP proxy capability index

Agent skill lifecycle placeholder

Pinned state

Decision state

Curated memory

Session packet candidate

Session-start/restore packet candidate

Current task

Longer-term ideas / experiments

Runtime hints and notices

Operational notices

Recent failures

Persistent context manifest health

Native scoped grants

Provider capability probes

Native session summary

Native hot tail

Bounded keyword retrieval

Vector search / RAG placeholder

Turn packet candidate

Native skill: prompt-governance

Native skill: senior-architect

Native identity and behavior

Generic smoke/native worker profile

Native tool use contract

Persistent-chat prompt port placeholder

MCP proxy capability index

Agent skill lifecycle placeholder

Pinned state

Decision state

Curated memory

Session packet candidate

Session-start/restore packet candidate

Current task

Longer-term ideas / experiments

Runtime hints and notices

Operational notices

Recent failures

Persistent context manifest health

Native scoped grants

Provider capability probes

Native session summary

Native hot tail

Bounded keyword retrieval

Vector search / RAG placeholder

Turn packet candidate

Native skill: prompt-governance

Native skill: senior-architect

2:
3: import argparse
4: import asyncio
5: import html
6: import hashlib
7: import fcntl
8: import json
9: import mimetypes
10: import os
11: import posixpath
12: import queue
13: import re
14: import select
15: import shlex
16: import signal
17: import shutil
18: import sqlite3
19: import subprocess
20: import sys
21: import threading
22: import tempfile
23: import time
24: import uuid
25: from contextlib import contextmanager
26: from datetime import datetime, timedelta, timezone
27: from pathlib import Path
28: from zoneinfo import ZoneInfo
29: from typing import Any
30: from urllib import parse, request
31: from urllib.parse import quote
32: from urllib.error import HTTPError, URLError
33:
34:
35: MATRIX_LITE_CONFIG_FILE = Path(
36:     os.environ.get(
37:         "MATRIX_LITE_CONFIG", "/home/matrix-lite/config/matrix-lite.json"
38:     )
39: )
40:
41:
42: def _read_boot_config() -> dict[str, Any]:
43:     try:
44:         data = json.loads(MATRIX_LITE_CONFIG_FILE.read_text(encoding="utf-8"))
45:     except Exception:
46:         return {}
47:     return data if isinstance(data, dict) else {}
48:
49:
50: BOOT_CONFIG = _read_boot_config()
51:
52:
53: def _boot_path(key: str, default: str) -> str:
54:     value = BOOT_CONFIG.get(key)
55:     return str(value) if value else default
56:
57:
58: def _boot_agent_config(agent_id: str) -> dict[str, Any]:
59:     agents = (
60:         BOOT_CONFIG.get("agents") if isinstance(BOOT_CONFIG.get("agents"), dict) else {}
61:     )
62:     agent = agents.get(agent_id) if isinstance(agents, dict) else None
63:     return agent if isinstance(agent, dict) else {}
64:
65:
66: STATE_DIR = Path(
67:     os.environ.get(
68:         "MATRIX_LITE_STATE_DIR",
69:         os.environ.get(
70:             "OPENCLAW_MATRIX_LITE_STATE_DIR",
71:             _boot_path("state_dir", "/home/matrix-lite/state"),
72:         ),
73:     )
74: )
75: ROOM_MAP_FILE = Path(
76:     os.environ.get("OPENCLAW_MATRIX_LITE_ROOM_MAP", str(STATE_DIR / "rooms.json"))
77: )
78: SYNC_STATE_FILE = Path(
79:     os.environ.get("OPENCLAW_MATRIX_LITE_SYNC_STATE", str(STATE_DIR / "sync.json"))
80: )
81: EVENT_LOG_FILE = Path(
82:     os.environ.get("OPENCLAW_MATRIX_LITE_EVENT_LOG", str(STATE_DIR / "events.jsonl"))
83: )
84: CONVERSATION_REGISTRY_FILE = Path(
85:     os.environ.get(
86:         "OPENCLAW_MATRIX_LITE_CONVERSATIONS",
87:         str(STATE_DIR / "conversations.json"),
88:     )
89: )
90: REPLY_LOG_FILE = Path(
91:     os.environ.get("OPENCLAW_MATRIX_LITE_REPLY_LOG", str(STATE_DIR / "replies.jsonl"))
92: )
93: TURN_LOG_FILE = Path(
94:     os.environ.get("OPENCLAW_MATRIX_LITE_TURN_LOG", str(STATE_DIR / "turns.jsonl"))
95: )
96: NATIVE_KIMI_SESSION_FILE = Path(
97:     os.environ.get("MATRIX_LITE_NATIVE_KIMI_SESSION_FILE", str(STATE_DIR / "native-kimi-sessions.json"))
98: )
99: NATIVE_KIMI_RAW_LOG_FILE = Path(
100:     os.environ.get("MATRIX_LITE_NATIVE_KIMI_RAW_LOG", str(STATE_DIR / "native-kimi-raw-log.jsonl"))
101: )
102: NATIVE_KIMI_MODEL_LOG_FILE = Path(
103:     os.environ.get("MATRIX_LITE_NATIVE_KIMI_MODEL_LOG", str(STATE_DIR / "native-kimi-model-log.jsonl"))
104: )
105: NATIVE_KIMI_SUMMARY_FILE = Path(
106:     os.environ.get("MATRIX_LITE_NATIVE_KIMI_SUMMARY_FILE", str(STATE_DIR / "native-kimi-summary.json"))
107: )
108: NATIVE_CONTEXT_MODULES_FILE = Path(
109:     os.environ.get("MATRIX_LITE_NATIVE_CONTEXT_MODULES", str(STATE_DIR / "native-context-modules.json"))
110: )
111: NATIVE_CONTEXT_MODULE_AUDIT_FILE = Path(
112:     os.environ.get("MATRIX_LITE_NATIVE_CONTEXT_MODULE_AUDIT", str(STATE_DIR / "native-context-module-audit.jsonl"))
113: )
114: NATIVE_CONTEXT_MODULES_LOCK = threading.RLock()
115:
116: OPENCODE_DB_FILE = Path(
117:     os.environ.get(
118:         "MATRIX_LITE_OPENCODE_DB_FILE",
119:         f"/home/matrix-lite/.local/share/opencode/{os.environ.get('OPENCODE_DB', 'opencode-matrix-lite.db')}",
120:     )
121: )

Native tool use contract

Native tool use contract

Generic smoke/native worker profile

Generic smoke/native worker profile

read-only jq/rg/sort/head/wc/json inspection pipelines from /tmp

inspect native-kimi-summary.json with jq, sort, and head

Native scoped grants

Native scoped grants

206:SUBAGENT_STATE_DIR = Path(os.environ.get("MATRIX_LITE_SUBAGENT_STATE_DIR", str(WORKSPACE_DIR / "state" / "subagents")))
207:NATIVE_SUBAGENT_STATE_DIR = Path(os.environ.get("MATRIX_LITE_NATIVE_SUBAGENT_STATE_DIR", str(STATE_DIR / "native-subagents")))
1405:            summary = {k: raw_input.get(k) for k in ("description", "subagent_type") if raw_input.get(k) is not None}
1455:        subtype = summary.get("subagent_type")
1458:            bits.append(f"subagent={subtype}")
3429:def safe_subagent_result_path(job_path: Path, job: dict[str, Any]) -> Path:
3441:def recent_subagent_completion_context(limit: int = 4, max_chars: int = 2400) -> str:
3466:        result_path = safe_subagent_result_path(path, job)
3482:    text = "Recent async subagent completions (use as continuation context; inspect job/log for details):\n" + "\n".join(rows)
3524:    subagent_context = recent_subagent_completion_context()
3527:        if subagent_context:
3528:            parts += ["", subagent_context]
3535:        if subagent_context:
3536:            parts += ["", subagent_context]
3543:        if subagent_context:
3544:            parts += ["", subagent_context]
4122:        lines = ["# Matrix-lite status", "", f"Updated: {utc_now()}", "", "- [Native context modules](native-context-modules.md)", "- [Native subagents](native-subagents.md)", ""]
4193:        (PAGES_DIR / "native-subagents.md").write_text(
4194:            native_subagents_report_markdown(), encoding="utf-8"
5346:    if agent_id == "opencode-subagent":
7712:    # Sync subagents may use bounded, non-mutating native tools. Edits, root,
7722:def native_subagent_default_profiles() -> list[dict[str, Any]]:
7739:            "can_spawn_subagents": False,
7756:def native_subagent_profile_id(value: str) -> str:
7760:def native_subagent_profile_path(profile_id: str) -> Path:
7761:    return (NATIVE_SUBAGENT_PROFILES_DIR / f"{native_subagent_profile_id(profile_id)}.json").resolve()
7764:def native_subagent_job_path(job_id: str) -> Path:
7769:def native_subagent_result_path(job_id: str) -> Path:
7774:def native_subagent_log_path(job_id: str) -> Path:
7781:def native_subagent_sanitize_policy(policy: dict[str, Any] | None) -> dict[str, Any]:
7789:        "can_spawn_subagents": False,
7792:def native_subagent_profiles() -> list[dict[str, Any]]:
7793:    merged = {p["id"]: dict(p) for p in native_subagent_default_profiles()}
7799:            profile_id = native_subagent_profile_id(str(data.get("id") or path.stem))
7800:            base = merged.get(profile_id, {"id": profile_id, "version": 1, "title": profile_id, "description": "Custom native subagent profile", "instructions": "Return concise findings with evidence.", "tool_policy": {"native_tools": []}, "timeouts": {"sync_seconds": 45}, "max_tool_rounds": 0, "max_result_chars": 6000})
7803:            base["tool_policy"] = native_subagent_sanitize_policy(base.get("tool_policy") if isinstance(base.get("tool_policy"), dict) else {})
7810:def native_subagent_get_profile(profile_id: str) -> dict[str, Any] | None:
7811:    wanted = native_subagent_profile_id(profile_id or "explore")
7812:    for profile in native_subagent_profiles():
7818:def native_subagent_public_profile(profile: dict[str, Any]) -> dict[str, Any]:
7821:        public["tool_policy"] = native_subagent_sanitize_policy(public.get("tool_policy"))
7825:def native_subagent_list_profiles() -> dict[str, Any]:
7826:    return {"ok": True, "name": "list_native_subagent_profiles", "profiles": [native_subagent_public_profile(p) for p in native_subagent_profiles()]}
7829:def native_subagent_list_jobs(limit: int = 20, conversation: dict[str, Any] | None = None, *, include_all: bool = False) -> dict[str, Any]:
7843:    return {"ok": True, "name": "list_native_subagent_jobs", "count": len(rows), "jobs": rows}
7846:def native_subagent_read_job(job_id: str, include_result: bool = True, conversation: dict[str, Any] | None = None, *, include_all: bool = False) -> dict[str, Any]:
7847:    path = native_subagent_job_path(job_id)
7849:        return {"ok": False, "name": "read_native_subagent_job", "error": f"unknown native subagent job: {job_id}"}
7853:        return {"ok": False, "name": "read_native_subagent_job", "error": "native subagent job is outside this conversation scope"}
7854:    result = load_json(native_subagent_result_path(job_id), {}) if include_result else {}
7856:    return {"ok": True, "name": "read_native_subagent_job", "job": public_job, "result": result if isinstance(result, dict) else {}}
7859:def native_subagent_write_job(job: dict[str, Any]) -> None:
7861:    save_json(native_subagent_job_path(str(job.get("job_id") or "")), job)
7864:def native_subagent_append_log(job_id: str, text: str) -> None:
7866:    with native_subagent_log_path(job_id).open("a", encoding="utf-8", errors="replace") as handle:
7872:def native_subagent_budget_output(text: str, max_chars: int) -> tuple[str, bool]:
7880:    return head + f"\n\n...[native subagent output truncated; {omitted} chars omitted; read_native_subagent_job has the persisted result]...\n\n" + tail, True
7883:def native_subagent_tool_result_payload(round_results: list[dict[str, Any]], limit: int = 20000) -> str:
7889:    return head + f"\n...[tool result JSON truncated for subagent prompt; {len(text) - len(head) - len(tail)} chars omitted]...\n" + tail
7907:def native_subagent_tool_contract(allowed_tools: set[str]) -> str:
7931:def native_subagent_log_json(job_id: str, event: dict[str, Any]) -> None:
7933:        native_subagent_append_log(job_id, json.dumps(event, ensure_ascii=False, default=str))
7937:def native_subagent_model_call(system: str, messages: list[dict[str, str]], *, timeout_seconds: int, conversation: dict[str, Any]) -> tuple[dict[str, Any], str, dict[str, Any]]:
7940:        raise RuntimeError("missing Kimi/OpenRouter API key for native subagent")
7952:def native_subagent_parse_tool_calls(text: str) -> list[dict[str, Any]]:
7957:def native_subagent_parse_raw_tool_calls(text: str) -> list[Any] | None:
7990:def native_subagent_prompt_has_check_intent(prompt: str) -> bool:
7998:def native_subagent_prompt_requires_check(profile_id: str, prompt: str) -> bool:
7999:    return str(profile_id or "") == "test-runner" and native_subagent_prompt_has_check_intent(prompt)
8002:def native_subagent_check_call_is_classified(call: dict[str, Any], conversation: dict[str, Any]) -> bool:
8015:def native_subagent_first_call_is_check(calls: list[Any], conversation: dict[str, Any]) -> bool:
8022:    return name in {"run_check", "bash"} and native_subagent_check_call_is_classified(call, conversation)
8024:def native_subagent_execute_tool(call: dict[str, Any], conversation: dict[str, Any], allowed_tools: set[str]) -> dict[str, Any]:
8028:    # subagents, but transparently route classified check commands through the
8031:        if not native_subagent_check_call_is_classified(call, conversation):
8032:            return {"ok": False, "name": "bash", "error": "raw bash is not allowed for native sync subagents; use run_check for classified checks"}
8037:        return {"ok": False, "name": name, "error": "tool not allowed for native sync subagent"}
8041:def native_subagent_run_sync(profile_id: str, prompt: str, conversation: dict[str, Any], *, timeout_seconds: int | None = None, context: str = "") -> dict[str, Any]:
8042:    profile = native_subagent_get_profile(profile_id)
8044:        return {"ok": False, "name": "run_subagent", "error": f"unknown native subagent profile: {profile_id}"}
8047:        return {"ok": False, "name": "run_subagent", "error": "prompt is required"}
8053:    job = {"job_id": job_id, "profile_id": profile.get("id"), "profile_version": profile.get("version"), "mode": "sync", "status": "running", "created_at": created, "started_at": created, "timeout_seconds": timeout, "parent_conversation_key": parent_scope_key, "prompt_chars": len(clean_prompt), "log_path": str(native_subagent_log_path(job_id))}
8054:    native_subagent_write_job(job)
8057:    tool_policy = native_subagent_sanitize_policy(tool_policy)
8061:    tool_contract = native_subagent_tool_contract(allowed_tools)
8063:        f"You are a native Matrix-lite sync subagent profile={profile.get('id')}.\n"
8071:    native_subagent_log_json(job_id, {"event": "start", "profile_id": profile.get("id"), "allowed_tools": sorted(allowed_tools), "system": system, "messages": messages})
8077:        requires_check = native_subagent_prompt_requires_check(str(profile.get("id") or ""), clean_prompt)
8081:                raise TimeoutError(f"native subagent timed out after {timeout}s")
8083:            _raw_data, text, usage = native_subagent_model_call(system, messages, timeout_seconds=remaining, conversation=conversation)
8084:            native_subagent_log_json(job_id, {"event": "model_response", "round": round_idx, "text": text, "usage": usage})
8086:                raise TimeoutError(f"native subagent timed out after {timeout}s")
8087:            raw_calls = native_subagent_parse_raw_tool_calls(text)
8088:            calls = native_subagent_parse_tool_calls(text)
8089:            native_subagent_log_json(job_id, {"event": "parsed_tool_calls", "round": round_idx, "raw_calls": raw_calls, "calls": calls})
8092:            if requires_check and not check_already_ran and not native_subagent_first_call_is_check(gate_calls, conversation):
8094:                    native_subagent_log_json(job_id, {"event": "contract_retry", "round": round_idx, "reason": "test-runner did not request classified run_check first"})
8105:                native_subagent_log_json(job_id, {"event": "tool_round_limit", "round": round_idx, "pending_calls": calls})
8113:                            "You have reached the native subagent sync tool-round limit. Do NOT request more tools. "
8120:                        _final_raw, final_candidate, final_usage = native_subagent_model_call(system, finalize_messages, timeout_seconds=max(1, int(remaining_float)), conversation=conversation)
8122:                        native_subagent_log_json(job_id, {"event": "finalize_response", "round": round_idx, "text": final_candidate, "usage": final_usage})
8123:                        if native_subagent_parse_tool_calls(final_candidate):
8124:                            native_subagent_log_json(job_id, {"event": "finalize_failed_requested_tools", "round": round_idx, "text": final_candidate})
8125:                            final_text = "Incomplete: native subagent reached its sync tool-round limit and still requested more tools during finalization. Inspect the job log for pending tool calls and rerun with a narrower prompt or async mode."
8127:                            final_text = str(final_candidate or "").strip() or "Incomplete: native subagent finalization returned no answer after reaching the sync tool-round limit."
8129:                        native_subagent_log_json(job_id, {"event": "finalize_error", "round": round_idx, "error": str(exc)[-1000:]})
8130:                        final_text = "Incomplete: native subagent reached its sync tool-round limit and finalization failed. Inspect the job log and rerun with a narrower prompt or async mode."
8132:                    final_text = "Incomplete: native subagent reached its sync tool-round limit while still requesting more tools. Inspect the job log for pending tool calls and rerun with a narrower prompt or async mode."
8138:                    raise TimeoutError(f"native subagent timed out after {timeout}s")
8140:                    round_results.append(native_subagent_execute_tool(call, conversation, allowed_tools))
8142:            native_subagent_log_json(job_id, {"event": "tool_results", "round": round_idx, "results": round_results})
8144:            messages.append({"role": "user", "content": "Native subagent tool results (JSON):\n" + native_subagent_tool_result_payload(round_results) + "\nNow provide final report or request remaining allowed tools."})
8147:        final_text, output_truncated = native_subagent_budget_output(raw_final_text, max_result_chars)
8148:        incomplete = raw_final_text.startswith("Incomplete: native subagent")
8149:        result = {"ok": not incomplete, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "incomplete" if incomplete else "completed", "summary": final_text[:1000], "output": final_text, "output_truncated": output_truncated, "full_output_chars": len(raw_final_text), "tool_results": tool_results, "usage": usage, "elapsed_seconds": elapsed}
8153:        save_json(native_subagent_result_path(job_id), persisted_result)
8155:        native_subagent_write_job(job)
8159:        result = {"ok": False, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "timed_out", "timed_out": True, "error": str(exc) or f"timed out after {timeout}s", "tool_results": tool_results, "elapsed_seconds": elapsed}
8160:        save_json(native_subagent_result_path(job_id), result)
8162:        native_subagent_write_job(job)
8166:        result = {"ok": False, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "failed", "error": str(exc)[-1000:], "tool_results": tool_results, "elapsed_seconds": elapsed}
8167:        save_json(native_subagent_result_path(job_id), result)
8169:        native_subagent_write_job(job)
8173:def native_subagents_report_markdown() -> str:
8174:    lines = ["# Native subagents", "", f"Updated: {utc_now()}", "", "Native subagents are Matrix-lite-owned workers. OpenCode/ACPX subagents remain compatibility-only.", "", "## Profiles", "", "| id | version | title | sync timeout | tools |", "|---|---:|---|---:|---:|"]
8175:    for profile in native_subagent_profiles():
8179:    jobs = native_subagent_list_jobs(20, include_all=True).get("jobs") or []
8465:        return "Use native tools for current-state checks, file operations, edits, checks, grants, context-module management, capability discovery, skill inventory, package installs, attachments, and subagents. File/edit/test claims require matching tool evidence. Use install_python_package for Python packages and install_system_package for apt/root installs; do not use bash pip/venv unless an explicit human override is added later. Stable and semi-stable modules are ordered first for provider read-cache reuse; hot mutable turn data is last."
8557:    tools = ["inspect_native_session", "list_context_modules", "read_context_module", "create_context_module", "update_context_module", "load_context_module", "unload_context_module", "list_capabilities", "describe_capability", "call_capability", "list_native_skills", "load_native_skill", "unload_native_skill", "shell_status", "set_cwd", "read_file", "read_text_file", "list_directory", "glob_files", "grep_files", "git_status", "git_diff", "list_native_grants", "revoke_native_grant", "probe_provider_tools", "run_check", "compact_session", "apply_patch", "replace_text", "write_text_file", "bash", "privileged_bash", "send_file", "send_image", "install_python_package", "install_system_package", "list_native_subagent_profiles", "list_native_subagent_jobs", "read_native_subagent_job", "run_subagent"]
8703:{"tool_calls":[{"name":"inspect_native_session","args":{}},{"name":"list_context_modules","args":{}},{"name":"read_context_module","args":{"id":"ops.current-task"}},{"name":"update_context_module","args":{"id":"ops.current-task","content":"Active task: ...","loaded":true}},{"name":"load_context_module","args":{"id":"memory.curated"}},{"name":"unload_context_module","args":{"id":"ops.notices"}},{"name":"list_capabilities","args":{}},{"name":"describe_capability","args":{"id":"context7"}},{"name":"call_capability","args":{"id":"context7","tool":"resolve-library-id","args":{"libraryName":"example"}}},{"name":"list_native_skills","args":{}},{"name":"load_native_skill","args":{"id":"senior-architect"}},{"name":"unload_native_skill","args":{"id":"senior-architect"}},{"name":"list_native_subagent_profiles","args":{}},{"name":"run_subagent","args":{"profile":"review","prompt":"Review this short summary for gaps","timeout_seconds":30}},{"name":"shell_status","args":{}},{"name":"set_cwd","args":{"cwd":"/home/matrix-lite/app"}},{"name":"read_file","args":{"path":"/home/matrix-lite/app/matrix_lite/daemon.py","offset":0,"limit":120}},{"name":"list_directory","args":{"path":"/home/matrix-lite/app"}},{"name":"glob_files","args":{"root":"/home/matrix-lite/app","pattern":"**/*.py"}},{"name":"grep_files","args":{"root":"/home/matrix-lite/app","pattern":"native_parse_tool_request","include":"*.py"}},{"name":"git_status","args":{"cwd":"/home/matrix-lite/app"}},{"name":"git_diff","args":{"cwd":"/home/matrix-lite/app","path":"matrix_lite/daemon.py"}},{"name":"list_native_grants","args":{"include_inactive":false}},{"name":"revoke_native_grant","args":{"grant_id":"ngrant-..."}},{"name":"probe_provider_tools","args":{}},{"name":"run_check","args":{"command":"PYTHONPYCACHEPREFIX=/tmp/matrix-lite-pyc python3 -m py_compile matrix_lite/daemon.py","cwd":"/home/matrix-lite/app","summary":"compile check"}},{"name":"compact_session","args":{"reason":"manual"}},{"name":"apply_patch","args":{"cwd":"/home/matrix-lite/app","patch":"diff --git a/path b/path\n...","summary":"why patch"}},{"name":"replace_text","args":{"path":"/home/matrix-lite/app/file.py","old":"old exact text","new":"new exact text","summary":"why replace"}},{"name":"write_text_file","args":{"path":"/home/matrix-lite/workspace/native-smoke/example.txt","content":"text","summary":"why write"}},{"name":"bash","args":{"command":"jq . /home/matrix-lite/state/example.json | sort | head -n 50","cwd":"/home/matrix-lite/app","summary":"inspect json"}},{"name":"privileged_bash","args":{"command":"cat /root/example","summary":"why root is needed"}},{"name":"send_file","args":{"path":"/home/matrix-lite/workspace/native-smoke/result.txt","caption":"processed file","summary":"send processed file"}},{"name":"install_python_package","args":{"packages":["python-docx"],"summary":"read DOCX attachment"}},{"name":"install_system_package","args":{"packages":["pandoc"],"summary":"convert attachment"}}]}
8704:Use read-only tools directly when useful. You may request context-module lifecycle tools to create, update, load, or unload experimental native context modules for the current task. Use list_capabilities/describe_capability to inspect the MCP proxy manifest and schemas; call_capability executes enabled configured MCP servers. Disabled MCPs are visible but hard-denied until enabled in config. Use list_native_skills/load_native_skill/unload_native_skill for temporary procedural skills. Use list_native_subagent_profiles/list_native_subagent_jobs/read_native_subagent_job/run_subagent for bounded native sync subagents; failures/timeouts are non-fatal envelopes. For Matrix attachments, inspect the matrix.event-context module/local_path and use read_file/read_text_file/list_directory/bash as needed; use send_file/send_image to return processed files after approval. If a missing tool/package blocks the workflow, use install_python_package for workflow venv packages or install_system_package for apt/root installs; both create one contained approval with verification. Do not request bash for pip install or python -m venv. Prefer updating ops.current-task for active goals/blockers and creating narrowly scoped modules over burying important state in chat prose. For file/path read, verify, create, edit, replace, or write requests, you MUST request an appropriate native tool and must not claim success from memory or assumption. If the user asks you to pretend/claim a file action without tools, do not use tools and state that you cannot truthfully claim it happened. For programming, inspect with shell_status/set_cwd/glob_files/grep_files/read_file/git_status/git_diff/list_native_grants, edit with replace_text or apply_patch, then run compile/test/lint/typecheck checks with run_check. Approval grants may be once, repeated, or permanent when th

matrix-lite-timer-dispatch.py:23:SUBAGENT_STATE_DIR = WORKSPACE / "state" / "subagents"
matrix-lite-timer-dispatch.py:877:def subagent_status_text(timer: dict[str, Any]) -> tuple[str, bool, bool]:
matrix-lite-timer-dispatch.py:879:    job_id = str(payload.get("subagent_job_id") or "")
matrix-lite-timer-dispatch.py:882:            "Timer: background task check\nNo subagent job id was stored.",
matrix-lite-timer-dispatch.py:890:            f"Timer: background task check\nSubagent job `{job_id}` is missing.",
matrix-lite-timer-dispatch.py:897:    label = job.get("label") or payload.get("subagent_label") or job_id
matrix-lite-timer-dispatch.py:945:    job_id = str(payload.get("subagent_job_id") or "")
matrix-lite-timer-dispatch.py:959:Subagent job id: {job_id or "<missing>"}
matrix-lite-timer-dispatch.py:1009:    status_text, done, should_deliver = subagent_status_text(timer)
matrix-lite-persistent-chat-repo-mcp.py:188:        "permissions": {"read": True, "write": True, "run": True, "subagents": True},
matrix-lite-approval-policy-mcp.py:113:    "subagent",
matrix-lite-status-mcp.py:80:SUBAGENT_STATE_DIR = WORKSPACE / "state" / "subagents"
matrix-lite-status-mcp.py:104:    "opencode-subagent",
matrix-lite-status-mcp.py:105:    "opencode-subagent-temp01",
matrix-lite-status-mcp.py:106:    "opencode-subagent-temp02",
matrix-lite-status-mcp.py:107:    "opencode-subagent-temp05",
matrix-lite-status-mcp.py:120:    "main": "opencode-subagent",
matrix-lite-status-mcp.py:121:    "dm": "opencode-subagent",
matrix-lite-status-mcp.py:122:    "images": "opencode-subagent",
matrix-lite-status-mcp.py:123:    "work": "opencode-subagent",
matrix-lite-status-mcp.py:124:    "synced-work": "opencode-subagent",
matrix-lite-status-mcp.py:125:    "personal-work": "opencode-subagent",
matrix-lite-status-mcp.py:126:    "automeat": "opencode-subagent",
matrix-lite-status-mcp.py:127:    "synced-automeat": "opencode-subagent",
matrix-lite-status-mcp.py:128:    "approvals": "opencode-subagent",
matrix-lite-status-mcp.py:129:    "synced-approvals": "opencode-subagent",
matrix-lite-status-mcp.py:130:    "chat": "opencode-subagent",
matrix-lite-status-mcp.py:131:    "synced-chat": "opencode-subagent",
matrix-lite-status-mcp.py:132:    "personal-chat": "opencode-subagent",
matrix-lite-status-mcp.py:135:    "gpt55": "opencode-subagent",
matrix-lite-status-mcp.py:136:    "gpt-5.5": "opencode-subagent",
matrix-lite-status-mcp.py:137:    "openai": "opencode-subagent",
matrix-lite-status-mcp.py:138:    "synced-gpt55": "opencode-subagent",
matrix-lite-status-mcp.py:139:    "personal-gpt55": "opencode-subagent",
matrix-lite-status-mcp.py:140:    "rp": "opencode-subagent",
matrix-lite-status-mcp.py:141:    "default": "opencode-subagent",
matrix-lite-status-mcp.py:144:    0.1: "opencode-subagent-temp01",
matrix-lite-status-mcp.py:145:    0.2: "opencode-subagent-temp02",
matrix-lite-status-mcp.py:146:    0.5: "opencode-subagent-temp05",
matrix-lite-status-mcp.py:541:def _safe_subagent_job_path(job_id: str) -> Path:
matrix-lite-status-mcp.py:548:def _load_subagent_job(job_id: str) -> dict[str, Any]:
matrix-lite-status-mcp.py:549:    path = _safe_subagent_job_path(job_id)
matrix-lite-status-mcp.py:551:        raise ValueError(f"unknown subagent job: {job_id}")
matrix-lite-status-mcp.py:554:        raise ValueError(f"unreadable subagent job: {job_id}")
matrix-lite-status-mcp.py:818:def _update_subagent_job(path: Path, **updates: Any) -> dict[str, Any]:
matrix-lite-status-mcp.py:828:def _append_subagent_log(log_path: Path, message: str) -> None:
matrix-lite-status-mcp.py:834:def _resolve_subagent_agent(agent: str, parent_label: str | None = None) -> str:
matrix-lite-status-mcp.py:845:            f"unknown subagent agent {agent!r}; allowed: {', '.join(allowed)}"
matrix-lite-status-mcp.py:851:def _resolve_subagent_temperature(temperature: float) -> tuple[float | None, str]:
matrix-lite-status-mcp.py:865:    raise ValueError(f"unsupported subagent temperature {temperature!r}; allowed: {allowed_values}")
matrix-lite-status-mcp.py:868:def _subagent_agent_for_temperature(agent_id: str, temperature_agent: str) -> str:
matrix-lite-status-mcp.py:871:    if agent_id != "opencode-subagent":
matrix-lite-status-mcp.py:872:        raise ValueError("temperature overrides are only supported for the default opencode-subagent launcher")
matrix-lite-status-mcp.py:875:def _resolve_subagent_model(model: str) -> str:
matrix-lite-status-mcp.py:885:def _subagent_agent_for_model(
matrix-lite-status-mcp.py:888:    """Resolve subagent agent, routing default jobs to the unified launcher.
matrix-lite-status-mcp.py:891:    continuity/policy prompt baggage. Background subagents should instead use a
matrix-lite-status-mcp.py:896:        return _resolve_subagent_agent(agent, parent_label)
matrix-lite-status-mcp.py:897:    return "opencode-subagent"
matrix-lite-status-mcp.py:900:def _subagent_model_to_set(agent_id: str, model: str) -> str:
matrix-lite-status-mcp.py:918:def _public_subagent_result(payload: dict[str, Any]) -> dict[str, Any]:
matrix-lite-status-mcp.py:925:def _safe_subagent_result_path(job_path: Path, job: dict[str, Any]) -> Path:
matrix-lite-status-mcp.py:944:def _write_subagent_result(job_path: Path, job: dict[str, Any], *, status: str, result: dict[str, Any] | None = None, error: str = "") -> dict[str, Any]:
matrix-lite-status-mcp.py:945:    result_path = _safe_subagent_result_path(job_path, job)
matrix-lite-status-mcp.py:963:def _subagent_state_row(path: Path) -> dict[str, Any]:
matrix-lite-status-mcp.py:989:def _subagent_report_text(job: dict[str, Any], result: dict[str, Any]) -> str:
matrix-lite-status-mcp.py:994:        "Background subagent finished.\n"
matrix-lite-status-mcp.py:1000:        "Subagent output:\n"
matrix-lite-status-mcp.py:1005:def _run_subagent_worker(job_path: Path) -> int:
matrix-lite-status-mcp.py:1010:    agent_id = str(job.get("agent_id") or "opencode-subagent")
matrix-lite-status-mcp.py:1024:    result_path = _safe_subagent_result_path(job_path, job)
matrix-lite-status-mcp.py:1025:    _update_subagent_job(
matrix-lite-status-mcp.py:1034:    _write_subagent_result(job_path, job, status="running", result={"output": "worker started"})
matrix-lite-status-mcp.py:1035:    _append_subagent_log(log_path, f"starting {label} with {agent_id} temperature={temperature}")
matrix-lite-status-mcp.py:1043:        _append_subagent_log(
matrix-lite-status-mcp.py:1047:        _update_subagent_job(job_path, partial_summary="ensure_session completed", steps=steps)
matrix-lite-status-mcp.py:1048:        _write_subagent_result(job_path, job, status="running", result={"returncode": ensure.get("returncode"), "output": ensure.get("output")})
matrix-lite-status-mcp.py:1050:            raise RuntimeError("failed to ensure subagent session")
matrix-lite-status-mcp.py:1057:            _append_subagent_log(
matrix-lite-status-mcp.py:1061:            _update_subagent_job(job_path, partial_summary="set_model completed", steps=steps)
matrix-lite-status-mcp.py:1062:            _write_subagent_result(job_path, job, status="running", result={"returncode": set_model.get("returncode"), "output": set_model.get("output")})
matrix-lite-status-mcp.py:1064:                raise RuntimeError("failed to set subagent model")
matrix-lite-status-mcp.py:1071:        _append_subagent_log(
matrix-lite-status-mcp.py:1078:            report = _subagent_report_text(job, result)
matrix-lite-status-mcp.py:1084:            _append_subagent_log(
matrix-lite-status-mcp.py:1094:            _append_subagent_log(
matrix-lite-status-mcp.py:1098:        result_payload = _write_subagent_result(job_path, job, status=status, result=result)
matrix-lite-status-mcp.py:1099:        _update_subagent_job(
matrix-lite-status-mcp.py:1111:        _append_subagent_log(log_path, f"worker error: {exc}")
matrix-lite-status-mcp.py:1112:        _write_subagent_result(job_path, job, status="failed", error=str(exc))
matrix-lite-status-mcp.py:1113:        _update_subagent_job(
matrix-lite-status-mcp.py:1119:            result_path=str(_safe_subagent_result_path(job_path, job)),
matrix-lite-status-mcp.py:2791:    subagent_job_id: str,
matrix-lite-status-mcp.py:2804:    """Create a timer that checks a subagent job and reports done/running/stalled."""
matrix-lite-status-mcp.py:2806:        job = _load_subagent_job(subagent_job_id)
matrix-lite-status-mcp.py:2813:        "subagent_job_id": subagent_job_id,
matrix-lite-status-mcp.py:2822:        "subagent_job_id": subagent_job_id,
matrix-lite-status-mcp.py:2823:        "subagent_label": job.get("label"),
matrix-lite-status-mcp.py:2984:def subagent_spawn(
matrix-lite-status-mcp.py:2998:    """Spawn a bounded async OpenCode/acpx subagent job.
matrix-lite-status-mcp.py:3026:        resolved_model = _resolve_subagent_model(model)
matrix-lite-status-mcp.py:3027:        resolved_temperature, temperature_agent = _resolve_subagent_temperature(temperature)
matrix-lite-status-mcp.py:3028:        agent_id = _subagent_agent_for_model(
matrix-lite-status-mcp.py:3031:        agent_id = _subagent_agent_for_temperature(agent_id, temperature_agent)
matrix-lite-status-mcp.py:3032:        model_to_set = _subagent_model_to_set(agent_id, resolved_model)
matrix-lite-status-mcp.py:3039:    label = f"subagent-{role}-{job_id}"
matrix-lite-status-mcp.py:3044:        "You are an async background subagent for Matrix-lite.\n"
matrix-lite-status-mcp.py:3086:            "message": "Call again with confirm='SPAWN' to start this background subagent.",
matrix-lite-status-mcp.py:3106:                "--subagent-worker",
matrix-lite-status-mcp.py:3114:    _update_subagent_job(job_path, pid=process.pid)
matrix-lite-status-mcp.py:3119:                message=f"Check background subagent {job_id}",
matrix-lite-status-mcp.py:3125:                    "subagent_job_id": job_id,
matrix-lite-status-mcp.py:3126:                    "subagent_label": label,
matrix-lite-status-mcp.py:3141:            _append_subagent_log(log_path, f"workflow timer create failed: {exc}")
matrix-lite-status-mcp.py:3149:def subagent_list(limit: int = 20) -> dict[str, Any]:
matrix-lite-status-mcp.py:3150:    """List recent async subagent jobs."""
matrix-lite-status-mcp.py:3159:    rows = [_subagent_state_row(path) for path in paths[:max_items]]
matrix-lite-status-mcp.py:3164:def subagent_status(job_id: str) -> dict[str, Any]:
matrix-lite-status-mcp.py:3165:    """Return state for one async subagent job without raw prompt text."""
matrix-lite-status-mcp.py:3167:        path = _safe_subagent_job_path(job_id)
matrix-lite-status-mcp.py:3168:        job = _load_subagent_job(job_id)
matrix-lite-status-mcp.py:3171:    row = _subagent_state_row(path)
matrix-lite-status-mcp.py:3175:    result_path = _safe_subagent_result_path(path, job)
matrix-lite-status-mcp.py:3179:            result_payload = _public_subagent_result(loaded_result)
matrix-lite-status-mcp.py:3201:def subagent_log(
matrix-lite-status-mcp.py:3204:    """Load a bounded slice from one async subagent log."""
matrix-lite-status-mcp.py:3206:        job = _load_subagent_job(job_id)
matrix-lite-status-mcp.py:3212:        return {"ok": False, "error": "subagent log path is outside state root"}
matrix-lite-status-mcp.py:3230:        "privacy_note": "Subagent logs may contain private prompt/output text. Share minimal relevant facts.",
matrix-lite-status-mcp.py:3235:def subagent_kill(job_id: str, confirm: str = "") -> dict[str, Any]:
matrix-lite-status-mcp.py:3236:    """Cancel a running async subagent worker. Requires confirm='KILL'."""
matrix-lite-status-mcp.py:3238:        path = _safe_subagent_job_path(job_id)
matrix-lite-status-mcp.py:3239:        job = _load_subagent_job(job_id)
matrix-lite-status-mcp.py:3246:            "error": "subagent worker is not running",
matrix-lite-status-mcp.py:3247:            "job": _subagent_state_row(path),
matrix-lite-status-mcp.py:3253:            "message": "Call again with confirm='KILL' to terminate this subagent worker process group.",
matrix-lite-status-mcp.py:3260:    _update_subagent_job(path, status="cancelled", finished_at=_utc_timestamp())
matrix-lite-status-mcp.py:4237:    if len(sys.argv) == 3 and sys.argv[1] == "--subagent-worker":
matrix-lite-status-mcp.py:4238:        raise SystemExit(_run_subagent_worker(Path(sys.argv[2])))

121: )
122: APPROVALS_FILE = Path(
123:     os.environ.get("OPENCLAW_MATRIX_LITE_APPROVALS", str(STATE_DIR / "approvals.json"))
124: )
125: NATIVE_TOOL_GRANTS_FILE = Path(
126:     os.environ.get("MATRIX_LITE_NATIVE_TOOL_GRANTS", str(STATE_DIR / "native-tool-grants.json"))
127: )
128: NATIVE_PROVIDER_CAPS_FILE = Path(
129:     os.environ.get("MATRIX_LITE_NATIVE_PROVIDER_CAPS", str(STATE_DIR / "native-provider-capabilities.json"))
130: )
131: STANDING_PERMISSIONS_FILE = Path(
132:     os.environ.get(
133:         "OPENCLAW_MATRIX_LITE_STANDING_PERMISSIONS",
134:         str(STATE_DIR / "standing-permissions.json"),
135:     )
136: )
137: TIMERS_FILE = Path(
138:     os.environ.get("OPENCLAW_MATRIX_LITE_TIMERS", "/home/matrix-lite/workspace/state/timers/timers.json")
139: )
140: MEDIA_LOG_FILE = Path(
141:     os.environ.get("OPENCLAW_MATRIX_LITE_MEDIA_LOG", str(STATE_DIR / "media.jsonl"))
142: )
143: INBOX_DIR = Path(
144:     os.environ.get("OPENCLAW_MATRIX_LITE_INBOX_DIR", str(STATE_DIR / "inbox"))
145: )
146: OPENCODE_SUBAGENT_CONFIG_FILE = Path(
147:     os.environ.get(
148:         "MATRIX_LITE_SUBAGENT_OPENCODE_CONFIG",
149:         os.environ.get(
150:             "OPENCLAW_MATRIX_LITE_SUBAGENT_OPENCODE_CONFIG",
151:             str(
152:                 _boot_agent_config("smoke-agent").get(
153:                     "opencode_config",
154:                     "/home/matrix-lite/xdg/smoke-agent/opencode/opencode.json",
155:                 )
156:             ),
157:         ),
158:     )
159: )
160: OPENCODE_PERSISTENT_CHAT_CONFIG_FILE = Path(
161:     os.environ.get(
162:         "MATRIX_LITE_PERSISTENT_CHAT_OPENCODE_CONFIG",
163:         os.environ.get(
164:             "OPENCLAW_MATRIX_LITE_PERSISTENT_CHAT_OPENCODE_CONFIG",
165:             str(
166:                 _boot_agent_config("persistent-chat").get(
167:                     "opencode_config",
168:                     "/home/matrix-lite/xdg/persistent-chat/opencode/opencode.json",
169:                 )
170:             ),
171:         ),
172:     )
173: )
174: ACPX_CONFIG_FILE = Path(os.environ.get("MATRIX_LITE_ACPX_CONFIG", "/home/matrix-lite/.acpx/config.json"))
175: ACPX_SESSIONS_DIR = Path(os.environ.get("MATRIX_LITE_ACPX_SESSIONS_DIR", "/home/matrix-lite/.acpx/sessions"))
176: OBSERVABILITY_LOG_FILE = Path(
177:     os.environ.get(
178:         "OPENCLAW_MATRIX_LITE_OBSERVABILITY_LOG",
179:         str(STATE_DIR / "observability.jsonl"),
180:     )
181: )
182: MESSAGE_STATUS_DIR = Path(
183:     os.environ.get(
184:         "OPENCLAW_MATRIX_LITE_MESSAGE_STATUS_DIR",
185:         str(STATE_DIR / "messages"),
186:     )
187: )
188: LOCK_DIR = Path(
189:     os.environ.get("OPENCLAW_MATRIX_LITE_LOCK_DIR", str(STATE_DIR / "locks"))
190: )
191: QUEUE_DIR = Path(
192:     os.environ.get("OPENCLAW_MATRIX_LITE_QUEUE_DIR", str(STATE_DIR / "queues"))
193: )
194: PAGES_DIR = Path(
195:     os.environ.get(
196:         "MATRIX_LITE_PAGES_DIR",
197:         str(BOOT_CONFIG.get("pages_dir") or "/home/matrix-lite/pages"),
198:     )
199: )
200: WORKSPACE_DIR = Path(
201:     os.environ.get(
202:         "MATRIX_LITE_WORKSPACE_DIR",
203:         str(BOOT_CONFIG.get("workspace_dir") or "/home/matrix-lite/workspace"),
204:     )
205: )
206: SUBAGENT_STATE_DIR = Path(os.environ.get("MATRIX_LITE_SUBAGENT_STATE_DIR", str(WORKSPACE_DIR / "state" / "subagents")))
207: NATIVE_SUBAGENT_STATE_DIR = Path(os.environ.get("MATRIX_LITE_NATIVE_SUBAGENT_STATE_DIR", str(STATE_DIR / "native-subagents")))
208: NATIVE_SUBAGENT_PROFILES_DIR = NATIVE_SUBAGENT_STATE_DIR / "profiles"
209: NATIVE_SUBAGENT_JOBS_DIR = NATIVE_SUBAGENT_STATE_DIR / "jobs"
210: NATIVE_SUBAGENT_RESULTS_DIR = NATIVE_SUBAGENT_STATE_DIR / "results"
211: NATIVE_SUBAGENT_LOGS_DIR = NATIVE_SUBAGENT_STATE_DIR / "logs"
212: LOCK_FILE = Path(
213:     os.environ.get("OPENCLAW_MATRIX_LITE_LOCK", str(STATE_DIR / "matrix-lite.lock"))
214: )
215: OPENCLAW_CONFIG = Path(
216:     os.environ.get("MATRIX_LITE_LEGACY_CONFIG", "")
217: )
218: DEFAULT_SYNC_TIMEOUT_MS = max(
219:     1_000,
220:     min(int(os.environ.get("OPENCLAW_MATRIX_LITE_SYNC_TIMEOUT_MS", "30000")), 60_000),
221: )
222: MATRIX_TEXT_LIMIT = 3900
223: MATRIX_APPROVAL_TEXT_LIMIT = int(os.environ.get("MATRIX_LITE_APPROVAL_TEXT_LIMIT", "3600"))
224: DRY_RUN = os.environ.get("OPENCLAW_MATRIX_LITE_DRY_RUN", "").strip().lower() in {
225:     "1",
226:     "true",
227:     "yes",
228:     "on",
229: }
230: DEFAULT_ACP_TIMEOUT_SECONDS = max(
231:     30, min(int(os.environ.get("OPENCLAW_MATRIX_LITE_ACP_TIMEOUT_SECONDS", "300")), 3600)
232: )
233: MATRIX_LITE_HANDLED_MSGTYPES = {
234:     "m.text",
235:     "m.notice",
236:     "m.image",
237:     "m.file",
238:     "m.audio",
239:     "m.video",
240: }
241: RECENT_TURN_LIMIT = max(
242:     1, min(int(os.environ.get("OPENCLAW_MATRIX_LITE_RECENT_TURN_LIMIT", "4")), 30)
243: )
244: RUNTIME_CAPABILITIES_FILE = STATE_DIR / "runtime-capabilities.json"
245: RUNTIME_REFRESH_NOTICE_FILE = STATE_DIR / "runtime-refresh-notice.json"
246: PERSISTENT_USAGE_SUMMARY_FILE = Path(os.environ.get("MATRIX_LITE_PERSISTENT_USAGE_SUMMARY_FILE", str(STATE_DIR / "persistent-chat" / "usage-summary.json")))
247: PERSISTENT_USAGE_CONFIG_FILE = Path(os.environ.get("MATRIX_LITE_PERSISTENT_USAGE_CONFIG_FILE", str(STATE_DIR / "persistent-chat" / "usage-summary-config.json")))
248: PERSISTENT_MEMORY_CONTEXT_CHARS = max(1000, int(os.environ.get("MATRIX_LITE_PERSISTENT_MEMORY_CONTEXT_CHARS", "5000")))
249: OPENCODE_LOG_DIR = Path(os.environ.get("MATRIX_LITE_OPENCODE_LOG_DIR", "/home/matrix-lite/.local/share/opencode/log"))
250: STATUS_PANEL_HEARTBEAT_SECONDS = max(
251:     1, min(int(os.environ.get("MATRIX_LITE_STATUS_PANEL_HEARTBEAT_SECONDS", "5")), 30)
252: )
253: APPROVAL_GRANT_REACTIONS = {"✅", "👍", "♻", "🔁"}
254: APPROVAL_ALWAYS_REACTIONS = {"♾", "🔓"}
255: APPROVAL_DENY_REACTIONS = {"❌", "👎"}
256: DIRECT_ACP_CANCEL_REACTIONS = {"🛑", "⏹", "✋"}
257:
258: STOP = False
259: ACTIVE_WORKERS: set[str] = set()
260: ACTIVE_WORKERS_LOCK = threading.Lock()
261: DIRECT_ACP_CANCEL_REQUESTS: dict[str, dict[str, Any]] = {}
262: DIRECT_ACP_CANCEL_REQUESTS_LOCK = threading.Lock()
263:
264:
265: def utc_now() -> str:
266:     return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
267:
268:
269: def _handle_signal(_signum: int, _frame: object) -> None:
270:     global STOP
271:     STOP = True
272:
273:
274: for _sig in (signal.SIGINT, signal.SIGTERM):
275:     signal.signal(_sig, _handle_signal)
276:
277:
278: def matrix_env() -> tuple[str, str]:
279:     matrix_cfg = (
280:         BOOT_CONFIG.get("matrix") if isinstance(BOOT_CONFIG.get("matrix"), dict) else {}
281:     )
282:     homeserver_env = str(matrix_cfg.get("homeserver_env") or "MATRIX_HOMESERVER_URL")
283:     token_env = str(matrix_cfg.get("access_token_env") or "MATRIX_ACCESS_TOKEN")
284:     token_file = str(matrix_cfg.get("access_token_file") or "").strip()
285:     homeserver = (
286:         (
287:             os.environ.get(homeserver_env)
288:             or str(matrix_cfg.get("homeserver") or "")
289:             or os.environ.get("MATRIX_HOMESERVER_URL")
290:             or os.environ.get("MATRIX_HOMESERVER")
291:             or ""
292:         )
293:         .strip()
294:         .rstrip("/")
295:     )
296:     token = (
297:         os.environ.get(token_env) or os.environ.get("MATRIX_ACCESS_TOKEN") or ""
298:     ).strip()
299:     if not token and token_file:
300:         try:
301:             token = Path(token_file).read_text(encoding="utf-8").strip()
302:         except Exception:
303:             token = ""
304:     if (not homeserver or not token) and OPENCLAW_CONFIG.exists():
305:         cfg = load_json(OPENCLAW_CONFIG, {})
306:         channels = cfg.get("channels") if isinstance(cfg, dict) else {}
307:         matrix = channels.get("matrix") if isinstance(channels, dict) else {}
308:         if isinstance(matrix, dict):
309:             homeserver = homeserver or os.path.expandvars(
310:                 str(matrix.get("homeserver") or "")
311:             ).strip().rstrip("/")
312:             token = (
313:                 token
314:                 or os.path.expandvars(str(matrix.get("accessToken") or "")).strip()
315:             )
316:     if not homeserver or not token:
317:         raise RuntimeError(
318:             "MATRIX_HOMESERVER_URL/MATRIX_HOMESERVER and MATRIX_ACCESS_TOKEN are required"
319:         )
320:     return homeserver, token

8042:     profile = native_subagent_get_profile(profile_id)
8043:     if not profile:
8044:         return {"ok": False, "name": "run_subagent", "error": f"unknown native subagent profile: {profile_id}"}
8045:     clean_prompt = str(prompt or "").strip()
8046:     if not clean_prompt:
8047:         return {"ok": False, "name": "run_subagent", "error": "prompt is required"}
8048:     timeouts = profile.get("timeouts") if isinstance(profile.get("timeouts"), dict) else {}
8049:     timeout = max(5, min(int(timeout_seconds or timeouts.get("sync_seconds") or 45), 120))
8050:     job_id = "nsub-" + uuid.uuid4().hex[:12]
8051:     created = utc_now()
8052:     parent_scope_key = native_context_scope_raw_key(conversation)
8053:     job = {"job_id": job_id, "profile_id": profile.get("id"), "profile_version": profile.get("version"), "mode": "sync", "status": "running", "created_at": created, "started_at": created, "timeout_seconds": timeout, "parent_conversation_key": parent_scope_key, "prompt_chars": len(clean_prompt), "log_path": str(native_subagent_log_path(job_id))}
8054:     native_subagent_write_job(job)
8055:     started = time.monotonic()
8056:     tool_policy = profile.get("tool_policy") if isinstance(profile.get("tool_policy"), dict) else {}
8057:     tool_policy = native_subagent_sanitize_policy(tool_policy)
8058:     allowed_tools = {str(t) for t in tool_policy.get("native_tools", []) if isinstance(t, str)}
8059:     max_rounds = max(0, min(int(profile.get("max_tool_rounds") or 0), 6))
8060:     max_result_chars = max(1000, min(int(profile.get("max_result_chars") or 6000), 20000))
8061:     tool_contract = native_subagent_tool_contract(allowed_tools)
8062:     system = (
8063:         f"You are a native Matrix-lite sync subagent profile={profile.get('id')}.\n"
8064:         "You are bounded and non-interactive. Do not ask the user questions. Do not edit files, install packages, use root, spawn children, or request approvals. If blocked, return a concise blocked reason.\n"
8065:         f"Allowed native tools for this profile: {', '.join(sorted(allowed_tools)) or 'none'}.\n"
8066:         f"Profile instructions:\n{profile.get('instructions') or ''}\n\n"
8067:         f"{tool_contract}\n"
8068:     )
8069:     messages = [{"role": "user", "content": ("Context:\n" + str(context).strip() + "\n\n" if str(context).strip() else "") + "Task:\n" + clean_prompt}]
8070:     tool_results: list[dict[str, Any]] = []
8071:     native_subagent_log_json(job_id, {"event": "start", "profile_id": profile.get("id"), "allowed_tools": sorted(allowed_tools), "system": system, "messages": messages})
8072:     try:
8073:         final_text = ""
8074:         usage: dict[str, Any] = {}
8075:         deadline = started + timeout
8076:         strict_check_retry_used = False
8077:         requires_check = native_subagent_prompt_requires_check(str(profile.get("id") or ""), clean_prompt)
8078:         for round_idx in range(max_rounds + 1):
8079:             remaining_float = deadline - time.monotonic()
8080:             if remaining_float <= 0:
8081:                 raise TimeoutError(f"native subagent timed out after {timeout}s")
8082:             remaining = max(1, int(remaining_float))
8083:             _raw_data, text, usage = native_subagent_model_call(system, messages, timeout_seconds=remaining, conversation=conversation)
8084:             native_subagent_log_json(job_id, {"event": "model_response", "round": round_idx, "text": text, "usage": usage})
8085:             if time.monotonic() >= deadline:
8086:                 raise TimeoutError(f"native subagent timed out after {timeout}s")
8087:             raw_calls = native_subagent_parse_raw_tool_calls(text)
8088:             calls = native_subagent_parse_tool_calls(text)
8089:             native_subagent_log_json(job_id, {"event": "parsed_tool_calls", "round": round_idx, "raw_calls": raw_calls, "calls": calls})
8090:             check_already_ran = any(isinstance(item, dict) and str(item.get("name") or "") == "run_check" and item.get("returncode") is not None for item in tool_results)
8091:             gate_calls = raw_calls if raw_calls is not None else calls
8092:             if requires_check and not check_already_ran and not native_subagent_first_call_is_check(gate_calls, conversation):
8093:                 if not strict_check_retry_used and round_idx < max_rounds:
8094:                     native_subagent_log_json(job_id, {"event": "contract_retry", "round": round_idx, "reason": "test-runner did not request classified run_check first"})
8095:                     strict_check_retry_used = True
8096:                     messages.append({"role": "assistant", "content": text})
8097:                     messages.append({"role": "user", "content": "You were asked to run deterministic tests/checks but did not request run_check. Reply ONLY with JSON tool_calls containing run_check (or bash with the exact check command, which will be routed to run_check). Do not use shell_status and do not run unrelated code."})
8098:                     continue
8099:                 final_text = "Blocked: test-runner did not request run_check for the requested tests/checks, so no tests were run."
8100:                 break
8101:             if not calls:
8102:                 final_text = text
8103:                 break
8104:             if round_idx >= max_rounds:
8105:                 native_subagent_log_json(job_id, {"event": "tool_round_limit", "round": round_idx, "pending_calls": calls})
8106:                 remaining_float = deadline - time.monotonic()
8107:                 if remaining_float > 1:
8108:                     finalize_messages = list(messages)
8109:                     finalize_messages.append({"role": "assistant", "content": text})
8110:                     finalize_messages.append({
8111:                         "role": "user",
8112:                         "content": (
8113:                             "You have reached the native subagent sync tool-round limit. Do NOT request more tools. "
8114:                             "Using only the evidence already gathered in this job, produce the best final answer for the original task. "
8115:                             "If evidence is insufficient, say exactly what is uncertain and what narrower follow-up would be needed. "
8116:                             "Do not output JSON tool_calls."
8117:                         ),
8118:                     })
8119:                     try:
8120:                         _final_raw, final_candidate, final_usage = native_subagent_model_call(system, finalize_messages, timeout_seconds=max(1, int(remaining_float)), conversation=conversation)
8121:                         usage = final_usage or usage
8122:                         native_subagent_log_json(job_id, {"event": "finalize_response", "round": round_idx, "text": final_candidate, "usage": final_usage})
8123:                         if native_subagent_parse_tool_calls(final_candidate):
8124:                             native_subagent_log_json(job_id, {"event": "finalize_failed_requested_tools", "round": round_idx, "text": final_candidate})
8125:                             final_text = "Incomplete: native subagent reached its sync tool-round limit and still requested more tools during finalization. Inspect the job log for pending tool calls and rerun with a narrower prompt or async mode."
8126:                         else:
8127:                             final_text = str(final_candidate or "").strip() or "Incomplete: native subagent finalization returned no answer after reaching the sync tool-round limit."
8128:                     except Exception as exc:
8129:                         native_subagent_log_json(job_id, {"event": "finalize_error", "round": round_idx, "error": str(exc)[-1000:]})
8130:                         final_text = "Incomplete: native subagent reached its sync tool-round limit and finalization failed. Inspect the job log and rerun with a narrower prompt or async mode."
8131:                 else:
8132:                     final_text = "Incomplete: native subagent reached its sync tool-round limit while still requesting more tools. Inspect the job log for pending tool calls and rerun with a narrower prompt or async mode."
8133:                 break
8134:             round_results = []
8135:             executable_calls = calls[:1] if requires_check and not check_already_ran else calls[:6]
8136:             for call in executable_calls:
8137:                 if time.monotonic() >= deadline:
8138:                     raise TimeoutError(f"native subagent timed out after {timeout}s")
8139:                 if isinstance(call, dict):
8140:                     round_results.append(native_subagent_execute_tool(call, conversation, allowed_tools))
8141:             tool_results.extend(round_results)
8142:             native_subagent_log_json(job_id, {"event": "tool_results", "round": round_idx, "results": round_results})
8143:             messages.append({"role": "assistant", "content": text})
8144:             messages.append({"role": "user", "content": "Native subagent tool results (JSON):\n" + native_subagent_tool_result_payload(round_results) + "\nNow provide final report or request remaining allowed tools."})
8145:         elapsed = elapsed_seconds(started)
8146:         raw_final_text = str(final_text or "").strip()
8147:         final_text, output_truncated = native_subagent_budget_output(raw_final_text, max_result_chars)
8148:         incomplete = raw_final_text.startswith("Incomplete: native subagent")
8149:         result = {"ok": not incomplete, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "incomplete" if incomplete else "completed", "summary": final_text[:1000], "output": final_text, "output_truncated": output_truncated, "full_output_chars": len(raw_final_text), "tool_results": tool_results, "usage": usage, "elapsed_seconds": elapsed}
8150:         persisted_result = dict(result)
8151:         if output_truncated:
8152:             persisted_result["full_output"] = raw_final_text
8153:         save_json(native_subagent_result_path(job_id), persisted_result)
8154:         job.update({"status": result["status"], "finished_at": utc_now(), "elapsed_seconds": elapsed, "result_summary": result["summary"], "tool_calls": len(tool_results)})
8155:         native_subagent_write_job(job)
8156:         return result
8157:     except TimeoutError as exc:
8158:         elapsed = elapsed_seconds(started)
8159:         result = {"ok": False, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "timed_out", "timed_out": True, "error": str(exc) or f"timed out after {timeout}s", "tool_results": tool_results, "elapsed_seconds": elapsed}
8160:         save_json(native_subagent_result_path(job_id), result)
8161:         job.update({"status": "timed_out", "finished_at": utc_now(), "elapsed_seconds": elapsed, "error": result["error"], "result_summary": result["error"], "tool_calls": len(tool_results)})
8162:         native_subagent_write_job(job)
8163:         return result
8164:     except Exception as exc:
8165:         elapsed = elapsed_seconds(started)
8166:         result = {"ok": False, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "failed", "error": str(exc)[-1000:], "tool_results": tool_results, "elapsed_seconds": elapsed}
8167:         save_json(native_subagent_result_path(job_id), result)
8168:         job.update({"status": "failed", "finished_at": utc_now(), "elapsed_seconds": elapsed, "error": result["error"], "result_summary": result["error"], "tool_calls": len(tool_results)})
8169:         native_subagent_write_job(job)
8170:         return result
8171:
8172:
8173: def native_subagents_report_markdown() -> str:
8174:     lines = ["# Native subagents", "", f"Updated: {utc_now()}", "", "Native subagents are Matrix-lite-owned workers. OpenCode/ACPX subagents remain compatibility-only.", "", "## Profiles", "", "| id | version | title | sync timeout | tools |", "|---|---:|---|---:|---:|"]
8175:     for profile in native_subagent_profiles():
8176:         timeouts = profile.get("timeouts") if isinstance(profile.get("timeouts"), dict) else {}
8177:         tools = (profile.get("tool_policy") or {}).get("native_tools") if isinstance(profile.get("tool_policy"), dict) else []
8178:         lines.append(f"| `{profile.get('id')}` | {profile.get('version') or ''} | {profile.get('title') or ''} | {timeouts.get('sync_seconds') or ''} | {len(tools) if isinstance(tools, list) else 0} |")
8179:     jobs = native_subagent_list_jobs(20, include_all=True).get("jobs") or []
8180:     lines.extend(["", "## Recent jobs", "", "This public status view intentionally omits cross-conversation result summaries. Use scoped native tools to read job results.", "", "| job | profile | mode | status | elapsed |", "|---|---|---|---|---:|"])
8181:     for job in jobs:
8182:         lines.append(f"| `{job.get('job_id')}` | `{job.get('profile_id')}` | {job.get('mode') or ''} | {job.get('status') or ''} | {job.get('elapsed_seconds') or ''} |")
8183:     if not jobs:
8184:         lines.append("| — | — | — | — | — |")
8185:     return "\n".join(lines).rstrip() + "\n"
8186:
8187: def native_mcp_proxy_config_entries() -> dict[str, dict[str, Any]]:
8188:     entries: dict[str, dict[str, Any]] = {}
8189:     for cfg_path in [OPENCODE_SUBAGENT_CONFIG_FILE]:
8190:         data = load_json(cfg_path, {})
8191:         mcp = data.get("mcp") if isinstance(data, dict) and isinstance(data.get("mcp"), dict) else {}
8192:         for name, spec in sorted(mcp.items()):
8193:             if isinstance(spec, dict) and str(name) not in entries:
8194:                 entries[str(name)] = {**spec, "_config_path": str(cfg_path)}
8195:     return entries
8196:
8197:
8198: def native_mcp_proxy_manifest() -> list[dict[str, Any]]:
8199:     """Compact capability manifest derived from existing OpenCode MCP wiring."""
8200:     manifest: list[dict[str, Any]] = []
8201:     for name, spec in native_mcp_proxy_config_entries().items():
8202:         entry = {
8203:             "id": str(name),
8204:             "enabled": bool(spec.get("enabled", False)),
8205:             "type": str(spec.get("type") or "local"),
8206:             "timeout": spec.get("timeout"),
8207:             "config": str(spec.get("_config_path") or ""),
8208:         }
8209:         if spec.get("url"):
8210:             entry["transport"] = "remote"
8211:             entry["url"] = str(spec.get("url"))
8212:         elif isinstance(spec.get("command"), list):
8213:             entry["transport"] = "local"
8214:             entry["command"] = " ".join(str(part) for part in spec.get("command", [])[:4])[:240]
8215:         category = "general"
8216:         lname = str(name).lower()
8217:         if any(term in lname for term in ["search", "searx", "duckduckgo", "brave"]): category = "web-search"
8218:         elif any(term in lname for term in ["context7", "grep", "repo"]): category = "code-docs"
8219:         elif any(term in lname for term in ["vision", "media", "image", "dezgo"]): category = "media"
8220:         elif any(term in lname for term in ["status", "approval", "broker", "bridge"]): category = "matrix-lite-runtime"
8221:         elif "playwright" in lname: category = "browser"
8222:         elif "skill" in lname: category = "skills"
8223:         entry["category"] = category
8224:         manifest.append(entry)
8225:     return manifest
8226:
8227:
8228: def native_mcp_proxy_summary(max_items: int = 18) -> str:
8229:     manifest = native_mcp_proxy_manifest()
8230:     enabled = [m for m in manifest if m.get("enabled")]
8231:     disabled = [m for m in manifest if not m.get("enabled")]
8232:     lines = [
8233:         "MCP proxy: reuse existing OpenCode MCP/wrapper wiring through compact list/describe/call native tools instead of injecting full schemas.",
8234:         f"Discovered capabilities: {len(manifest)} total; {len(enabled)} enabled in smoke OpenCode config; {len(disabled)} disabled/optional.",
8235:     ]
8236:     for item in manifest[:max_items]:
8237:         state = "enabled" if item.get("enabled") else "disabled"
8238:         lines.append(f"- {item.get('id')}: {state}; {item.get('category')}; {item.get('type')}; {item.get('transport')}")
8239:     if len(manifest) > max_items:
8240:         lines.append(f"- ... {len(manifest) - max_items} more")
8241:     lines.append("Use describe_capability before first call when arguments are unclear. Disabled capabilities are listed for visibility but cannot be executed natively until enabled in config.")

8242:     return "\n".join(lines)
8243:
8244:
8245: def native_skill_manifest() -> list[dict[str, Any]]:
8246:     roots = [("user opencode skills", Path(os.environ.get("MATRIX_LITE_NATIVE_USER_SKILLS_DIR", "/home/gleb/.config/opencode/skills"))), ("repo skills", Path(os.environ.get("MATRIX_LITE_NATIVE_REPO_SKILLS_DIR", "/home/matrix-lite/app/skills")))]
8247:     out: list[dict[str, Any]] = []
8248:     for label, root in roots:
8249:         try:
8250:             if not root.is_dir():
8251:                 continue
8252:             for child in sorted(root.iterdir()):
8253:                 if not child.is_dir():
8254:                     continue
8255:                 skill_md = child / "SKILL.md"
8256:                 out.append({"id": native_context_module_id(child.name), "name": child.name, "root_label": label, "root": str(child), "skill_md": str(skill_md), "has_skill_md": skill_md.is_file(), "loaded": False})
8257:         except Exception:
8258:             continue
8259:     return out
8260:
8261:
8262: def native_skill_summary(max_items: int = 12) -> str:
8263:     skills = native_skill_manifest()
8264:     lines = [
8265:         "Agent skill lifecycle: native skills are temporary/procedural context modules, separate from persistent-chat identity.",
8266:         "Use list_native_skills, load_native_skill, unload_native_skill. Skills should point to modules/RAG/tools rather than duplicate large memory.",
8267:     ]
8268:     for item in skills[:max_items]:
8269:         lines.append(f"- {item.get('name')}: source={item.get('root_label')} skill_md={item.get('has_skill_md')}")
8270:     if len(skills) > max_items:
8271:         lines.append(f"- ... {len(skills) - max_items} more")
8272:     return "\n".join(lines)
8273:
8274:
8275: def native_capability_tool_list() -> dict[str, Any]:
8276:     return {"ok": True, "name": "list_capabilities", "result": native_mcp_proxy_manifest(), "note": "call_capability executes enabled configured MCP servers; disabled capabilities are listed but hard-denied"}
8277:
8278:
8279:
8280: def native_ensure_mcp_import_path() -> None:
8281:     for site in Path("/home/matrix-lite/venvs/mcp/lib").glob("python*/site-packages"):
8282:         text = str(site)
8283:         if text not in sys.path:
8284:             sys.path.insert(0, text)
8285:
8286: def native_mcp_stdio_command(spec: dict[str, Any]) -> tuple[list[str], dict[str, str], str]:
8287:     if spec.get("url"):
8288:         command = ["npx", "--yes", "mcp-remote", str(spec.get("url"))]
8289:         env = {
8290:             "HOME": "/home/matrix-lite",
8291:             "PATH": os.environ.get("PATH", "/usr/bin:/bin:/usr/local/bin:/home/matrix-lite/bin"),
8292:             "XDG_RUNTIME_DIR": os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"),
8293:             "NPM_CONFIG_CACHE": "/home/matrix-lite/.npm",
8294:         }
8295:         return command, env, str(Path("/home/matrix-lite/app"))
8296:     command = spec.get("command") if isinstance(spec.get("command"), list) else []
8297:     if not command:
8298:         raise RuntimeError("MCP capability has no command/url")
8299:     env = {
8300:         "HOME": "/home/matrix-lite",
8301:         "PATH": os.environ.get("PATH", "/usr/bin:/bin:/usr/local/bin:/home/matrix-lite/bin"),
8302:         "XDG_RUNTIME_DIR": os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"),
8303:         "NPM_CONFIG_CACHE": "/home/matrix-lite/.npm",
8304:     }
8305:     if isinstance(spec.get("environment"), dict):
8306:         env.update({str(k): str(v) for k, v in spec.get("environment", {}).items()})
8307:     return [str(part) for part in command], env, str(Path("/home/matrix-lite/app"))
8308:
8309:
8310: async def native_mcp_list_tools_async(spec: dict[str, Any]) -> list[dict[str, Any]]:
8311:     native_ensure_mcp_import_path()
8312:     from mcp import ClientSession
8313:     from mcp.client.stdio import StdioServerParameters, stdio_client
8314:     command, env, cwd = native_mcp_stdio_command(spec)
8315:     params = StdioServerParameters(command=command[0], args=command[1:], env=env, cwd=cwd)
8316:     async with stdio_client(params) as (read, write):
8317:         async with ClientSession(read, write) as session:
8318:             await session.initialize()
8319:             listed = await session.list_tools()
8320:             return [{"name": tool.name, "description": tool.description or "", "input_schema": tool.inputSchema} for tool in listed.tools]
8321:
8322:
8323: async def native_mcp_call_tool_async(spec: dict[str, Any], tool: str, args: dict[str, Any]) -> dict[str, Any]:
8324:     native_ensure_mcp_import_path()
8325:     from mcp import ClientSession
8326:     from mcp.client.stdio import StdioServerParameters, stdio_client
8327:     command, env, cwd = native_mcp_stdio_command(spec)
8328:     params = StdioServerParameters(command=command[0], args=command[1:], env=env, cwd=cwd)
8329:     async with stdio_client(params) as (read, write):
8330:         async with ClientSession(read, write) as session:
8331:             await session.initialize()
8332:             result = await session.call_tool(tool, args)
8333:             content = []
8334:             for item in result.content:
8335:                 if hasattr(item, "model_dump"):
8336:                     content.append(item.model_dump(mode="json"))
8337:                 else:
8338:                     content.append({"type": getattr(item, "type", "unknown"), "text": str(item)})
8339:             return {"ok": not bool(result.isError), "tool": tool, "content": content, "is_error": bool(result.isError)}
8340:
8341:
8342: def native_run_async(coro: Any, timeout: float) -> Any:
8343:     return asyncio.run(asyncio.wait_for(coro, timeout=max(1.0, float(timeout))))
8344:
8345:
8346: def native_capability_tool_describe(capability_id: str, *, include_schema: bool = True, allow_disabled: bool = False) -> dict[str, Any]:
8347:     wanted = str(capability_id or "").strip()
8348:     entries = native_mcp_proxy_config_entries()
8349:     spec = entries.get(wanted)
8350:     if not spec:
8351:         return {"ok": False, "name": "describe_capability", "error": f"unknown capability: {wanted}"}
8352:     manifest_item = next((item for item in native_mcp_proxy_manifest() if item.get("id") == wanted), {"id": wanted})
8353:     if not bool(spec.get("enabled", False)):
8354:         return {"ok": True, "name": "describe_capability", "result": manifest_item, "tools": [], "note": "capability is disabled in config; native describe/call will not start it"}
8355:     if include_schema and not native_capability_readonly_allowed(wanted):
8356:         return {"ok": True, "name": "describe_capability", "result": manifest_item, "tools": [], "note": "schema introspection is disabled for this capability until a native approval/policy gate exists"}
8357:     tools: list[dict[str, Any]] = []
8358:     if include_schema:
8359:         try:
8360:             tools = native_run_async(native_mcp_list_tools_async(spec), float(spec.get("timeout") or 30) / 1000 if int(spec.get("timeout") or 0) > 1000 else float(spec.get("timeout") or 30))
8361:         except Exception as exc:
8362:             return {"ok": False, "name": "describe_capability", "result": manifest_item, "error": str(exc)}
8363:     return {"ok": True, "name": "describe_capability", "result": manifest_item, "tools": tools}
8364:
8365:
8366:
8367:
8368: NATIVE_MCP_READONLY_TOOLS: dict[str, set[str]] = {
8369:     "duckduckgo": {"duckduckgo_web_search", "duckduckgo_news_search"},
8370:     "searxng": {"searxng_web_search", "web_url_read", "searxng_url_read", "searxng_web_url_read"},
8371:     "brave-search": {"brave_web_search", "brave_local_search"},
8372:     "context7": {"resolve-library-id", "get-library-docs"},
8373:     "gh_grep": {"searchGitHub"},
8374:     "matrix_lite_vision": {"vision_models", "vision_analyze_image", "vision_recent_images"},
8375:     "fixture_search": {"echo"},
8376: }
8377:
8378:
8379: def native_capability_readonly_allowed(capability_id: str, tool: str = "") -> bool:
8380:     allowed = NATIVE_MCP_READONLY_TOOLS.get(str(capability_id or ""), set())
8381:     if not allowed:
8382:         return False
8383:     return not tool or str(tool) in allowed
8384:
8385: def native_capability_tool_call(capability_id: str, tool: str = "", args: dict[str, Any] | None = None, *, allow_disabled: bool = False) -> dict[str, Any]:
8386:     wanted = str(capability_id or "").strip()
8387:     tool_name = str(tool or "").strip()
8388:     if not wanted or not tool_name:
8389:         return {"ok": False, "name": "call_capability", "error": "capability id and tool are required"}
8390:     entries = native_mcp_proxy_config_entries()
8391:     spec = entries.get(wanted)
8392:     if not spec:
8393:         return {"ok": False, "name": "call_capability", "error": f"unknown capability: {wanted}"}
8394:     if not bool(spec.get("enabled", False)):
8395:         return {"ok": False, "name": "call_capability", "capability": wanted, "tool": tool_name, "error": "capability is disabled in config; enable it explicitly before native execution"}
8396:     if not native_capability_readonly_allowed(wanted, tool_name):
8397:         return {"ok": False, "name": "call_capability", "capability": wanted, "tool": tool_name, "error": "capability/tool requires explicit approval or a future policy gate before native execution"}
8398:     timeout = float(spec.get("timeout") or 120000)
8399:     if timeout > 1000:
8400:         timeout = timeout / 1000.0
8401:     try:
8402:         result = native_run_async(native_mcp_call_tool_async(spec, tool_name, dict(args or {})), timeout)
8403:         return {"name": "call_capability", "capability": wanted, **result}
8404:     except Exception as exc:
8405:         return {"ok": False, "name": "call_capability", "capability": wanted, "tool": tool_name, "error": str(exc)}
8406:
8407:
8408: def native_skill_tool_list() -> dict[str, Any]:
8409:     public = [{k: v for k, v in item.items() if k not in {"root", "skill_md"}} for item in native_skill_manifest()]
8410:     return {"ok": True, "name": "list_native_skills", "result": public, "note": "load/unload creates temporary smoke_experimental context modules from SKILL.md"}
8411:
8412:
8413: def native_find_skill(skill_id: str) -> dict[str, Any] | None:
8414:     wanted = native_context_module_id(skill_id)
8415:     for item in native_skill_manifest():
8416:         if item.get("id") == wanted or native_context_module_id(str(item.get("name") or "")) == wanted:
8417:             return item
8418:     return None
8419:
8420:
8421: def native_skill_tool_load(skill_id: str, conversation: dict[str, Any], *, budget_chars: int = 6000) -> dict[str, Any]:
8422:     skill = native_find_skill(skill_id)
8423:     if not skill:
8424:         return {"ok": False, "name": "load_native_skill", "error": f"unknown native skill: {skill_id}"}
8425:     path = Path(str(skill.get("skill_md") or ""))
8426:     if not path.is_file():
8427:         return {"ok": False, "name": "load_native_skill", "error": f"skill has no SKILL.md: {skill.get('name')}"}
8428:     try:
8429:         content = path.read_text(encoding="utf-8", errors="replace")[:50000]
8430:     except Exception as exc:
8431:         return {"ok": False, "name": "load_native_skill", "error": str(exc)}
8432:     module_id = "skill." + native_context_module_id(str(skill.get("id") or skill.get("name")))
8433:     module = {
8434:         "id": module_id,
8435:         "kind": "native_skill",
8436:         "title": f"Native skill: {skill.get('name')}",
8437:         "band": "hot",
8438:         "cache_class": "hot",
8439:         "source": "inline",
8440:         "loaded": True,
8441:         "budget_chars": max(1000, min(int(budget_chars or 6000), 20000)),

8442:         "order": 450,
8443:         "mutable": True,
8444:         "classification": "smoke_experimental",
8445:         "port_status": "loaded_skill",
8446:         "content": "Loaded native skill instructions (temporary/procedural):\n" + content,
8447:     }
8448:     native_save_scoped_context_module(conversation, module_id, module)
8449:     return {"ok": True, "name": "load_native_skill", "skill": {k: v for k, v in skill.items() if k not in {"root", "skill_md"}}, "module_id": module_id, "budget_chars": module["budget_chars"]}
8450:
8451:
8452: def native_skill_tool_unload(skill_id: str, conversation: dict[str, Any]) -> dict[str, Any]:
8453:     raw = str(skill_id or "")
8454:     module_id = raw if raw.startswith("skill.") else "skill." + native_context_module_id(raw)
8455:     return native_context_tool_set_loaded(module_id, False, conversation)
8456:
8457:
8458: def native_generated_context_module_content(module: dict[str, Any], conversation: dict[str, Any], current_user_text: str = "") -> str:
8459:     module_id = str(module.get("id") or "")
8460:     if module_id == "stable.identity":
8461:         return "Matrix-lite owns this native backend's memory/context. Treat modules below as explicit runtime context; do not infer hidden OpenCode session state. Keep answers user-visible and truthful about tool evidence."
8462:     if module_id == "stable.smoke-profile":
8463:         return "Native smoke is a generic/transient high-capability Matrix-lite worker, not persistent-chat. It may use modules/tools aggressively for concrete tasks, but durable identity/personality and long-lived relationship memory are only loaded by explicit modules/profiles."
8464:     if module_id == "stable.native-tools":
8465:         return "Use native tools for current-state checks, file operations, edits, checks, grants, context-module management, capability discovery, skill inventory, package installs, attachments, and subagents. File/edit/test claims require matching tool evidence. Use install_python_package for Python packages and install_system_package for apt/root installs; do not use bash pip/venv unless an explicit human override is added later. Stable and semi-stable modules are ordered first for provider read-cache reuse; hot mutable turn data is last."
8466:     if module_id == "stable.persistent-chat-port":
8467:         return "Reserved placeholder for a future persistent-chat native profile. Do not port or inject persistent-chat personality/bootstrap into generic native smoke yet; map artifacts only until the runtime is ready."
8468:     if module_id == "capabilities.mcp-proxy":
8469:         return native_mcp_proxy_summary()
8470:     if module_id == "capabilities.agent-skills":
8471:         return native_skill_summary()
8472:     if module_id == "conversation.summary":
8473:         summary = native_kimi_summary_state(conversation)
8474:         return str(summary.get("summary") or "").strip()
8475:     if module_id == "conversation.hot-tail":
8476:         state = native_kimi_session_state(conversation)
8477:         turns = state.get("turns") if isinstance(state.get("turns"), list) else []
8478:         return f"Native hot tail is supplied as role messages after context modules. turns={len(turns)} limit={int(state.get('hot_turn_limit') or conversation.get('native_hot_turns') or 8)}"
8479:     if module_id == "retrieval.bounded-keyword":
8480:         if str(conversation.get("conversation_key") or "") == "persistent:main" or conversation.get("memory_promotion_enabled"):
8481:             return bounded_persistent_memory_retrieval(current_user_text)
8482:         return ""
8483:     if module_id == "ops.grants":
8484:         try:
8485:             return json.dumps(native_list_tool_grants(conversation, include_inactive=False).get("result") or [], ensure_ascii=False)[:4000]
8486:         except Exception as exc:
8487:             return f"grant state unavailable: {exc}"
8488:     if module_id == "ops.provider-capabilities":
8489:         caps = load_json(NATIVE_PROVIDER_CAPS_FILE, {"capabilities": {}})
8490:         return json.dumps(caps.get("capabilities") if isinstance(caps, dict) else {}, ensure_ascii=False)[:4000]
8491:     if module_id == "ops.runtime-notices":
8492:         caps = ensure_runtime_capabilities()
8493:         lines = []
8494:         for item in caps.get("summary", []) if isinstance(caps, dict) else []:
8495:             lines.append(f"- {item}")
8496:         notice = runtime_dynamic_notice_prompt_hint(500)
8497:         if notice:
8498:             lines.append(f"- {notice}")
8499:         return "Runtime hints/notices from current ACP-era wiring:\n" + "\n".join(lines) if lines else ""
8500:     if module_id == "retrieval.vector-search":
8501:         return "Vector search over memory/repos may already be available through agent/provider tools. Native RAG is deferred; when added, it should be a retrieval module behind the context registry, not a parallel memory system."
8502:     if module_id == "matrix.event-context":
8503:         return "Reserved candidate for native attachment, replied-to, thread, and batched Matrix message context. Keep mapped but unloaded until native prompt builder receives structured row/event context modules."
8504:     return ""
8505:
8506:
8507: def native_render_context_module(module: dict[str, Any], conversation: dict[str, Any], current_user_text: str = "") -> tuple[str, dict[str, Any]]:
8508:     source = str(module.get("source") or "inline")
8509:     raw = ""
8510:     error = ""
8511:     try:
8512:         if source == "generated":
8513:             raw = native_generated_context_module_content(module, conversation, current_user_text)
8514:         elif source == "file":
8515:             path = native_readable_path(str(module.get("path") or ""))
8516:             if path.is_file():
8517:                 raw = path.read_text(encoding="utf-8", errors="replace")
8518:             else:
8519:                 error = f"not a file: {path}"
8520:         else:
8521:             raw = str(module.get("content") or "")
8522:     except Exception as exc:
8523:         error = str(exc)
8524:     try:
8525:         budget = max(0, min(int(module.get("budget_chars") or 0), 100000))
8526:     except Exception:
8527:         budget = 1200
8528:     rendered, truncated = native_budget_text(raw, budget, str(module.get("excerpt") or "tail"))
8529:     meta = {k: module.get(k) for k in ("id", "kind", "title", "band", "cache_class", "source", "path", "loaded", "budget_chars", "order", "classification", "port_status") if k in module}
8530:     meta.update({"included": bool(module.get("loaded")) and bool(rendered), "chars": len(rendered), "truncated": truncated})
8531:     if error:
8532:         meta.update({"included": False, "error": error})
8533:     return rendered, meta
8534:
8535:
8536: def native_select_context_modules(conversation: dict[str, Any], current_user_text: str = "") -> tuple[list[dict[str, Any]], str]:
8537:     selected: list[dict[str, Any]] = []
8538:     sections: list[str] = []
8539:     for module in native_context_module_registry(conversation):
8540:         if not bool(module.get("loaded", False)):
8541:             meta = {k: module.get(k) for k in ("id", "kind", "title", "band", "cache_class", "source", "path", "loaded", "budget_chars", "order", "classification", "port_status") if k in module}
8542:             meta.update({"included": False, "chars": 0})
8543:             selected.append(meta)
8544:             continue
8545:         content, meta = native_render_context_module(module, conversation, current_user_text)
8546:         selected.append(meta)
8547:         if content and meta.get("included") and str(module.get("id")) != "conversation.hot-tail":
8548:             title = str(module.get("title") or module.get("id") or "context module")
8549:             sections.append(f"## {module.get('id')} — {title}\n{content}")
8550:     if not sections:
8551:         return selected, ""
8552:     return selected, "Native context modules (cache-banded, source-attributed):\n" + "\n\n".join(sections)
8553:
8554:
8555: def native_kimi_context_modules(conversation: dict[str, Any]) -> list[dict[str, Any]]:
8556:     selected, _ = native_select_context_modules(conversation, "")
8557:     tools = ["inspect_native_session", "list_context_modules", "read_context_module", "create_context_module", "update_context_module", "load_context_module", "unload_context_module", "list_capabilities", "describe_capability", "call_capability", "list_native_skills", "load_native_skill", "unload_native_skill", "shell_status", "set_cwd", "read_file", "read_text_file", "list_directory", "glob_files", "grep_files", "git_status", "git_diff", "list_native_grants", "revoke_native_grant", "probe_provider_tools", "run_check", "compact_session", "apply_patch", "replace_text", "write_text_file", "bash", "privileged_bash", "send_file", "send_image", "install_python_package", "install_system_package", "list_native_subagent_profiles", "list_native_subagent_jobs", "read_native_subagent_job", "run_subagent"]
8558:     if conversation.get("native_tools_enabled", True):
8559:         selected.append({"id": "native-tools", "kind": "tool_schema", "cache_class": "semi_stable", "tools": tools, "included": True})
8560:     return selected
8561:
8562:
8563: def native_context_module_public(module: dict[str, Any]) -> dict[str, Any]:
8564:     return {k: v for k, v in module.items() if k not in {"content"}}
8565:
8566:
8567: def native_context_tool_list(conversation: dict[str, Any], current_user_text: str = "") -> dict[str, Any]:
8568:     selected, _ = native_select_context_modules(conversation, current_user_text)
8569:     registry = [native_context_module_public(m) for m in native_context_module_registry(conversation)]
8570:     return {"ok": True, "name": "list_context_modules", "result": registry, "selected": selected, "state_file": str(NATIVE_CONTEXT_MODULES_FILE)}
8571:
8572:
8573: def native_context_tool_read(conversation: dict[str, Any], module_id: str, current_user_text: str = "") -> dict[str, Any]:
8574:     wanted = native_context_module_id(module_id)
8575:     for module in native_context_module_registry(conversation):
8576:         if str(module.get("id")) == wanted:
8577:             public = native_context_module_public(module)
8578:             if str(module.get("kind") or "") == "native_skill" and not bool(module.get("loaded", False)):
8579:                 return {
8580:                     "ok": False,
8581:                     "name": "read_context_module",
8582:                     "module": public,
8583:                     "metadata": {"id": wanted, "included": False, "loaded": False, "kind": "native_skill"},
8584:                     "content": "",
8585:                     "error": "native skill is unloaded; use load_native_skill to load/read it again",
8586:                 }
8587:             content, meta = native_render_context_module(module, conversation, current_user_text)
8588:             return {"ok": not bool(meta.get("error")), "name": "read_context_module", "module": public, "metadata": meta, "content": content, "error": meta.get("error") or ""}
8589:     return {"ok": False, "name": "read_context_module", "error": f"unknown context module: {wanted}"}
8590:
8591:
8592: def native_context_default_by_id(conversation: dict[str, Any] | None, module_id: str) -> dict[str, Any] | None:
8593:     for module in native_default_context_modules(conversation):
8594:         if str(module.get("id")) == module_id:
8595:             return module
8596:     return None
8597:
8598:
8599: def native_context_file_module_allowed(path: Path, conversation: dict[str, Any] | None) -> bool:
8600:     allowed = {str(Path(str(m.get("path"))).resolve()) for m in native_default_context_modules(conversation) if m.get("source") == "file" and m.get("path")}
8601:     try:
8602:         native_root = (STATE_DIR / "native-context-modules").resolve()
8603:         resolved = path.resolve()
8604:         return str(resolved) in allowed or resolved == native_root or native_root in resolved.parents
8605:     except Exception:
8606:         return False
8607:
8608:
8609: def native_save_scoped_context_module(conversation: dict[str, Any] | None, module_id: str, module: dict[str, Any]) -> None:
8610:     with NATIVE_CONTEXT_MODULES_LOCK:
8611:         store = load_native_context_module_store()
8612:         scopes = store.setdefault("scopes", {})
8613:         key = native_context_scope_key(conversation)
8614:         raw_key = native_context_scope_raw_key(conversation)
8615:         scoped = scopes.setdefault(key, {"modules": {}, "created_at": utc_now()})
8616:         if raw_key != key and isinstance(scopes.get(raw_key), dict):
8617:             legacy_modules = scopes.get(raw_key, {}).get("modules") if isinstance(scopes.get(raw_key, {}).get("modules"), dict) else {}
8618:             if legacy_modules:
8619:                 scoped_modules = scoped.setdefault("modules", {})
8620:                 for legacy_id, legacy_module in legacy_modules.items():
8621:                     scoped_modules.setdefault(legacy_id, legacy_module)
8622:             scopes.pop(raw_key, None)
8623:         modules = scoped.setdefault("modules", {})
8624:         module["updated_at"] = utc_now()
8625:         modules[module_id] = module
8626:         scoped["updated_at"] = module["updated_at"]
8627:         save_native_context_module_store(store)
8628:
8629:
8630: def native_context_tool_upsert(args: dict[str, Any], conversation: dict[str, Any], *, create: bool) -> dict[str, Any]:
8631:     module_id = native_context_module_id(str(args.get("id") or args.get("module_id") or ""))
8632:     scoped_modules = native_context_store_modules(conversation)
8633:     existing = scoped_modules.get(module_id) if isinstance(scoped_modules.get(module_id), dict) else None
8634:     default = native_context_default_by_id(conversation, module_id)
8635:     if create and (existing or default):
8636:         return {"ok": False, "name": "create_context_module", "error": f"module already exists: {module_id}"}
8637:     if not create and not existing:
8638:         existing = {"id": module_id}
8639:     base = dict(default or {})
8640:     base.update(existing or {"id": module_id, "kind": "custom", "title": module_id, "band": "mutable", "cache_class": "mutable", "source": "inline", "loaded": False, "budget_chars": 1200, "order": 1000, "mutable": True})
8641:     module = dict(base)

8147:         final_text, output_truncated = native_subagent_budget_output(raw_final_text, max_result_chars)
8148:         incomplete = raw_final_text.startswith("Incomplete: native subagent")
8149:         result = {"ok": not incomplete, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "incomplete" if incomplete else "completed", "summary": final_text[:1000], "output": final_text, "output_truncated": output_truncated, "full_output_chars": len(raw_final_text), "tool_results": tool_results, "usage": usage, "elapsed_seconds": elapsed}
8150:         persisted_result = dict(result)
8151:         if output_truncated:
8152:             persisted_result["full_output"] = raw_final_text
8153:         save_json(native_subagent_result_path(job_id), persisted_result)
8154:         job.update({"status": result["status"], "finished_at": utc_now(), "elapsed_seconds": elapsed, "result_summary": result["summary"], "tool_calls": len(tool_results)})
8155:         native_subagent_write_job(job)
8156:         return result
8157:     except TimeoutError as exc:
8158:         elapsed = elapsed_seconds(started)
8159:         result = {"ok": False, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "timed_out", "timed_out": True, "error": str(exc) or f"timed out after {timeout}s", "tool_results": tool_results, "elapsed_seconds": elapsed}
8160:         save_json(native_subagent_result_path(job_id), result)
8161:         job.update({"status": "timed_out", "finished_at": utc_now(), "elapsed_seconds": elapsed, "error": result["error"], "result_summary": result["error"], "tool_calls": len(tool_results)})
8162:         native_subagent_write_job(job)
8163:         return result
8164:     except Exception as exc:
8165:         elapsed = elapsed_seconds(started)
8166:         result = {"ok": False, "name": "run_subagent", "job_id": job_id, "profile_id": profile.get("id"), "status": "failed", "error": str(exc)[-1000:], "tool_results": tool_results, "elapsed_seconds": elapsed}
8167:         save_json(native_subagent_result_path(job_id), result)
8168:         job.update({"status": "failed", "finished_at": utc_now(), "elapsed_seconds": elapsed, "error": result["error"], "result_summary": result["error"], "tool_calls": len(tool_results)})
8169:         native_subagent_write_job(job)
8170:         return result
8171:
8172:
8173: def native_subagents_report_markdown() -> str:
8174:     lines = ["# Native subagents", "", f"Updated: {utc_now()}", "", "Native subagents are Matrix-lite-owned workers. OpenCode/ACPX subagents remain compatibility-only.", "", "## Profiles", "", "| id | version | title | sync timeout | tools |", "|---|---:|---|---:|---:|"]
8175:     for profile in native_subagent_profiles():
8176:         timeouts = profile.get("timeouts") if isinstance(profile.get("timeouts"), dict) else {}
8177:         tools = (profile.get("tool_policy") or {}).get("native_tools") if isinstance(profile.get("tool_policy"), dict) else []
8178:         lines.append(f"| `{profile.get('id')}` | {profile.get('version') or ''} | {profile.get('title') or ''} | {timeouts.get('sync_seconds') or ''} | {len(tools) if isinstance(tools, list) else 0} |")
8179:     jobs = native_subagent_list_jobs(20, include_all=True).get("jobs") or []
8180:     lines.extend(["", "## Recent jobs", "", "This public status view intentionally omits cross-conversation result summaries. Use scoped native tools to read job results.", "", "| job | profile | mode | status | elapsed |", "|---|---|---|---|---:|"])
8181:     for job in jobs:
8182:         lines.append(f"| `{job.get('job_id')}` | `{job.get('profile_id')}` | {job.get('mode') or ''} | {job.get('status') or ''} | {job.get('elapsed_seconds') or ''} |")
8183:     if not jobs:
8184:         lines.append("| — | — | — | — | — |")
8185:     return "\n".join(lines).rstrip() + "\n"
8186:
8187: def native_mcp_proxy_config_entries() -> dict[str, dict[str, Any]]:
8188:     entries: dict[str, dict[str, Any]] = {}
8189:     for cfg_path in [OPENCODE_SUBAGENT_CONFIG_FILE]:
8190:         data = load_json(cfg_path, {})
8191:         mcp = data.get("mcp") if isinstance(data, dict) and isinstance(data.get("mcp"), dict) else {}
8192:         for name, spec in sorted(mcp.items()):
8193:             if isinstance(spec, dict) and str(name) not in entries:
8194:                 entries[str(name)] = {**spec, "_config_path": str(cfg_path)}
8195:     return entries
8196:
8197:
8198: def native_mcp_proxy_manifest() -> list[dict[str, Any]]:
8199:     """Compact capability manifest derived from existing OpenCode MCP wiring."""
8200:     manifest: list[dict[str, Any]] = []
8201:     for name, spec in native_mcp_proxy_config_entries().items():
8202:         entry = {
8203:             "id": str(name),
8204:             "enabled": bool(spec.get("enabled", False)),
8205:             "type": str(spec.get("type") or "local"),
8206:             "timeout": spec.get("timeout"),
8207:             "config": str(spec.get("_config_path") or ""),
8208:         }
8209:         if spec.get("url"):
8210:             entry["transport"] = "remote"
8211:             entry["url"] = str(spec.get("url"))
8212:         elif isinstance(spec.get("command"), list):
8213:             entry["transport"] = "local"
8214:             entry["command"] = " ".join(str(part) for part in spec.get("command", [])[:4])[:240]
8215:         category = "general"
8216:         lname = str(name).lower()
8217:         if any(term in lname for term in ["search", "searx", "duckduckgo", "brave"]): category = "web-search"
8218:         elif any(term in lname for term in ["context7", "grep", "repo"]): category = "code-docs"
8219:         elif any(term in lname for term in ["vision", "media", "image", "dezgo"]): category = "media"
8220:         elif any(term in lname for term in ["status", "approval", "broker", "bridge"]): category = "matrix-lite-runtime"
8221:         elif "playwright" in lname: category = "browser"
8222:         elif "skill" in lname: category = "skills"
8223:         entry["category"] = category
8224:         manifest.append(entry)
8225:     return manifest
8226:
8227:
8228: def native_mcp_proxy_summary(max_items: int = 18) -> str:
8229:     manifest = native_mcp_proxy_manifest()
8230:     enabled = [m for m in manifest if m.get("enabled")]
8231:     disabled = [m for m in manifest if not m.get("enabled")]
8232:     lines = [
8233:         "MCP proxy: reuse existing OpenCode MCP/wrapper wiring through compact list/describe/call native tools instead of injecting full schemas.",
8234:         f"Discovered capabilities: {len(manifest)} total; {len(enabled)} enabled in smoke OpenCode config; {len(disabled)} disabled/optional.",
8235:     ]
8236:     for item in manifest[:max_items]:
8237:         state = "enabled" if item.get("enabled") else "disabled"
8238:         lines.append(f"- {item.get('id')}: {state}; {item.get('category')}; {item.get('type')}; {item.get('transport')}")
8239:     if len(manifest) > max_items:
8240:         lines.append(f"- ... {len(manifest) - max_items} more")
8241:     lines.append("Use describe_capability before first call when arguments are unclear. Disabled capabilities are listed for visibility but cannot be executed natively until enabled in config.")
8242:     return "\n".join(lines)
8243:
8244:
8245: def native_skill_manifest() -> list[dict[str, Any]]:
8246:     roots = [("user opencode skills", Path(os.environ.get("MATRIX_LITE_NATIVE_USER_SKILLS_DIR", "/home/gleb/.config/opencode/skills"))), ("repo skills", Path(os.environ.get("MATRIX_LITE_NATIVE_REPO_SKILLS_DIR", "/home/matrix-lite/app/skills")))]
8247:     out: list[dict[str, Any]] = []
8248:     for label, root in roots:
8249:         try:
8250:             if not root.is_dir():
8251:                 continue
8252:             for child in sorted(root.iterdir()):
8253:                 if not child.is_dir():
8254:                     continue
8255:                 skill_md = child / "SKILL.md"
8256:                 out.append({"id": native_context_module_id(child.name), "name": child.name, "root_label": label, "root": str(child), "skill_md": str(skill_md), "has_skill_md": skill_md.is_file(), "loaded": False})
8257:         except Exception:
8258:             continue
8259:     return out
8260:
8261:
8262: def native_skill_summary(max_items: int = 12) -> str:
8263:     skills = native_skill_manifest()
8264:     lines = [
8265:         "Agent skill lifecycle: native skills are temporary/procedural context modules, separate from persistent-chat identity.",
8266:         "Use list_native_skills, load_native_skill, unload_native_skill. Skills should point to modules/RAG/tools rather than duplicate large memory.",

not a directory: /home/matrix-lite/app/matrix_lite/daemon.py

Run a plan subagent: create a concise plan to add async
     native subagent jobs. Do not edit files.