[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71G85WA6XB6Y5ED32KDW7XJ":3,"skill-same:01M71G85WA6XB6Y5ED32KDW7XJ":45},{"skillNo":4,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":6,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":14,"categories":15,"keywords":16,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":6,"ownerHtmlUrl":29,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":30,"install":31,"resources":44},"01M71G85WA6XB6Y5ED32KDW7XJ","owlynn-diagnostics",null,"maThiaslI152\u002FOwlynnV2","maThiaslI152","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F194800114?v=4","01M71G7XM62JBZ1M6KBTKEDKMQ","01M71G7XMVFW6YM2ZFHECBP4S1",1,"MIT","https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2\u002Fblob\u002Fmain\u002F.opencode\u002Fskills\u002Fowlynn-diagnostics\u002FSKILL.md",[],[17,18],"diagnostics","owlynn diagnostics","2026-11-10 10:12:30",false,"---\nname: owlynn-diagnostics\ndescription: Use when diagnosing Owlynn agent behavior, debugging conversations, checking routing decisions, reviewing logs or traces, fixing tool failures, or understanding the agent graph. Covers architecture, node flow, routing, memory, HITL, logging, and conversation traces. Trigger on keywords: owlynn, agent, router, trace, logcat, audit, conversation, thread, HITL, memory, tool failure, complex-cloud, simple route, defaults.yaml, start.sh.\n---\n\n# Owlynn Diagnostics\n\nComprehensive reference for diagnosing and fixing issues in the Owlynn AI coworker agent.\n\n## Quick Start\n\n### Health Check\n\n```bash\n# Containers (Qdrant + Redis)\npodman ps --format \"table {{.Names}}\\t{{.Status}}\" 2>\u002Fdev\u002Fnull | grep -E \"qdrant|redis\"\n\n# LM Studio (expect >= 1 model loaded)\ncurl -s http:\u002F\u002F127.0.0.1:1234\u002Fv1\u002Fmodels | python3 -c \"import sys,json; d=json.load(sys.stdin); print(len(d.get('data',[])))\"\n\n# Backend\ncurl -s http:\u002F\u002F127.0.0.1:8000\u002Fapi\u002Funified-settings | head -c 80\n\n# CI gate\n.\u002Fscripts\u002Fci.sh --quick\n```\n\n### \"What happened in conversation X?\"\n\n```bash\n# View latest conversation trace\npython scripts\u002Ftrace_view.py --latest\n\n# View specific thread (JSON for programmatic use)\npython scripts\u002Ftrace_view.py \u003Cthread_id> --json\n\n# List all traces\npython scripts\u002Ftrace_view.py --list\n```\n\n### Start the App\n\n```bash\n.\u002Fstart.sh          # Containers → LM Studio → Backend + Frontend\n.\u002Fstart.sh --debug  # With DEBUG logging + audit enabled\n```\n\n---\n\n## Architecture\n\n### Port Map\n\n| Service | URL | Purpose |\n|---------|-----|---------|\n| Frontend | `http:\u002F\u002F127.0.0.1:5173` | React 19 + Vite + Electron |\n| Backend | `http:\u002F\u002F127.0.0.1:8000` | FastAPI (REST + WebSocket) |\n| LM Studio | `http:\u002F\u002F127.0.0.1:1234` | Local LLM inference |\n| Qdrant | `http:\u002F\u002F127.0.0.1:6333` | Vector DB (LTM \u002F Mem0) |\n| Redis | `redis:\u002F\u002Flocalhost:6379` | LangGraph checkpointing |\n| StirlingPDF | `http:\u002F\u002Flocalhost:8090` | PDF text extraction + OCR |\n\n### Key Modules\n\n| Module | Path |\n|--------|------|\n| Config (single source of truth) | `src\u002Fconfig\u002Fdefaults.yaml` |\n| Config loader | `src\u002Fconfig\u002Fconfig_loader.py` |\n| Agent graph | `src\u002Fagent\u002Fcore\u002Fgraph.py` |\n| Router | `src\u002Fagent\u002Frouting\u002Frouter.py`, `src\u002Fagent\u002Frouting\u002Fclassifier.py`, `src\u002Fagent\u002Frouting\u002Fbudget.py`, `src\u002Fagent\u002Frouting\u002Fselector.py` |\n| Simple node | `src\u002Fagent\u002Fcore\u002Fsimple.py` |\n| Complex node | `src\u002Fagent\u002Fcore\u002Fcomplex.py` |\n| Cloud payload | `src\u002Fagent\u002Fcore\u002Fcomplex_utils\u002Fcloud_payload.py` |\n| Cloud invoke | `src\u002Fagent\u002Fcore\u002Fcomplex_utils\u002Fcloud_invoke.py` |\n| Vision proxy | `src\u002Fagent\u002Fcore\u002Fcomplex_utils\u002Fvision_proxy.py` |\n| Memory nodes | `src\u002Fagent\u002Fnodes\u002Fmemory.py` |\n| Memory managers | `src\u002Fmemory\u002F` (STM: `memory_manager.py`, LTM: `long_term.py`, personal: `personal_assistant.py`) |\n| Summarizer | `src\u002Fagent\u002Fnodes\u002Fsummarize.py` |\n| Security proxy | `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py` |\n| Scope clarify | `src\u002Fagent\u002Fnodes\u002Fscope_clarify.py` |\n| Plan review | `src\u002Fagent\u002Fnodes\u002Fplan_review.py` |\n| Coherence check | `src\u002Fagent\u002Fnodes\u002Fcoherence.py` |\n| LLM pool | `src\u002Fagent\u002Fllm.py` |\n| Tool registry | `src\u002Fagent\u002Ftool_sets.py` |\n| Tool implementations | `src\u002Ftools\u002F` |\n| API server | `src\u002Fapi\u002Fserver.py` |\n| WebSocket handler | `src\u002Fapi\u002Fws\u002Fhandler.py` |\n| Graph session | `src\u002Fapi\u002Fcontrollers\u002Fgraph_session.py` |\n| Audit log | `src\u002Fconfig\u002Faudit_log.py` |\n| Log middleware | `src\u002Fconfig\u002Flog_middleware.py` |\n| Trace writer | `src\u002Fconfig\u002Ftrace_writer.py` |\n| AgentState definition | `src\u002Fagent\u002Fcore\u002Fstate.py` |\n| User profile | `src\u002Fmemory\u002Fuser_profile.py` |\n\n### Config Override Priority\n\n```\ndefaults.yaml  →  environment variables (.env, .env.local)  →  user_profile.json\n```\n\nValidate config at startup:\n```bash\npython3 -c \"from src.config.config_loader import validate_config; print(validate_config())\"\n```\n\n---\n\n## Agent Graph\n\n### Node Flow\n\n```\nSTART\n  → memory_inject_lite    (profile, persona, topics — no vector search)\n  → router                (classify: simple | complex-cloud)\n  → memory_retrieve       (gated Qdrant\u002FMem0 + scenario markdown)\n  → [simple] OR [scope_clarify → complex_llm]\n      │\n      ├─ simple → simple_node (Qwen3-VL-4B) → memory_write → END\n      │\n      └─ complex → scope_clarify → complex_llm\n                       │\n                       ├─ cloud path (complex-cloud, DeepSeek V4)\n                       ├─ plan_review \u002F security_proxy (HITL gates)\n                       ├─ tool_action → web search, file ops, REPL\n                       ├─ complex_llm (cycle until no tools pending)\n                       └─ memory_write → END\n```\n\n### Routes\n\n| Route | Model | When |\n|-------|-------|------|\n| `simple` | Qwen3-VL-4B (local) | Simple factual, greetings, short tasks |\n| `complex-cloud` | DeepSeek V4 (cloud) | Tool-augmented, complex reasoning, web search |\n\n### Classification Sources\n\n`keyword_bypass`, `deterministic`, `llm_classifier`, `hitl`\n\n### Key AgentState Fields\n\n| Field | Type | Purpose |\n|-------|------|---------|\n| `messages` | `Sequence[BaseMessage]` | Conversation history |\n| `route` | `str \\| None` | `\"simple\"` or `\"complex-cloud\"` |\n| `model_used` | `str \\| None` | `\"small-local\"`, `\"large-cloud\"`, or with `-fallback` suffix |\n| `needs_memory_retrieval` | `bool \\| None` | Router gate for vector retrieval |\n| `selected_toolboxes` | `list[str] \\| None` | e.g. `[\"web_search\", \"file_ops\"]` or `[\"all\"]` |\n| `token_budget` | `int \\| None` | Dynamic budget set by router |\n| `pending_tool_calls` | `bool \\| None` | Whether tools are queued |\n| `security_decision` | `str \\| None` | `\"approved\"` or `\"denied\"` |\n| `active_tokens` | `int \\| None` | Current token count |\n| `context_window` | `int \\| None` | Active model's context window |\n| `api_tokens_used` | `dict \\| None` | `{\"prompt_tokens\": int, \"completion_tokens\": int}` |\n| `router_metadata` | `dict \\| None` | Telemetry from routing decision |\n| `fallback_chain` | `list[dict] \\| None` | Ordered model attempt list |\n| `clarified_scope` | `dict \\| None` | User-approved requirements from scope_clarify |\n| `plan_review_approved` | `bool \\| None` | Plan review HITL result |\n| `response_confidence` | `float \\| None` | Coherence score |\n| `coherence_retry_reason` | `str \\| None` | Last coherence issue |\n| `_coherence_retry_round` | `int \\| None` | Coherence retry counter |\n\n---\n\n## Routing\n\n### Current Models\n\n| Slot | Model | Purpose |\n|------|-------|---------|\n| `models.small` | `qwen3-vl-4b-instruct-c_abliterated-v2-mlx` | Router, vision, extraction, simple answers |\n| `models.cloud` | `deepseek-v4-flash` | Complex reasoning (tool-augmented) |\n| `models.embedding` | `text-embedding-nomic-embed-text-v1.5-embedding` | LTM vector embeddings |\n\n**Changing models:** Edit only `defaults.yaml` (`models.small.model_name`, `models.cloud.model_name`). ConfigLoader centralizes all model name resolution via `get_small_model_name()` \u002F `get_cloud_model_name()`.\n\n### Key Routing Config (`defaults.yaml`)\n\n```yaml\nrouting:\n  confidence_threshold: 0.6         # HITL triggered below this\n  skill_clarification_threshold: 0.5\n  scope_clarification_enabled: true\n  plan_review_enabled: true\n  keyword_bypass: true\n  hitl_enabled: true\n  max_input_chars: 2000             # Router input truncation\n  budget_tiers:\n    - [40, 256]\n    - [150, 512]\n    - [400, 1536]\n    - [800, 3072]\n    - [1600, 4096]\n```\n\n### Environment Variable Overrides\n\n| Env Var | Overrides |\n|---------|-----------|\n| `SMALL_LLM_BASE_URL` | `models.small.base_url` |\n| `SMALL_LLM_MODEL_NAME` | `models.small.model_name` |\n| `CLOUD_LLM_BASE_URL` | `models.cloud.base_url` |\n| `CLOUD_LLM_MODEL_NAME` | `models.cloud.model_name` |\n| `DEEPSEEK_API_KEY` | Cloud API key |\n\n---\n\n## Memory\n\n### Three-Tier Architecture\n\n| Tier | Storage | Retrieval |\n|------|---------|-----------|\n| **STM** | `data\u002Fmemories.json` | Keyword search (recent 50 entries) |\n| **LTM** | Qdrant via Mem0 | Semantic search (gated by router) |\n| **Personal** | `data\u002Ftopics.json`, `data\u002Finterests.json`, `data\u002Fconversations.json` | Time-decay-weighted relevance |\n| **Scenarios** | `scenarios\u002F*\u002Fplaybook.md`, `constraints.md` | Router `scenario_id` + markdown loader |\n\n### Injection Flow\n\n```\nmemory_inject_lite → router → memory_retrieve → ...\n```\n\n- `memory_inject_lite`: Profile, persona, topics (no vector search). Cached 5 min.\n- Router sets `needs_memory_retrieval` and optional `scenario_id`.\n- `memory_retrieve`: Qdrant\u002FMem0 only when gated; loads scenario markdown.\n\n### Write Flow (after agent responds)\n\n1. PII-scrubs content\n2. Enqueues custom extraction (Redis stream → worker → L1 atoms in Qdrant)\n3. Extracts topics and updates topic\u002Finterests tracking\n4. Records conversation in `conversations.json`\n5. Invalidates memory cache for next turn\n\n### Config\n\n```yaml\nmemory:\n  max_facts: 200\n  search_window: 50\n  cache:\n    ttl: 300                  # 5 min\n  decay:\n    topic_half_life_days: 14\n    interest_half_life_days: 21\n  extraction:\n    idle_cooldown_seconds: 8\n    defer_while_graph_active: true\n```\n\n---\n\n## Tools\n\n### Toolbox Categories\n\n| Toolbox | Tools |\n|---------|-------|\n| Always | `ask_user` |\n| `web_search` | `web_search`, `fetch_webpage`, `deep_research` |\n| `file_ops` | `read_workspace_file`, `write_workspace_file`, `edit_workspace_file`, `list_workspace_files`, `delete_workspace_file`, `download_to_workspace`, `upload_from_workspace` |\n| `data_viz` | `create_docx`, `create_xlsx`, `create_pptx`, `create_pdf`, `notebook_run`, `notebook_reset`, `notebook_vars`, `read_ipynb`, `write_ipynb`, `export_ipynb_html`, `render_interactive_block` |\n| `productivity` | `todo_add`, `todo_list`, `todo_complete`, `list_skills`, `invoke_skill`, `render_interactive_block` |\n| `study` | `course_register`, `course_list`, `course_get`, `study_note_save`, `study_note_search`, `flashcard_deck_create`, `flashcard_review`, `quiz_session_start`, `quiz_session_answer`, `mastery_record`, `export_study_sheet` |\n| `memory` | `recall_memories`, `recall_all_memories`, `forget_memory`, `search_workspace_docs` |\n\n### Sensitive Tools (require HITL approval)\n\n`write_workspace_file`, `edit_workspace_file`, `delete_workspace_file`, `notebook_run`\n\n### Adding a New Tool\n\n1. Create `@tool` function in `src\u002Ftools\u002F\u003Cmodule>.py`\n2. Register in `src\u002Fagent\u002Ftool_sets.py` (`ToolboxRegistry`)\n3. Add to appropriate toolbox category\n4. If sensitive, add to `SENSITIVE_TOOLS` in `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py`\n\n---\n\n## HITL (Human-in-the-Loop)\n\n### Four HITL Gates\n\n| Gate | File | Trigger |\n|------|------|---------|\n| Router HITL | `src\u002Fagent\u002Frouting\u002Frouter.py` | LLM confidence \u003C 60% or ambiguous skill match |\n| Scope Clarify | `src\u002Fagent\u002Fnodes\u002Fscope_clarify.py` | Underspecified build\u002Fcreate requests |\n| Plan Review | `src\u002Fagent\u002Fnodes\u002Fplan_review.py` | Sensitive tool plan before execution |\n| Security Proxy | `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py` | Destructive\u002Fnetwork tool calls |\n\n### API Mode Behavior\n\n- Interrupts disabled when `mode == \"api\"` or `execution_policy == \"auto_approve\"`\n- Sensitive tools denied by default unless `auto_approve_sensitive: true`\n\n### Known Issues\n\n- **Confidence uncalibrated**: Always ~95%, does not reflect actual quality.\n- **Refactoring false positive**: \"write an improved version\" triggers scope_clarify. Mitigated by `_REFACTOR_SIGNALS` in `src\u002Fagent\u002Fhitl\u002Fscope_heuristics.py`.\n\n---\n\n## Logging & Traces\n\nOwlynn has a two-tier logging system.\n\n### Tier 1: Audit Log (Metadata)\n\nStructured JSON-lines capturing system-wide metadata — routing, model selection, HITL, memory, tools, timing. No user content.\n\n**Output:** `~\u002F.owlynn\u002Flogs\u002Faudit.jsonl` (5 × 10 MB rotating) and `audit-errors.jsonl` (3 × 5 MB rotating).\n\n**CLI tool:**\n```bash\npython scripts\u002Flogcat.py --channel agent.model              # Model selection\npython scripts\u002Flogcat.py --channel agent.hitl --level WARN   # HITL decisions\npython scripts\u002Flogcat.py --channel memory.cache              # Cache hits\u002Fmisses\npython scripts\u002Flogcat.py --thread-id abc-123                 # Filter by thread\npython scripts\u002Flogcat.py --follow                            # Live tail\npython scripts\u002Flogcat.py --channel agent.model --follow      # Live model activity\n```\n\n**15 channels:** `agent.lifecycle`, `agent.model`, `agent.hitl`, `agent.tool`, `agent.token`, `agent.cloud`, `agent.coherence`, `memory.inject`, `memory.write`, `memory.cache`, `memory.ltm`, `memory.stm`, `memory.summarize`, `memory.topics`, `api.ws`, `api.file`, `system`\n\n### Tier 2: Conversation Trace (Content)\n\nPer-thread JSONL files capturing what happened in a conversation — user messages, router decisions, LLM responses, tool calls with I\u002FO, errors, timing.\n\n**Output:** `~\u002F.owlynn\u002Ftraces\u002F{thread_id}.jsonl`\n\n**CLI tool:**\n```bash\npython scripts\u002Ftrace_view.py --list                          # List all traces\npython scripts\u002Ftrace_view.py --latest                        # View latest\npython scripts\u002Ftrace_view.py \u003Cthread_id>                     # View specific thread\npython scripts\u002Ftrace_view.py \u003Cthread_id> --json              # JSON output\npython scripts\u002Ftrace_view.py \u003Cthread_id> --follow            # Live tail\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type router_decision,llm_response  # Filter\n```\n\n### Trace Event Types\n\n| Type | When | Key Fields |\n|------|------|------------|\n| `turn_start` | Graph run begins | `ts` |\n| `user_message` | User sends message | `content` (truncated 2000 chars) |\n| `router_decision` | Router completes | `route`, `confidence`, `source`, `task_category`, `toolbox` |\n| `llm_response` | LLM node completes | `node`, `model`, `content_preview`, `token_usage`, `fallback_chain` |\n| `tool_call` | Tool completes | `tool_name`, `input`, `output`, `error`, `duration_s` |\n| `coherence_check` | Coherence check | `coherent`, `confidence`, `reason`, `duration_ms` |\n| `hitl_interrupt` | HITL fires | `interrupt_count` |\n| `error` | Graph error | `message` |\n| `turn_end` | Graph run ends | `ts` |\n\n### IDE Agent Trace Patterns\n\n```python\nimport json\n\nwith open(\"~\u002F.owlynn\u002Ftraces\u002F{thread_id}.jsonl\") as f:\n    events = [json.loads(line) for line in f]\n\n# Full timeline\nfor e in events:\n    print(f\"{e['ts']} [{e['type']}]\")\n\n# Routing decisions\n[e for e in events if e[\"type\"] == \"router_decision\"]\n\n# What did the user ask?\n[e for e in events if e[\"type\"] == \"user_message\"]\n\n# What did the LLM respond?\n[e for e in events if e[\"type\"] == \"llm_response\"]\n\n# What tools failed?\n[e for e in events if e[\"type\"] == \"tool_call\" and e.get(\"error\")]\n\n# Errors\n[e for e in events if e[\"type\"] == \"error\"]\n```\n\n### Emitting Audit Events in Code\n\n```python\nfrom src.config.audit_log import audit_info, audit_debug, audit_warn\n\naudit_info(\n    \"agent.hitl\", \"tool_classified\", tool=\"write_workspace_file\", decision=\"sensitive\"\n)\naudit_debug(\"memory.cache\", \"cache_hit\", age_seconds=12)\naudit_warn(\"agent.model\", \"swap_load_failed\", variant=\"vision\", error=str(e))\n```\n\n### Using @log_node Decorator\n\n```python\nfrom src.config.log_middleware import log_node\n\n\n@log_node(\"complex_llm\")\nasync def complex_llm_node(state: AgentState) -> AgentState: ...\n```\n\n---\n\n## Debugging Recipes\n\n### Wrong route (simple vs complex)\n\n```bash\n# Check what the router decided\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type router_decision\n\n# Check audit log for routing details\npython scripts\u002Flogcat.py --channel agent.model --thread-id \u003Cthread_id>\n\n# Look at classification_source in router_info WS event\n# Source types: keyword_bypass, deterministic, llm_classifier, hitl\n```\n\n**Fix:** Check `src\u002Fagent\u002Frouting\u002Frouter.py` for keyword bypass rules, or `defaults.yaml` `routing.confidence_threshold`.\n\n### LM Studio not reachable\n\n```bash\n# Check if LM Studio is running\ncurl -s http:\u002F\u002F127.0.0.1:1234\u002Fv1\u002Fmodels | head -c 200\n\n# Check model name matches exactly\npython scripts\u002Flogcat.py --channel agent.model --level WARN\n```\n\n**Fix:** Open LM Studio, load the model. Model name in `defaults.yaml` must match what LM Studio reports at `\u002Fv1\u002Fmodels`.\n\n### DeepSeek 401\u002F403\u002F429\n\n```bash\n# Check API key\npython scripts\u002Flogcat.py --channel agent.model --level WARN\n\n# Check circuit breaker\npython scripts\u002Flogcat.py --channel agent.cloud\n```\n\n**Fix:** Set `DEEPSEEK_API_KEY` in `.env.local`. Circuit breaker triggers after 3 failures (60s cooldown). Config: `cloud.circuit_breaker`.\n\n### Tool execution fails\n\n```bash\n# See tool calls and errors\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type tool_call\n\n# See tool classification (safe\u002Fsensitive)\npython scripts\u002Flogcat.py --channel agent.hitl\n```\n\n**Fix:** Check tool implementation in `src\u002Ftools\u002F`. Check `_tool_status_from_content` in `handler.py` for false error detection.\n\n### HITL approval missing\n\n```bash\n# Check HITL decisions\npython scripts\u002Flogcat.py --channel agent.hitl --thread-id \u003Cthread_id>\n\n# Check if auto_approve is set\npython scripts\u002Flogcat.py --channel agent.hitl --level WARN\n```\n\n**Fix:** If `execution_policy: auto_approve` in `data\u002Fuser_profile.json`, all HITL is skipped. Check `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py` `SENSITIVE_TOOLS` list.\n\n### Context overflow \u002F summarization\n\n```bash\n# Check summarization triggers\npython scripts\u002Flogcat.py --channel memory.summarize\n\n# Check token usage in trace\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type llm_response --json | \\\n  python -c \"import sys,json; [print(e.get('token_usage',{})) for e in [json.loads(l) for l in sys.stdin]]\"\n```\n\n**Fix:** Summarization triggers at 85% of context window (`summarization.threshold_ratio`). Config in `defaults.yaml`.\n\n### Memory not injected\n\n```bash\n# Check memory injection\npython scripts\u002Flogcat.py --channel memory.inject --thread-id \u003Cthread_id>\n\n# Check cache\npython scripts\u002Flogcat.py --channel memory.cache\n\n# Check extraction\npython scripts\u002Flogcat.py --channel memory.stm\n```\n\n**Fix:** Memory injection gated by router (`needs_memory_retrieval`). Cache TTL is 5 min. Check Redis\u002FQdrant are running.\n\n### Slow responses\n\n```bash\n# Check model fallbacks\npython scripts\u002Flogcat.py --channel agent.model --level WARN\n\n# Check tool durations\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type tool_call --json | \\\n  python -c \"import sys,json; [print(f\\\"{e['tool_name']}: {e.get('duration_s','?')}s\\\") for e in [json.loads(l) for l in sys.stdin]]\"\n\n# Check coherence retries\npython scripts\u002Flogcat.py --channel agent.coherence\n```\n\n**Fix:** Cloud fallback adds latency. Coherence retries add extra LLM calls. Check `defaults.yaml` timeouts.\n\n### Infinite agent loop\n\n```bash\n# Check recursion\npython scripts\u002Flogcat.py --channel agent.lifecycle --thread-id \u003Cthread_id>\n```\n\n**Fix:** `complex.recursion_limit` defaults to 100 in `defaults.yaml`. Tool-calling cycles can hit this. Check edge conditions in `src\u002Fagent\u002Fcore\u002Fgraph.py`.\n\n### Frontend blank \u002F WS desync\n\n```bash\n# Check WS connection\npython scripts\u002Flogcat.py --channel api.ws\n\n# Check for WS errors\npython scripts\u002Flogcat.py --channel api.ws --level WARN\n```\n\n**Fix:** Check `frontend-v2\u002Fsrc\u002Flib\u002FwsClient.ts` for reconnection logic. Check `frontend-v2\u002Fsrc\u002FApp.tsx` for error boundaries.\n\n### Cloud fallback triggered unexpectedly\n\n```bash\n# Check cloud availability\npython scripts\u002Flogcat.py --channel agent.model | grep -i cloud\n\n# Check circuit breaker\npython scripts\u002Flogcat.py --channel agent.cloud\n```\n\n**Fix:** `_check_cloud_available()` checks: `cloud_escalation_enabled` in profile, circuit breaker state, API key presence. Check `data\u002Fuser_profile.json` `cloud_escalation_enabled`.\n\n### WebSocket crash (transfer_data_task)\n\n**Symptom:** `AttributeError: 'WebSocketProtocol' object has no attribute 'transfer_data_task'`\n\n**Cause:** websockets 15+ \u002F uvicorn race condition. Client disconnects before `connection_open()` sets `transfer_data_task`.\n\n**Fix:** Already fixed in `src\u002Fapi\u002Fws\u002Fhandler.py` — catches `AttributeError` alongside `RuntimeError` in WebSocket close handler.\n\n---\n\n## Known Gotchas\n\n1. **Model name must match exactly** — `defaults.yaml` \u002F `.env` must be identical to what LM Studio reports at `\u002Fv1\u002Fmodels`.\n\n2. **Redis unavailable = in-memory fallback** — LangGraph falls back to `MemorySaver`; session persistence lost.\n\n3. **Port conflicts** — Kill stale: `lsof -ti:8000 | xargs kill -9` and `lsof -ti:5173 | xargs kill -9`.\n\n4. **PYTHONPATH must be set** — `start.sh` does this. Manual: `export PYTHONPATH=\"$(pwd):$PYTHONPATH\"`.\n\n5. **Confidence uncalibrated** — Router confidence always ~95%, does not reflect actual quality.\n\n6. **No self-awareness** — System never detects wrong answers.\n\n7. **Memory context cap** — Injected memory text capped at 12,000 chars in `format_memory_context`.\n\n8. **StirlingPDF needs 2GB RAM** — 1GB causes Metaspace OOM. Falls back to PyMuPDF automatically.\n\n9. **Background extraction defers** — LTM atom extraction waits 8s idle cooldown to avoid GPU contention on Apple Silicon.\n\n10. **Cloud circuit breaker** — 3 failures triggers 60s cooldown.\n\n11. **Security proxy danger patterns** — Scans for `rm -rf`, `sudo`, `curl`, `ssh`; legitimate commands may match.\n\n12. **API mode HITL skip** — When `mode == \"api\"`, all interrupts disabled.\n\n13. **Recursion limit** — Default 100. Tool-calling cycles can hit this.\n\n14. **Mem0 requires NLP deps** — `pip install mem0ai[nlp]` for spaCy\u002Ffastembed.\n\n15. **E2B model leak** — Backend warmup auto-loads E2B if it's the default in LM Studio. Workaround: unload E2B before starting backend.\n\n---\n\n## Commands Reference\n\n### CI & Testing\n\n```bash\n.\u002Fscripts\u002Fci.sh --quick                    # Ruff, mypy, pytest, vitest\n.\u002Fscripts\u002Fci.sh                            # Above + frontend build\n.\u002Fscripts\u002Fci.sh --network                  # Live DeepSeek tests\n.\u002Fscripts\u002Fci.sh --benchmarks               # Router\u002Fcomplex\u002Fmemory benchmarks\n```\n\n### Key Test Suites\n\n```bash\npytest tests\u002Ftest_router_properties.py tests\u002Ftest_router_web_intent.py -q\npytest tests\u002Ftest_toolbox_registry.py -q\npytest tests\u002Ftest_websocket_event_contract.py -q\npytest tests\u002Ftest_memory_nodes.py tests\u002Ftest_crud_operations.py -q\npytest tests\u002Ftest_llm_pool.py -q\npytest tests\u002Ftest_graph.py -q\npytest tests\u002Ftest_audit_log.py -q\n```\n\n### Evaluations\n\n```bash\npython scripts\u002Frun_local_frontier_eval.py  # ~19-turn mechanical eval\npython scripts\u002Frun_educator_eval.py        # 8-turn educator eval\npython scripts\u002Frun_browser_eval.py         # 12-turn conversation eval\n```\n\n### Format & Lint\n\n```bash\nruff check .                # Python lint\nruff format .               # Python format\ncd frontend-v2 && npm run lint    # TS\u002FJS lint\ncd frontend-v2 && npm run format  # TS\u002FJS format\n```\n\n### Logs & Traces\n\n```bash\npython scripts\u002Flogcat.py --channel agent.model --follow\npython scripts\u002Ftrace_view.py --latest\npython scripts\u002Ftrace_view.py \u003Cthread_id> --json\n```\n\n### Config Validation\n\n```bash\npython3 -c \"from src.config.config_loader import validate_config; print(validate_config())\"\n```\n\n---\n\n## File Map\n\n### By Task\n\n| I want to… | File(s) |\n|------------|---------|\n| Change routing \u002F model selection | `src\u002Fconfig\u002Fdefaults.yaml` (models section), `src\u002Fagent\u002Frouting\u002Frouter.py`, `src\u002Fagent\u002Frouting\u002Fclassifier.py` |\n| Add or change a tool | `src\u002Ftools\u002F`, `src\u002Fagent\u002Ftool_sets.py` |\n| Fix memory \u002F context injection | `src\u002Fagent\u002Fnodes\u002Fmemory.py`, `src\u002Fmemory\u002F` |\n| Change HITL \u002F approvals | `src\u002Fagent\u002Fhitl\u002F`, `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py`, `src\u002Fagent\u002Fnodes\u002Fscope_clarify.py`, `src\u002Fagent\u002Fnodes\u002Fplan_review.py` |\n| Debug a symptom | `docs\u002Fdebugging\u002FREADME.md` → symptom table |\n| Change cloud \u002F anonymization | `src\u002Fagent\u002Fcore\u002Fcomplex.py`, `src\u002Fagent\u002Fcore\u002Fcomplex_utils\u002F` |\n| Run the app | `start.sh`, `setup.sh`, `.env` |\n| Run CI \u002F tests \u002F evals | `scripts\u002Fci.sh`, `scripts\u002Frun_*_eval.py` |\n| View logs | `scripts\u002Flogcat.py`, `scripts\u002Ftrace_view.py` |\n| Change config | `src\u002Fconfig\u002Fdefaults.yaml` (single source of truth) |\n| Change WebSocket events | `src\u002Fapi\u002Fws\u002Fhandler.py`, `src\u002Fapi\u002Fws\u002Fschemas.py`, `frontend-v2\u002Fsrc\u002F` |\n","# Owlynn Diagnostics\n\nComprehensive reference for diagnosing and fixing issues in the Owlynn AI coworker agent.\n\n## Quick Start\n\n### Health Check\n\n```bash\n# Containers (Qdrant + Redis)\npodman ps --format \"table {{.Names}}\\t{{.Status}}\" 2>\u002Fdev\u002Fnull | grep -E \"qdrant|redis\"\n\n# LM Studio (expect >= 1 model loaded)\ncurl -s http:\u002F\u002F127.0.0.1:1234\u002Fv1\u002Fmodels | python3 -c \"import sys,json; d=json.load(sys.stdin); print(len(d.get('data',[])))\"\n\n# Backend\ncurl -s http:\u002F\u002F127.0.0.1:8000\u002Fapi\u002Funified-settings | head -c 80\n\n# CI gate\n.\u002Fscripts\u002Fci.sh --quick\n```\n\n### \"What happened in conversation X?\"\n\n```bash\n# View latest conversation trace\npython scripts\u002Ftrace_view.py --latest\n\n# View specific thread (JSON for programmatic use)\npython scripts\u002Ftrace_view.py \u003Cthread_id> --json\n\n# List all traces\npython scripts\u002Ftrace_view.py --list\n```\n\n### Start the App\n\n```bash\n.\u002Fstart.sh          # Containers → LM Studio → Backend + Frontend\n.\u002Fstart.sh --debug  # With DEBUG logging + audit enabled\n```\n\n---\n\n## Architecture\n\n### Port Map\n\n| Service | URL | Purpose |\n|---------|-----|---------|\n| Frontend | `http:\u002F\u002F127.0.0.1:5173` | React 19 + Vite + Electron |\n| Backend | `http:\u002F\u002F127.0.0.1:8000` | FastAPI (REST + WebSocket) |\n| LM Studio | `http:\u002F\u002F127.0.0.1:1234` | Local LLM inference |\n| Qdrant | `http:\u002F\u002F127.0.0.1:6333` | Vector DB (LTM \u002F Mem0) |\n| Redis | `redis:\u002F\u002Flocalhost:6379` | LangGraph checkpointing |\n| StirlingPDF | `http:\u002F\u002Flocalhost:8090` | PDF text extraction + OCR |\n\n### Key Modules\n\n| Module | Path |\n|--------|------|\n| Config (single source of truth) | `src\u002Fconfig\u002Fdefaults.yaml` |\n| Config loader | `src\u002Fconfig\u002Fconfig_loader.py` |\n| Agent graph | `src\u002Fagent\u002Fcore\u002Fgraph.py` |\n| Router | `src\u002Fagent\u002Frouting\u002Frouter.py`, `src\u002Fagent\u002Frouting\u002Fclassifier.py`, `src\u002Fagent\u002Frouting\u002Fbudget.py`, `src\u002Fagent\u002Frouting\u002Fselector.py` |\n| Simple node | `src\u002Fagent\u002Fcore\u002Fsimple.py` |\n| Complex node | `src\u002Fagent\u002Fcore\u002Fcomplex.py` |\n| Cloud payload | `src\u002Fagent\u002Fcore\u002Fcomplex_utils\u002Fcloud_payload.py` |\n| Cloud invoke | `src\u002Fagent\u002Fcore\u002Fcomplex_utils\u002Fcloud_invoke.py` |\n| Vision proxy | `src\u002Fagent\u002Fcore\u002Fcomplex_utils\u002Fvision_proxy.py` |\n| Memory nodes | `src\u002Fagent\u002Fnodes\u002Fmemory.py` |\n| Memory managers | `src\u002Fmemory\u002F` (STM: `memory_manager.py`, LTM: `long_term.py`, personal: `personal_assistant.py`) |\n| Summarizer | `src\u002Fagent\u002Fnodes\u002Fsummarize.py` |\n| Security proxy | `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py` |\n| Scope clarify | `src\u002Fagent\u002Fnodes\u002Fscope_clarify.py` |\n| Plan review | `src\u002Fagent\u002Fnodes\u002Fplan_review.py` |\n| Coherence check | `src\u002Fagent\u002Fnodes\u002Fcoherence.py` |\n| LLM pool | `src\u002Fagent\u002Fllm.py` |\n| Tool registry | `src\u002Fagent\u002Ftool_sets.py` |\n| Tool implementations | `src\u002Ftools\u002F` |\n| API server | `src\u002Fapi\u002Fserver.py` |\n| WebSocket handler | `src\u002Fapi\u002Fws\u002Fhandler.py` |\n| Graph session | `src\u002Fapi\u002Fcontrollers\u002Fgraph_session.py` |\n| Audit log | `src\u002Fconfig\u002Faudit_log.py` |\n| Log middleware | `src\u002Fconfig\u002Flog_middleware.py` |\n| Trace writer | `src\u002Fconfig\u002Ftrace_writer.py` |\n| AgentState definition | `src\u002Fagent\u002Fcore\u002Fstate.py` |\n| User profile | `src\u002Fmemory\u002Fuser_profile.py` |\n\n### Config Override Priority\n\n```\ndefaults.yaml  →  environment variables (.env, .env.local)  →  user_profile.json\n```\n\nValidate config at startup:\n```bash\npython3 -c \"from src.config.config_loader import validate_config; print(validate_config())\"\n```\n\n---\n\n## Agent Graph\n\n### Node Flow\n\n```\nSTART\n  → memory_inject_lite    (profile, persona, topics — no vector search)\n  → router                (classify: simple | complex-cloud)\n  → memory_retrieve       (gated Qdrant\u002FMem0 + scenario markdown)\n  → [simple] OR [scope_clarify → complex_llm]\n      │\n      ├─ simple → simple_node (Qwen3-VL-4B) → memory_write → END\n      │\n      └─ complex → scope_clarify → complex_llm\n                       │\n                       ├─ cloud path (complex-cloud, DeepSeek V4)\n                       ├─ plan_review \u002F security_proxy (HITL gates)\n                       ├─ tool_action → web search, file ops, REPL\n                       ├─ complex_llm (cycle until no tools pending)\n                       └─ memory_write → END\n```\n\n### Routes\n\n| Route | Model | When |\n|-------|-------|------|\n| `simple` | Qwen3-VL-4B (local) | Simple factual, greetings, short tasks |\n| `complex-cloud` | DeepSeek V4 (cloud) | Tool-augmented, complex reasoning, web search |\n\n### Classification Sources\n\n`keyword_bypass`, `deterministic`, `llm_classifier`, `hitl`\n\n### Key AgentState Fields\n\n| Field | Type | Purpose |\n|-------|------|---------|\n| `messages` | `Sequence[BaseMessage]` | Conversation history |\n| `route` | `str \\| None` | `\"simple\"` or `\"complex-cloud\"` |\n| `model_used` | `str \\| None` | `\"small-local\"`, `\"large-cloud\"`, or with `-fallback` suffix |\n| `needs_memory_retrieval` | `bool \\| None` | Router gate for vector retrieval |\n| `selected_toolboxes` | `list[str] \\| None` | e.g. `[\"web_search\", \"file_ops\"]` or `[\"all\"]` |\n| `token_budget` | `int \\| None` | Dynamic budget set by router |\n| `pending_tool_calls` | `bool \\| None` | Whether tools are queued |\n| `security_decision` | `str \\| None` | `\"approved\"` or `\"denied\"` |\n| `active_tokens` | `int \\| None` | Current token count |\n| `context_window` | `int \\| None` | Active model's context window |\n| `api_tokens_used` | `dict \\| None` | `{\"prompt_tokens\": int, \"completion_tokens\": int}` |\n| `router_metadata` | `dict \\| None` | Telemetry from routing decision |\n| `fallback_chain` | `list[dict] \\| None` | Ordered model attempt list |\n| `clarified_scope` | `dict \\| None` | User-approved requirements from scope_clarify |\n| `plan_review_approved` | `bool \\| None` | Plan review HITL result |\n| `response_confidence` | `float \\| None` | Coherence score |\n| `coherence_retry_reason` | `str \\| None` | Last coherence issue |\n| `_coherence_retry_round` | `int \\| None` | Coherence retry counter |\n\n---\n\n## Routing\n\n### Current Models\n\n| Slot | Model | Purpose |\n|------|-------|---------|\n| `models.small` | `qwen3-vl-4b-instruct-c_abliterated-v2-mlx` | Router, vision, extraction, simple answers |\n| `models.cloud` | `deepseek-v4-flash` | Complex reasoning (tool-augmented) |\n| `models.embedding` | `text-embedding-nomic-embed-text-v1.5-embedding` | LTM vector embeddings |\n\n**Changing models:** Edit only `defaults.yaml` (`models.small.model_name`, `models.cloud.model_name`). ConfigLoader centralizes all model name resolution via `get_small_model_name()` \u002F `get_cloud_model_name()`.\n\n### Key Routing Config (`defaults.yaml`)\n\n```yaml\nrouting:\n  confidence_threshold: 0.6         # HITL triggered below this\n  skill_clarification_threshold: 0.5\n  scope_clarification_enabled: true\n  plan_review_enabled: true\n  keyword_bypass: true\n  hitl_enabled: true\n  max_input_chars: 2000             # Router input truncation\n  budget_tiers:\n    - [40, 256]\n    - [150, 512]\n    - [400, 1536]\n    - [800, 3072]\n    - [1600, 4096]\n```\n\n### Environment Variable Overrides\n\n| Env Var | Overrides |\n|---------|-----------|\n| `SMALL_LLM_BASE_URL` | `models.small.base_url` |\n| `SMALL_LLM_MODEL_NAME` | `models.small.model_name` |\n| `CLOUD_LLM_BASE_URL` | `models.cloud.base_url` |\n| `CLOUD_LLM_MODEL_NAME` | `models.cloud.model_name` |\n| `DEEPSEEK_API_KEY` | Cloud API key |\n\n---\n\n## Memory\n\n### Three-Tier Architecture\n\n| Tier | Storage | Retrieval |\n|------|---------|-----------|\n| **STM** | `data\u002Fmemories.json` | Keyword search (recent 50 entries) |\n| **LTM** | Qdrant via Mem0 | Semantic search (gated by router) |\n| **Personal** | `data\u002Ftopics.json`, `data\u002Finterests.json`, `data\u002Fconversations.json` | Time-decay-weighted relevance |\n| **Scenarios** | `scenarios\u002F*\u002Fplaybook.md`, `constraints.md` | Router `scenario_id` + markdown loader |\n\n### Injection Flow\n\n```\nmemory_inject_lite → router → memory_retrieve → ...\n```\n\n- `memory_inject_lite`: Profile, persona, topics (no vector search). Cached 5 min.\n- Router sets `needs_memory_retrieval` and optional `scenario_id`.\n- `memory_retrieve`: Qdrant\u002FMem0 only when gated; loads scenario markdown.\n\n### Write Flow (after agent responds)\n\n1. PII-scrubs content\n2. Enqueues custom extraction (Redis stream → worker → L1 atoms in Qdrant)\n3. Extracts topics and updates topic\u002Finterests tracking\n4. Records conversation in `conversations.json`\n5. Invalidates memory cache for next turn\n\n### Config\n\n```yaml\nmemory:\n  max_facts: 200\n  search_window: 50\n  cache:\n    ttl: 300                  # 5 min\n  decay:\n    topic_half_life_days: 14\n    interest_half_life_days: 21\n  extraction:\n    idle_cooldown_seconds: 8\n    defer_while_graph_active: true\n```\n\n---\n\n## Tools\n\n### Toolbox Categories\n\n| Toolbox | Tools |\n|---------|-------|\n| Always | `ask_user` |\n| `web_search` | `web_search`, `fetch_webpage`, `deep_research` |\n| `file_ops` | `read_workspace_file`, `write_workspace_file`, `edit_workspace_file`, `list_workspace_files`, `delete_workspace_file`, `download_to_workspace`, `upload_from_workspace` |\n| `data_viz` | `create_docx`, `create_xlsx`, `create_pptx`, `create_pdf`, `notebook_run`, `notebook_reset`, `notebook_vars`, `read_ipynb`, `write_ipynb`, `export_ipynb_html`, `render_interactive_block` |\n| `productivity` | `todo_add`, `todo_list`, `todo_complete`, `list_skills`, `invoke_skill`, `render_interactive_block` |\n| `study` | `course_register`, `course_list`, `course_get`, `study_note_save`, `study_note_search`, `flashcard_deck_create`, `flashcard_review`, `quiz_session_start`, `quiz_session_answer`, `mastery_record`, `export_study_sheet` |\n| `memory` | `recall_memories`, `recall_all_memories`, `forget_memory`, `search_workspace_docs` |\n\n### Sensitive Tools (require HITL approval)\n\n`write_workspace_file`, `edit_workspace_file`, `delete_workspace_file`, `notebook_run`\n\n### Adding a New Tool\n\n1. Create `@tool` function in `src\u002Ftools\u002F\u003Cmodule>.py`\n2. Register in `src\u002Fagent\u002Ftool_sets.py` (`ToolboxRegistry`)\n3. Add to appropriate toolbox category\n4. If sensitive, add to `SENSITIVE_TOOLS` in `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py`\n\n---\n\n## HITL (Human-in-the-Loop)\n\n### Four HITL Gates\n\n| Gate | File | Trigger |\n|------|------|---------|\n| Router HITL | `src\u002Fagent\u002Frouting\u002Frouter.py` | LLM confidence \u003C 60% or ambiguous skill match |\n| Scope Clarify | `src\u002Fagent\u002Fnodes\u002Fscope_clarify.py` | Underspecified build\u002Fcreate requests |\n| Plan Review | `src\u002Fagent\u002Fnodes\u002Fplan_review.py` | Sensitive tool plan before execution |\n| Security Proxy | `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py` | Destructive\u002Fnetwork tool calls |\n\n### API Mode Behavior\n\n- Interrupts disabled when `mode == \"api\"` or `execution_policy == \"auto_approve\"`\n- Sensitive tools denied by default unless `auto_approve_sensitive: true`\n\n### Known Issues\n\n- **Confidence uncalibrated**: Always ~95%, does not reflect actual quality.\n- **Refactoring false positive**: \"write an improved version\" triggers scope_clarify. Mitigated by `_REFACTOR_SIGNALS` in `src\u002Fagent\u002Fhitl\u002Fscope_heuristics.py`.\n\n---\n\n## Logging & Traces\n\nOwlynn has a two-tier logging system.\n\n### Tier 1: Audit Log (Metadata)\n\nStructured JSON-lines capturing system-wide metadata — routing, model selection, HITL, memory, tools, timing. No user content.\n\n**Output:** `~\u002F.owlynn\u002Flogs\u002Faudit.jsonl` (5 × 10 MB rotating) and `audit-errors.jsonl` (3 × 5 MB rotating).\n\n**CLI tool:**\n```bash\npython scripts\u002Flogcat.py --channel agent.model              # Model selection\npython scripts\u002Flogcat.py --channel agent.hitl --level WARN   # HITL decisions\npython scripts\u002Flogcat.py --channel memory.cache              # Cache hits\u002Fmisses\npython scripts\u002Flogcat.py --thread-id abc-123                 # Filter by thread\npython scripts\u002Flogcat.py --follow                            # Live tail\npython scripts\u002Flogcat.py --channel agent.model --follow      # Live model activity\n```\n\n**15 channels:** `agent.lifecycle`, `agent.model`, `agent.hitl`, `agent.tool`, `agent.token`, `agent.cloud`, `agent.coherence`, `memory.inject`, `memory.write`, `memory.cache`, `memory.ltm`, `memory.stm`, `memory.summarize`, `memory.topics`, `api.ws`, `api.file`, `system`\n\n### Tier 2: Conversation Trace (Content)\n\nPer-thread JSONL files capturing what happened in a conversation — user messages, router decisions, LLM responses, tool calls with I\u002FO, errors, timing.\n\n**Output:** `~\u002F.owlynn\u002Ftraces\u002F{thread_id}.jsonl`\n\n**CLI tool:**\n```bash\npython scripts\u002Ftrace_view.py --list                          # List all traces\npython scripts\u002Ftrace_view.py --latest                        # View latest\npython scripts\u002Ftrace_view.py \u003Cthread_id>                     # View specific thread\npython scripts\u002Ftrace_view.py \u003Cthread_id> --json              # JSON output\npython scripts\u002Ftrace_view.py \u003Cthread_id> --follow            # Live tail\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type router_decision,llm_response  # Filter\n```\n\n### Trace Event Types\n\n| Type | When | Key Fields |\n|------|------|------------|\n| `turn_start` | Graph run begins | `ts` |\n| `user_message` | User sends message | `content` (truncated 2000 chars) |\n| `router_decision` | Router completes | `route`, `confidence`, `source`, `task_category`, `toolbox` |\n| `llm_response` | LLM node completes | `node`, `model`, `content_preview`, `token_usage`, `fallback_chain` |\n| `tool_call` | Tool completes | `tool_name`, `input`, `output`, `error`, `duration_s` |\n| `coherence_check` | Coherence check | `coherent`, `confidence`, `reason`, `duration_ms` |\n| `hitl_interrupt` | HITL fires | `interrupt_count` |\n| `error` | Graph error | `message` |\n| `turn_end` | Graph run ends | `ts` |\n\n### IDE Agent Trace Patterns\n\n```python\nimport json\n\nwith open(\"~\u002F.owlynn\u002Ftraces\u002F{thread_id}.jsonl\") as f:\n    events = [json.loads(line) for line in f]\n\n# Full timeline\nfor e in events:\n    print(f\"{e['ts']} [{e['type']}]\")\n\n# Routing decisions\n[e for e in events if e[\"type\"] == \"router_decision\"]\n\n# What did the user ask?\n[e for e in events if e[\"type\"] == \"user_message\"]\n\n# What did the LLM respond?\n[e for e in events if e[\"type\"] == \"llm_response\"]\n\n# What tools failed?\n[e for e in events if e[\"type\"] == \"tool_call\" and e.get(\"error\")]\n\n# Errors\n[e for e in events if e[\"type\"] == \"error\"]\n```\n\n### Emitting Audit Events in Code\n\n```python\nfrom src.config.audit_log import audit_info, audit_debug, audit_warn\n\naudit_info(\n    \"agent.hitl\", \"tool_classified\", tool=\"write_workspace_file\", decision=\"sensitive\"\n)\naudit_debug(\"memory.cache\", \"cache_hit\", age_seconds=12)\naudit_warn(\"agent.model\", \"swap_load_failed\", variant=\"vision\", error=str(e))\n```\n\n### Using @log_node Decorator\n\n```python\nfrom src.config.log_middleware import log_node\n\n\n@log_node(\"complex_llm\")\nasync def complex_llm_node(state: AgentState) -> AgentState: ...\n```\n\n---\n\n## Debugging Recipes\n\n### Wrong route (simple vs complex)\n\n```bash\n# Check what the router decided\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type router_decision\n\n# Check audit log for routing details\npython scripts\u002Flogcat.py --channel agent.model --thread-id \u003Cthread_id>\n\n# Look at classification_source in router_info WS event\n# Source types: keyword_bypass, deterministic, llm_classifier, hitl\n```\n\n**Fix:** Check `src\u002Fagent\u002Frouting\u002Frouter.py` for keyword bypass rules, or `defaults.yaml` `routing.confidence_threshold`.\n\n### LM Studio not reachable\n\n```bash\n# Check if LM Studio is running\ncurl -s http:\u002F\u002F127.0.0.1:1234\u002Fv1\u002Fmodels | head -c 200\n\n# Check model name matches exactly\npython scripts\u002Flogcat.py --channel agent.model --level WARN\n```\n\n**Fix:** Open LM Studio, load the model. Model name in `defaults.yaml` must match what LM Studio reports at `\u002Fv1\u002Fmodels`.\n\n### DeepSeek 401\u002F403\u002F429\n\n```bash\n# Check API key\npython scripts\u002Flogcat.py --channel agent.model --level WARN\n\n# Check circuit breaker\npython scripts\u002Flogcat.py --channel agent.cloud\n```\n\n**Fix:** Set `DEEPSEEK_API_KEY` in `.env.local`. Circuit breaker triggers after 3 failures (60s cooldown). Config: `cloud.circuit_breaker`.\n\n### Tool execution fails\n\n```bash\n# See tool calls and errors\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type tool_call\n\n# See tool classification (safe\u002Fsensitive)\npython scripts\u002Flogcat.py --channel agent.hitl\n```\n\n**Fix:** Check tool implementation in `src\u002Ftools\u002F`. Check `_tool_status_from_content` in `handler.py` for false error detection.\n\n### HITL approval missing\n\n```bash\n# Check HITL decisions\npython scripts\u002Flogcat.py --channel agent.hitl --thread-id \u003Cthread_id>\n\n# Check if auto_approve is set\npython scripts\u002Flogcat.py --channel agent.hitl --level WARN\n```\n\n**Fix:** If `execution_policy: auto_approve` in `data\u002Fuser_profile.json`, all HITL is skipped. Check `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py` `SENSITIVE_TOOLS` list.\n\n### Context overflow \u002F summarization\n\n```bash\n# Check summarization triggers\npython scripts\u002Flogcat.py --channel memory.summarize\n\n# Check token usage in trace\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type llm_response --json | \\\n  python -c \"import sys,json; [print(e.get('token_usage',{})) for e in [json.loads(l) for l in sys.stdin]]\"\n```\n\n**Fix:** Summarization triggers at 85% of context window (`summarization.threshold_ratio`). Config in `defaults.yaml`.\n\n### Memory not injected\n\n```bash\n# Check memory injection\npython scripts\u002Flogcat.py --channel memory.inject --thread-id \u003Cthread_id>\n\n# Check cache\npython scripts\u002Flogcat.py --channel memory.cache\n\n# Check extraction\npython scripts\u002Flogcat.py --channel memory.stm\n```\n\n**Fix:** Memory injection gated by router (`needs_memory_retrieval`). Cache TTL is 5 min. Check Redis\u002FQdrant are running.\n\n### Slow responses\n\n```bash\n# Check model fallbacks\npython scripts\u002Flogcat.py --channel agent.model --level WARN\n\n# Check tool durations\npython scripts\u002Ftrace_view.py \u003Cthread_id> --type tool_call --json | \\\n  python -c \"import sys,json; [print(f\\\"{e['tool_name']}: {e.get('duration_s','?')}s\\\") for e in [json.loads(l) for l in sys.stdin]]\"\n\n# Check coherence retries\npython scripts\u002Flogcat.py --channel agent.coherence\n```\n\n**Fix:** Cloud fallback adds latency. Coherence retries add extra LLM calls. Check `defaults.yaml` timeouts.\n\n### Infinite agent loop\n\n```bash\n# Check recursion\npython scripts\u002Flogcat.py --channel agent.lifecycle --thread-id \u003Cthread_id>\n```\n\n**Fix:** `complex.recursion_limit` defaults to 100 in `defaults.yaml`. Tool-calling cycles can hit this. Check edge conditions in `src\u002Fagent\u002Fcore\u002Fgraph.py`.\n\n### Frontend blank \u002F WS desync\n\n```bash\n# Check WS connection\npython scripts\u002Flogcat.py --channel api.ws\n\n# Check for WS errors\npython scripts\u002Flogcat.py --channel api.ws --level WARN\n```\n\n**Fix:** Check `frontend-v2\u002Fsrc\u002Flib\u002FwsClient.ts` for reconnection logic. Check `frontend-v2\u002Fsrc\u002FApp.tsx` for error boundaries.\n\n### Cloud fallback triggered unexpectedly\n\n```bash\n# Check cloud availability\npython scripts\u002Flogcat.py --channel agent.model | grep -i cloud\n\n# Check circuit breaker\npython scripts\u002Flogcat.py --channel agent.cloud\n```\n\n**Fix:** `_check_cloud_available()` checks: `cloud_escalation_enabled` in profile, circuit breaker state, API key presence. Check `data\u002Fuser_profile.json` `cloud_escalation_enabled`.\n\n### WebSocket crash (transfer_data_task)\n\n**Symptom:** `AttributeError: 'WebSocketProtocol' object has no attribute 'transfer_data_task'`\n\n**Cause:** websockets 15+ \u002F uvicorn race condition. Client disconnects before `connection_open()` sets `transfer_data_task`.\n\n**Fix:** Already fixed in `src\u002Fapi\u002Fws\u002Fhandler.py` — catches `AttributeError` alongside `RuntimeError` in WebSocket close handler.\n\n---\n\n## Known Gotchas\n\n1. **Model name must match exactly** — `defaults.yaml` \u002F `.env` must be identical to what LM Studio reports at `\u002Fv1\u002Fmodels`.\n\n2. **Redis unavailable = in-memory fallback** — LangGraph falls back to `MemorySaver`; session persistence lost.\n\n3. **Port conflicts** — Kill stale: `lsof -ti:8000 | xargs kill -9` and `lsof -ti:5173 | xargs kill -9`.\n\n4. **PYTHONPATH must be set** — `start.sh` does this. Manual: `export PYTHONPATH=\"$(pwd):$PYTHONPATH\"`.\n\n5. **Confidence uncalibrated** — Router confidence always ~95%, does not reflect actual quality.\n\n6. **No self-awareness** — System never detects wrong answers.\n\n7. **Memory context cap** — Injected memory text capped at 12,000 chars in `format_memory_context`.\n\n8. **StirlingPDF needs 2GB RAM** — 1GB causes Metaspace OOM. Falls back to PyMuPDF automatically.\n\n9. **Background extraction defers** — LTM atom extraction waits 8s idle cooldown to avoid GPU contention on Apple Silicon.\n\n10. **Cloud circuit breaker** — 3 failures triggers 60s cooldown.\n\n11. **Security proxy danger patterns** — Scans for `rm -rf`, `sudo`, `curl`, `ssh`; legitimate commands may match.\n\n12. **API mode HITL skip** — When `mode == \"api\"`, all interrupts disabled.\n\n13. **Recursion limit** — Default 100. Tool-calling cycles can hit this.\n\n14. **Mem0 requires NLP deps** — `pip install mem0ai[nlp]` for spaCy\u002Ffastembed.\n\n15. **E2B model leak** — Backend warmup auto-loads E2B if it's the default in LM Studio. Workaround: unload E2B before starting backend.\n\n---\n\n## Commands Reference\n\n### CI & Testing\n\n```bash\n.\u002Fscripts\u002Fci.sh --quick                    # Ruff, mypy, pytest, vitest\n.\u002Fscripts\u002Fci.sh                            # Above + frontend build\n.\u002Fscripts\u002Fci.sh --network                  # Live DeepSeek tests\n.\u002Fscripts\u002Fci.sh --benchmarks               # Router\u002Fcomplex\u002Fmemory benchmarks\n```\n\n### Key Test Suites\n\n```bash\npytest tests\u002Ftest_router_properties.py tests\u002Ftest_router_web_intent.py -q\npytest tests\u002Ftest_toolbox_registry.py -q\npytest tests\u002Ftest_websocket_event_contract.py -q\npytest tests\u002Ftest_memory_nodes.py tests\u002Ftest_crud_operations.py -q\npytest tests\u002Ftest_llm_pool.py -q\npytest tests\u002Ftest_graph.py -q\npytest tests\u002Ftest_audit_log.py -q\n```\n\n### Evaluations\n\n```bash\npython scripts\u002Frun_local_frontier_eval.py  # ~19-turn mechanical eval\npython scripts\u002Frun_educator_eval.py        # 8-turn educator eval\npython scripts\u002Frun_browser_eval.py         # 12-turn conversation eval\n```\n\n### Format & Lint\n\n```bash\nruff check .                # Python lint\nruff format .               # Python format\ncd frontend-v2 && npm run lint    # TS\u002FJS lint\ncd frontend-v2 && npm run format  # TS\u002FJS format\n```\n\n### Logs & Traces\n\n```bash\npython scripts\u002Flogcat.py --channel agent.model --follow\npython scripts\u002Ftrace_view.py --latest\npython scripts\u002Ftrace_view.py \u003Cthread_id> --json\n```\n\n### Config Validation\n\n```bash\npython3 -c \"from src.config.config_loader import validate_config; print(validate_config())\"\n```\n\n---\n\n## File Map\n\n### By Task\n\n| I want to… | File(s) |\n|------------|---------|\n| Change routing \u002F model selection | `src\u002Fconfig\u002Fdefaults.yaml` (models section), `src\u002Fagent\u002Frouting\u002Frouter.py`, `src\u002Fagent\u002Frouting\u002Fclassifier.py` |\n| Add or change a tool | `src\u002Ftools\u002F`, `src\u002Fagent\u002Ftool_sets.py` |\n| Fix memory \u002F context injection | `src\u002Fagent\u002Fnodes\u002Fmemory.py`, `src\u002Fmemory\u002F` |\n| Change HITL \u002F approvals | `src\u002Fagent\u002Fhitl\u002F`, `src\u002Fagent\u002Fnodes\u002Fsecurity_proxy.py`, `src\u002Fagent\u002Fnodes\u002Fscope_clarify.py`, `src\u002Fagent\u002Fnodes\u002Fplan_review.py` |\n| Debug a symptom | `docs\u002Fdebugging\u002FREADME.md` → symptom table |\n| Change cloud \u002F anonymization | `src\u002Fagent\u002Fcore\u002Fcomplex.py`, `src\u002Fagent\u002Fcore\u002Fcomplex_utils\u002F` |\n| Run the app | `start.sh`, `setup.sh`, `.env` |\n| Run CI \u002F tests \u002F evals | `scripts\u002Fci.sh`, `scripts\u002Frun_*_eval.py` |\n| View logs | `scripts\u002Flogcat.py`, `scripts\u002Ftrace_view.py` |\n| Change config | `src\u002Fconfig\u002Fdefaults.yaml` (single source of truth) |\n| Change WebSocket events | `src\u002Fapi\u002Fws\u002Fhandler.py`, `src\u002Fapi\u002Fws\u002Fschemas.py`, `frontend-v2\u002Fsrc\u002F` |\n",{},"main","b66b26cd1c7581bf8af8f0781b92bd072318b1c9",".opencode\u002Fskills\u002Fowlynn-diagnostics",0,"https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2","https:\u002F\u002Fgithub.com\u002FmaThiaslI152","2026-09-08 12:27:42",{"source":7,"skillName":5,"commands":32,"prompt":42,"zipAvailable":43},[33,36,39],{"tool":34,"command":35},"npx","npx skills add maThiaslI152\u002FOwlynnV2 --skill owlynn-diagnostics",{"tool":37,"command":38},"bunx","bunx skills add maThiaslI152\u002FOwlynnV2 --skill owlynn-diagnostics",{"tool":40,"command":41},"pnpm","pnpm dlx skills add maThiaslI152\u002FOwlynnV2 --skill owlynn-diagnostics","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2\u002Fblob\u002Fmain\u002F.opencode\u002Fskills\u002Fowlynn-diagnostics\u002FSKILL.md",true,[],[46,60,70,86,96,110],{"skillNo":47,"name":48,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":6,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":50,"categories":51,"keywords":52,"publishedAt":19},"01M71G8QVFENJMT5Q4EHR7Y6G4","weather_lookup","Procedural skill synthesized from user workflow","https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2\u002Fblob\u002Fmain\u002Fskills\u002Fweather_lookup\u002FSKILL.md",[],[53,54,55,56,57,58,59],"weather lookup","weather","lookup","general","workflow","procedural","user",{"skillNo":61,"name":62,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":6,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":63,"categories":64,"keywords":65,"publishedAt":19},"01M71G8P5WSK13A308EPFPYV8K","reprompt_on_no_tools","https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2\u002Fblob\u002Fmain\u002Fskills\u002Freprompt_on_no_tools\u002FSKILL.md",[],[66,67,68,69,58,59,57],"reprompt on no tools","no tools","Tools","productivity",{"skillNo":71,"name":72,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":6,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":80,"publishedAt":19},"01M71G8M5FQQ8PVXYNVJDWP1A5","prioritize_official_gov_sources_over_aggregators","https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2\u002Fblob\u002Fmain\u002Fskills\u002Fprioritize_official_gov_sources_over_aggregators\u002FSKILL.md",[75],{"categoryNo":76,"categoryName":77,"taxonomyType":78,"taxonomyTypeDesc":79},"01JBSEEDCAT000000000000021","资料检索与研究","SCENARIO","使用场景",[81,82,83,84,58,57,59,85],"official","prioritize official gov sources over aggregators","prioritize","sources","research",{"skillNo":87,"name":88,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":6,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":91,"publishedAt":19},"01M71G8JTC4NPQ5Q67QS243M5Y","matplotlib_horizontal_bar","https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2\u002Fblob\u002Fmain\u002Fskills\u002Fmatplotlib_horizontal_bar\u002FSKILL.md",[],[92,93,94,95,58,57,59],"matplotlib","bar","matplotlib horizontal bar","data",{"skillNo":97,"name":98,"nameZh":6,"description":49,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":6,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":106,"publishedAt":19},"01M71G8H910CPXZQ4ACKV8M0Y3","live-data-lookup","https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2\u002Fblob\u002Fmain\u002Fskills\u002Flive-data-lookup\u002FSKILL.md",[101],{"categoryNo":102,"categoryName":103,"taxonomyType":104,"taxonomyTypeDesc":105},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",[107,108,55,109,95,85,58,57,59],"data lookup","live data lookup","live",{"skillNo":111,"name":112,"nameZh":6,"description":113,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":6,"repositoryNo":11,"starsCount":12,"licenseSpdxId":13,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":123,"publishedAt":19},"01M71G8FNE5SQSCY5E4RG4QH2H","html_comparison_chart","Offline Chart.js HTML bar\u002Fline\u002Fpie charts for pre-known comparison data","https:\u002F\u002Fgithub.com\u002FmaThiaslI152\u002FOwlynnV2\u002Fblob\u002Fmain\u002Fskills\u002Fhtml_comparison_chart\u002FSKILL.md",[116,119,122],{"categoryNo":117,"categoryName":118,"taxonomyType":78,"taxonomyTypeDesc":79},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":120,"categoryName":121,"taxonomyType":78,"taxonomyTypeDesc":79},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":102,"categoryName":103,"taxonomyType":104,"taxonomyTypeDesc":105},[124,125,126,127,128,129,130,131,132,95,133,93,134],"comparison","chart","html","html comparison chart","js","Charts","for","line","price","offline","bar chart"]