read last handoff from memory (mem_7e571be6dcee4a0b8d53c3cf27898c50), then explore api to find it


---
name: memory-read
description: Read persisted research memory, claims, and tasks
---

# personal-memory-search

## Workflow

1. Execute:
   - `curl -fsSL "${AGENTS_DB_API:-http://100.116.176.16:8091}/api/search?q=<query>&scope=global&scope=project&limit=20"`
2. Return matching runs, claims, and tasks from the JSON response.
3. Prefer reuse of prior work before starting a new run.


---
name: ballbox-company
description: Load Ballbox company context and operating namespace
---

# ballbox-company

## Purpose

Load the Ballbox company operating context when the request is about Ballbox.

## Workflow

1. Use the Ballbox namespace at `/mnt/rpi/repos/ballbox-company-agent` as the working context root.
2. Read `/mnt/rpi/repos/ballbox-company-agent/AGENTS.md` and follow its rules.
3. Use `/mnt/rpi/repos/ballbox-company-agent/index.md` as the default dashboard entry point.
4. Prefer shared-memory search before assuming facts; avoid invented certainty.
5. If the work implies repo/code changes, delegate to `~/ai-dev-workflow`.


Research step: capture https://help.matchable.io/en/articles/23-how-to-use-credits

Captured https://help.matchable.io/en/articles/23-how-to-use-credits

Explanation: facets=0.00 lexical=0.00 semantic=1.00 confidence=0.75 freshness=0.75

Research step: capture https://help.matchable.io/en/articles/10-how-to-book-a-training

Captured https://help.matchable.io/en/articles/10-how-to-book-a-training

Explanation: facets=0.00 lexical=0.00 semantic=0.99 confidence=0.75 freshness=0.75

mercadoPagoProvider.ts

mercadoPagoProvider.ts
Real provider uses placeholder collector/POS identifiers and generic payload assumptions; requires account-specific validation before production.
file:///Users/sebas/Code/ballbox-payment-integration/src/providers/mercadoPagoProvider.ts

Explanation: facets=0.00 lexical=0.00 semantic=0.99 confidence=0.75 freshness=0.70

NSWindow.CollectionBehavior.canJoinAllSpaces

NSWindow.CollectionBehavior.canJoinAllSpaces
Apple doc: ventana puede aparecer en todos los spaces
https://developer.apple.com/documentation/appkit/nswindow/collectionbehavior-swift.struct/canjoinallspaces

Capture Verification

Capture Verification
local verification capture
content_type=text/html
file:///tmp/personal-agent-capture.html

Summary:
local verification capture
content_type=text/html

local verification capture
content_type=text/html

webhook.ts

webhook.ts
Signature verification currently returns placeholder validity when a secret exists instead of comparing against provider-derived signature input.
file:///Users/sebas/Code/ballbox-payment-integration/src/services/webhook.ts

Integrations

Integrations
Integrations overview: ecosystem and automation claims
content_type=text/html; charset=utf-8
https://matchable.io/business/integrations

Search results: periodic memory consolidation agent memory knowledge base deduplication TTL deca

Question: periodic memory consolidation agent memory knowledge base deduplication TTL decay best practices

AI Agent Memory: Architectures, Retrieval, Governance

How to Build AI Agents with Redis Memory Management

How to Build Memory Consolidation - oneuptime.com

Agent Memory in Azure Cosmos DB for NoSQL - learn.microsoft.com

Memory Consolidation Guide - AgentDock Documentation

Research step: capture https://help.matchable.io/en/articles/22-manage-credits

Captured https://help.matchable.io/en/articles/22-manage-credits

HDFocus 55-inch outdoor weatherproof digital signage
Alibaba; Android OS; IP55/IP65; 1000+ nits; outdoor/high-traffic fit
https://www.alibaba.com/product-introduction/Digital-Signage-And-Displays-Inch-Outdoor_1600345353452.html

Implementing the Full-Screen Experience

Implementing the Full-Screen Experience
Apple doc: fullscreen crea un Space separado; ventanas auxiliares pueden convivir con fullscreen
https://developer.apple.com/library/archive/documentation/General/Conceptual/MOSXAppProgrammingGuide/FullScreenApp/FullScreenApp.html

WSC 55-inch outdoor floor standing signage

WSC 55-inch outdoor floor standing signage
Alibaba; 2500 nits; waterproof; direct sun candidate
https://www.alibaba.com/product-introduction/2500-Nits-Outdoor-Floor-Standing-Digital_1600516763688.html

How to Build AI Agents with Redis Memory Management
Mentions consolidation, summarization, extraction, decay, TTL/eviction policies
content_type=text/html; charset=utf-8
https://redis.io/blog/build-smarter-ai-agents-manage-short-term-and-long-term-memory-with-redis/

Getting started with your Storefront | Matchable

Getting started with your Storefront | Matchable
Storefront public profile and client journey details
content_type=text/html; charset=utf-8
https://help.matchable.io/en/articles/38-getting-started-with-your-storefront

Search results: site:matchable.crunch.help OR site:help.matchable.io Matchable onboarding bookin

Question: site:matchable.crunch.help OR site:help.matchable.io Matchable onboarding booking trainer classes courses invoice

Made-in-China outdoor digital signage category

Made-in-China outdoor digital signage category
Category page with 55/65 inch Android/IP55-IP65/high-brightness options including Aiyos
https://www.made-in-china.com/products-search/hot-china-products/Outdoor_Digital_Signage_Display.html

Matchable | Book trainers, venues & classes

Matchable | Book trainers, venues & classes
Consumer homepage: core marketplace proposition and consumer-facing features
content_type=text/html;charset=utf-8
https://www.matchable.io/

Create and manage your course schedule | Matchable

Create and manage your course schedule | Matchable
Course schedule logic and statuses
content_type=text/html; charset=utf-8
https://help.matchable.io/en/articles/44-create-and-manage-your-course-schedule

Getting started with the Matchable Trainer App | Matchable
Trainer mobile app capabilities
content_type=text/html; charset=utf-8
https://help.matchable.io/en/articles/13-getting-started-with-the-matchable-trainer-app

Example source

Example source
verification source
https://example.com/personal-assistants

Summary: verification source


questionnaire

Prompt: No encontré ese id exacto en memory search. Querés que busque el último handoff por contenido/fecha en la API, o tenés otro id/rango para filtrar?


Intent: user selected: 3. Parar

Prompt: No encontré ese id exacto en memory search. Querés que busque el último handoff por contenido/fecha en la API, o tenés otro id/rango para filtrar?


No encontré `mem_7e571be6dcee4a0b8d53c3cf27898c50` en `api/search`.

Lo que sí apareció fue una serie de memorias de Matchable y fuentes sueltas, no un handoff con ese id.

Para seguir, necesito una de estas:
- el texto aproximado del handoff
- otro id
- o que busque el “último handoff” por fecha/contenido en la API y te lo traigo

Si querés, hago esa búsqueda ahora.


contiene "ORDTST01KPPFWZBJD0M1PACY3257J7KH". seguro que ese id no existe?


Ballbox payments validation handoff and open blockers

PAY-2 native MercadoPago flow is merged and deployed. Legacy approved callback was removed. Canonical external_reference format is `payment-vm_{vendingMachineId}-txn_{machineTxnId}`. Valid test POS is `BALLBOXTESTPOS01`. Approved test order `ORDTST01KPPFWZBJD0M1PACY3257J7KH` reached `processed/accredited`. Open issues: Ballbox did not persist `PaymentSession`; production token still cannot access `/users/me` or `/pos` due to `PA_UNAUTHORIZED_RESULT_FROM_POLICIES`; homologator still fails with `error_code 1006`. Next useful steps: inspect logs for `/notification/mercadopago` around the approved order, reproduce another approved order and tail logs live if needed, and resolve POS/prod account API access.

