fix(agent_jobs): mover cola de SQLite a ficheros JSON (cross-9p safe)

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.
This commit is contained in:
2026-05-03 16:23:18 +02:00
parent 82a576b844
commit 3e7b3adc16
3 changed files with 196 additions and 67 deletions
+31 -22
View File
@@ -597,9 +597,15 @@ def _parse_yaml_minimal(text: str) -> dict:
def cmd_enricher_run(args) -> None:
"""Inserta un job en la cola agent_jobs. main.cpp lo recoge cada frame y
lo somete via jobs_submit (que arranca el subprocess). Asi reusamos el
pool de workers existente sin duplicar logica."""
"""Encola un job en `<app_dir>/agent_jobs_queue/<req_id>.json`.
main.cpp escanea ese directorio cada frame, lee cada JSON, somete
via jobs_submit y borra el fichero. Usamos directorio de ficheros
en lugar de tabla SQLite por la misma razon que el marker
`.mutations.marker`: graph_explorer.db esta abierta en WAL desde
el lado Windows, y gx-cli escribiendo via /mnt/c (9p) hace que el
mmap del .shm falle silenciosamente -> disk I/O error.
"""
edir = _enrichers_dir()
if not (edir / args.enricher / "manifest.yaml").is_file():
_die(f"enricher not found: {args.enricher}")
@@ -616,26 +622,29 @@ def cmd_enricher_run(args) -> None:
node_name = ""
req_id = f"areq_{_now_ms()}"
payload = {
"id": req_id,
"enricher_id": args.enricher,
"node_id": args.node or "",
"node_name": node_name,
"params_json": args.params or "{}",
"created_at": _now_ms(),
}
app_dir = os.environ.get("GX_APP_DIR", "")
if not app_dir:
_die("GX_APP_DIR env var is empty")
queue_dir = Path(app_dir) / "agent_jobs_queue"
try:
cn = sqlite3.connect(_app_db())
cn.execute(
"CREATE TABLE IF NOT EXISTS agent_jobs ("
" id TEXT PRIMARY KEY,"
" enricher_id TEXT NOT NULL,"
" node_id TEXT NOT NULL DEFAULT '',"
" node_name TEXT NOT NULL DEFAULT '',"
" params_json TEXT NOT NULL DEFAULT '{}',"
" created_at INTEGER NOT NULL)"
)
cn.execute(
"INSERT INTO agent_jobs (id, enricher_id, node_id, node_name, "
"params_json, created_at) VALUES (?, ?, ?, ?, ?, ?)",
(req_id, args.enricher, args.node or "", node_name,
args.params or "{}", _now_ms()),
)
cn.commit()
cn.close()
except sqlite3.Error as e:
queue_dir.mkdir(parents=True, exist_ok=True)
# Atomic write: tmp + rename. main.cpp nunca lee un JSON a medias
# porque el rename es atomico en NTFS y en 9p.
tmp = queue_dir / f"{req_id}.json.tmp"
final = queue_dir / f"{req_id}.json"
tmp.write_text(json.dumps(payload, ensure_ascii=False),
encoding="utf-8")
os.replace(tmp, final)
except OSError as e:
_die(f"could not enqueue: {e}")
_ok(request_id=req_id, enricher=args.enricher, node=args.node or "",
message="job encolado, lo recoge el panel Jobs")