.config/systemd/user/agents-database-http.service
.config/systemd/user/agents-database-maintenance.service
.config/systemd/user/ballbox-services.target
.config/systemd/user/install-session-memory-dispatcher.service
.config/systemd/user/pi-telegram.service
.config/systemd/user/vending-status-refresh.service
.config/systemd/user/vending-status-refresh.timer
.config/systemd/user/wifi-presence-log.service
.config/systemd/user/wifi-presence-log.timer
.config/systemd/user/wifi-tracker-api.service
---
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:4:const DISPATCH_URL = process.env.PI_SESSION_MEMORY_DISPATCH_URL || "http://127.0.0.1:17882/api/session-memory";
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:75:		memory_hints: {
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:82:async function dispatchMemory(payload: any) {
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:89:		throw new Error(`session memory dispatcher failed: ${response.status} ${response.statusText}`);
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:123:			const result = await dispatchMemory({
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:125:				trigger: "session-memory-dispatcher",
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:128:			ctx.ui.notify(`Session analysis queued${result?.job_file ? `: ${result.job_file}` : ""}`, "info");
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:135:			const ok = await ctx.ui.confirm("Session analysis failed", "Continue closing this session without queueing?", dialogOptions);
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:143:		const result = await runPrompt(ctx, `before_switch:${event.reason}`, { allowCancel: true, title: "Analyze this session for memory?" });
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:149:		const result = await runPrompt(ctx, "before_fork", { allowCancel: true, title: "Analyze this session for memory?" });
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:155:		const result = await runPrompt(ctx, `before_shutdown:${event.reason}`, { allowCancel: true, title: "Analyze this session for memory?", timeoutMs: 12000 });
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:161:		await runPrompt(ctx, "shutdown_fallback", { allowCancel: false, title: "Analyze this session for memory?", timeoutMs: 12000 });
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:165:		description: "Queue a session analysis from the current session",
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts:168:			await runPrompt(ctx, "manual_command", { allowCancel: false, title: "Analyze this session for memory?" });
/home/sebas/pi-config/README.md:11:- `extensions/session-memory-prompt.ts`
/home/sebas/pi-config/README.md:13:- `scripts/session-memory-analyzer.py`
/home/sebas/pi-config/README.md:14:- `scripts/session-memory-dispatcher.py`
/home/sebas/pi-config/README.md:15:- `scripts/install-session-memory-dispatcher.service`
/home/sebas/pi-config/README.md:37:- durable memory: `/home/sebas/agents-database`
/home/sebas/pi-config/README.md:46:- Session close hook now prompts once before switch/fork/exit to encourage saving durable memory into agents-database.
/home/sebas/pi-config/README.md:47:- Session memory now uses an external local dispatcher on `127.0.0.1:17882`; the hook only captures session identity/context, the dispatcher enqueues metadata, and a background analyzer decides whether anything durable is worth saving.
/home/sebas/pi-config/README.md:48:- The canonical memory pipeline lives in `agents-database` (`MemoryService` / `MemoryEngine`). Repo-local helpers should only capture session context and hand off to that service.
/home/sebas/pi-config/README.md:49:- The analyzer uses `pi` in background with a cheap model (`gpt-5.4-mini`) and thinking off when possible.
/home/sebas/pi-config/README.md:51:- Use `./scripts/install-session-memory-dispatcher.service` to keep the dispatcher alive under systemd user service if desired.
/home/sebas/pi-config/SYSTEM.md:31:Use Agents Database as shared durable memory, not just search.
/home/sebas/pi-config/SYSTEM.md:32:Create a memory when new information is likely useful beyond the current chat or task.
/home/sebas/pi-config/SYSTEM.md:37:When writing memory, include the best available scope/type and attach project_id, repo_id, task_id, run_id, source_ref, origin_agent, url, and domain when known.
/home/sebas/pi-config/SYSTEM.md:38:Default memory types: `profile` for user preferences/priorities, `decision` for chosen approaches, `project` for durable project facts, `artifact` for reusable references/sources, `episode` for concise session or handoff summaries.
/home/sebas/pi-config/SYSTEM.md:41:- `GET /api/context` returns a contextual bundle around a memory, not a reliable exact-id fetch.
/home/sebas/agents-database/systemd/user/agents-database-http.service:9:Environment=DB_PATH=/home/sebas/agents-database/data/shared-agent-memory.sqlite3
/home/sebas/.config/systemd/user/agents-database-http.service:9:Environment=DB_PATH=/home/sebas/agents-database/data/shared-agent-memory.sqlite3
/home/sebas/pi-config/docs/agents-database-memory-contract.md:1:# Agents Database memory contract
/home/sebas/pi-config/docs/agents-database-memory-contract.md:3:This repo uses `agents-database` as the canonical memory store.
/home/sebas/pi-config/docs/agents-database-memory-contract.md:7:- `MemoryService` / `MemoryEngine` own the memory pipeline.
/home/sebas/pi-config/docs/agents-database-memory-contract.md:8:- `ingest()` writes memory records.
/home/sebas/pi-config/docs/agents-database-memory-contract.md:12:- `maintenance_jobs` and `maintenance_runs` schedule and audit periodic consolidation.
/home/sebas/pi-config/docs/agents-database-memory-contract.md:14:- `memory_conflicts` and `memory_links` are the right place for contradiction and relation semantics.
/home/sebas/pi-config/docs/agents-database-memory-contract.md:21:- queue analyzer jobs
/home/sebas/pi-config/docs/gastos-europa.md:15:- frontend: `analysis.html`
/home/sebas/agents-database/systemd/user/agents-database-maintenance.service:2:Description=Agents Database Maintenance Daemon
/home/sebas/agents-database/systemd/user/agents-database-maintenance.service:9:Environment=DB_PATH=/home/sebas/agents-database/data/shared-agent-memory.sqlite3
/home/sebas/agents-database/systemd/user/agents-database-maintenance.service:12:ExecStart=/home/sebas/agents-database/scripts/run-maintenance-daemon.sh
/home/sebas/pi-config/docs/system-layout.md:15:- durable memory service: `/home/sebas/agents-database`
/home/sebas/agents-database/src/shared_agent_memory/schema.py:1:SCHEMA_VERSION = "shared-agent-memory-v2"
/home/sebas/agents-database/src/shared_agent_memory/schema.py:81:CREATE TABLE IF NOT EXISTS memory_links (
/home/sebas/agents-database/src/shared_agent_memory/schema.py:83:    from_memory_id TEXT NOT NULL,
/home/sebas/agents-database/src/shared_agent_memory/schema.py:84:    to_memory_id TEXT NOT NULL,
/home/sebas/agents-database/src/shared_agent_memory/schema.py:88:    FOREIGN KEY(from_memory_id) REFERENCES memories(id),
/home/sebas/agents-database/src/shared_agent_memory/schema.py:89:    FOREIGN KEY(to_memory_id) REFERENCES memories(id)
/home/sebas/agents-database/src/shared_agent_memory/schema.py:100:CREATE TABLE IF NOT EXISTS maintenance_jobs (
/home/sebas/agents-database/src/shared_agent_memory/schema.py:117:CREATE TABLE IF NOT EXISTS maintenance_runs (
/home/sebas/agents-database/src/shared_agent_memory/schema.py:126:    FOREIGN KEY(job_id) REFERENCES maintenance_jobs(id)
/home/sebas/agents-database/src/shared_agent_memory/schema.py:129:CREATE TABLE IF NOT EXISTS memory_conflicts (
/home/sebas/agents-database/src/shared_agent_memory/schema.py:131:    memory_id TEXT NOT NULL,
/home/sebas/agents-database/src/shared_agent_memory/schema.py:132:    conflicting_memory_id TEXT NOT NULL,
/home/sebas/agents-database/src/shared_agent_memory/schema.py:136:    FOREIGN KEY(memory_id) REFERENCES memories(id),
/home/sebas/agents-database/src/shared_agent_memory/schema.py:137:    FOREIGN KEY(conflicting_memory_id) REFERENCES memories(id)
/home/sebas/agents-database/src/shared_agent_memory/schema.py:222:CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_due ON maintenance_jobs(next_due_at);
/home/sebas/agents-database/src/shared_agent_memory/schema.py:223:CREATE INDEX IF NOT EXISTS idx_maintenance_runs_job ON maintenance_runs(job_id);
/home/sebas/agents-database/src/shared_agent_memory/schema.py:224:CREATE INDEX IF NOT EXISTS idx_memory_conflicts_mem ON memory_conflicts(memory_id);
/home/sebas/agents-database/src/shared_agent_memory/schema.py:227:    memory_id UNINDEXED,
/home/sebas/pi-config/AGENTS.md:36:- Use Agents Database as durable shared memory.
/home/sebas/pi-config/AGENTS.md:37:- Prefer saving durable memory at session end via session analysis, not mid-conversation.
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:13:JOB_DIR = Path(os.environ.get("PI_SESSION_MEMORY_JOB_DIR", BASE_DIR / "sessions" / "session-memory-jobs"))
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:14:ANALYZER = Path(os.environ.get("PI_SESSION_MEMORY_ANALYZER", BASE_DIR / "scripts" / "session-memory-analyzer.py"))
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:25:        "job_type": "session_memory_analysis",
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:27:        "analysis": {
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:30:            "memory_id": None,
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:37:def launch_analyzer(job_file: Path) -> None:
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:41:        ["python3", str(ANALYZER), "--job-file", str(job_file)],
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:51:        if self.path != "/api/session-memory":
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:69:            launch_analyzer(job_file)
/home/sebas/pi-config/scripts/session-memory-dispatcher.py:91:    print(f"session-memory-dispatcher listening on http://{args.host}:{args.port}/api/session-memory", flush=True)
/home/sebas/pi-config/scripts/session-memory-analyzer.py:15:PI_MODEL = os.environ.get("PI_SESSION_MEMORY_MODEL", "gpt-5.4-mini")
/home/sebas/pi-config/scripts/session-memory-analyzer.py:16:PI_THINKING = os.environ.get("PI_SESSION_MEMORY_THINKING", "off")
/home/sebas/pi-config/scripts/session-memory-analyzer.py:17:PI_BIN = os.environ.get("PI_SESSION_MEMORY_PI_BIN")
/home/sebas/pi-config/scripts/session-memory-analyzer.py:18:MAX_CANDIDATES = int(os.environ.get("PI_SESSION_MEMORY_MAX_CANDIDATES", "6"))
/home/sebas/pi-config/scripts/session-memory-analyzer.py:19:ACTIVE_IMPORTANCE_THRESHOLD = float(os.environ.get("PI_SESSION_MEMORY_ACTIVE_IMPORTANCE_THRESHOLD", "0.92"))
/home/sebas/pi-config/scripts/session-memory-analyzer.py:20:ACTIVE_CONFIDENCE_THRESHOLD = float(os.environ.get("PI_SESSION_MEMORY_ACTIVE_CONFIDENCE_THRESHOLD", "0.9"))
/home/sebas/pi-config/scripts/session-memory-analyzer.py:68:def ingest(memory: dict[str, Any]) -> dict[str, Any]:
/home/sebas/pi-config/scripts/session-memory-analyzer.py:72:        data=json.dumps(memory).encode(),
/home/sebas/pi-config/scripts/session-memory-analyzer.py:100:    return f"""You are extracting reusable memory candidates from a Pi session.
/home/sebas/pi-config/scripts/session-memory-analyzer.py:109:- Do NOT save operational notes about this session-memory system, dispatch flow, or local implementation work unless they affect broader workflows beyond this repo.
/home/sebas/pi-config/scripts/session-memory-analyzer.py:113:- Prefer at most one memory per distinct insight.
/home/sebas/pi-config/scripts/session-memory-analyzer.py:137:- confidence: 0 to 1, how well supported the memory is.
/home/sebas/pi-config/scripts/session-memory-analyzer.py:160:def normalize_memories(analysis: dict[str, Any]) -> list[dict[str, Any]]:
/home/sebas/pi-config/scripts/session-memory-analyzer.py:161:    memories = analysis.get("memories")
/home/sebas/pi-config/scripts/session-memory-analyzer.py:164:    if bool(analysis.get("save")):
/home/sebas/pi-config/scripts/session-memory-analyzer.py:165:        return [analysis]
/home/sebas/pi-config/scripts/session-memory-analyzer.py:169:def choose_status(memory_type: str, confidence: float, importance: float) -> str:
/home/sebas/pi-config/scripts/session-memory-analyzer.py:170:    if memory_type in DURABLE_TYPES and confidence >= ACTIVE_CONFIDENCE_THRESHOLD and importance >= ACTIVE_IMPORTANCE_THRESHOLD:
/home/sebas/pi-config/scripts/session-memory-analyzer.py:175:def build_memory_payload(candidate: dict[str, Any], meta: dict[str, Any], job_file: Path, analysis_reason: str | None, index: int) -> dict[str, Any]:
/home/sebas/pi-config/scripts/session-memory-analyzer.py:176:    memory_type = candidate.get("type") or "episode"
/home/sebas/pi-config/scripts/session-memory-analyzer.py:179:    status = choose_status(memory_type, confidence, importance)
/home/sebas/pi-config/scripts/session-memory-analyzer.py:181:        "type": memory_type,
/home/sebas/pi-config/scripts/session-memory-analyzer.py:193:        "origin_agent": "pi-session-memory-analyzer",
/home/sebas/pi-config/scripts/session-memory-analyzer.py:202:            "analysis_reason": analysis_reason,
/home/sebas/pi-config/scripts/session-memory-analyzer.py:219:    job["analysis"] = {
/home/sebas/pi-config/scripts/session-memory-analyzer.py:223:        "memory_id": None,
/home/sebas/pi-config/scripts/session-memory-analyzer.py:224:        "memory_ids": [],
/home/sebas/pi-config/scripts/session-memory-analyzer.py:231:        analysis = extract_json(raw)
/home/sebas/pi-config/scripts/session-memory-analyzer.py:232:        candidates = normalize_memories(analysis)[:MAX_CANDIDATES]
/home/sebas/pi-config/scripts/session-memory-analyzer.py:236:            memory = build_memory_payload(candidate, meta, job_file, analysis.get("reason"), index)
/home/sebas/pi-config/scripts/session-memory-analyzer.py:237:            if not memory["content"].strip() or not memory["title"].strip():
/home/sebas/pi-config/scripts/session-memory-analyzer.py:239:            saved_items.append(ingest(memory))
/home/sebas/pi-config/scripts/session-memory-analyzer.py:241:        job["analysis"]["raw"] = raw
/home/sebas/pi-config/scripts/session-memory-analyzer.py:242:        job["analysis"]["result"] = analysis
/home/sebas/pi-config/scripts/session-memory-analyzer.py:243:        job["analysis"]["status"] = "completed"
/home/sebas/pi-config/scripts/session-memory-analyzer.py:244:        job["analysis"]["save"] = bool(saved_items)
/home/sebas/pi-config/scripts/session-memory-analyzer.py:245:        job["analysis"]["saved"] = bool(saved_items)
/home/sebas/pi-config/scripts/session-memory-analyzer.py:246:        job["analysis"]["saved_count"] = len(saved_items)
/home/sebas/pi-config/scripts/session-memory-analyzer.py:247:        job["analysis"]["memory_ids"] = [item.get("id") for item in saved_items if item.get("id")]
/home/sebas/pi-config/scripts/session-memory-analyzer.py:248:        job["analysis"]["memory_id"] = job["analysis"]["memory_ids"][0] if job["analysis"]["memory_ids"] else None
/home/sebas/pi-config/scripts/session-memory-analyzer.py:251:        print(json.dumps(job["analysis"], ensure_ascii=False))
/home/sebas/pi-config/scripts/session-memory-analyzer.py:254:        job["analysis"]["status"] = "failed"
/home/sebas/pi-config/scripts/session-memory-analyzer.py:255:        job["analysis"]["error"] = str(exc)
/home/sebas/pi-config/scripts/session-memory-analyzer.py:257:        print(json.dumps(job["analysis"], ensure_ascii=False), file=sys.stderr)
/home/sebas/pi-config/scripts/install-session-memory-dispatcher.service:2:Description=Pi session memory dispatcher
/home/sebas/pi-config/scripts/install-session-memory-dispatcher.service:8:ExecStart=/usr/bin/env python3 /home/sebas/pi-config/scripts/session-memory-dispatcher.py --host 127.0.0.1 --port 17882
/home/sebas/pi-config/scripts/install-session-memory-dispatcher.service:12:Environment=PI_SESSION_MEMORY_PI_BIN=/home/sebas/.npm-global/bin/pi
/home/sebas/pi-config/scripts/install-session-memory-dispatcher.service:13:Environment=PI_SESSION_MEMORY_MAX_CANDIDATES=6
/home/sebas/pi-config/scripts/install-session-memory-dispatcher.service:14:Environment=PI_SESSION_MEMORY_ACTIVE_IMPORTANCE_THRESHOLD=0.92
/home/sebas/pi-config/scripts/install-session-memory-dispatcher.service:15:Environment=PI_SESSION_MEMORY_ACTIVE_CONFIDENCE_THRESHOLD=0.9
/home/sebas/agents-database/src/shared_agent_memory/http_api.py:9:from .service import MemoryService
/home/sebas/agents-database/src/shared_agent_memory/http_api.py:47:def build_handler(service: MemoryService):
/home/sebas/agents-database/src/shared_agent_memory/http_api.py:48:    class MemoryAPIHandler(BaseHTTPRequestHandler):
/home/sebas/agents-database/src/shared_agent_memory/http_api.py:120:                                memory_type=_first(query, "type"),
/home/sebas/agents-database/src/shared_agent_memory/http_api.py:138:                    self._send_json(service.get_memory(parsed.path.split("/")[3]))
/home/sebas/agents-database/src/shared_agent_memory/http_api.py:354:    return MemoryAPIHandler
/home/sebas/agents-database/src/shared_agent_memory/http_api.py:358:    service = MemoryService(db_path)
/home/sebas/.config/systemd/user/install-session-memory-dispatcher.service:2:Description=Pi session memory dispatcher
/home/sebas/.config/systemd/user/install-session-memory-dispatcher.service:8:ExecStart=/usr/bin/env python3 /home/sebas/pi-config/scripts/session-memory-dispatcher.py --host 127.0.0.1 --port 17882
/home/sebas/.config/systemd/user/install-session-memory-dispatcher.service:12:Environment=PI_SESSION_MEMORY_PI_BIN=/home/sebas/.npm-global/bin/pi
/home/sebas/.config/systemd/user/install-session-memory-dispatcher.service:13:Environment=PI_SESSION_MEMORY_MAX_CANDIDATES=6
/home/sebas/.config/systemd/user/install-session-memory-dispatcher.service:14:Environment=PI_SESSION_MEMORY_ACTIVE_IMPORTANCE_THRESHOLD=0.92
/home/sebas/.config/systemd/user/install-session-memory-dispatcher.service:15:Environment=PI_SESSION_MEMORY_ACTIVE_CONFIDENCE_THRESHOLD=0.9
/home/sebas/.config/systemd/user/agents-database-maintenance.service:2:Description=Agents Database Maintenance Daemon
/home/sebas/.config/systemd/user/agents-database-maintenance.service:9:Environment=DB_PATH=/home/sebas/agents-database/data/shared-agent-memory.sqlite3
/home/sebas/.config/systemd/user/agents-database-maintenance.service:12:ExecStart=/home/sebas/agents-database/scripts/run-maintenance-daemon.sh
/home/sebas/agents-database/src/shared_agent_memory/cli.py:9:from .engine import MemoryEngine
/home/sebas/agents-database/src/shared_agent_memory/cli.py:11:from .service import MemoryService
/home/sebas/agents-database/src/shared_agent_memory/cli.py:15:    parser = argparse.ArgumentParser(prog="shared-agent-memory")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:16:    parser.add_argument("--db", default="shared-agent-memory.sqlite3")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:56:    maintenance_tick = subparsers.add_parser("maintenance-tick")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:57:    maintenance_tick.add_argument("--limit", type=int)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:58:    maintenance_tick.add_argument("--job-id")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:60:    maintenance_status = subparsers.add_parser("maintenance-status")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:61:    maintenance_status.add_argument("--runs", type=int, default=10)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:63:    maintenance_config = subparsers.add_parser("maintenance-config")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:64:    maintenance_config.add_argument("job_id")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:65:    maintenance_config.add_argument("--cadence")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:66:    maintenance_config.add_argument("--interval-minutes", type=int)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:67:    maintenance_config.add_argument("--window-start")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:68:    maintenance_config.add_argument("--window-end")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:69:    maintenance_config.add_argument("--enable", action="store_true")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:70:    maintenance_config.add_argument("--disable", action="store_true")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:71:    maintenance_config.add_argument("--metadata-json")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:72:    maintenance_config.add_argument("--recompute-next", action="store_true")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:74:    maintenance_daemon = subparsers.add_parser("maintenance-daemon")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:75:    maintenance_daemon.add_argument("--interval-seconds", type=int, default=900)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:76:    maintenance_daemon.add_argument("--jitter-seconds", type=int, default=60)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:77:    maintenance_daemon.add_argument("--limit", type=int)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:78:    maintenance_daemon.add_argument("--job-id")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:79:    maintenance_daemon.add_argument("--once", action="store_true")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:100:    consolidate.add_argument("memory_ids", nargs="+")
/home/sebas/agents-database/src/shared_agent_memory/cli.py:138:    service = MemoryService(args.db)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:139:    engine = MemoryEngine(service)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:169:    elif args.command == "maintenance-tick":
/home/sebas/agents-database/src/shared_agent_memory/cli.py:170:        print(json.dumps(engine.run_due_maintenance(limit=args.limit, job_id=args.job_id), indent=2))
/home/sebas/agents-database/src/shared_agent_memory/cli.py:171:    elif args.command == "maintenance-status":
/home/sebas/agents-database/src/shared_agent_memory/cli.py:174:                {"jobs": service.list_maintenance_jobs(), "runs": service.list_maintenance_runs(limit=args.runs)},
/home/sebas/agents-database/src/shared_agent_memory/cli.py:178:    elif args.command == "maintenance-config":
/home/sebas/agents-database/src/shared_agent_memory/cli.py:179:        job = service.get_maintenance_job(args.job_id)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:200:        updated = service.update_maintenance_job(
/home/sebas/agents-database/src/shared_agent_memory/cli.py:211:    elif args.command == "maintenance-daemon":
/home/sebas/agents-database/src/shared_agent_memory/cli.py:213:            results = engine.run_due_maintenance(limit=args.limit, job_id=args.job_id)
/home/sebas/agents-database/src/shared_agent_memory/cli.py:239:        print(json.dumps(service.consolidate(args.memory_ids), indent=2))
/home/sebas/agents-database/src/shared_agent_memory/cli.py:254:                    memory_type=args.type,
/home/sebas/agents-database/pyproject.toml:6:name = "shared-agent-memory"
/home/sebas/agents-database/pyproject.toml:8:description = "Local shared memory service for agents across projects"
/home/sebas/agents-database/pyproject.toml:12:shared-agent-memory = "shared_agent_memory.cli:main"
/home/sebas/agents-database/scripts/run-maintenance-daemon.sh:5:DB_PATH="${DB_PATH:-${REPO_ROOT}/data/shared-agent-memory.sqlite3}"
/home/sebas/agents-database/scripts/run-maintenance-daemon.sh:21:exec "${PYTHON_BIN}" -m shared_agent_memory.cli \
/home/sebas/agents-database/scripts/run-maintenance-daemon.sh:23:  maintenance-daemon \
/home/sebas/agents-database/scripts/run-http-api.sh:5:DB_PATH="${DB_PATH:-${REPO_ROOT}/data/shared-agent-memory.sqlite3}"
/home/sebas/agents-database/scripts/run-http-api.sh:12:exec python3 -m shared_agent_memory.cli \
/home/sebas/agents-database/tests/test_memory_service.py:13:from shared_agent_memory import MemoryEngine, MemoryService
/home/sebas/agents-database/tests/test_memory_service.py:14:from shared_agent_memory.http_api import build_handler
/home/sebas/agents-database/tests/test_memory_service.py:15:from shared_agent_memory.schema import SCHEMA_VERSION
/home/sebas/agents-database/tests/test_memory_service.py:18:def make_service(tmp_path: Path) -> MemoryService:
/home/sebas/agents-database/tests/test_memory_service.py:19:    return MemoryService(str(tmp_path / "memory.sqlite3"))
/home/sebas/agents-database/tests/test_memory_service.py:31:def _start_api(service: MemoryService) -> tuple[ThreadingHTTPServer, str]:
/home/sebas/agents-database/tests/test_memory_service.py:46:    assert results["results"][0]["memory"]["type"] == "profile"
/home/sebas/agents-database/tests/test_memory_service.py:47:    assert results["results"][0]["memory"]["schema_version"] == SCHEMA_VERSION
/home/sebas/agents-database/tests/test_memory_service.py:52:    engine = MemoryEngine(service)
/home/sebas/agents-database/tests/test_memory_service.py:57:    memory_id = processed[0]["memory_id"]
/home/sebas/agents-database/tests/test_memory_service.py:58:    memory = service.get_memory(memory_id)
/home/sebas/agents-database/tests/test_memory_service.py:59:    assert memory["status"] == "inbox"
/home/sebas/agents-database/tests/test_memory_service.py:64:    promoted = service.consolidate([memory_id])
/home/sebas/agents-database/tests/test_memory_service.py:65:    assert promoted["promoted"] == [memory_id]
/home/sebas/agents-database/tests/test_memory_service.py:68:    assert search_with_active["results"][0]["memory"]["id"] == memory_id
/home/sebas/agents-database/tests/test_memory_service.py:76:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:83:            "content": "Alpha shares memory across projects.",
/home/sebas/agents-database/tests/test_memory_service.py:86:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:96:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:141:    second = MemoryService(str(tmp_path / "second.sqlite3"))
/home/sebas/agents-database/tests/test_memory_service.py:154:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:166:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:188:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:202:    assert results["results"][0]["memory"]["subtype"] == "research_claim"
/home/sebas/agents-database/tests/test_memory_service.py:221:                "title": "HTTP memory",
/home/sebas/agents-database/tests/test_memory_service.py:222:                "content": "Remote API keeps shared memory reachable.",
/home/sebas/agents-database/tests/test_memory_service.py:232:        assert search_payload["results"][0]["memory"]["id"] == "mem_http_1"
/home/sebas/agents-database/tests/test_memory_service.py:281:def test_http_api_supports_memory_lookup_by_id(tmp_path: Path) -> None:
/home/sebas/agents-database/tests/test_memory_service.py:283:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:288:            "title": "Lookup memory",
/home/sebas/agents-database/tests/test_memory_service.py:319:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:324:            "title": "Lookup memory",
/home/sebas/agents-database/tests/test_memory_service.py:341:def test_maintenance_jobs_exist_by_default(tmp_path: Path) -> None:
/home/sebas/agents-database/tests/test_memory_service.py:344:    jobs = {job["id"] for job in service.list_maintenance_jobs()}
/home/sebas/agents-database/tests/test_memory_service.py:351:def test_maintenance_tick_records_run(tmp_path: Path) -> None:
/home/sebas/agents-database/tests/test_memory_service.py:353:    engine = MemoryEngine(service)
/home/sebas/agents-database/tests/test_memory_service.py:356:    service.update_maintenance_job("job_consolidation_daily_10", next_due_at=now)
/home/sebas/agents-database/tests/test_memory_service.py:357:    results = engine.run_due_maintenance(job_id="job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:360:    job = service.get_maintenance_job("job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:361:    runs = service.list_maintenance_runs(job_id="job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:367:def test_service_startup_runs_due_maintenance(tmp_path: Path) -> None:
/home/sebas/agents-database/tests/test_memory_service.py:368:    db_path = tmp_path / "memory.sqlite3"
/home/sebas/agents-database/tests/test_memory_service.py:369:    service = MemoryService(str(db_path))
/home/sebas/agents-database/tests/test_memory_service.py:372:    service.update_maintenance_job("job_consolidation_daily_10", next_due_at=now)
/home/sebas/agents-database/tests/test_memory_service.py:373:    restarted = MemoryService(str(db_path))
/home/sebas/agents-database/tests/test_memory_service.py:374:    job = restarted.get_maintenance_job("job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:375:    runs = restarted.list_maintenance_runs(job_id="job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:381:def test_write_operations_trigger_due_maintenance_check(tmp_path: Path) -> None:
/home/sebas/agents-database/tests/test_memory_service.py:385:    service.update_maintenance_job("job_consolidation_daily_10", next_due_at=now)
/home/sebas/agents-database/tests/test_memory_service.py:388:    job = service.get_maintenance_job("job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:389:    runs = service.list_maintenance_runs(job_id="job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:395:def test_high_confidence_inbox_memory_auto_promotes_on_write(tmp_path: Path) -> None:
/home/sebas/agents-database/tests/test_memory_service.py:397:    service.update_maintenance_job("job_consolidation_daily_10", next_due_at="2026-03-20T10:00:00+00:00")
/home/sebas/agents-database/tests/test_memory_service.py:399:    memory = service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:412:    assert memory["status"] == "active"
/home/sebas/agents-database/tests/test_memory_service.py:417:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:427:    service.create_memory(
/home/sebas/agents-database/tests/test_memory_service.py:449:        conflicts = conn.execute("SELECT COUNT(*) AS count FROM memory_conflicts").fetchone()["count"]
/home/sebas/agents-database/tests/test_memory_service.py:454:def test_run_maintenance_daemon_wrapper_runs_from_repo_checkout(tmp_path: Path) -> None:
/home/sebas/agents-database/tests/test_memory_service.py:455:    db_path = tmp_path / "memory.sqlite3"
/home/sebas/agents-database/tests/test_memory_service.py:456:    service = MemoryService(str(db_path))
/home/sebas/agents-database/tests/test_memory_service.py:457:    service.update_maintenance_job("job_consolidation_daily_10", next_due_at="2026-03-20T10:00:00+00:00")
/home/sebas/agents-database/tests/test_memory_service.py:459:    script = Path(__file__).resolve().parents[1] / "scripts" / "run-maintenance-daemon.sh"
/home/sebas/agents-database/tests/test_memory_service.py:472:    restarted = MemoryService(str(db_path))
/home/sebas/agents-database/tests/test_memory_service.py:473:    job = restarted.get_maintenance_job("job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:474:    runs = restarted.list_maintenance_runs(job_id="job_consolidation_daily_10")
/home/sebas/agents-database/tests/test_memory_service.py:481:def test_versioned_maintenance_systemd_service_exists() -> None:
/home/sebas/agents-database/tests/test_memory_service.py:482:    service_file = Path(__file__).resolve().parents[1] / "systemd" / "user" / "agents-database-maintenance.service"
/home/sebas/agents-database/tests/test_memory_service.py:486:    assert "ExecStart=/mnt/rpi/agents-database/scripts/run-maintenance-daemon.sh" in content
/home/sebas/agents-database/tests/test_memory_service.py:578:        legacy_memory = conn.execute("SELECT * FROM memories WHERE id = 'legacy_run'").fetchone()
/home/sebas/agents-database/tests/test_memory_service.py:584:    assert legacy_memory is not None
/home/sebas/agents-database/docs/launchd/agents-database-maintenance.plist:6:    <string>com.sebas.agents-database.maintenance</string>
/home/sebas/agents-database/docs/launchd/agents-database-maintenance.plist:10:      <string>/Users/sebas/agents-database/scripts/run-maintenance-daemon.sh</string>
/home/sebas/agents-database/docs/launchd/agents-database-maintenance.plist:19:    <string>/Users/sebas/Library/Logs/agents-database/maintenance.out.log</string>
/home/sebas/agents-database/docs/launchd/agents-database-maintenance.plist:21:    <string>/Users/sebas/Library/Logs/agents-database/maintenance.err.log</string>
/home/sebas/agents-database/docs/launchd/agents-database-maintenance.plist:27:      <string>/Users/sebas/agents-database/data/shared-agent-memory.sqlite3</string>
/home/sebas/agents-database/docs/system-v1.md:12:- authoritative place to implement memory promotion, consolidation, and conflict handling
/home/sebas/agents-database/docs/system-v1.md:16:Use the local CLI against `data/shared-agent-memory.sqlite3`:
/home/sebas/agents-database/docs/system-v1.md:19:python3 -m shared_agent_memory.cli --db data/shared-agent-memory.sqlite3 task-bundle task_<id>
/home/sebas/agents-database/docs/system-v1.md:20:python3 -m shared_agent_memory.cli --db data/shared-agent-memory.sqlite3 list-memories \
/home/sebas/agents-database/docs/system-v1.md:29:## Memory pipeline
/home/sebas/agents-database/docs/system-v1.md:31:The memory pipeline lives in `MemoryService` / `MemoryEngine`, not in repo-local side scripts.
/home/sebas/agents-database/docs/system-v1.md:35:- `ingest()` accepts durable memory writes
/home/sebas/agents-database/docs/system-v1.md:39:- `maintenance_jobs` + `maintenance_runs` drive scheduled consolidation
/home/sebas/agents-database/docs/system-v1.md:46:- conflicting claims produce explicit `memory_conflicts` rows and `contradicts` links
/home/sebas/agents-database/docs/system-v1.md:50:## Maintenance
/home/sebas/agents-database/docs/system-v1.md:53:.venv/bin/python -m shared_agent_memory.cli --db data/shared-agent-memory.sqlite3 maintenance-status
/home/sebas/agents-database/docs/system-v1.md:54:.venv/bin/python -m shared_agent_memory.cli --db data/shared-agent-memory.sqlite3 maintenance-tick --limit 10
/home/sebas/agents-database/docs/system-v1.md:55:.venv/bin/python -m shared_agent_memory.cli --db data/shared-agent-memory.sqlite3 maintenance-daemon --interval-seconds 900
/home/sebas/agents-database/docs/system-v1.md:60:- `systemd/user/agents-database-maintenance.service`
/home/sebas/agents-database/docs/system-v1.md:61:- `scripts/run-maintenance-daemon.sh`
/home/sebas/agents-database/docs/system-v1.md:65:- `~/.config/systemd/user/agents-database-maintenance.service`
/home/sebas/agents-database/docs/system-v1.md:66:- enabled with `systemctl --user enable --now agents-database-maintenance.service`
/home/sebas/agents-database/docs/system-v1.md:73:systemctl --user status agents-database-maintenance.service --no-pager
/home/sebas/agents-database/docs/system-v1.md:74:systemctl --user show agents-database-maintenance.service --property=NRestarts,ExecMainPID,SubState
/home/sebas/agents-database/docs/system-v1.md:75:journalctl --user -u agents-database-maintenance.service -n 50 --no-pager
/home/sebas/agents-database/docs/system-v1.md:80:- `docs/launchd/agents-database-maintenance.plist`
/home/sebas/agents-database/docs/system-v1.md:84:- the daemon now starts through `scripts/run-maintenance-daemon.sh`
/home/sebas/agents-database/docs/system-v1.md:86:- the service was verified to auto-restart after a manual `systemctl --user kill agents-database-maintenance.service`
/home/sebas/agents-database/docs/system-v1.md:87:- `MemoryService` also performs opportunistic catch-up checks on startup and after normal write operations, so due jobs do not depend exclusively on the long-lived daemon
/home/sebas/agents-database/docs/system-v1.md:96:- `docs/memory-consolidation-rfc.md`
/home/sebas/agents-database/src/shared_agent_memory/service.py:38:    memory: dict[str, Any]
/home/sebas/agents-database/src/shared_agent_memory/service.py:47:            "memory": self.memory,
/home/sebas/agents-database/src/shared_agent_memory/service.py:53:                "confidence": self.memory["confidence"],
/home/sebas/agents-database/src/shared_agent_memory/service.py:54:                "freshness": self.memory["freshness"],
/home/sebas/agents-database/src/shared_agent_memory/service.py:56:            "matched_scope": self.memory["scope"],
/home/sebas/agents-database/src/shared_agent_memory/service.py:57:            "evidence_ref": self.memory["evidence_ref"],
/home/sebas/agents-database/src/shared_agent_memory/service.py:62:class MemoryService:
/home/sebas/agents-database/src/shared_agent_memory/service.py:65:        self._maintenance_check_running = False
/home/sebas/agents-database/src/shared_agent_memory/service.py:69:        self._run_opportunistic_maintenance(limit=10)
/home/sebas/agents-database/src/shared_agent_memory/service.py:71:    def create_memory(self, memory_input: dict[str, Any]) -> dict[str, Any]:
/home/sebas/agents-database/src/shared_agent_memory/service.py:73:        metadata = dict(memory_input.get("metadata") or {})
/home/sebas/agents-database/src/shared_agent_memory/service.py:74:        subtype = memory_input.get("subtype")
/home/sebas/agents-database/src/shared_agent_memory/service.py:75:        run_id = memory_input.get("run_id")


# /home/sebas/.config/systemd/user/install-session-memory-dispatcher.service
[Unit]
Description=Pi session memory dispatcher
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/env python3 /home/sebas/pi-config/scripts/session-memory-dispatcher.py --host 127.0.0.1 --port 17882
Restart=always
RestartSec=2
Environment=AGENTS_DB_API=http://100.116.176.16:8091
Environment=PI_SESSION_MEMORY_PI_BIN=/home/sebas/.npm-global/bin/pi
Environment=PI_SESSION_MEMORY_MAX_CANDIDATES=6
Environment=PI_SESSION_MEMORY_ACTIVE_IMPORTANCE_THRESHOLD=0.92
Environment=PI_SESSION_MEMORY_ACTIVE_CONFIDENCE_THRESHOLD=0.9
WorkingDirectory=/home/sebas/pi-config

[Install]
WantedBy=default.target

# /home/sebas/.config/systemd/user/agents-database-maintenance.service
[Unit]
Description=Agents Database Maintenance Daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/home/sebas/agents-database
Environment=DB_PATH=/home/sebas/agents-database/data/shared-agent-memory.sqlite3
Environment=INTERVAL_SECONDS=900
Environment=JITTER_SECONDS=60
ExecStart=/home/sebas/agents-database/scripts/run-maintenance-daemon.sh
Restart=always
RestartSec=10

[Install]
WantedBy=default.target


pi-telegram-new-session
install-session-memory-dispatcher.service
session-memory-analyzer.py
session-memory-dispatcher.py
session-memory-analyzer.cpython-313.pyc
session-memory-dispatcher.cpython-313.pyc


#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import os
import subprocess
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
JOB_DIR = Path(os.environ.get("PI_SESSION_MEMORY_JOB_DIR", BASE_DIR / "sessions" / "session-memory-jobs"))
ANALYZER = Path(os.environ.get("PI_SESSION_MEMORY_ANALYZER", BASE_DIR / "scripts" / "session-memory-analyzer.py"))

def write_job(payload: dict[str, object]) -> Path:
    JOB_DIR.mkdir(parents=True, exist_ok=True)
    job_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
    job_file = JOB_DIR / f"{job_id}.json"
    job = {
        "job_id": job_id,
        "queued_at": datetime.now(timezone.utc).isoformat(),
        "status": "queued",
        "job_type": "session_memory_analysis",
        "payload": payload,
        "analysis": {
            "status": "queued",
            "save": None,
            "memory_id": None,
        },
    }
    job_file.write_text(json.dumps(job, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    return job_file

def launch_analyzer(job_file: Path) -> None:
    log_file = job_file.with_suffix(".log")
    log_handle = log_file.open("ab")
    subprocess.Popen(
        ["python3", str(ANALYZER), "--job-file", str(job_file)],
        stdout=log_handle,
        stderr=subprocess.STDOUT,
        start_new_session=True,
        close_fds=True,
    )

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path != "/api/session-memory":
            self.send_response(404)
            self.end_headers()
            return

        length = int(self.headers.get("Content-Length", "0") or "0")
        raw = self.rfile.read(length)
        try:
            payload = json.loads(raw.decode("utf-8"))
        except Exception as exc:
            self.send_response(400)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({"error": f"invalid json: {exc}"}).encode())
            return

        try:
            job_file = write_job(payload)
            launch_analyzer(job_file)
            self.send_response(202)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({"status": "queued", "job_file": str(job_file)}).encode())
        except Exception as exc:
            self.send_response(502)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({"error": str(exc)}).encode())

    def log_message(self, fmt, *args):
        return

def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--host", default="127.0.0.1")
    parser.add_argument("--port", type=int, default=17882)
    args = parser.parse_args()

    server = ThreadingHTTPServer((args.host, args.port), Handler)
    print(f"session-memory-dispatcher listening on http://{args.host}:{args.port}/api/session-memory", flush=True)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        return 0
    return 0

if __name__ == "__main__":
    raise SystemExit(main())


#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
from urllib import request

AGENTS_DB_API = os.environ.get("AGENTS_DB_API", "http://100.116.176.16:8091")
PI_MODEL = os.environ.get("PI_SESSION_MEMORY_MODEL", "gpt-5.4-mini")
PI_THINKING = os.environ.get("PI_SESSION_MEMORY_THINKING", "off")
PI_BIN = os.environ.get("PI_SESSION_MEMORY_PI_BIN")
MAX_CANDIDATES = int(os.environ.get("PI_SESSION_MEMORY_MAX_CANDIDATES", "6"))
ACTIVE_IMPORTANCE_THRESHOLD = float(os.environ.get("PI_SESSION_MEMORY_ACTIVE_IMPORTANCE_THRESHOLD", "0.92"))
ACTIVE_CONFIDENCE_THRESHOLD = float(os.environ.get("PI_SESSION_MEMORY_ACTIVE_CONFIDENCE_THRESHOLD", "0.9"))
DURABLE_TYPES = {"profile", "decision", "project", "artifact", "procedural", "semantic"}

def resolve_pi_bin() -> str:
    candidates = [
        PI_BIN,
        shutil.which("pi"),
        str(Path.home() / ".npm-global" / "bin" / "pi"),
        "/usr/local/bin/pi",
        "/usr/bin/pi",
    ]
    for candidate in candidates:
        if not candidate:
            continue
        if Path(candidate).exists():
            return candidate
    raise FileNotFoundError("pi")

def load_job(job_file: Path) -> dict[str, Any]:
    return json.loads(job_file.read_text(encoding="utf-8"))

def save_job(job_file: Path, payload: dict[str, Any]) -> None:
    job_file.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

def call_pi(prompt: str) -> str:
    cmd = [
        resolve_pi_bin(),
        "-p",
        "--provider",
        "openai-codex",
        "--model",
        PI_MODEL,
        "--thinking",
        PI_THINKING,
        "--no-tools",
        "--no-session",
        prompt,
    ]
    proc = subprocess.run(cmd, capture_output=True, text=True)
    if proc.returncode != 0:
        raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or f"pi failed ({proc.returncode})")
    return proc.stdout.strip()

def ingest(memory: dict[str, Any]) -> dict[str, Any]:
    req = request.Request(
        f"{AGENTS_DB_API}/api/ingest",
        method="POST",
        data=json.dumps(memory).encode(),
        headers={"Content-Type": "application/json"},
    )
    with request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode("utf-8"))

def extract_json(text: str) -> dict[str, Any]:
    raw = text.strip()
    if raw.startswith("```"):
        raw = raw.strip("`")
        raw = raw.split("\n", 1)[-1] if "\n" in raw else raw
    start = raw.find("{")
    end = raw.rfind("}")
    if start >= 0 and end > start:
        raw = raw[start : end + 1]
    return json.loads(raw)

def session_meta(job: dict[str, Any]) -> dict[str, Any]:
    payload = job.get("payload")
    if isinstance(payload, dict):
        return payload
    return job

def build_prompt(job: dict[str, Any]) -> str:
    meta = session_meta(job)
    return f"""You are extracting reusable memory candidates from a Pi session.

Goal:
- Prefer broad candidate capture over premature rejection.
- Return 0 to {MAX_CANDIDATES} memories from this session.
- Include strong durable memories and weaker candidate memories that may be worth later consolidation.

Rules:
- Session metadata alone is not enough.
- Do NOT save operational notes about this session-memory system, dispatch flow, or local implementation work unless they affect broader workflows beyond this repo.
- Favor stable user preferences, enduring project facts, reusable external artifacts, decisions, procedures, blockers, and compact episode summaries with future reuse value.
- It is OK to include lower-confidence candidates if they could become useful after consolidation.
- Avoid trivial chatter, one-off execution noise, and duplicates of the same point unless they capture materially different memories.
- Prefer at most one memory per distinct insight.
- Output JSON only. No markdown. No extra text.

Return this schema:
{{
  "memories": [
    {{
      "type": "profile" | "decision" | "project" | "artifact" | "episode" | "procedural" | "semantic",
      "subtype": string | null,
      "scope": "global" | "project",
      "title": string,
      "content": string,
      "source_ref": string,
      "evidence_ref": string | null,
      "confidence": number,
      "freshness": number,
      "importance": number,
      "reason": string
    }}
  ],
  "reason": string
}}

Scoring guidance:
- confidence: 0 to 1, how well supported the memory is.
- freshness: 0 to 1, how current it is.
- importance: 0 to 1, how much future value it has.
- evidence_ref: cite the strongest source in the session or branch excerpt when possible.
- Use lower confidence/importance for tentative candidates rather than omitting them.

Session metadata:
- session_id: {meta.get('session_id')}
- session_file: {meta.get('session_file')}
- cwd: {meta.get('cwd')}
- scope: {meta.get('scope')}
- reason: {meta.get('reason')}
- user_messages: {meta.get('user_messages')}
- assistant_messages: {meta.get('assistant_messages')}
- last_user_text: {str(meta.get('last_user_text') or '')[:2000]}
- last_assistant_text: {str(meta.get('last_assistant_text') or '')[:2000]}
- trigger: {meta.get('trigger')}

Branch excerpt JSON:
{json.dumps(meta.get('branch') or [], ensure_ascii=False)[:30000]}
"""

def normalize_memories(analysis: dict[str, Any]) -> list[dict[str, Any]]:
    memories = analysis.get("memories")
    if isinstance(memories, list):
        return [item for item in memories if isinstance(item, dict)]
    if bool(analysis.get("save")):
        return [analysis]
    return []

def choose_status(memory_type: str, confidence: float, importance: float) -> str:
    if memory_type in DURABLE_TYPES and confidence >= ACTIVE_CONFIDENCE_THRESHOLD and importance >= ACTIVE_IMPORTANCE_THRESHOLD:
        return "active"
    return "inbox"

def build_memory_payload(candidate: dict[str, Any], meta: dict[str, Any], job_file: Path, analysis_reason: str | None, index: int) -> dict[str, Any]:
    memory_type = candidate.get("type") or "episode"
    confidence = float(candidate.get("confidence") or 0.6)
    importance = float(candidate.get("importance") or 0.5)
    status = choose_status(memory_type, confidence, importance)
    return {
        "type": memory_type,
        "subtype": candidate.get("subtype"),
        "scope": candidate.get("scope") or meta.get("scope") or "global",
        "status": status,
        "source_kind": "manual",
        "title": candidate.get("title") or f"Session insight {index}",
        "content": candidate.get("content") or "",
        "confidence": confidence,
        "freshness": float(candidate.get("freshness") or 0.9),
        "importance": importance,
        "source_ref": candidate.get("source_ref") or f"pi-session:{meta.get('session_file') or meta.get('session_id') or 'unknown'}",
        "evidence_ref": candidate.get("evidence_ref"),
        "origin_agent": "pi-session-memory-analyzer",
        "metadata": {
            "job_file": str(job_file),
            "session_id": meta.get("session_id"),
            "session_file": meta.get("session_file"),
            "cwd": meta.get("cwd"),
            "reason": meta.get("reason"),
            "user_messages": meta.get("user_messages"),
            "assistant_messages": meta.get("assistant_messages"),
            "analysis_reason": analysis_reason,
            "candidate_reason": candidate.get("reason"),
            "trigger": meta.get("trigger"),
            "promotion_candidate": status == "inbox",
            "session_candidate_index": index,
        },
    }

def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--job-file", required=True)
    args = parser.parse_args()

    job_file = Path(args.job_file)
    job = load_job(job_file)
    meta = session_meta(job)
    job["analysis"] = {
        "status": "running",
        "save": None,
        "saved": None,
        "memory_id": None,
        "memory_ids": [],
        "saved_count": 0,
    }
    save_job(job_file, job)

    try:
        raw = call_pi(build_prompt(job))
        analysis = extract_json(raw)
        candidates = normalize_memories(analysis)[:MAX_CANDIDATES]
        saved_items: list[dict[str, Any]] = []

        for index, candidate in enumerate(candidates, start=1):
            memory = build_memory_payload(candidate, meta, job_file, analysis.get("reason"), index)
            if not memory["content"].strip() or not memory["title"].strip():
                continue
            saved_items.append(ingest(memory))

        job["analysis"]["raw"] = raw
        job["analysis"]["result"] = analysis
        job["analysis"]["status"] = "completed"
        job["analysis"]["save"] = bool(saved_items)
        job["analysis"]["saved"] = bool(saved_items)
        job["analysis"]["saved_count"] = len(saved_items)
        job["analysis"]["memory_ids"] = [item.get("id") for item in saved_items if item.get("id")]
        job["analysis"]["memory_id"] = job["analysis"]["memory_ids"][0] if job["analysis"]["memory_ids"] else None

        save_job(job_file, job)
        print(json.dumps(job["analysis"], ensure_ascii=False))
        return 0
    except Exception as exc:
        job["analysis"]["status"] = "failed"
        job["analysis"]["error"] = str(exc)
        save_job(job_file, job)
        print(json.dumps(job["analysis"], ensure_ascii=False), file=sys.stderr)
        return 1

if __name__ == "__main__":
    raise SystemExit(main())


import path from "node:path";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

const DISPATCH_URL = process.env.PI_SESSION_MEMORY_DISPATCH_URL || "http://127.0.0.1:17882/api/session-memory";
const PROMPT_SAVE = "Yes";
const PROMPT_SKIP = "No";
const PROMPT_CANCEL = "Cancel";

function textFromMessage(message: any): string {
	if (!message || !Array.isArray(message.content)) return "";
	return message.content
		.filter((part: any) => part?.type === "text" && typeof part.text === "string")
		.map((part: any) => part.text)
		.join("\n")
		.trim();
}

function truncate(text: string | undefined, max = 220): string {
	const value = (text || "").replace(/\s+/g, " ").trim();
	if (!value) return "";
	return value.length > max ? value.slice(0, max - 1) + "…" : value;
}

function inferScope(cwd: string): "global" | "project" {
	const home = path.resolve(process.env.HOME || "/home/sebas");
	const resolved = path.resolve(cwd);
	if (resolved === home) return "global";
	if (resolved.startsWith(path.join(home, ".pi"))) return "global";
	return "project";
}

function getSessionStats(ctx: any) {
	const branch = ctx.sessionManager.getBranch();
	let userCount = 0;
	let assistantCount = 0;
	let lastUserText = "";
	let lastAssistantText = "";

	for (const entry of branch) {
		if (entry?.type !== "message") continue;
		const message = entry.message;
		if (!message || typeof message !== "object" || !("role" in message)) continue;
		if (message.role === "user") {
			userCount += 1;
			lastUserText = textFromMessage(message) || lastUserText;
		}
		if (message.role === "assistant") {
			assistantCount += 1;
			lastAssistantText = textFromMessage(message) || lastAssistantText;
		}
	}

	return {
		branch,
		userCount,
		assistantCount,
		lastUserText,
		lastAssistantText,
		shouldPrompt: userCount > 0 && assistantCount > 0,
	};
}

function buildHookPayload(ctx: any, stats: ReturnType<typeof getSessionStats>, reason: string) {
	return {
		session_id: ctx.sessionManager.getLeafId?.() || null,
		session_file: ctx.sessionManager.getSessionFile?.() || null,
		reason,
		cwd: ctx.cwd,
		scope: inferScope(ctx.cwd),
		user_messages: stats.userCount,
		assistant_messages: stats.assistantCount,
		last_user_text: truncate(stats.lastUserText, 2000),
		last_assistant_text: truncate(stats.lastAssistantText, 2000),
		branch: stats.branch,
		memory_hints: {
			observed_messages: stats.userCount + stats.assistantCount,
			has_latest_dialogue: Boolean(stats.lastUserText || stats.lastAssistantText),
		},
	};
}

async function dispatchMemory(payload: any) {
	const response = await fetch(DISPATCH_URL, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify(payload),
	});
	if (!response.ok) {
		throw new Error(`session memory dispatcher failed: ${response.status} ${response.statusText}`);
	}
	return response.json();
}

export default function (pi: ExtensionAPI) {
	let promptHandledForCurrentSession = false;

	async function runPrompt(ctx: any, reason: string, options?: { allowCancel?: boolean; title?: string; timeoutMs?: number }) {
		if (promptHandledForCurrentSession) return { cancel: false, skipped: true };
		if (!ctx.hasUI) return { cancel: false, skipped: true };

		const stats = getSessionStats(ctx);
		if (!stats.shouldPrompt) return { cancel: false, skipped: true };

		const allowCancel = options?.allowCancel ?? false;
		const timeoutMs = options?.timeoutMs;
		const title = options?.title || "Before closing this session";
		const choices = allowCancel ? [PROMPT_SAVE, PROMPT_SKIP, PROMPT_CANCEL] : [PROMPT_SAVE, PROMPT_SKIP];
		const dialogOptions = timeoutMs ? { timeout: timeoutMs } : undefined;
		const choice = await ctx.ui.select(title, choices, dialogOptions);

		if (choice === PROMPT_CANCEL || (allowCancel && choice === undefined)) {
			promptHandledForCurrentSession = false;
			return { cancel: true, skipped: false };
		}

		if (choice !== PROMPT_SAVE) {
			promptHandledForCurrentSession = true;
			return { cancel: false, skipped: true };
		}

		const payload = buildHookPayload(ctx, stats, reason);
		try {
			const result = await dispatchMemory({
				...payload,
				trigger: "session-memory-dispatcher",
			});
			promptHandledForCurrentSession = true;
			ctx.ui.notify(`Session analysis queued${result?.job_file ? `: ${result.job_file}` : ""}`, "info");
			return { cancel: false, skipped: false, queued: true, jobFile: result?.job_file || null };
		} catch (error) {
			promptHandledForCurrentSession = false;
			const message = error instanceof Error ? error.message : String(error);
			ctx.ui.notify(message, "error");
			if (!allowCancel) return { cancel: false, skipped: true, error: message };
			const ok = await ctx.ui.confirm("Session analysis failed", "Continue closing this session without queueing?", dialogOptions);
			if (!ok) return { cancel: true, skipped: false, error: message };
			promptHandledForCurrentSession = true;
			return { cancel: false, skipped: true, error: message };
		}
	}

	pi.on("session_before_switch", async (event, ctx) => {
		const result = await runPrompt(ctx, `before_switch:${event.reason}`, { allowCancel: true, title: "Analyze this session for memory?" });
		if (result.cancel) return { cancel: true };
		return undefined;
	});

	pi.on("session_before_fork", async (_event, ctx) => {
		const result = await runPrompt(ctx, "before_fork", { allowCancel: true, title: "Analyze this session for memory?" });
		if (result.cancel) return { cancel: true };
		return undefined;
	});

	pi.on("session_before_shutdown", async (event, ctx) => {
		const result = await runPrompt(ctx, `before_shutdown:${event.reason}`, { allowCancel: true, title: "Analyze this session for memory?", timeoutMs: 12000 });
		if (result.cancel) return { cancel: true };
		return undefined;
	});

	pi.on("session_shutdown", async (_event, ctx) => {
		await runPrompt(ctx, "shutdown_fallback", { allowCancel: false, title: "Analyze this session for memory?", timeoutMs: 12000 });
	});

	pi.registerCommand("remember-session", {
		description: "Queue a session analysis from the current session",
		handler: async (_args, ctx) => {
			promptHandledForCurrentSession = false;
			await runPrompt(ctx, "manual_command", { allowCancel: false, title: "Analyze this session for memory?" });
		},
	});
}


**Simplifying design possibilities**

I need to simplify the design. Maybe I could create a periodic sweeper script to replace the prompt and dispatcher functions. I should inspect the job directory for this. A periodic scan using a cron job or service with a state file could analyze recent sessions directly. I could leave the dispatcher for manual hooks while unifying the analyzer logic to be reusable by the sweeper. It might also help to build metadata from the session file and queue unanalyzed recent sessions.

bash

cd /home/sebas/pi-config && rg -n "session_file|last_user_text|branch" scripts extensions -S | head -200

cd /home/sebas/pi-config && find sessions -type f -name '*.jsonl' | tail -5 && echo 'jobs:' && ls sessions/session-memory-jobs 2>/dev/null | tail -10

python3 - <<'PY'
import json, pathlib
jobs=sorted(pathlib.Path('/home/sebas/pi-config/sessions/session-memory-jobs').glob('*.json'))
print('jobs', len(jobs))
for p in jobs[-5:]:
 d=json.loads(p.read_text())
 print(p.name, d.get('status'), d.get('payload',{}).get('session_file'), d.get('analysis',{}).get('status'), d.get('analysis',{}).get('saved_count'))
PY


scripts/session-memory-analyzer.py:140:- evidence_ref: cite the strongest source in the session or branch excerpt when possible.
scripts/session-memory-analyzer.py:145:- session_file: {meta.get('session_file')}
scripts/session-memory-analyzer.py:151:- last_user_text: {str(meta.get('last_user_text') or '')[:2000]}
scripts/session-memory-analyzer.py:155:Branch excerpt JSON:
scripts/session-memory-analyzer.py:156:{json.dumps(meta.get('branch') or [], ensure_ascii=False)[:30000]}
scripts/session-memory-analyzer.py:191:        "source_ref": candidate.get("source_ref") or f"pi-session:{meta.get('session_file') or meta.get('session_id') or 'unknown'}",
scripts/session-memory-analyzer.py:197:            "session_file": meta.get("session_file"),
extensions/session-memory-dispatcher.ts:33:	const branch = ctx.sessionManager.getBranch();
extensions/session-memory-dispatcher.ts:39:	for (const entry of branch) {
extensions/session-memory-dispatcher.ts:54:		branch,
extensions/session-memory-dispatcher.ts:66:		session_file: ctx.sessionManager.getSessionFile?.() || null,
extensions/session-memory-dispatcher.ts:72:		last_user_text: truncate(stats.lastUserText, 2000),
extensions/session-memory-dispatcher.ts:74:		branch: stats.branch,


sessions/--home-sebas-pi-config--/2026-05-07T21-49-05-595Z_019e046a-72ba-7510-8fd7-cf37fe35ac62.jsonl
sessions/--tmp-pi-tau-mux-test-project-a--/2026-04-29T16-49-04-845Z_019dda24-e74d-72aa-8f07-5b90064db8f0.jsonl
sessions/--mnt-rpi-pi-config--/2026-04-30T23-34-20-314Z_019de0be-4999-754c-b83b-b457c64be321.jsonl
sessions/--mnt-rpi-pi-config--/2026-04-30T23-44-02-399Z_019de0c7-2b5e-77f8-b8d0-337156f72056.jsonl
sessions/--mnt-rpi-pi-config--/2026-04-30T23-33-50-305Z_019de0bd-d460-71b9-a0d3-7d185ba3b079.jsonl
jobs:
20260507T200037556064Z.log
20260507T200135903751Z.json
20260507T200135903751Z.log
20260507T202641961301Z.json
20260507T202641961301Z.log
20260507T210856564296Z.json
20260507T210856564296Z.log
consolidation-20260422T142817021443Z.json
consolidation-20260422T142832064769Z.json
consolidation-20260422T164146241829Z.json


jobs 43
20260507T202641961301Z.json queued /home/sebas/pi-config/sessions/--home-sebas--/2026-05-06T05-21-05-935Z_019dfbbb-8d8e-717d-8fc3-d1f6e0b2a955.jsonl failed 0
20260507T210856564296Z.json queued /home/sebas/pi-config/sessions/--home-sebas--/2026-05-07T20-26-41-990Z_019e041f-03c5-7357-9411-4c7dd1d8f3af.jsonl failed 0
consolidation-20260422T142817021443Z.json None None None None
consolidation-20260422T142832064769Z.json None None None None
consolidation-20260422T164146241829Z.json None None None None


**Inspecting job failures**

I’m thinking about the failed jobs and I believe I need to check the logs to understand what went wrong. Maybe there's an issue with the PI binary? I'll start by reviewing the recent failed logs to gather more information. I want to make sure I understand the errors so I can find a solution. Let's take a closer look and see what specifics pop up—it could lead me to the root cause!