Summary: PAY-2 native MercadoPago flow is merged and deployed. Legacy approved callback was removed. Canonical external_reference format is `payment-vm_{vendingMachineId}-txn_{machineTxnId…

Durable project state with concrete external identifiers, validated payment flow status, and unresolved blockers that matter for future sessions.

Explanation: facets=0.00 lexical=1.00 semantic=0.00 confidence=1.00 freshness=1.00

Ballbox PAY-2 native MercadoPago handoff

PAY-2 native MercadoPago migration is merged/deployed. Legacy approved callback removed. Canonical external_reference is payment-vm_{vendingMachineId}-txn_{machineTxnId}. Verified test POS BALLBOXTESTPOS01 and approved test order ORDTST01KPPFWZBJD0M1PACY3257J7KH reached processed/accredited, but Ballbox still did not persist a PaymentSession and /api/integrations/payments/sessions returned empty. Real prod token APP_USR-3257bf35-b277-4045-92ba-09fe77a1e787 returns PA_UNAUTHORIZED_RESULT_FROM_POLICIES on /users/me and /pos, so real prod POS remains unresolved. Homologator evaluate-order fails with error_code 1006 / Cannot read properties of null (reading 'blocks'). Next: inspect /notification/mercadopago logs around the approved test order and locate/create a prod POS or account with API access.

Summary: PAY-2 native MercadoPago migration is merged/deployed. Legacy approved callback removed. Canonical external_reference is payment-vm_{vendingMachineId}-txn_{machineTxnId}. Verified…

Research step: capture https://help.matchable.io/en/articles/13-getting-started-with-the-matchable-trai

Captured https://help.matchable.io/en/articles/13-getting-started-with-the-matchable-trainer-app

Search results: site:matchable.io Matchable product features use cases

Question: site:matchable.io Matchable product features use cases

Find About Product on eBay - Fantastic Prices on eBay

Sport marketplace and management software | Matchable

Integrate with Matchable

Press kit - matchable.io

Request a product demo | Matchable

Integrations your tools - matchable.io

Matchable is partering up with Matchpoint | NEWS

Introducing Matchable

Integrations | Matchable x MATCHi

Research step: capture https://help.matchable.io/en/articles/44-create-and-manage-your-course-schedule

Captured https://help.matchable.io/en/articles/44-create-and-manage-your-course-schedule

Research step: capture https://www.matchable.io/

Captured https://www.matchable.io/

Summary: Captured https://www.matchable.io/

Research step: capture https://byaiteam.com/blog/2025/12/20/ai-agent-memory-architectures-retrieval-gov

Captured https://byaiteam.com/blog/2025/12/20/ai-agent-memory-architectures-retrieval-governance/

Research step: capture https://help.matchable.io/en/articles/38-getting-started-with-your-storefront

Captured https://help.matchable.io/en/articles/38-getting-started-with-your-storefront

Question: site:matchable.crunch.help OR site:help.matchable.io Matchable onboarding booking trainer classes courses invoice

Search results: periodic memory consolidation agent memory knowledge base deduplication TTL deca

Question: periodic memory consolidation agent memory knowledge base deduplication TTL decay best practices

AI Agent Memory: Architectures, Retrieval, Governance

How to Build AI Agents with Redis Memory Management

How to Build Memory Consolidation - oneuptime.com

Agent Memory in Azure Cosmos DB for NoSQL - learn.microsoft.com

Memory Consolidation Guide - AgentDock Documentation

Research step: capture https://help.matchable.io/en/articles/22-manage-credits

Captured https://help.matchable.io/en/articles/22-manage-credits

Research step: capture https://matchable.io/business/integrations

Captured https://matchable.io/business/integrations

Explanation: facets=0.00 lexical=0.00 semantic=0.98 confidence=0.75 freshness=0.75

Research step: capture https://help.matchable.io/en/articles/17-manage-trainings-and-classes

Captured https://help.matchable.io/en/articles/17-manage-trainings-and-classes

Research step: capture https://help.matchable.io/en/articles/41-getting-started-with-courses

Captured https://help.matchable.io/en/articles/41-getting-started-with-courses

Research step: capture https://business.matchable.io/

Captured https://business.matchable.io/

Summary: Captured https://business.matchable.io/

Research step: capture https://help.matchable.io/en/articles/10-how-to-book-a-training

Captured https://help.matchable.io/en/articles/10-how-to-book-a-training

Research step: capture https://matchable.io/business/pricing

Captured https://matchable.io/business/pricing

Research step: capture https://help.matchable.io/en/articles/23-how-to-use-credits

Captured https://help.matchable.io/en/articles/23-how-to-use-credits

NSWindow.CollectionBehavior.canJoinAllSpaces

NSWindow.CollectionBehavior.canJoinAllSpaces
Apple doc: ventana puede aparecer en todos los spaces
https://developer.apple.com/documentation/appkit/nswindow/collectionbehavior-swift.struct/canjoinallspaces

Explanation: facets=0.00 lexical=0.00 semantic=0.98 confidence=0.75 freshness=0.70

Made-in-China outdoor digital signage category
Category page with 55/65 inch Android/IP55-IP65/high-brightness options including Aiyos
https://www.made-in-china.com/products-search/hot-china-products/Outdoor_Digital_Signage_Display.html


Sí, existe. Yo busqué mal el primer id.

Hallazgo:
- `mem_7e571be6dcee4a0b8d53c3cf27898c50`
- tipo: `project`
- título: **Ballbox payments validation handoff and open blockers**
- contiene el orden `ORDTST01KPPFWZBJD0M1PACY3257J7KH`

Resumen útil del handoff:
- `PAY-2` native MercadoPago flow merged y deployed
- callback legacy removido
- `external_reference` canónico: `payment-vm_{vendingMachineId}-txn_{machineTxnId}`
- POS de test válido: `BALLBOXTESTPOS01`
- orden test aprobada: `ORDTST01KPPFWZBJD0M1PACY3257J7KH` → `processed/accredited`
- pendientes:
  - Ballbox no persistió `PaymentSession`
  - token prod sigue sin acceso a `/users/me` ni `/pos` por `PA_UNAUTHORIZED_RESULT_FROM_POLICIES`
  - homologator falla con `error_code 1006`

Si querés, ahora exploro la API para sacar:
1. el run/session completo de ese handoff, o
2. los logs/claims relacionados con ese `ORDTST...` exacto.


arregla la busqueda por id explicito. creo que usaste `q=`. capaz no hay busqueda por id


bash

python - <<'PY'
import urllib.parse, requests
base='http://100.116.176.16:8091'
for path in ['/api/search','/api/context']:
    try:
        r=requests.get(base+path, timeout=10)
        print(path, r.status_code, r.text[:500])
    except Exception as e:
        print(path, 'ERR', e)
PY


/api/search 400 <!DOCTYPE HTML>

        <meta charset="utf-8">
        <title>Error response</title>

Error response

Error code: 400

Message: Missing query parameter 'q'.

Error code explanation: 400 - Bad request syntax or unsupported method.

/api/context 200 {"profile_facts": [{"id": "mem_507d4c3e4d84409883a7bf1b31c73e2e", "schema_version": "shared-agent-memory-v2", "type": "profile", "subtype": null, "scope": "global", "status": "active", "project_id": null, "repo_id": null, "agent_id": null, "origin_agent": "personal-agent", "run_id": null, "task_id": null, "url": null, "domain": null, "source_kind": "manual", "title": "Sebas prefers HTML as doc entry point", "content": "When generating plans, reports, or docs, use HTML as the entry point. Link on


curl -fsSL 'http://100.116.176.16:8091/api/context' | python - <<'PY'
import sys, json
obj=json.load(sys.stdin)
print(obj.keys())
print('profile_facts', len(obj.get('profile_facts',[])))
print('project_facts', len(obj.get('project_facts',[])))
print('recent_runs', len(obj.get('recent_runs',[])))
PY


curl: (23) Failure writing output to destination, passed 16384 returned 0
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "/usr/lib/python3.13/json/__init__.py", line 293, in load
    return loads(fp.read(),
        cls=cls, object_hook=object_hook,
        parse_float=parse_float, parse_int=parse_int,
        parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
  File "/usr/lib/python3.13/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^
  File "/usr/lib/python3.13/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/json/decoder.py", line 363, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Command exited with code 1


python - <<'PY'
import requests, json
r=requests.get('http://100.116.176.16:8091/api/context', timeout=20)
print(r.status_code)
obj=r.json()
print(obj.keys())
for k in ['profile_facts','project_facts','recent_runs']:
    print(k, len(obj.get(k,[])))
PY


200
dict_keys(['profile_facts', 'project_memories', 'active_decisions', 'recent_episodes', 'task_relevant_artifacts', 'citations', 'agent_id'])
profile_facts 8
project_facts 0
recent_runs 0


Sí: la API de `search` exige `q=`. No parece haber búsqueda por id explícito.

Lo que probé:
- `GET /api/search` sin `q` → `400 Missing query parameter 'q'`
- `GET /api/context` → devuelve contexto, no búsqueda por id

Conclusión:
- `api/search` es por texto, no por id
- para encontrar un memory id, hay que buscar por contenido o usar `/api/context` si ya está en contexto reciente

Ojo: en este caso `q=ORDTST...` sí devolvió el handoff exacto, así que el id existe y el texto también.

Si querés, hago el fix mental/procedimental:
- tratar `q=` como búsqueda full-text
- no asumir lookup por id
- usar `/api/context` para contexto reciente y `search` para recuperar por texto

Si querés, te dejo una mini nota de uso para no repetir este error.


pprefiero que agregues endpoint/param de busqueda por id


todo

Add explicit memory lookup by id in agents-database API

Investigate agents-database API implementation and add explicit endpoint or query param for direct memory id lookup, then verify behavior and update docs if needed.


Created #1: Add explicit memory lookup by id in agents-database API (pending)


Owner-managed repo. Telegraph. Min tokens.

## Memory
- durable prefs/rules/notes to `~/agents-database`
- do not rely on Codex-only local state as source of truth

## Git / Deploy
- default branch: `main`
- this repo auto-deploys to Vercel
- for every change, start from a fresh branch created from up-to-date `origin/main`
- do not block local commits on working branches; commit freely when the work is coherent
- do not push directly to `main` unless the user explicitly asks for it
- protect `main` from direct changes: no direct push and no merge without explicit user confirmation
- treat this as near-future client production

## Workflow
- safe by default: check `git status/diff` first
- prefer end-to-end verify before handoff
- for Playwright/e2e, prefer a stable app-like server path over dev-only watcher flows when either can satisfy the test objective; keep the repo default framed as the most repeatable cross-machine setup, not a workaround for one host
- when a working branch is considered done/ready, open a PR automatically; do not ask the user for permission
- when user asks for handoff to another agent, leave clean repo state first: docs, tests, and code aligned across whole repo; no half-done, incomplete, stale, or misleading leftovers
- keep `PLAN.md` as the source of truth for current state and next implementation steps
- every meaningful change must have its plan and progress documented in repo so work is visible and resumable
- use `docs/next-steps-queue.md` to pick the next task by priority/dependency (`ready` first)
- when implementation changes ship, update `PLAN.md` in the same work
- seeded demo/e2e fixtures used by operator flows should stay valid across normal near-future local runs; avoid near-term hard-coded expiry windows unless the task explicitly tests expiry behavior
- subsystem docs under `docs/` must describe only what is already implemented; no future promises there
- whenever code changes materially affect behavior or architecture, update both `PLAN.md` and the relevant implemented docs in the same work
- push working-branch progress frequently to improve team communication and resumability, but keep commits coherent
- before proposing or building a demo-oriented feature, read `docs/demo-iteration-mode.md` and follow its L1/L2/L3 delivery model and anti-debt guardrails
- use `docs/index.md` as the docs entrypoint to find current discovery/implementation context before deciding scope
- use `docs/repo-map.md` to find the right app/lib/components entrypoints before editing
- for sports/classes demo wedges, check `docs/coaches-solution-thesis.md` and `docs/classes-subroute-plan.md` first, then keep docs aligned with shipped behavior

## Founder Role
- founder is available here mainly to unblock business-logic and product-decision questions
- default: keep moving without asking unless one of those decisions materially changes direction
- when blocked on product/business choice, ask here concisely with the current recommendation first


# Docs Index

## How To Navigate
- `PLAN.md`: current repo execution state and next implementation steps
- `docs/next-steps-queue.md`: prioritized backlog with dependency states
- `docs/repo-map.md`: fast file-level entrypoint for each domain
- `docs/implemented-*.md`: shipped behavior only
- `docs/*-rfc.md`: proposed implementation slices
- `docs/*-plan.md`: route/product planning
- `docs/*-discovery*.md`: validation and research framing
- `docs/*-gap-analysis.md`: what Ballbox still needs to build
- `docs/maintainability-priorities-2026-03.md`: prioritized frontend/backend maintainability work for the next refactor cycle
- `docs/demo-iteration-mode.md`: rapid demo delivery model with anti-debt guardrails

## Operator Priority Path
1. `PLAN.md`
2. `docs/next-steps-queue.md`
3. `docs/repo-map.md`
4. `docs/demo-iteration-mode.md`
5. relevant `docs/implemented-*.md` for the touched domain

## Current Refactor Input
- `docs/maintainability-priorities-2026-03.md`

## Execution And Coordination
- `docs/next-steps-queue.md`
- `docs/repo-map.md`
- `docs/demo-iteration-mode.md`

## Implemented State
- `docs/implemented-host-app.md`
- `docs/implemented-network.md`
- `docs/implemented-payments.md`
- `docs/implemented-players.md`
- `docs/implemented-sports.md`
- `docs/implemented-tvs.md`

## Discovery And Strategy
- `docs/coaches-discovery-stage.md`
- `docs/coaches-form-current.md`
- `docs/atc-integration-discovery.md`
- `docs/coaches-opportunity-brief.md`
- `docs/coaches-research-repository.md`
- `docs/coaches-validation-checklist-2-weeks.md`
- `docs/coaches-solution-thesis.md`

## Current Build Docs
- `docs/classes-subroute-plan.md`
- `docs/classes-mvp-rfc.md`
- `docs/coaches-coordination-gap-analysis.md`
- `docs/atc-mock-integration-plan.md`


# Repo Map

## Purpose
- Fast entrypoint for agents and maintainers to find the right files before editing.

## Top-Level Map
- `app/`: Next.js routes and API handlers.
- `components/`: UI clients and route-level presentation pieces.
- `lib/`: backend/domain modules, integration helpers, contracts, shared runtime helpers.
- `prisma/`: schema and seed sources.
- `tests/`: `contracts`, `unit`, `integration`, `e2e`, and support helpers.
- `docs/`: implemented state, discovery context, plans, and execution queue.

## Product Domains

### TVs
- UI surfaces: `app/admin/tvs`, `app/admin/screens`, `app/player/[screenId]/route.ts`
- APIs: `app/api/tvs/*`
- Backend: `lib/tvs-backend-*.ts`, `lib/tvs-player-runtime-core.ts`, `lib/tvs-runtime.ts`
- Implemented doc: `docs/implemented-tvs.md`

### Payments
- UI surface: `app/admin/payments`
- APIs: `app/api/admin/payments/*`, `app/api/integrations/payments/*`
- Backend: `lib/payments.ts`, `lib/payments-auth.ts`, `lib/shared-secret.ts`, `lib/integrations.ts`
- Implemented doc: `docs/implemented-payments.md`

### Network
- UI surface: `app/admin/network`
- APIs: `app/api/network*`
- Backend: `lib/network-backend-*.ts`
- Implemented doc: `docs/implemented-network.md`

### Sports / Classes / Coaches
- UI surfaces: `app/admin/sports`, `app/classes/*`, `app/classes-by-date`, `app/coaches/settings`
- APIs: `app/api/sports`, `app/api/classes/*`, `app/api/coaches/*`
- Backend: `lib/sports-backend-*.ts`, `lib/classes-backend.ts`, `lib/classes.ts`, `lib/coaches-backend.ts`
- Implemented doc: `docs/implemented-sports.md`

### Players
- UI surface: `app/admin/players`
- APIs: `app/api/players/*`
- Backend: `lib/players-backend.ts`
- Implemented doc: `docs/implemented-players.md`

## Shared Entry Docs
- `PLAN.md`: current state + active next-step queue pointer.
- `docs/index.md`: docs navigation index.
- `docs/demo-iteration-mode.md`: L1/L2/L3 demo delivery model.
- `docs/next-steps-queue.md`: prioritized backlog with dependency states.

## Shared HTTP Helpers
- Route JSON/envelope helpers: `lib/http/json.ts`, `lib/http/network-route-helpers.ts`, `lib/http/payments-route-helpers.ts`, `lib/http/players-route-helpers.ts`, `lib/http/sports-route-helpers.ts`


bash

git status --short && git branch --show-current && git remote -v && rg -n "api/search|api/context|memory" -S . --glob '!node_modules' --glob '! .next'


 M .gitignore
 D proxy.ts
pay2-native-payments-main
origin	https://github.com/sebasfavaron/ballbox.git (fetch)
origin	https://github.com/sebasfavaron/ballbox.git (push)
./AGENTS.md:3:## Memory
./PLAN.md:233:- Do not invest further in memory-backed or file-backed persistence for TVs.
./PLAN.md:275:- Bridge-era in-memory TVs modules were removed; seed fixtures now live under `prisma/seed-data.ts`.
./PLAN.md:283:- TVs persistence cleanup is complete; do not reintroduce in-memory compatibility layers.
./docs/coaches-jtbd-workflow-map.md:56:- Workaround 2: calendar/sheet tracking plus memory/manual notes
./docs/implemented-tvs.md:7:- No Streetcast compatibility layer or in-memory TVs bridge remains in runtime code.
./docs/ballbox-db-plan.md:5:- No more memory-backed or file-backed persistence work for TVs.


find . -maxdepth 3 \( -path './node_modules' -o -path './.next' -o -path './.git' \) -prune -o -type f | sed 's#^./##' | sort | rg 'api|server|route|memory|database|agents'


components/route-feedback.tsx
docs/classes-subroute-plan.md
lib/database-errors.ts
lib/http/network-route-helpers.ts
lib/http/payments-route-helpers.ts
lib/http/players-route-helpers.ts
lib/http/sports-route-helpers.ts


Sí, te sigo. Pero este repo no parece ser `agents-database`: acá no veo su código, solo el uso desde afuera.

Hallazgo:
- `api/search` exige `q=`
- `api/context` existe
- este repo `ballbox` no contiene el backend de `agents-database`

Para agregar lookup por id hay dos caminos:
1. modificar `agents-database` en su repo real
2. si solo querés consumo desde acá, agregar un helper cliente que haga lookup por id vía endpoint nuevo cuando exista

Necesito una sola confirmación:
- ¿me das el repo/carpeta de `agents-database` para tocarlo?
- o querés que lo busque en la máquina y lo ubique yo?

Ahora mismo no voy a inventar archivos que no están en este repo.


anda a el entonces...


find /mnt/rpi/repos -maxdepth 2 -type d \( -name 'agents-database' -o -name '*agents*database*' -o -name 'agents-database-*' \) | sort


(no output)


find /mnt/rpi -maxdepth 3 -type d \( -name '.git' -o -name 'api' \) 2>/dev/null | sed 's#/.git##' | sort | head -200


/mnt/rpi/agents-database
/mnt/rpi/cast_tools/vidsrc-resolver
/mnt/rpi/cast_tools/vidsrc-scraper
/mnt/rpi/pi-config
/mnt/rpi/repos/ballbox
/mnt/rpi/repos/ballbox-company-agent


git -C /mnt/rpi/agents-database status --short && git -C /mnt/rpi/agents-database branch --show-current && git -C /mnt/rpi/agents-database remote -v


ENOENT: no such file or directory, access '/mnt/rpi/agents-database/AGENTS.md'


 M .gitignore
 M data/shared-agent-memory.sqlite3
 M docs/memory-consolidation-rfc.md
 M docs/system-v1.md
 M scripts/run-maintenance-daemon.sh
 M src/shared_agent_memory/cli.py
 M src/shared_agent_memory/service.py
 M tests/test_memory_service.py
?? data/shared-agent-memory.pre-repair-20260419-185338.sqlite3
?? data/shared-agent-memory.rebuild-20260419.sqlite3
?? data/shared-agent-memory.recovered-20260419-184949.sqlite3
?? data/shared-agent-memory.recovery-dump.sql
?? data/shared-agent-memory.recovery-work.sqlite3
?? data/shared-agent-memory.sqlite3.bak-20260419-184938
?? docs/http-api.md
?? scripts/run-http-api.sh
?? src/shared_agent_memory/http_api.py
?? systemd/
main
origin	https://github.com/sebasfavaron/agents-database.git (fetch)
origin	https://github.com/sebasfavaron/agents-database.git (push)


ENOENT: no such file or directory, access '/mnt/rpi/agents-database/README.md'


# Agents Database HTTP API

Remote-first topology:

- server `personal-agent`: task runner and dashboard API on `:8082`
- server `agents-database`: shared-memory and operational-data API on `:8091`
- local laptop: HTTP client only; no local shared DB required for normal operation

## Endpoints

- `GET /api/status`
- `GET /api/search?q=<query>&scope=global&scope=project&limit=10`
- `GET /api/context?project=<id>&repo=<id>&task=<id>`
- `GET /api/memories?...filters...`
- `GET /api/memories/<id>`
- `POST /api/ingest`
- `GET /api/tasks?...filters...`
- `POST /api/tasks`
- `PATCH /api/tasks/<id>`
- `GET /api/tasks/<id>`
- `GET /api/tasks/<id>/bundle`
- `GET /api/task-runs?...filters...`
- `GET /api/task-runs/<id>`
- `POST /api/task-runs`
- `POST /api/task-runs/<id>/finish`
- `GET /api/artifacts?task_id=<id>`
- `GET /api/artifacts/<id>`
- `POST /api/artifacts`

## Run locally

```bash
python3 -m shared_agent_memory.cli \
  --db data/shared-agent-memory.sqlite3 \
  serve-http \
  --host 0.0.0.0 \
  --port 8091
```

## Server service

- script: `scripts/run-http-api.sh`
- systemd user unit: `systemd/user/agents-database-http.service`

## Gotchas

- This service is the correct remote boundary for shared memory. Avoid routing memory CRUD/search through `personal-agent` unless the runtime actually owns that behavior.
- The existing `SERVICES-ARCHITECTURE.md` on the server may be stale relative to live routing. Verify real HTTP reachability with `curl`.


from __future__ import annotations

import json
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any
from urllib.parse import parse_qs, urlparse

from .service import MemoryService

def _parse_bool(value: str | None, *, default: bool = False) -> bool:
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}

def _first(query: dict[str, list[str]], key: str) -> str | None:
    values = query.get(key)
    if not values:
        return None
    value = values[0].strip()
    return value or None

def _first_int(query: dict[str, list[str]], key: str, default: int) -> int:
    value = _first(query, key)
    if value is None:
        return default
    return int(value)

def _metadata_filters(query: dict[str, list[str]]) -> dict[str, Any] | None:
    metadata: dict[str, Any] = {}
    for key, values in query.items():
        if not key.startswith("metadata."):
            continue
        name = key.removeprefix("metadata.").strip()
        if not name:
            continue
        value = values[0].strip()
        if value:
            metadata[name] = value
    return metadata or None

def build_handler(service: MemoryService):
    class MemoryAPIHandler(BaseHTTPRequestHandler):
        def do_OPTIONS(self) -> None:  # noqa: N802
            self.send_response(HTTPStatus.NO_CONTENT)
            self._send_common_headers(content_type="application/json; charset=utf-8", content_length=0)
            self.end_headers()

        def do_GET(self) -> None:  # noqa: N802
            parsed = urlparse(self.path)
            query = parse_qs(parsed.query)

            try:
                if parsed.path == "/api/status":
                    payload = {
                        "ok": True,
                        "db_path": service.store.db_path,
                        "dashboard": service.dashboard_snapshot(),
                    }
                    try:
                        payload["audit"] = service.audit_v2()
                    except Exception as exc:
                        payload["audit_error"] = str(exc)
                    self._send_json(payload)
                    return
                if parsed.path == "/api/search":
                    search_query = _first(query, "q")
                    if search_query is None:
                        self.send_error(HTTPStatus.BAD_REQUEST, "Missing query parameter 'q'")
                        return
                    scopes = [scope.strip() for scope in query.get("scope", []) if scope.strip()]
                    filters = {
                        key: value
                        for key, value in {
                            "type": _first(query, "type"),
                            "subtype": _first(query, "subtype"),
                            "project_id": _first(query, "project_id"),
                            "repo_id": _first(query, "repo_id"),
                            "source_ref": _first(query, "source_ref"),
                            "evidence_ref": _first(query, "evidence_ref"),
                            "run_id": _first(query, "run_id"),
                            "task_id": _first(query, "task_id"),
                            "origin_agent": _first(query, "origin_agent"),
                            "url": _first(query, "url"),
                            "domain": _first(query, "domain"),
                        }.items()
                        if value is not None
                    }
                    self._send_json(
                        service.search(
                            search_query,
                            scopes or None,
                            filters=filters or None,
                            limit=_first_int(query, "limit", 10),
                            include_inbox=_parse_bool(_first(query, "include_inbox")),
                        )
                    )
                    return
                if parsed.path == "/api/context":
                    self._send_json(
                        service.context_for(
                            project=_first(query, "project"),
                            repo=_first(query, "repo"),
                            agent=_first(query, "agent"),
                            task=_first(query, "task"),
                        )
                    )
                    return
                if parsed.path == "/api/memories":
                    self._send_json(
                        {
                            "items": service.list_memories(
                                status=_first(query, "status") if "status" in query else "active",
                                scope=_first(query, "scope"),
                                memory_type=_first(query, "type"),
                                subtype=_first(query, "subtype"),
                                project_id=_first(query, "project_id"),
                                repo_id=_first(query, "repo_id"),
                                source_ref=_first(query, "source_ref"),
                                evidence_ref=_first(query, "evidence_ref"),
                                run_id=_first(query, "run_id"),
                                task_id=_first(query, "task_id"),
                                origin_agent=_first(query, "origin_agent"),
                                url=_first(query, "url"),
                                domain=_first(query, "domain"),
                                metadata=_metadata_filters(query),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/memories/"):
                    self._send_json(service.get_memory(parsed.path.split("/")[3]))
                    return
                if parsed.path == "/api/tasks":
                    requires_human_input = None
                    if "requires_human_input" in query:
                        requires_human_input = _parse_bool(_first(query, "requires_human_input"))
                    self._send_json(
                        {
                            "items": service.list_tasks(
                                status=_first(query, "status"),
                                owner_agent=_first(query, "owner_agent"),
                                requires_human_input=requires_human_input,
                                run_id=_first(query, "run_id"),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/tasks/") and parsed.path.endswith("/bundle"):
                    self._send_json(service.task_bundle(parsed.path.split("/")[3]))
                    return
                if parsed.path.startswith("/api/tasks/"):
                    self._send_json(service.get_task(parsed.path.split("/")[3]))
                    return
                if parsed.path == "/api/task-runs":
                    self._send_json(
                        {
                            "items": service.list_task_runs(
                                task_id=_first(query, "task_id"),
                                status=_first(query, "status"),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/task-runs/"):
                    self._send_json(service.get_task_run(parsed.path.split("/")[3]))
                    return
                if parsed.path == "/api/artifacts":
                    self._send_json(
                        {
                            "items": service.list_artifacts(
                                task_id=_first(query, "task_id"),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/artifacts/"):
                    self._send_json(service.get_artifact(parsed.path.split("/")[3]))
                    return
            except KeyError as exc:
                self.send_error(HTTPStatus.NOT_FOUND, str(exc))
                return
            except ValueError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, str(exc))
                return
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
                return

            self.send_error(HTTPStatus.NOT_FOUND)

        def do_POST(self) -> None:  # noqa: N802
            parsed = urlparse(self.path)
            try:
                payload = self._json_body()
            except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
                self.send_error(HTTPStatus.BAD_REQUEST, "Invalid JSON body")
                return

            try:
                if parsed.path == "/api/ingest":
                    self._send_json(service.ingest(payload), status=HTTPStatus.CREATED)
                    return
                if parsed.path == "/api/tasks":
                    self._send_json(
                        service.create_task(
                            title=payload["title"],
                            intent=payload["intent"],
                            kind=payload.get("kind", "task"),
                            status=payload.get("status", "open"),
                            priority=payload.get("priority", 3),
                            project_id=payload.get("project_id"),
                            repo_id=payload.get("repo_id"),
                            parent_task_id=payload.get("parent_task_id"),
                            origin=payload.get("origin"),
                            owner_agent=payload.get("owner_agent"),
                            blocked_reason=payload.get("blocked_reason"),
                            requires_human_input=bool(payload.get("requires_human_input", False)),
                            due_at=payload.get("due_at"),
                            metadata=payload.get("metadata"),
                            task_id=payload.get("task_id"),
                            run_id=payload.get("run_id"),
                        ),
                        status=HTTPStatus.CREATED,
                    )
                    return
                if parsed.path == "/api/task-runs":
                    task_id = payload.get("task_id")
                    agent_id = payload.get("agent_id")
                    if not isinstance(task_id, str) or not task_id.strip():
                        self.send_error(HTTPStatus.BAD_REQUEST, "Missing or invalid 'task_id'")
                        return
                    if not isinstance(agent_id, str) or not agent_id.strip():
                        self.send_error(HTTPStatus.BAD_REQUEST, "Missing or invalid 'agent_id'")
                        return
                    self._send_json(
                        service.start_task_run(
                            task_id,
                            agent_id,
                            input_payload=payload.get("input_payload"),
                            metadata=payload.get("metadata"),
                        ),
                        status=HTTPStatus.CREATED,
                    )
                    return
                if parsed.path.startswith("/api/task-runs/") and parsed.path.endswith("/finish"):
                    run_id = parsed.path.split("/")[3]
                    status = payload.get("status")
                    if not isinstance(status, str) or not status.strip():
                        self.send_error(HTTPStatus.BAD_REQUEST, "Missing or invalid 'status'")
                        return
                    self._send_json(
                        service.finish_task_run(
                            run_id,
                            status=status,
                            result_summary=payload.get("result_summary"),
                            error_message=payload.get("error_message"),
                        )
                    )
                    return
                if parsed.path == "/api/artifacts":
                    task_id = payload.get("task_id")
                    artifact_type = payload.get("artifact_type")
                    title = payload.get("title")
                    content = payload.get("content")
                    if not all(isinstance(value, str) and value.strip() for value in (task_id, artifact_type, title, content)):
                        self.send_error(
                            HTTPStatus.BAD_REQUEST,
                            "Missing or invalid required artifact fields",
                        )
                        return
                    self._send_json(
                        service.create_artifact(
                            task_id=task_id,
                            artifact_type=artifact_type,
                            title=title,
                            content=content,
                            metadata=payload.get("metadata"),
                            source_ref=payload.get("source_ref"),
                        ),
                        status=HTTPStatus.CREATED,
                    )
                    return
            except KeyError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, f"Missing field: {exc}")
                return
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
                return

            self.send_error(HTTPStatus.NOT_FOUND)

        def do_PATCH(self) -> None:  # noqa: N802
            parsed = urlparse(self.path)
            if not parsed.path.startswith("/api/tasks/"):
                self.send_error(HTTPStatus.NOT_FOUND)
                return
            try:
                payload = self._json_body()
            except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
                self.send_error(HTTPStatus.BAD_REQUEST, "Invalid JSON body")
                return
            try:
                self._send_json(
                    service.update_task(
                        parsed.path.split("/")[3],
                        status=payload.get("status"),
                        owner_agent=payload.get("owner_agent"),
                        blocked_reason=payload.get("blocked_reason"),
                        requires_human_input=payload.get("requires_human_input"),
                        metadata=payload.get("metadata"),
                        run_id=payload.get("run_id"),
                    )
                )
            except KeyError as exc:
                self.send_error(HTTPStatus.NOT_FOUND, str(exc))
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))

        def log_message(self, fmt: str, *args: Any) -> None:
            return

        def _json_body(self) -> dict[str, Any]:
            length = int(self.headers.get("Content-Length", "0"))
            raw = self.rfile.read(length) if length else b"{}"
            payload = json.loads(raw.decode("utf-8"))
            if not isinstance(payload, dict):
                raise ValueError("JSON body must be an object")
            return payload

        def _send_common_headers(self, *, content_type: str, content_length: int) -> None:
            self.send_header("Content-Type", content_type)
            self.send_header("Content-Length", str(content_length))
            self.send_header("Access-Control-Allow-Origin", "*")
            self.send_header("Access-Control-Allow-Headers", "Content-Type")
            self.send_header("Access-Control-Allow-Methods", "GET,POST,PATCH,OPTIONS")

        def _send_json(self, payload: dict[str, Any], *, status: HTTPStatus = HTTPStatus.OK) -> None:
            body = json.dumps(payload, ensure_ascii=True).encode("utf-8")
            self.send_response(status)
            self._send_common_headers(content_type="application/json; charset=utf-8", content_length=len(body))
            self.end_headers()
            self.wfile.write(body)

    return MemoryAPIHandler

def run_http_server(*, db_path: str, host: str = "127.0.0.1", port: int = 8091) -> None:
    service = MemoryService(db_path)
    server = ThreadingHTTPServer((host, port), build_handler(service))
    try:
        server.serve_forever()
    finally:
        server.server_close()


from __future__ import annotations

import json
import math
import subprocess
import uuid
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import Any
from urllib.parse import urlparse

from .schema import SCHEMA_VERSION
from .store import SQLiteStore

def utcnow() -> str:
    return datetime.now(UTC).isoformat()

def make_id(prefix: str) -> str:
    return f"{prefix}_{uuid.uuid4().hex}"

def normalize_text(value: str) -> str:
    return " ".join(value.lower().split())

def _line_value(content: str, label: str) -> str:
    prefix = f"{label}:"
    for line in content.splitlines():
        if line.startswith(prefix):
            return line[len(prefix) :].strip()
    return ""

@dataclass(slots=True)
class SearchResult:
    memory: dict[str, Any]
    score: float
    explanation: str
    facet_score: float
    lexical_score: float
    semantic_score: float

    def as_dict(self) -> dict[str, Any]:
        return {
            "memory": self.memory,
            "score": self.score,
            "score_breakdown": {
                "facet": self.facet_score,
                "lexical": self.lexical_score,
                "semantic": self.semantic_score,
                "confidence": self.memory["confidence"],
                "freshness": self.memory["freshness"],
            },
            "matched_scope": self.memory["scope"],
            "evidence_ref": self.memory["evidence_ref"],
            "explanation": self.explanation,
        }

class MemoryService:
    def __init__(self, db_path: str) -> None:
        self.store = SQLiteStore(db_path)
        self._maintenance_check_running = False
        self.store.initialize()
        self.migrate_v2()
        self._ensure_default_jobs()
        self._run_opportunistic_maintenance(limit=10)

    def create_memory(self, memory_input: dict[str, Any]) -> dict[str, Any]:
        now = utcnow()
        metadata = dict(memory_input.get("metadata") or {})
        subtype = memory_input.get("subtype")
        run_id = memory_input.get("run_id")
        task_id = memory_input.get("task_id")
        url = memory_input.get("url")
        domain = memory_input.get("domain") or (urlparse(url).netloc if url else None)
        origin_agent = memory_input.get("origin_agent") or metadata.get("origin_agent") or memory_input.get("agent_id")
        record = {
            "id": memory_input.get("id", make_id("mem")),
            "schema_version": memory_input.get("schema_version", SCHEMA_VERSION),
            "type": memory_input["type"],
            "subtype": subtype,
            "scope": memory_input["scope"],
            "status": memory_input.get("status", "active"),
            "project_id": memory_input.get("project_id"),
            "repo_id": memory_input.get("repo_id"),
            "agent_id": memory_input.get("agent_id"),
            "origin_agent": origin_agent,
            "run_id": run_id,
            "task_id": task_id,
            "url": url,
            "domain": domain,
            "source_kind": memory_input.get("source_kind", "manual"),
            "title": memory_input["title"],
            "content": memory_input["content"],
            "summary": memory_input.get("summary") or self._make_summary(memory_input["content"]),
            "confidence": float(memory_input.get("confidence", 1.0)),
            "freshness": float(memory_input.get("freshness", 1.0)),
            "created_at": memory_input.get("created_at", now),
            "updated_at": now,
            "observed_at": memory_input.get("observed_at", now),
            "source_ref": memory_input.get("source_ref"),
            "evidence_ref": memory_input.get("evidence_ref"),
            "embedding_json": self._encode_embedding(memory_input.get("embedding")),
            "metadata_json": self.store.dumps(metadata),
        }
        with self.store.connection() as conn:
            conn.execute(
                """
                INSERT OR REPLACE INTO memories (
                    id, schema_version, type, subtype, scope, status, project_id, repo_id, agent_id, origin_agent,
                    run_id, task_id, url, domain, source_kind, title, content, summary, confidence, freshness,
                    created_at, updated_at, observed_at, source_ref, evidence_ref, embedding_json, metadata_json
                ) VALUES (
                    :id, :schema_version, :type, :subtype, :scope, :status, :project_id, :repo_id, :agent_id, :origin_agent,
                    :run_id, :task_id, :url, :domain, :source_kind, :title, :content, :summary, :confidence, :freshness,
                    :created_at, :updated_at, :observed_at, :source_ref, :evidence_ref, :embedding_json, :metadata_json
                )
                """,
                record,
            )
            self._upsert_fts(conn, record)
        self._run_opportunistic_maintenance(limit=1)
        return self.get_memory(record["id"])

    def ingest(self, memory_input: dict[str, Any]) -> dict[str, Any]:
        return self.create_memory(memory_input)

    def capture_session(self, source_ref: str, scope: str, content: str, **metadata: Any) -> dict[str, Any]:
        return self._capture_source(
            trigger_type="session_close",
            source_kind="run",
            source_ref=source_ref,
            scope=scope,
            content=content,
            metadata=metadata,
        )

    def capture_conversation(
        self, source_ref: str, scope: str, content: str, capture_mode: str = "manual_import", **metadata: Any
    ) -> dict[str, Any]:
        metadata = {"capture_mode": capture_mode, **metadata}
        return self._capture_source(
            trigger_type="conversation_capture",
            source_kind="conversation",
            source_ref=source_ref,
            scope=scope,
            content=content,
            metadata=metadata,
        )

    def search(
        self,
        query: str,
        scopes: list[str] | None = None,
        filters: dict[str, Any] | None = None,
        limit: int = 10,
        include_inbox: bool = False,
    ) -> dict[str, Any]:
        filters = filters or {}
        query_terms = [term for term in normalize_text(query).split() if term]
        sql = ["SELECT * FROM memories WHERE 1=1"]
        params: list[Any] = []

        if scopes:
            sql.append(f"AND scope IN ({','.join('?' for _ in scopes)})")
            params.extend(scopes)
        if not include_inbox:
            sql.append("AND status = ?")
            params.append("active")
        params.extend(self._memory_filter_sql(sql, filters))

        with self.store.connection() as conn:
            rows = [self._row_to_memory(row) for row in conn.execute(" ".join(sql), params)]

        results: list[SearchResult] = []
        query_embedding = self._text_embedding(query) if query_terms else []
        for row in rows:
            haystack = normalize_text(" ".join([row["title"], row["summary"], row["content"]]))
            lexical = self._lexical_score(query_terms, haystack)
            semantic = self._semantic_score(query_embedding, row.get("embedding")) if query_terms else 0.0
            facet = self._facet_score(row, filters)
            if query_terms and lexical == 0 and semantic == 0 and facet == 0:
                continue
            score = facet * 0.45 + lexical * 0.3 + semantic * 0.15 + row["confidence"] * 0.06 + row["freshness"] * 0.04
            explanation = (
                f"facets={facet:.2f} lexical={lexical:.2f} semantic={semantic:.2f} "
                f"confidence={row['confidence']:.2f} freshness={row['freshness']:.2f}"
            )
            results.append(SearchResult(row, score, explanation, facet, lexical, semantic))

        ordered = sorted(
            results,
            key=lambda item: (
                item.facet_score,
                item.lexical_score,
                item.semantic_score,
                item.memory["confidence"],
                item.memory["freshness"],
                item.memory["updated_at"],
            ),
            reverse=True,
        )[:limit]
        payload = [item.as_dict() for item in ordered]

        retrieval_id = make_id("ret")
        with self.store.connection() as conn:
            conn.execute(
                "INSERT INTO retrieval_logs (id, query_text, filters_json, results_json, created_at) VALUES (?, ?, ?, ?, ?)",
                (
                    retrieval_id,
                    query,
                    self.store.dumps({"scopes": scopes, "filters": filters, "include_inbox": include_inbox}),
                    self.store.dumps(payload),
                    utcnow(),
                ),
            )
        return {"retrieval_id": retrieval_id, "results": payload}

    def context_for(
        self, project: str | None = None, repo: str | None = None, agent: str | None = None, task: str | None = None
    ) -> dict[str, Any]:
        profile_facts = self.list_memories(status="active", scope="global", memory_type="profile", limit=8)
        project_filters = {"project_id": project, "repo_id": repo}
        active_decisions = self.list_memories(
            status="active",
            scope="project" if project else None,
            memory_type="decision",
            project_id=project,
            repo_id=repo,
            limit=8,
        )
        project_memories = self.list_memories(
            status="active",
            project_id=project,
            repo_id=repo,
            limit=12,
        )
        project_memories = [item for item in project_memories if item["type"] in {"project", "decision", "artifact", "episode"}][:12]
        recent_episodes = self.list_memories(
            status="active",
            memory_type="episode",
            project_id=project,
            repo_id=repo,
            limit=10,
        )
        task_relevant_artifacts = self.list_memories(
            status="active",
            memory_type="artifact",
            project_id=project,
            repo_id=repo,
            task_id=task,
            limit=10,
        )
        if task and not task_relevant_artifacts:
            task_relevant_artifacts = self.search(task, filters={**project_filters, "type": "artifact"}, limit=5)["results"]
        citations = [
            item["id"]
            for item in [*profile_facts, *project_memories, *active_decisions, *recent_episodes]
            if isinstance(item, dict) and "id" in item
        ]
        return {
            "profile_facts": profile_facts,
            "project_memories": project_memories,
            "active_decisions": active_decisions,
            "recent_episodes": recent_episodes,
            "task_relevant_artifacts": task_relevant_artifacts,
            "citations": citations,
            "agent_id": agent,
        }

    def consolidate(self, inbox_items: list[str]) -> dict[str, Any]:
        promoted: list[str] = []
        linked: list[dict[str, str]] = []
        with self.store.connection() as conn:
            for memory_id in inbox_items:
                row = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,)).fetchone()
                if row is None:
                    continue
                memory = self._row_to_memory(row)
                duplicate = conn.execute(
                    """
                    SELECT id FROM memories
                    WHERE id != ?
                      AND status = 'active'
                      AND type = ?
                      AND scope = ?
                      AND content = ?
                    LIMIT 1
                    """,
                    (memory_id, memory["type"], memory["scope"], memory["content"]),
                ).fetchone()
                if duplicate:
                    link_id = make_id("lnk")
                    conn.execute(
                        """
                        INSERT INTO memory_links (id, from_memory_id, to_memory_id, relation, created_at, metadata_json)
                        VALUES (?, ?, ?, ?, ?, ?)
                        """,
                        (
                            link_id,
                            memory_id,
                            duplicate["id"],
                            "related_to",
                            utcnow(),
                            self.store.dumps({"reason": "exact_duplicate"}),
                        ),
                    )
                    conn.execute("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", ("archived", utcnow(), memory_id))
                    linked.append({"from": memory_id, "to": duplicate["id"], "relation": "related_to"})
                    continue
                conn.execute("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", ("active", utcnow(), memory_id))
                promoted.append(memory_id)
        return {"promoted": promoted, "linked": linked}

    def consolidate_candidates(self, *, config: dict[str, Any]) -> dict[str, Any]:
        stats = {
            "candidates": 0,
            "promoted": 0,
            "archived": 0,
            "linked": 0,
            "conflicts": 0,
            "llm_summaries": 0,
        }
        age_days = int(config.get("candidate_age_days", 7))
        max_candidates = int(config.get("max_candidates", 200))
        high_threshold = float(config.get("dedupe_high_threshold", 0.9))
        mid_threshold = float(config.get("dedupe_mid_threshold", 0.75))
        promote_threshold = float(config.get("promote_confidence", 0.7))
        conflict_threshold = float(config.get("conflict_threshold", 0.55))
        llm_enabled = bool(config.get("llm_enabled", False))
        llm_provider = config.get("llm_provider") or "opencode"
        llm_command = config.get("llm_command") or ["opencode", "-m", "MiniMax M2.5 Free"]
        cutoff = (datetime.now(UTC) - timedelta(days=age_days)).isoformat()

        with self.store.connection() as conn:
            rows = conn.execute(
                """
                SELECT * FROM memories
                WHERE (
                    status = 'inbox'
                    OR (type = 'episode' AND status = 'active')
                )
                  AND created_at <= ?
                ORDER BY created_at ASC
                LIMIT ?
                """,
                (cutoff, max_candidates),
            ).fetchall()

            candidates = [self._row_to_memory(row) for row in rows]
            stats["candidates"] = len(candidates)

            for candidate in candidates:
                related = conn.execute(
                    """
                    SELECT * FROM memories
                    WHERE status = 'active'
                      AND id != ?
                      AND type = ?
                      AND scope = ?
                    ORDER BY updated_at DESC
                    LIMIT 50
                    """,
                    (candidate["id"], candidate["type"], candidate["scope"]),
                ).fetchall()
                related_memories = [self._row_to_memory(row) for row in related]

                best_match = None
                best_score = 0.0
                for other in related_memories:
                    lexical = self._lexical_score(
                        normalize_text(candidate["content"]).split(),
                        normalize_text(other["content"]),
                    )
                    semantic = self._semantic_score(candidate.get("embedding") or [], other.get("embedding"))
                    score = lexical if semantic == 0.0 else (lexical * 0.6 + semantic * 0.4)
                    if score > best_score:
                        best_score = score
                        best_match = other

                if best_match and candidate["content"] == best_match["content"]:
                    self._link_memories(conn, candidate["id"], best_match["id"], "related_to", "exact_duplicate")
                    conn.execute(
                        "UPDATE memories SET status = ?, updated_at = ? WHERE id = ?",
                        ("archived", utcnow(), candidate["id"]),
                    )
                    stats["archived"] += 1
                    stats["linked"] += 1
                    continue

                if best_match and best_score >= high_threshold:
                    self._link_memories(conn, candidate["id"], best_match["id"], "related_to", "high_similarity")
                    conn.execute(
                        "UPDATE memories SET status = ?, updated_at = ? WHERE id = ?",
                        ("archived", utcnow(), candidate["id"]),
                    )
                    stats["archived"] += 1
                    stats["linked"] += 1
                    continue

                if best_match and best_score >= mid_threshold:
                    if llm_enabled:
                        synthesized = self._llm_summarize_pair(
                            candidate,
                            best_match,
                            provider=llm_provider,
                            command=llm_command,
                        )
                        if synthesized:
                            conn.execute(
                                "UPDATE memories SET summary = ?, updated_at = ?, metadata_json = ? WHERE id = ?",
                                (
                                    synthesized,
                                    utcnow(),
                                    self.store.dumps(
                                        {
                                            **candidate.get("metadata", {}),
                                            "synthesis_sources": [candidate["id"], best_match["id"]],
                                            "synthesis_provider": llm_provider,
                                            "synthesis_command": llm_command,
                                        }
                                    ),
                                    candidate["id"],
                                ),
                            )
                            stats["llm_summaries"] += 1
                    self._link_memories(conn, candidate["id"], best_match["id"], "related_to", "mid_similarity")
                    stats["linked"] += 1

                if best_match and best_score >= conflict_threshold and self._conflict_signal(candidate, best_match):
                    self._record_conflict(conn, candidate["id"], best_match["id"], "potential_contradiction")
                    self._link_memories(conn, candidate["id"], best_match["id"], "contradicts", "heuristic")
                    stats["conflicts"] += 1

                if candidate["status"] == "inbox" and candidate.get("confidence", 0.0) >= promote_threshold:
                    conn.execute(
                        "UPDATE memories SET status = ?, updated_at = ? WHERE id = ?",
                        ("active", utcnow(), candidate["id"]),
                    )
                    stats["promoted"] += 1

        return stats

    def upsert_profile_fact(
        self, title: str, content: str, evidence_ref: str | None = None, source_ref: str | None = None
    ) -> dict[str, Any]:
        return self.create_memory(
            {
                "type": "profile",
                "scope": "global",
                "title": title,
                "content": content,
                "summary": self._make_summary(content),
                "source_ref": source_ref,
                "evidence_ref": evidence_ref,
                "metadata": {"kind": "profile_fact"},
            }
        )

    def explain(self, identifier: str) -> dict[str, Any]:
        with self.store.connection() as conn:
            retrieval = conn.execute("SELECT * FROM retrieval_logs WHERE id = ?", (identifier,)).fetchone()
            if retrieval:
                return {
                    "kind": "retrieval",
                    "id": retrieval["id"],
                    "query_text": retrieval["query_text"],
                    "filters": self.store.loads(retrieval["filters_json"]),
                    "results": self.store.loads(retrieval["results_json"]),
                }
            memory = conn.execute("SELECT * FROM memories WHERE id = ?", (identifier,)).fetchone()
            if memory:
                mem = self._row_to_memory(memory)
                sources = conn.execute("SELECT * FROM sources WHERE source_ref = ? OR id = ?", (mem["source_ref"], mem["source_ref"])).fetchall()
                return {"kind": "memory", "memory": mem, "sources": [dict(source) for source in sources]}
        raise KeyError(f"Unknown identifier: {identifier}")

    def export(self) -> dict[str, Any]:
        tables = [
            "projects",
            "repos",
            "sources",
            "ingestion_events",
            "memories",
            "memory_links",
            "memory_conflicts",
            "retrieval_logs",
            "maintenance_jobs",
            "maintenance_runs",
            "profiles",
            "tasks",
            "task_runs",
            "artifacts",
        ]
        exported: dict[str, list[dict[str, Any]]] = {}
        with self.store.connection() as conn:
            for table in tables:
                rows = conn.execute(f"SELECT * FROM {table}").fetchall()
                exported[table] = [dict(row) for row in rows]
        return exported

    def import_data(self, payload: dict[str, Any]) -> None:
        tables = [
            "projects",
            "repos",
            "sources",
            "ingestion_events",
            "memories",
            "memory_links",
            "memory_conflicts",
            "retrieval_logs",
            "maintenance_jobs",
            "maintenance_runs",
            "profiles",
            "tasks",
            "task_runs",
            "artifacts",
        ]
        with self.store.connection() as conn:
            for table in tables:
                rows = payload.get(table, [])
                if not rows:
                    continue
                columns = list(rows[0].keys())
                placeholder = ",".join("?" for _ in columns)
                sql = f"INSERT OR REPLACE INTO {table} ({','.join(columns)}) VALUES ({placeholder})"
                conn.executemany(sql, [tuple(row.get(col) for col in columns) for row in rows])
                if table == "memories":
                    for row in rows:
                        self._upsert_fts(conn, row)
        self.migrate_v2()
        self._run_opportunistic_maintenance(limit=10)

    def get_memory(self, memory_id: str) -> dict[str, Any]:
        with self.store.connection() as conn:
            row = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,)).fetchone()
            if row is None:
                raise KeyError(memory_id)
            return self._row_to_memory(row)

    def list_memories(
        self,
        *,
        status: str | None = "active",
        scope: str | None = None,
        memory_type: str | None = None,
        subtype: str | None = None,
        project_id: str | None = None,
        repo_id: str | None = None,
        source_ref: str | None = None,
        evidence_ref: str | None = None,
        run_id: str | None = None,
        task_id: str | None = None,
        origin_agent: str | None = None,
        url: str | None = None,
        domain: str | None = None,
        metadata: dict[str, Any] | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        sql = ["SELECT * FROM memories WHERE 1=1"]
        params: list[Any] = []
        if status is not None:
            sql.append("AND status = ?")
            params.append(status)
        if scope is not None:
            sql.append("AND scope = ?")
            params.append(scope)
        filters = {
            "type": memory_type,
            "subtype": subtype,
            "project_id": project_id,
            "repo_id": repo_id,
            "source_ref": source_ref,
            "evidence_ref": evidence_ref,
            "run_id": run_id,
            "task_id": task_id,
            "origin_agent": origin_agent,
            "url": url,
            "domain": domain,
        }
        params.extend(self._memory_filter_sql(sql, {key: value for key, value in filters.items() if value is not None}))
        for key, value in (metadata or {}).items():
            sql.append("AND json_extract(metadata_json, ?) = ?")
            params.extend((f"$.{key}", value))
        sql.append("ORDER BY updated_at DESC LIMIT ?")
        params.append(limit)
        with self.store.connection() as conn:
            rows = conn.execute(" ".join(sql), params).fetchall()
            return [self._row_to_memory(row) for row in rows]

    def list_pending_events(self) -> list[dict[str, Any]]:
        with self.store.connection() as conn:
            rows = conn.execute("SELECT * FROM ingestion_events WHERE job_state = 'pending' ORDER BY created_at ASC").fetchall()
            return [dict(row) for row in rows]

    def mark_event(self, event_id: str, state: str, outcome: str | None = None, error_message: str | None = None) -> None:
        with self.store.connection() as conn:
            conn.execute(
                """
                UPDATE ingestion_events
                SET job_state = ?, processor_outcome = ?, error_message = ?, updated_at = ?, processed_at = ?
                WHERE id = ?
                """,
                (state, outcome, error_message, utcnow(), utcnow(), event_id),
            )

    def create_project(self, project_id: str, name: str, description: str | None = None) -> None:
        now = utcnow()
        with self.store.connection() as conn:
            conn.execute(
                "INSERT OR REPLACE INTO projects (id, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
                (project_id, name, description, now, now),
            )
        self._run_opportunistic_maintenance(limit=1)

    def create_repo(self, repo_id: str, name: str, project_id: str | None = None, path: str | None = None) -> None:
        now = utcnow()
        with self.store.connection() as conn:
            conn.execute(
                "INSERT OR REPLACE INTO repos (id, project_id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
                (repo_id, project_id, name, path, now, now),
            )
        self._run_opportunistic_maintenance(limit=1)

    def create_task(
        self,
        *,
        title: str,
        intent: str,
        kind: str = "task",
        status: str = "open",
        priority: int = 3,
        project_id: str | None = None,
        repo_id: str | None = None,
        parent_task_id: str | None = None,
        origin: str | None = None,
        owner_agent: str | None = None,
        blocked_reason: str | None = None,
        requires_human_input: bool = False,
        due_at: str | None = None,
        metadata: dict[str, Any] | None = None,
        task_id: str | None = None,
        run_id: str | None = None,
    ) -> dict[str, Any]:
        now = utcnow()
        record = {
            "id": task_id or make_id("task"),
            "schema_version": SCHEMA_VERSION,
            "run_id": run_id,
            "title": title,
            "intent": intent,
            "kind": kind,
            "status": status,
            "priority": priority,
            "project_id": project_id,
            "repo_id": repo_id,
            "parent_task_id": parent_task_id,
            "origin": origin,
            "owner_agent": owner_agent,
            "blocked_reason": blocked_reason,
            "requires_human_input": 1 if requires_human_input else 0,
            "due_at": due_at,
            "created_at": now,
            "updated_at": now,
            "metadata_json": self.store.dumps(metadata or {}),
        }
        with self.store.connection() as conn:
            conn.execute(
                """
                INSERT INTO tasks (
                    id, schema_version, run_id, title, intent, kind, status, priority, project_id, repo_id,
                    parent_task_id, origin, owner_agent, blocked_reason, requires_human_input, due_at,
                    created_at, updated_at, metadata_json
                ) VALUES (
                    :id, :schema_version, :run_id, :title, :intent, :kind, :status, :priority, :project_id, :repo_id,
                    :parent_task_id, :origin, :owner_agent, :blocked_reason, :requires_human_input, :due_at,
                    :created_at, :updated_at, :metadata_json
                )
                """,
                record,
            )
        self._run_opportunistic_maintenance(limit=1)
        return self.get_task(record["id"])

    def get_task(self, task_id: str) -> dict[str, Any]:
        with self.store.connection() as conn:
            row = conn.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
            if row is None:
                raise KeyError(task_id)
            return self._row_to_task(row)

    def list_tasks(
        self,
        *,
        status: str | None = None,
        owner_agent: str | None = None,
        requires_human_input: bool | None = None,
        run_id: str | None = None,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        sql = ["SELECT * FROM tasks WHERE 1=1"]
        params: list[Any] = []
        if status is not None:
            sql.append("AND status = ?")
            params.append(status)
        if owner_agent is not None:
            sql.append("AND owner_agent = ?")
            params.append(owner_agent)
        if requires_human_input is not None:
            sql.append("AND requires_human_input = ?")
            params.append(1 if requires_human_input else 0)
        if run_id is not None:
            sql.append("AND run_id = ?")
            params.append(run_id)
        sql.append("ORDER BY priority ASC, created_at ASC LIMIT ?")
        params.append(limit)
        with self.store.connection() as conn:
            rows = conn.execute(" ".join(sql), params).fetchall()
            return [self._row_to_task(row) for row in rows]

    def update_task(
        self,
        task_id: str,
        *,
        status: str | None = None,
        owner_agent: str | None = None,
        blocked_reason: str | None = None,
        requires_human_input: bool | None = None,
        metadata: dict[str, Any] | None = None,
        run_id: str | None = None,
    ) -> dict[str, Any]:
        task = self.get_task(task_id)
        task["status"] = status or task["status"]
        task["owner_agent"] = owner_agent if owner_agent is not None else task["owner_agent"]
        task["blocked_reason"] = blocked_reason
        task["run_id"] = run_id if run_id is not None else task.get("run_id")
        if requires_human_input is not None:
            task["requires_human_input"] = requires_human_input
        if metadata is not None:
            task["metadata"] = metadata
        with self.store.connection() as conn:
            conn.execute(
                """
                UPDATE tasks
                SET status = ?, owner_agent = ?, blocked_reason = ?, requires_human_input = ?, run_id = ?, updated_at = ?, metadata_json = ?
                WHERE id = ?
                """,
                (
                    task["status"],
                    task["owner_agent"],
                    task["blocked_reason"],
                    1 if task["requires_human_input"] else 0,
                    task.get("run_id"),
                    utcnow(),
                    self.store.dumps(task["metadata"]),
                    task_id,
                ),
            )
        self._run_opportunistic_maintenance(limit=1)
        return self.get_task(task_id)

    def start_task_run(
        self,
        task_id: str,
        agent_id: str,
        *,
        input_payload: dict[str, Any] | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        now = utcnow()
        record = {
            "id": make_id("run"),
            "task_id": task_id,
            "agent_id": agent_id,
            "status": "running",
            "input_payload_json": self.store.dumps(input_payload or {}),
            "result_summary": None,
            "error_message": None,
            "started_at": now,
            "completed_at": None,
            "created_at": now,
            "updated_at": now,
            "metadata_json": self.store.dumps(metadata or {}),
        }
        with self.store.connection() as conn:
            conn.execute(
                """
                INSERT INTO task_runs (
                    id, task_id, agent_id, status, input_payload_json, result_summary, error_message,
                    started_at, completed_at, created_at, updated_at, metadata_json
                ) VALUES (
                    :id, :task_id, :agent_id, :status, :input_payload_json, :result_summary, :error_message,
                    :started_at, :completed_at, :created_at, :updated_at, :metadata_json
                )
                """,
                record,
            )
        self._run_opportunistic_maintenance(limit=1)
        return self.get_task_run(record["id"])

    def get_task_run(self, run_id: str) -> dict[str, Any]:
        with self.store.connection() as conn:
            row = conn.execute("SELECT * FROM task_runs WHERE id = ?", (run_id,)).fetchone()
            if row is None:
                raise KeyError(run_id)
            return self._row_to_task_run(row)

    def finish_task_run(
        self,
        run_id: str,
        *,
        status: str,
        result_summary: str | None = None,
        error_message: str | None = None,
    ) -> dict[str, Any]:
        now = utcnow()
        with self.store.connection() as conn:
            conn.execute(
                """
                UPDATE task_runs
                SET status = ?, result_summary = ?, error_message = ?, updated_at = ?, completed_at = ?
                WHERE id = ?
                """,
                (status, result_summary, error_message, now, now, run_id),
            )
        self._run_opportunistic_maintenance(limit=1)
        return self.get_task_run(run_id)

    def list_task_runs(self, *, task_id: str | None = None, status: str | None = None, limit: int = 50) -> list[dict[str, Any]]:
        sql = ["SELECT * FROM task_runs WHERE 1=1"]
        params: list[Any] = []
        if task_id is not None:
            sql.append("AND task_id = ?")
            params.append(task_id)
        if status is not None:
            sql.append("AND status = ?")
            params.append(status)
        sql.append("ORDER BY created_at DESC LIMIT ?")
        params.append(limit)
        with self.store.connection() as conn:
            rows = conn.execute(" ".join(sql), params).fetchall()
            return [self._row_to_task_run(row) for row in rows]

    def create_artifact(
        self,
        *,
        task_id: str,
        artifact_type: str,
        title: str,
        content: str,
        fmt: str = "md",
        status: str = "active",
        source_ref: str | None = None,
        metadata: dict[str, Any] | None = None,
        artifact_id: str | None = None,
    ) -> dict[str, Any]:
        now = utcnow()
        record = {
            "id": artifact_id or make_id("art"),
            "task_id": task_id,
            "artifact_type": artifact_type,
            "title": title,
            "content": content,
            "format": fmt,
            "status": status,
            "source_ref": source_ref,
            "created_at": now,
            "updated_at": now,
            "metadata_json": self.store.dumps(metadata or {}),
        }
        with self.store.connection() as conn:
            conn.execute(
                """
                INSERT INTO artifacts (
                    id, task_id, artifact_type, title, content, format, status, source_ref, created_at, updated_at, metadata_json
                ) VALUES (
                    :id, :task_id, :artifact_type, :title, :content, :format, :status, :source_ref, :created_at, :updated_at, :metadata_json
                )
                """,
                record,
            )
        self._run_opportunistic_maintenance(limit=1)
        return self.get_artifact(record["id"])

    def get_artifact(self, artifact_id: str) -> dict[str, Any]:
        with self.store.connection() as conn:
            row = conn.execute("SELECT * FROM artifacts WHERE id = ?", (artifact_id,)).fetchone()
            if row is None:
                raise KeyError(artifact_id)
            return self._row_to_artifact(row)

    def list_artifacts(self, *, task_id: str | None = None, limit: int = 50) -> list[dict[str, Any]]:
        sql = ["SELECT * FROM artifacts WHERE 1=1"]
        params: list[Any] = []
        if task_id is not None:
            sql.append("AND task_id = ?")
            params.append(task_id)
        sql.append("ORDER BY created_at DESC LIMIT ?")
        params.append(limit)
        with self.store.connection() as conn:
            rows = conn.execute(" ".join(sql), params).fetchall()
            return [self._row_to_artifact(row) for row in rows]

    def dashboard_snapshot(self, *, owner_agent: str | None = None) -> dict[str, Any]:
        active_tasks = self.list_tasks(owner_agent=owner_agent, limit=50)
        active_tasks = [task for task in active_tasks if task["status"] in {"open", "draft", "in_progress"}][:20]
        blocked_tasks = self.list_tasks(status="blocked", owner_agent=owner_agent, limit=20)
        recent_runs = self.list_task_runs(limit=20)
        recent_artifacts = self.list_artifacts(limit=20)
        return {
            "active_tasks": active_tasks,
            "blocked_tasks": blocked_tasks,
            "recent_runs": recent_runs,
            "recent_artifacts": recent_artifacts,
        }

    def task_bundle(self, task_id: str) -> dict[str, Any]:
        task = self.get_task(task_id)
        with self.store.connection() as conn:
            child_rows = conn.execute(
                "SELECT * FROM tasks WHERE parent_task_id = ? ORDER BY priority ASC, created_at ASC",
                (task_id,),
            ).fetchall()
        return {
            "task": task,
            "children": [self._row_to_task(row) for row in child_rows],
            "runs": self.list_task_runs(task_id=task_id, limit=100),
            "artifacts": self.list_artifacts(task_id=task_id, limit=100),
        }

    def audit_v2(self) -> dict[str, Any]:
        with self.store.connection() as conn:
            approvals = self._count_if_table_exists(conn, "approvals")
            handoffs = self._count_if_table_exists(conn, "handoffs")
            legacy_memory_rows = conn.execute(
                """
                SELECT COUNT(*) AS count
                FROM memories
                WHERE schema_version != ?
                   OR json_extract(metadata_json, '$.legacy_kind') IS NOT NULL
                   OR json_extract(metadata_json, '$.legacy_system') IS NOT NULL
                """,
                (SCHEMA_VERSION,),
            ).fetchone()["count"]
            legacy_task_rows = conn.execute(
                """
                SELECT COUNT(*) AS count
                FROM tasks
                WHERE schema_version != ?
                   OR json_extract(metadata_json, '$.legacy_kind') IS NOT NULL
                   OR json_extract(metadata_json, '$.legacy_system') IS NOT NULL
                """,
                (SCHEMA_VERSION,),
            ).fetchone()["count"]
            memories = conn.execute("SELECT COUNT(*) AS count FROM memories").fetchone()["count"]
            tasks = conn.execute("SELECT COUNT(*) AS count FROM tasks").fetchone()["count"]
            maintenance_jobs = self._count_if_table_exists(conn, "maintenance_jobs")
        return {
            "schema_version": SCHEMA_VERSION,
            "memories": memories,
            "tasks": tasks,
            "approvals": approvals,
            "handoffs": handoffs,
            "maintenance_jobs": maintenance_jobs,
            "legacy_memory_rows": legacy_memory_rows,
            "legacy_task_rows": legacy_task_rows,
        }

    def migrate_v2(self) -> dict[str, Any]:
        before = self.audit_v2()
        migrated = {"memories": 0, "tasks": 0, "approval_artifacts": 0, "handoff_artifacts": 0}
        with self.store.connection() as conn:
            memory_rows = conn.execute("SELECT * FROM memories").fetchall()
            for row in memory_rows:
                record = self._migrate_memory_row(dict(row))
                if record is None:
                    continue
                conn.execute(
                    """
                    UPDATE memories
                    SET schema_version = ?, type = ?, subtype = ?, origin_agent = ?, run_id = ?, task_id = ?,
                        url = ?, domain = ?, summary = ?, metadata_json = ?, updated_at = ?
                    WHERE id = ?
                    """,
                    (
                        record["schema_version"],
                        record["type"],
                        record["subtype"],
                        record["origin_agent"],
                        record["run_id"],
                        record["task_id"],
                        record["url"],
                        record["domain"],
                        record["summary"],
                        record["metadata_json"],
                        utcnow(),
                        record["id"],
                    ),
                )
                migrated["memories"] += 1
            task_rows = conn.execute("SELECT * FROM tasks").fetchall()
            for row in task_rows:
                record = self._migrate_task_row(dict(row))
                if record is None:
                    continue
                conn.execute(
                    """
                    UPDATE tasks
                    SET schema_version = ?, run_id = ?, metadata_json = ?, updated_at = ?
                    WHERE id = ?
                    """,
                    (record["schema_version"], record["run_id"], record["metadata_json"], utcnow(), record["id"]),
                )
                migrated["tasks"] += 1
            if self._table_exists(conn, "approvals"):
                for row in conn.execute("SELECT * FROM approvals").fetchall():
                    approval = dict(row)
                    task_row = conn.execute("SELECT * FROM tasks WHERE id = ?", (approval["task_id"],)).fetchone()
                    if task_row is None:
                        continue
                    task = self._row_to_task(task_row)
                    metadata = dict(task.get("metadata", {}))
                    metadata["approval"] = {
                        "kind": approval["kind"],
                        "risk_level": approval["risk_level"],
                        "payload": self.store.loads(approval["payload_json"]) or {},
                        "status": approval["status"],
                        "requested_at": approval["requested_at"],
                        "resolved_at": approval["resolved_at"],
                        "resolution_note": approval["resolution_note"],
                    }
                    conn.execute(
                        """
                        UPDATE tasks
                        SET metadata_json = ?, updated_at = ?
                        WHERE id = ?
                        """,
                        (self.store.dumps(metadata), utcnow(), approval["task_id"]),
                    )
                    conn.execute(
                        """
                        INSERT INTO artifacts (
                            id, task_id, artifact_type, title, content, format, status, source_ref, created_at, updated_at, metadata_json
                        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                        """,
                        (
                            make_id("art"),
                            approval["task_id"],
                            "approval_resolution",
                            f"Approval {approval['kind']}",
                            json.dumps(metadata["approval"], sort_keys=True),
                            "json",
                            "archived" if approval["status"] != "pending" else "active",
                            "migration:v2",
                            utcnow(),
                            utcnow(),
                            self.store.dumps({}),
                        ),
                    )
                    migrated["approval_artifacts"] += 1
                conn.execute("DROP TABLE approvals")
            if self._table_exists(conn, "handoffs"):
                for row in conn.execute("SELECT * FROM handoffs").fetchall():
                    handoff = dict(row)
                    payload = self.store.loads(handoff["payload_json"]) or {}
                    conn.execute(
                        """
                        INSERT INTO artifacts (
                            id, task_id, artifact_type, title, content, format, status, source_ref, created_at, updated_at, metadata_json
                        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                        """,
                        (
                            make_id("art"),
                            handoff["task_id"],
                            "delegation_note",
                            f"Handoff {handoff['from_agent']} -> {handoff['to_agent']}",
                            json.dumps(
                                {
                                    "from_agent": handoff["from_agent"],
                                    "to_agent": handoff["to_agent"],
                                    "reason": handoff["reason"],
                                    "status": handoff["status"],
                                    "payload": payload,
                                    "result_summary": handoff["result_summary"],
                                    "error_message": handoff["error_message"],
                                    "completed_at": handoff["completed_at"],
                                },
                                sort_keys=True,
                            ),
                            "json",
                            "active",
                            "migration:v2",
                            utcnow(),
                            utcnow(),
                            self.store.dumps({}),
                        ),
                    )
                    migrated["handoff_artifacts"] += 1
                conn.execute("DROP TABLE handoffs")
        after = self.audit_v2()
        return {"status": "ok", "before": before, "after": after, "migrated": migrated}

    def _capture_source(
        self, trigger_type: str, source_kind: str, source_ref: str, scope: str, content: str, metadata: dict[str, Any]
    ) -> dict[str, Any]:
        now = utcnow()
        source = {
            "id": make_id("src"),
            "source_kind": source_kind,
            "title": metadata.get("title") or source_ref,
            "content": content,
            "source_ref": source_ref,
            "metadata_json": self.store.dumps(metadata),
            "created_at": now,
        }
        event = {
            "id": make_id("ing"),
            "trigger_type": trigger_type,
            "source_id": source["id"],
            "requested_scope": scope,
            "job_state": "pending",
            "processor_outcome": None,
            "retry_count": 0,
            "error_message": None,
            "created_at": now,
            "updated_at": now,
            "processed_at": None,
        }
        with self.store.connection() as conn:
            conn.execute(
                """
                INSERT INTO sources (id, source_kind, title, content, source_ref, metadata_json, created_at)
                VALUES (:id, :source_kind, :title, :content, :source_ref, :metadata_json, :created_at)
                """,
                source,
            )
            conn.execute(
                """
                INSERT INTO ingestion_events (
                    id, trigger_type, source_id, requested_scope, job_state, processor_outcome,
                    retry_count, error_message, created_at, updated_at, processed_at
                ) VALUES (
                    :id, :trigger_type, :source_id, :requested_scope, :job_state, :processor_outcome,
                    :retry_count, :error_message, :created_at, :updated_at, :processed_at
                )
                """,
                event,
            )
        self._run_opportunistic_maintenance(limit=1)
        return {"source": source, "event": event}

    def _run_opportunistic_maintenance(self, *, limit: int) -> None:
        if self._maintenance_check_running:
            return
        due_jobs = self.list_due_maintenance_jobs(limit=1)
        if not due_jobs:
            return
        self._maintenance_check_running = True
        try:
            from .engine import MemoryEngine

            MemoryEngine(self).run_due_maintenance(limit=limit)
        except Exception:
            return
        finally:
            self._maintenance_check_running = False

    def _memory_filter_sql(self, sql: list[str], filters: dict[str, Any]) -> list[Any]:
        params: list[Any] = []
        allowed = {"project_id", "repo_id", "type", "subtype", "run_id", "task_id", "origin_agent", "url", "domain"}
        for key, value in filters.items():
            if key not in allowed or value is None:
                continue
            sql.append(f"AND {key} = ?")
            params.append(value)
        return params

    def _facet_score(self, memory: dict[str, Any], filters: dict[str, Any]) -> float:
        tracked = ["project_id", "repo_id", "type", "subtype", "run_id", "task_id", "origin_agent", "url", "domain"]
        applied = [key for key in tracked if filters.get(key) is not None]
        if not applied:
            return 0.0
        matched = sum(1 for key in applied if memory.get(key) == filters[key])
        return matched / len(applied)

    def _migrate_memory_row(self, row: dict[str, Any]) -> dict[str, Any] | None:
        metadata = self.store.loads(row.get("metadata_json")) or {}
        if row.get("schema_version") == SCHEMA_VERSION and not metadata.get("legacy_kind") and not metadata.get("legacy_system"):
            return None
        subtype = self._memory_subtype(metadata)
        origin_agent = row.get("origin_agent") or metadata.get("origin_agent") or metadata.get("legacy_system") or row.get("agent_id")
        run_id = row.get("run_id") or metadata.get("run_id") or metadata.get("legacy_run_id") or self._infer_run_id(row, subtype)
        task_id = row.get("task_id") or metadata.get("task_id")
        url = row.get("url") or metadata.get("url") or metadata.get("source_url") or self._infer_url(row, subtype)
        domain = row.get("domain") or metadata.get("domain") or (urlparse(url).netloc if url else None)

[Showing lines 1-1198 of 1706 (50.0KB limit). Use offset=1199 to continue.]