# debug-tcl-async-delete-send-message.md

**Session ID:** `tcl-async-delete-send-message`  
**Created At:** 2026-09-19  
**Status:** `[OPEN] STEP0 → STEP1_INIT`  
**Owner Bug Source:** Alex manual reproducido TRAE CERRADO, PC normal.  
**Symptom:** `Tcl_AsyncDelete: async handler deleted by the wrong thread` EXACTLY después de enviar mensaje: "Hola Optimus, ¿estás activo y funcionando correctamente?". UA muere, tray desaparece, PS prompt regresa.

---

## Preconditions
- TRAE: CERRADO (Alex confirmado). NO es saturación TRAE.
- PC normal: CPU ~12%, RAM ~52%. NO es sistema.
- Startup: Arrancado python.exe `runtime_v1/optimus_user_agent.py` SOLO.
- Espera antes mensaje: > 60 segundos. NO es startup.
- Trigger: Enviar mensaje EXACTO → inmediatamente crash antes/durante response delivery.
- NO es smoke-test residual (killed).

---

## Scientific Workflow
1. ✅ STEP0 KILL ORPHANS (matar procesos Python residuales)
2. 🔄 STEP1 INIT → create MD + hypotheses.
3. ➡️ STEP2 HYPOTHESES (3-5 falsificables).
4. ➡️ STEP3 INSTRUMENT → *ONLY* instrumentation. NO logic change.
5. ➡️ STEP4 REPRODUCE → 3+ runs, UA REAL, no mocks.
6. ➡️ STEP5 ROOT_CAUSE → OFFENDING_OBJECT + CREATED/DESTROYED_THREAD evidencia líneas.
7. ➡️ STEP6 MINIMAL_FIX → sólo lo mínimo.
8. ➡️ STEP7 VERIFY → 8 runs, TCL_ASYNC=0.
9. ➡️ STEP8 CLEANUP → 0 huérfanos.
10. ➡️ STEP9 REPORT.

---

## Session Constants (Paths / Commands)
- **UA REAL:** `C:\xampp\htdocs\Digital Optimus\runtime_v1\optimus_user_agent.py`
- **Python EXE:** `C:\Users\Intel\AppData\Local\Programs\Python\Python311\python.exe`
- **Mensaje trigger EXACTO:** `"Hola Optimus, ¿estás activo y funcionando correctamente?"`
- **TCL safety logs (existentes):** `data/__ua_tcl_crash_persistent.ndjson`, `data/__tcl_thread_safety_log.jsonl`
- **Tcl persistent NDJSON nuevo session:** `data/__debug_tcl_<sessionId>.ndjson` (con thread_id / tname / owner_tid / action / object_kind / line)
- **Wait before msg (min):** 35s (≥30s requerido Alex).
- **Wait after response (min):** 60s (≥60s requerido Alex).
- **Repetitions pre-fix:** 3
- **Repetitions post-fix:** 8
- **Criterio PASS:** TCL_ASYNC_DELETE_COUNT == 0; USER_AGENT_ALIVE=True; TRAY_ALIVE=True; ORPHAN_TEST_PROCESSES=0.

---

## Hypotheses (Falsificables) — 4 candidatas
| ID | Hypothesis | Prediction (qué veremos en instrumentation si es cierta) | Cómo falsear |
|---|---|---|---|
| H1 | **Closure léxico captura StringVar/Text widget en el worker thread (LightweightChatWindow._handle_send → worker target):** la `lambda`/`def` dentro de método captura `self.text`/`self.status_var` y en el momento del response delivery por `root.after`, si la ventana ha sido cerrada o GC ha pasado, el worker reference-count elimina el StringVar desde worker, NO GUI thread. | instrumentación muestra `tk_stringvar_DESTROY` o `tk_widget_DESTROY` same_object_id, create_tid≠destroy_tid, destroy_tid=worker. | Ver que GC y destroy de objetos es GUI thread; no closures. |
| H2 | **Toplevel/Window temporal (MemoryInspectorWindow) abre cierra en worker por alguna lógica fallback:** o Clipboard/dialog tkinter.messagebox / filedialog indirectamente invocado desde worker por algún route error handler (fallback técnico). | Instrumentación muestra `Toplevel_NEW`, `StringVar_NEW`, `messagebox_*` en worker_tid != gui_tid. | Ver que messagebox/toplevel siempre GUI thread. |
| H3 | **Queue delivery por medio de generadores/iteradores Tcl internos (Text widget `insert` con tags):** el `root.after(0, _append_text)` crea un handler Tcl; luego hay una segunda `queue.get_nowait()` inmediatamente en worker después de root.after → la cola interna Tcl se elimina desde worker. | Instrumentación muestra `after_enqueue tid=GUI` y `after_cancel tid=worker` same handler; o `Text_insert` tk_call desde worker. | Ver root.after idempotente y NO se tocan widgets desde worker. |
| H4 | **`root.update()` / `root.update_idletasks()` desde worker thread en algún punto de cleanup o fallback:** algún control `progress_var.set(...)` sin pasar por `root.after` → variable Tcl creada en GUI, usada por worker. | Instrumentación muestra `progress_var_SET` o `_append_text_DIRECT` (NO root.after wrapper) desde worker_tid. | Ver TODO Tk acceso pasa forzosamente por root.after wrapper. |

