NodeGroupsWindowState gana un discriminador `kind` (Table | Group) y
un flag `focus_request` (lo consumira 0036c). Por defecto Table, asi
que el flujo historico (DuckDB rows tras expand de un nodo Table) no
cambia.
kind=Group lee directamente operations.db consultando
`entities WHERE group_id = container_id` con columnas fijas
(id, name, type_ref, status, updated_at) ordenadas por updated_at DESC.
Los nuevos loaders viven en node_groups.cpp:
- node_groups_count_for_group -> SELECT count(*) ...
- node_groups_page_for_group -> SELECT id,name,type_ref,status,
updated_at ... LIMIT ? OFFSET ?
Para columnas, opcion (A) del issue: pre-popular meta.columns con la
lista fija al abrir kind=Group, asi el render se mantiene generico.
NodeGroupsRow.values guarda los 5 campos en ese orden y row.id es la
key natural (= entity_id de la fila — al ser ya entidad, no hace falta
promocionarla).
Render en views.cpp ramifica por kind:
- Table: layout original [id_col + columns + promoted] con doble
click -> promote/focus.
- Group: layout [columns fijas] sin promoted. Doble click sobre la
fila ya pone want_focus_entity = id (los flujos posteriores 0036c-e
afinan UX). Right click ofrece "Focus in Inspector".
main.cpp dispatcha por kind al refrescar paginas y, al cerrar via X,
solo llama a node_groups_set_expanded para kind=Table (Group no usa
ese flag).
views_node_groups_windows_sync se hace kind-aware: solo reconcilia
entries kind=Table contra el set de Tables expandidas; no toca las
entries kind=Group (las gestiona views_node_groups_open).
Nueva API publica:
views_node_groups_open(app, container_id, kind, ops_db)
Crea o reusa la entry, setea focus_request=true y para kind=Group
pre-popula meta.columns + intenta leer `name` del Group para el
titulo. Sin caller todavia — la consume 0036c.
Tests:
- tests/test_node_groups_loader.py (6 tests) verifica el contrato
SQL via gx-cli. Nuevo subcomando `gx-cli group page <id>` espejea
el loader C++ exactamente (mismo SQL); tambien expuesto como tool
MCP `group_page` para que Echo pueda inspeccionar Groups.
Resultado:
- WSL: 89 -> 95 passed
- Windows: 78+11 -> 84+11 passed
- Build C++ Windows limpio, sin warnings nuevos.
- Regresion kind=Table: comportamiento identico (mismo render,
mismo loader DuckDB).
Refs: issues/0036b-kind-discriminator-and-group-loader.md
Bug derivado del fix anterior: gx-cli escribia ficheros JSON en
`$GX_APP_DIR/agent_jobs_queue/` (apuntando al repo fuente) mientras
main.cpp escaneaba `parent(g_layout_db_path)/agent_jobs_queue/`
(install Windows). Dos directorios distintos -> jobs huerfanos.
Echo reportaba "encolado" pero el worker nunca veia los ficheros.
La causa: chat.cpp setea GX_APP_DIR=<registry>/projects/osint_graph/
apps/graph_explorer y GX_APP_DB=<install>/local_files/projects/<slug>/
graph_explorer.db. Dos sitios. Solo APP_DB coincide con donde
graph_explorer.exe escanea (parent del .db).
Fix:
* gx-cli cmd_enricher_run: queue_dir = parent(GX_APP_DB) /
agent_jobs_queue. Alineado con main.cpp.
* gx-cli: nuevo helper `_log(tag, msg)` que escribe a stderr Y a
`<parent(app_db)>/gx-cli.log` para auditoria persistente. Cubre
node_create, node_update, node_delete, rel_create, enricher_run.
* gx-cli mcp _mcp_log tambien persiste a gx-cli.log.
* main.cpp: log el queue scan dir una vez por sesion para detectar
mismatches a futuro.
* .gitignore: agent_jobs_queue/ y gx-cli.log son runtime, no se
commitean.
Tests:
* test_enricher_run_queue_dir_derives_from_app_db (regresion)
configura GX_APP_DB en un dir distinto de GX_APP_DIR y verifica
que el JSON aterriza junto a APP_DB.
* test_enricher_run_writes_log_to_gx_cli_log valida la auditoria.
WSL 81 / Windows 70 + 11 skipped.
Bug: Echo (gx-cli en WSL) recibia "disk I/O error" al INSERT en la
tabla `agent_jobs` de graph_explorer.db. Causa: graph_explorer.exe
mantiene esa BD abierta con journal_mode=WAL desde Windows, y SQLite
WAL exige mmap del .shm compartido entre procesos. Cuando un escritor
accede via /mnt/c (9p) y el otro nativo NTFS, ese mmap falla.
El proyecto ya habia resuelto este patron antes: el contador de
mutaciones (.mutations.marker) usa fichero plano en vez de SQL por
exactamente la misma razon. agent_jobs era la unica cola que se
quedo en SQLite — momento de aplicar el mismo fix.
Cambios:
* gx-cli cmd_enricher_run: en lugar de INSERT, escribe
`<app_dir>/agent_jobs_queue/<req_id>.json` con el payload del job.
Atomic write (tmp + rename, atomico tanto en NTFS como en 9p).
* main.cpp polling: en lugar de SELECT/DELETE sobre agent_jobs,
escanea ese directorio cada frame, lee cada JSON via json_extract
(sqlite3 in-memory, sin tocar archivos en disco), llama jobs_submit,
y borra el fichero. Throttle a 8 jobs por frame igual que antes.
* main.cpp: anyade <filesystem> y <fstream>.
* tests/test_gx_cli.py: 5 tests nuevos en TestCliEnricherRun:
- escribe fichero JSON con req_id como nombre
- NO crea tabla agent_jobs en graph_explorer.db (regresion)
- errores claros si enricher o nodo no existen
- no quedan .tmp tras encolado exitoso
WSL 79 / Windows 68 + 11 skipped.
Bug encontrado por el agente Echo: el MCP server gx-cli (subcomando
`mcp-server`) llamaba a cmd_node_create / cmd_node_update con un
SimpleNamespace que NO incluia `notes`, asi que `args.notes` lanzaba
AttributeError. Causa raiz: MCP_DISPATCH no defaulteaba `notes` ni
`append_notes`, y el inputSchema de las tools tampoco los anunciaba.
Cambios:
* MCP_TOOLS["node_create"].inputSchema.properties anyade `notes`.
* MCP_TOOLS["node_update"].inputSchema.properties anyade `notes`
+ `append_notes` (boolean, default false).
* MCP_DISPATCH["node_create"] defaultea `notes: None`.
* MCP_DISPATCH["node_update"] defaultea `notes: None`,
`append_notes: False`.
Tests nuevos en tests/test_gx_cli.py (30 tests):
* CLI: node create/update/delete con notes (replace + append),
list/show/search, rel create/list/delete con cascada, query
read-only que rechaza writes, autodetect de tipos.
* MCP dispatcher: cada cmd_* tolera args opcionales omitidos,
notes y append_notes funcionan via dispatch, MCP_TOOLS y
MCP_DISPATCH coinciden 1:1 (sanity contractual).
* Regresion 0035d: tests dedicados que congelan el contrato
notes/append_notes en defaults e inputSchema — si alguien
vuelve a quitarlos el test se queja inmediatamente.
WSL 74 / Windows 63 + 11 skipped.
El agente Echo (chat panel + claude -p) usa gx-cli para todas las
mutaciones del grafo. Antes solo podia setear name/type/status/
description/tags — pero los enrichers nuevos split_sentences y
extract_iocs_text leen entities.notes (lo que se escribe en el panel
Note del Inspector). Sin este flag, el agente no podia darle texto
largo a un nodo para luego enricharlo.
Cambios:
- node create: --notes "..." (vacio por default).
- node update: --notes "..." (replace) + --append-notes (concat con
doble newline como separador, util para acumular contexto sin
pisar lo previo).
Smoke test: replace y append funcionan, default vacio sigue ok.
Anade panel "Echo" — copiloto OSINT que invoca claude -p con un MCP
server propio (gx-cli) exponiendo el grafo como tools tipadas:
info, node_*, rel_*, table_*, enricher_*, query.
Cambios:
- chat.cpp/h: panel UI dockeable con history, raw stream-json toggle,
spawn de claude -p con system prompt OSINT, ChatMessage con USER/
ASSISTANT/TOOL_USE/TOOL_RESULT/SYSTEM/ERROR_MSG, escritura de
mcp.json con paths Linux para WSL en Windows.
- gx-cli: binario MCP standalone que valida cada tool, abre
operations.db en RW, escribe agent_mutations counter para que el
viewport detecte cambios en vivo.
- CMakeLists.txt: anade chat.cpp al target.
- views.h: panel_chat boolean en AppState.
- main.cpp: integracion del panel Chat (rename a Echo en menubar +
init), refresh de contexto al cambiar operations.db, drain de cola
agent_jobs tras enricher_run.
Mensajes del panel renderizan con fn_ui::selectable_text_wrapped_force
(wrap forzado + seleccion) para que URLs/JSON largos no se clippeen
y permitan copy/paste.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>