---
title: "Continuous Learning Log"
type: debug
created_at: 2026-03-06T05:18:58Z
updated_at: 2026-03-06T05:18:58Z
uuid: c21d2cfb-ed4a-4e47-a355-d05f4220c73f
---

# Continuous Learning Log

Bugs, smells, issues dumped here as encountered. Raw is fine. A skill will organize this into proper kb entries later.

Format: `[date] [type] description`

Types: bug, smell, pattern, lesson, antipattern

---

[2026-01-07] [lesson] When there's a type mismatch between A and B, fix where A or B is defined, not every place that uses them. Don't scatter changes across 5+ files when one line at the source fixes everything.

[2026-01-08] [pattern] Local mode config: `docs/configuration/tunacode.local.json.example` - use when running local models. Key settings: `local_mode: true`, `local_max_tokens: 1000`, `context_window_size: 10000`, `OPENAI_BASE_URL` for local server.

[2026-01-08] [pattern] Syntax highlighting in tool renderers: Use `syntax_utils.py` for `get_lexer(filepath)` and `syntax_or_text(content, filepath)`. Consistent monokai theme. 9 renderers now registered: bash, glob, grep, list_dir, read_file, research_codebase, update_file, web_fetch, write_file. Commit `9db8e92`.

[2026-01-08] [antipattern] **RichLog.write(expand=True) is NOT terminal width!** `expand=True` expands to `scrollable_content_region`, which excludes padding and scrollbar gutter (~4-8 chars narrower than terminal). For full-width panels, pass explicit `width=` to the Panel constructor. Never trust `expand=True` to mean "full width" - it means "fill container" and the container is smaller than you think. See Gate 5: Indirection Requires Verification.

[2026-01-14] [antipattern] **Semantically dead code: loaded but never read.** Static analysis (Vulture) only catches syntactically dead code (never called). It misses code that IS called but whose result is never consumed. Example: `glob.py` had `_load_gitignore_patterns()` that populated a global, but nothing ever read that global. The `use_gitignore` parameter was a lie - it triggered work but had zero effect. **Prevention:** When adding a "load X" function, grep for reads of X before shipping. If a parameter controls behavior, trace the data flow to prove it actually changes output.

[2026-01-17] [bug] **Dangling tool calls on user abort (PR #246).** User aborts mid-tool-call → `messages` has `ModelResponse` with tool calls but no `ToolReturn` → next API request fails. Root cause: exception path violated message invariant (every tool call needs a return). Fix: `_remove_dangling_tool_calls()` in `except UserAbortError`. **Prevention:** Document state invariants. Test exception scenarios. See Gate 6.

[2026-01-24] [bug] **Shallow copy corrupts DEFAULT_USER_CONFIG.** `.copy()` is shallow - nested dicts (`settings`, `env`) still reference the constant. Setup was mutating user_config in-place → polluted module-level default → first-run config missing all defaults. **Fix:** Don't mutate, replace. `state.py` assigns constant directly (reference replaced by setup). `setup.py` builds new dict from `deepcopy(DEFAULT_USER_CONFIG)`. **Key insight:** If you don't mutate, you don't need to copy. See `.claude/JOURNAL.md` 2026-01-24 entry.

[2026-01-25] [pattern] **Canonical messaging adoption complete (Task 01).** Migrated production code to use `adapter.get_content()` (P1) and routed sanitize.py through adapter helpers (P2). Key insight: adapter layer now handles ALL message polymorphism. Detection uses adapter (`find_dangling_tool_calls`, `get_tool_call_ids`), mutation stays in sanitize.py. Deleted ~117 LOC of duplicate accessors. Branch: `types-architect`. See `.claude/JOURNAL.md` 2026-01-25 entry.

[2026-01-25] [bug] **Orphaned retry-prompt parts after dangling tool call cleanup.** When pruning dangling tool calls, `_filter_dangling_tool_calls_from_parts()` only removed `tool-call` parts. This left behind `retry-prompt` parts (pydantic-ai's error response for failed tools like 403). **Fix:** Filter ANY part with `tool_call_id` matching a dangling ID, not just `part_kind == "tool-call"`. **Key insight:** pydantic-ai uses multiple part kinds (`tool-call`, `tool-return`, `retry-prompt`) that all reference the same `tool_call_id`. Cleanup must be ID-based, not kind-based.

[2026-02-11] [bug] **Detached Textual widget mount during teardown causes secondary crash.** `ChatContainer.write()` mounted unconditionally, so exception rendering during app teardown raised `MountError` and masked the original exception. **Fix:** Guard mount with `if not self.is_attached:`, emit stderr warning, and return the unmounted widget. Added unit tests for detached and attached paths in `tests/unit/ui/test_chat_container.py`. **Key insight:** Error-rendering paths must be teardown-safe and non-throwing, or they can hide the real failure.