---

## Instrumentation Points (PLANNED, NO applied YET)
Todos wrapped en `#region debug-point tcl-<id>` y `#endregion`:
1. **TCL_CREATE_MONKEY:** Monkey-patch mín. a `tkinter.Misc.__init__` y `StringVar.__init__` / `IntVar.__init__` → log `TK_OBJ_NEW {kind, oid, tname, tid, owner_tid, stack}`.
2. **WORKER_BEGIN_END:** `_run_sd_or_conversation_worker` → `WORKER_START`, `WORKER_END`, `WORKER_EXCEPTION`.
3. **MSG_DELIVERY_QUEUE:** `_append_text_wrapper`, `_update_status_wrapper`, `_root_after_safe` → log cada queue.submit.
4. **DESTROY/GC:** Monkey-patch `destroy()` y `__del__` (Text, Frame, Toplevel, StringVar) → `TK_OBJ_DESTROY {kind, oid, tname, tid, create_tid_compare}`.
5. **FINAL:** comparar lista TK_OBJ_NEW vs TK_OBJ_DESTROY, detectar `destroy_tid != create_tid`.

---

## Log Schema NDJSON (por línea JSON)
```json
{"ts_utc":"...","session":"tcl-async-delete-send-message","run":1,"event":"TK_OBJ_NEW","kind":"StringVar","oid":"1407...","create_tid":6420,"create_tname":"MainThread","gui_owner_tid":6420,"stack_frames":["..."]}
{"ts_utc":"...","session":"tcl-async-delete-send-message","run":1,"event":"WORKER_START","worker_tid":8432,"worker_tname":"ChatWorker-1","gui_owner_tid":6420,"trigger_msg":"..."}
{"ts_utc":"...","session":"tcl-async-delete-send-message","run":1,"event":"WORKER_CALLS_DIRECT_TK","action":"progress_var.set","called_tid":8432,"gui_owner_tid":6420}
```

---

## Run Tracker
| Run | Trigger Msg | Wait_before | Crash? | Tcl_AsyncDelete Count | UA Alive 60s after | Notes |
|---|---|---|---|---|---|---|
| pre-fix 1 | exacto Alex | 35s | ? | ? | ? | |
| pre-fix 2 | exacto Alex | 35s | ? | ? | ? | |
| pre-fix 3 | exacto Alex | 35s | ? | ? | ? | |
| --- | --- | --- | --- | --- | --- | --- |
| post-fix 1 | exacto Alex | 35s | ? | 0 | ? | |
| ... | ... | ... | ... | ... | ... | |
| post-fix 8 | exacto Alex | 35s | ? | 0 | ? | |

---

## Final Acceptance (when closed)
- ROOT_CAUSE= concreto.
- OFFENDING_OBJECT= nombre clase/instancia Python exacta y Tcl correspondiente.
- CREATED_THREAD= id y nombre.
- DESTROYED_THREAD= id y nombre.
- FIX= path:line.
- REAL_MESSAGE_TEST 8 runs PASS.
- TCL_ASYNC_DELETE_COUNT total = 0.
- USER_AGENT_ALIVE after 60s = True.
- TRAY_ALIVE after 60s = True.
- ORPHAN_TEST_PROCESSES después cleanup = 0.
- FINAL_STATUS = READY_FOR_OWNER_TCL_RETEST.
