memory hook for pi sessions still has an incomplete title


**Inspecting the code repository**

I need to check the repository, as it looks like we have to fix some code. I'll probably take a look at the memory database too. Maybe the project is related to pi-config? I'm considering if using a to-do list is necessary for this small task, but it feels like something that can be handled without one. So, I should search for any relevant information to help guide me!

bash

cd /mnt/rpi/pi-config && rg -n "memory hook|incomplete title|title" -S .


./extensions/questionnaire.ts:400:			let text = theme.fg("toolTitle", theme.bold("questionnaire "));
./extensions/session-memory-dispatcher.ts:97:	async function runPrompt(ctx: any, reason: string, options?: { allowCancel?: boolean; title?: string; timeoutMs?: number }) {
./extensions/session-memory-dispatcher.ts:106:		const title = options?.title || "Before closing this session";
./extensions/session-memory-dispatcher.ts:109:		const choice = await ctx.ui.select(title, choices, dialogOptions);
./extensions/session-memory-dispatcher.ts:143:		const result = await runPrompt(ctx, `before_switch:${event.reason}`, { allowCancel: true, title: "Before closing this session" });
./extensions/session-memory-dispatcher.ts:149:		const result = await runPrompt(ctx, "before_fork", { allowCancel: true, title: "Before forking away from this session" });
./extensions/session-memory-dispatcher.ts:155:		const result = await runPrompt(ctx, `before_shutdown:${event.reason}`, { allowCancel: true, title: "Guardar insights en memoria?", timeoutMs: 12000 });
./extensions/session-memory-dispatcher.ts:161:		await runPrompt(ctx, "shutdown_fallback", { allowCancel: false, title: "Guardar insights en memoria?", timeoutMs: 12000 });
./extensions/session-memory-dispatcher.ts:168:			await runPrompt(ctx, "manual_command", { allowCancel: false, title: "Queue session analysis from this session" });
./extensions/subagent/index.ts:732:					theme.fg("toolTitle", theme.bold("subagent ")) +
./extensions/subagent/index.ts:752:					theme.fg("toolTitle", theme.bold("subagent ")) +
./extensions/subagent/index.ts:765:				theme.fg("toolTitle", theme.bold("subagent ")) +
./extensions/subagent/index.ts:806:					let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
./extensions/subagent/index.ts:842:				let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
./extensions/subagent/index.ts:878:								theme.fg("toolTitle", theme.bold("chain ")) +
./extensions/subagent/index.ts:935:					theme.fg("toolTitle", theme.bold("chain ")) +
./extensions/subagent/index.ts:968:							`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
./extensions/subagent/index.ts:1017:				let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
./scripts/session-memory-analyzer.py:95:  "title": string,
./scripts/session-memory-analyzer.py:156:                "title": analysis.get("title") or "Session insight",


---
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: memory-write
description: Save a durable high-signal memory into agents-database
---

# memory-write

## Use When

- new information is likely useful beyond current chat or task
- saving a decision, preference, project fact, reusable source/artifact, or handoff summary
- user asks to remember something durable

## Do Not Use When

- content is trivial, redundant, ephemeral, or likely useless later
- content is secret unless user explicitly asked to store it

## Required Workflow

1. If useful context may already exist, optionally check first:
   - `curl -fsSL "${AGENTS_DB_API:-http://100.116.176.16:8091}/api/search?q=<query>&scope=global&scope=project&limit=10"`
2. Choose best memory type:
   - `profile`: stable user preference, priority, recurring constraint
   - `decision`: chosen approach or operating rule
   - `project`: durable project fact or invariant
   - `artifact`: reusable source, claim, reference, output
   - `episode`: concise session summary, blocked state, handoff context
3. Build a concise payload with:
   - required: `type`, `scope`, `title`, `content`
   - recommended when known: `subtype`, `project_id`, `repo_id`, `task_id`, `run_id`, `source_ref`, `origin_agent`, `url`, `domain`, `metadata`
4. Save it with the helper:
   - `~/.agents/skills/memory-write/memory-write --type <type> --scope <scope> --title "<title>" --content "<content>"`
5. Confirm the created memory id and what was stored.

## Quick Examples

```bash
~/.agents/skills/memory-write/memory-write \
  --type profile \
  --subtype working_preference \
  --scope global \
  --title "User prefers concise status updates" \
  --content "Sebas prefers short, direct updates with business impact first." \
  --source-ref "conversation:2026-04-17" \
  --origin-agent personal-agent

~/.agents/skills/memory-write/memory-write \
  --type decision \
  --subtype operating_rule \
  --scope global \
  --title "Agents should save durable high-signal memory opportunistically" \
  --content "Treat agents-database as shared durable memory, not only search." \
  --metadata '{"source":"conversation"}'
```

## Rules

- prefer one strong memory over many weak ones
- keep `title` short and searchable
- keep `content` factual and compact
- use `episode` for summaries, not for every chat turn
- include `url` and `domain` for web-derived artifacts when known
- include `project_id`, `repo_id`, `task_id`, and `run_id` when context exists


**Inspecting files**

I need to take a closer look at the files related to session-memory-analyzer and dispatcher. It sounds like there’s something specific I need to find or understand. I wonder what I’ll discover when I dig into those files. I’ll be sure to check for any errors or irregularities that might be present. This could help clarify any ongoing issues or confirm if everything's functioning as it should!


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

import argparse
import json
import os
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")

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 = [
        "pi",
        "-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 deciding whether a Pi session has durable value.

Rules:
- Prefer NOT saving anything unless it is durable and reusable.
- 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.
- Save only if the session contains a stable user preference, enduring project fact, reusable external artifact, decision, procedure, or blocker that will matter in future unrelated sessions.
- If nothing durable exists, return save=false.
- If the memory is likely durable, assign the best type and include evidence.
- Output JSON only. No markdown. No extra text.

Return this schema:
{{
  "save": boolean,
  "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
}}

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.

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 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.setdefault("analysis", {})
    job["analysis"]["status"] = "running"
    save_job(job_file, job)

    try:
        raw = call_pi(build_prompt(job))
        analysis = extract_json(raw)
        job["analysis"]["raw"] = raw
        job["analysis"]["result"] = analysis
        job["analysis"]["status"] = "completed"

        if bool(analysis.get("save")):
            memory_type = analysis.get("type") or "episode"
            is_candidate = memory_type == "episode" or bool(float(analysis.get("importance") or 0.5) < 0.75)
            memory = {
                "type": memory_type,
                "subtype": analysis.get("subtype"),
                "scope": analysis.get("scope") or meta.get("scope") or "global",
                "status": "inbox" if is_candidate else "active",
                "source_kind": "manual",
                "title": analysis.get("title") or "Session insight",
                "content": analysis.get("content") or "",
                "confidence": float(analysis.get("confidence") or 0.75),
                "freshness": float(analysis.get("freshness") or 0.9),
                "importance": float(analysis.get("importance") or 0.5),
                "source_ref": analysis.get("source_ref") or f"pi-session:{meta.get('session_file') or meta.get('session_id') or 'unknown'}",
                "evidence_ref": analysis.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.get("reason"),
                    "trigger": meta.get("trigger"),
                    "promotion_candidate": not is_candidate,
                },
            }
            saved = ingest(memory)
            job["analysis"]["memory_id"] = saved.get("id")
            job["analysis"]["saved"] = True
        else:
            job["analysis"]["saved"] = False
            job["analysis"]["memory_id"] = 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: "Before closing this session" });
		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: "Before forking away from this session" });
		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: "Guardar insights en memoria?", 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: "Guardar insights en memoria?", 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: "Queue session analysis from this session" });
		},
	});
}


**Investigating title issues**

I’m noticing an issue with an incomplete title, which might mean the analyzer is accepting an empty or truncated title. I should probably check the dispatcher service to see if the title is being pulled from the UI correctly. It’s also a good idea to review the search session memory to gather more context. I want to ensure I sort this out thoroughly to avoid any further complications!

cd /mnt/rpi/pi-config && rg -n "session-memory|analysis.get\(\"title\"|Session insight|title\"" extensions scripts -S


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";
extensions/session-memory-dispatcher.ts:125:				trigger: "session-memory-dispatcher",
scripts/session-memory-analyzer.py:83:- Do NOT save operational notes about this session-memory system, dispatch flow, or local implementation work unless they affect broader workflows beyond this repo.
scripts/session-memory-analyzer.py:95:  "title": string,
scripts/session-memory-analyzer.py:156:                "title": analysis.get("title") or "Session insight",
scripts/session-memory-analyzer.py:163:                "origin_agent": "pi-session-memory-analyzer",
scripts/session-memory-dispatcher.py:13:JOB_DIR = Path(os.environ.get("PI_SESSION_MEMORY_JOB_DIR", BASE_DIR / "sessions" / "session-memory-jobs"))
scripts/session-memory-dispatcher.py:14:ANALYZER = Path(os.environ.get("PI_SESSION_MEMORY_ANALYZER", BASE_DIR / "scripts" / "session-memory-analyzer.py"))
scripts/session-memory-dispatcher.py:49:        if self.path != "/api/session-memory":
scripts/session-memory-dispatcher.py:89:    print(f"session-memory-dispatcher listening on http://{args.host}:{args.port}/api/session-memory", flush=True)
scripts/install-session-memory-dispatcher.service:8:ExecStart=/usr/bin/env python3 /mnt/rpi/pi-config/scripts/session-memory-dispatcher.py --host 127.0.0.1 --port 17882


**Fixing title issues**

I need to inspect and possibly sanitize the title since it might be showing a known bug about incomplete titles in saved memories. It seems I should enhance the prompt to enforce complete sentence titles and perform validation. I suspect the title from the LLM got truncated, leading to the phrase “Fix session memory hook for pi sessions still has an incomplete.” I’ll look into adding repair logic to prevent trailing connectives from the content, and maybe update the analyzer with a normalize_title function and some prompt guidance.


#!/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:
    subprocess.Popen(
        ["python3", str(ANALYZER), "--job-file", str(job_file)],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        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())


**Checking git status**

I need to check the git status to see if there are any changes I should be aware of. I'll also want to look at the current branch and make sure everything's in order. It makes sense to patch the analyzer after I confirm the status to ensure I’m working with the latest information. Keeping everything updated is crucial for smooth progress! Let’s get started on that.

edit

from pathlib import Path
from typing import Any
from urllib import request
import re

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:

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

def normalize_title(title: Any, content: Any, fallback: str = "Session insight") -> str:
    value = re.sub(r"\s+", " ", str(title or "")).strip(" -:\n\t")
    content_text = re.sub(r"\s+", " ", str(content or "")).strip()

    bad_endings = {
        "a",
        "an",
        "and",
        "as",
        "at",
        "by",
        "de",
        "del",
        "el",
        "en",
        "for",
        "from",
        "has",
        "in",
        "into",
        "la",
        "las",
        "los",
        "of",
        "on",
        "or",
        "para",
        "por",
        "still",
        "the",
        "to",
        "with",
        "without",
        "y",
    }

    def looks_incomplete(text: str) -> bool:
        if not text:
            return True
        words = text.split()
        if len(words) < 3:
            return True
        if words[-1].lower() in bad_endings:
            return True
        if text.endswith(("/", "(", "[", "{", "-", ":")):
            return True
        if text.count("(") > text.count(")"):
            return True
        if text.count("[") > text.count("]"):
            return True
        return False

    if not looks_incomplete(value):
        return value[:140]

    if content_text:
        sentence = re.split(r"(?<=[.!?])\s+|\n+", content_text, maxsplit=1)[0].strip(" -:\n\t")
        sentence = re.sub(r"\s+", " ", sentence)
        if sentence and not looks_incomplete(sentence):
            return sentence[:140]
        if sentence:
            words = sentence.split()
            if len(words) >= 3:
                return " ".join(words[: min(len(words), 12)])[:140]

    return fallback

def build_prompt(job: dict[str, Any]) -> str:

- If the memory is likely durable, assign the best type and include evidence.
- Output JSON only. No markdown. No extra text.

- If the memory is likely durable, assign the best type and include evidence.
- Title must be a complete, standalone phrase. Never end it with a dangling connector, article, preposition, or unfinished clause.
- Output JSON only. No markdown. No extra text.

"title": analysis.get("title") or "Session insight",


Successfully replaced 4 block(s) in /mnt/rpi/pi-config/scripts/session-memory-analyzer.py.

     ...
   8 import sys
   9 from pathlib import Path
  10 from typing import Any
  11 from urllib import request
+ 12 import re
  12
  13 AGENTS_DB_API = os.environ.get("AGENTS_DB_API", "http://100.116.176.16:8091")
  14 PI_MODEL = os.environ.get("PI_SESSION_MEMORY_MODEL", "gpt-5.4-mini")
  15 PI_THINKING = os.environ.get("PI_SESSION_MEMORY_THINKING", "off")
     ...
  72         return payload
  73     return job
  74
  75
+ 77 def normalize_title(title: Any, content: Any, fallback: str = "Session insight") -> str:
+ 78     value = re.sub(r"\s+", " ", str(title or "")).strip(" -:\n\t")
+ 79     content_text = re.sub(r"\s+", " ", str(content or "")).strip()
+ 80
+ 81     bad_endings = {
+ 82         "a",
+ 83         "an",
+ 84         "and",
+ 85         "as",
+ 86         "at",
+ 87         "by",
+ 88         "de",
+ 89         "del",
+ 90         "el",
+ 91         "en",
+ 92         "for",
+ 93         "from",
+ 94         "has",
+ 95         "in",
+ 96         "into",
+ 97         "la",
+ 98         "las",
+ 99         "los",
+100         "of",
+101         "on",
+102         "or",
+103         "para",
+104         "por",
+105         "still",
+106         "the",
+107         "to",
+108         "with",
+109         "without",
+110         "y",
+111     }
+112
+113     def looks_incomplete(text: str) -> bool:
+114         if not text:
+115             return True
+116         words = text.split()
+117         if len(words) < 3:
+118             return True
+119         if words[-1].lower() in bad_endings:
+120             return True
+121         if text.endswith(("/", "(", "[", "{", "-", ":")):
+122             return True
+123         if text.count("(") > text.count(")"):
+124             return True
+125         if text.count("[") > text.count("]"):
+126             return True
+127         return False
+128
+129     if not looks_incomplete(value):
+130         return value[:140]
+131
+132     if content_text:
+133         sentence = re.split(r"(?<=[.!?])\s+|\n+", content_text, maxsplit=1)[0].strip(" -:\n\t")
+134         sentence = re.sub(r"\s+", " ", sentence)
+135         if sentence and not looks_incomplete(sentence):
+136             return sentence[:140]
+137         if sentence:
+138             words = sentence.split()
+139             if len(words) >= 3:
+140                 return " ".join(words[: min(len(words), 12)])[:140]
+141
+142     return fallback
+143
+144
  76 def build_prompt(job: dict[str, Any]) -> str:
  77     meta = session_meta(job)
  78     return f"""You are deciding whether a Pi session has durable value.
  79
     ...
  83 - Do NOT save operational notes about this session-memory system, dispatch flow, or local implementation work unless they affect broader workflows beyond this repo.
  84 - Save only if the session contains a stable user preference, enduring project fact, reusable external artifact, decision, procedure, or blocker that will matter in future unrelated sessions.
  85 - If nothing durable exists, return save=false.
  86 - If the memory is likely durable, assign the best type and include evidence.
+156 - Title must be a complete, standalone phrase. Never end it with a dangling connector, article, preposition, or unfinished clause.
  87 - Output JSON only. No markdown. No extra text.
  88
  89 Return this schema:
  90 {{
     ...
 152                 "subtype": analysis.get("subtype"),
 153                 "scope": analysis.get("scope") or meta.get("scope") or "global",
 154                 "status": "inbox" if is_candidate else "active",
 155                 "source_kind": "manual",
-156                 "title": analysis.get("title") or "Session insight",
+226                 "title": normalize_title(analysis.get("title"), analysis.get("content")),
 157                 "content": analysis.get("content") or "",
 158                 "confidence": float(analysis.get("confidence") or 0.75),
 159                 "freshness": float(analysis.get("freshness") or 0.9),
 160                 "importance": float(analysis.get("importance") or 0.5),
     ...


bash

cd /mnt/rpi/pi-config && python3 -m py_compile scripts/session-memory-analyzer.py && git diff -- scripts/session-memory-analyzer.py


diff --git a/scripts/session-memory-analyzer.py b/scripts/session-memory-analyzer.py
index aafda6b..b8ed31b 100644
--- a/scripts/session-memory-analyzer.py
+++ b/scripts/session-memory-analyzer.py
@@ -9,6 +9,7 @@ import sys
 from pathlib import Path
 from typing import Any
 from urllib import request
+import re

 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")
@@ -73,6 +74,74 @@ def session_meta(job: dict[str, Any]) -> dict[str, Any]:
     return job

+def normalize_title(title: Any, content: Any, fallback: str = "Session insight") -> str:
+    value = re.sub(r"\s+", " ", str(title or "")).strip(" -:\n\t")
+    content_text = re.sub(r"\s+", " ", str(content or "")).strip()
+
+    bad_endings = {
+        "a",
+        "an",
+        "and",
+        "as",
+        "at",
+        "by",
+        "de",
+        "del",
+        "el",
+        "en",
+        "for",
+        "from",
+        "has",
+        "in",
+        "into",
+        "la",
+        "las",
+        "los",
+        "of",
+        "on",
+        "or",
+        "para",
+        "por",
+        "still",
+        "the",
+        "to",
+        "with",
+        "without",
+        "y",
+    }
+
+    def looks_incomplete(text: str) -> bool:
+        if not text:
+            return True
+        words = text.split()
+        if len(words) < 3:
+            return True
+        if words[-1].lower() in bad_endings:
+            return True
+        if text.endswith(("/", "(", "[", "{", "-", ":")):
+            return True
+        if text.count("(") > text.count(")"):
+            return True
+        if text.count("[") > text.count("]"):
+            return True
+        return False
+
+    if not looks_incomplete(value):
+        return value[:140]
+
+    if content_text:
+        sentence = re.split(r"(?<=[.!?])\s+|\n+", content_text, maxsplit=1)[0].strip(" -:\n\t")
+        sentence = re.sub(r"\s+", " ", sentence)
+        if sentence and not looks_incomplete(sentence):
+            return sentence[:140]
+        if sentence:
+            words = sentence.split()
+            if len(words) >= 3:
+                return " ".join(words[: min(len(words), 12)])[:140]
+
+    return fallback
+
+
 def build_prompt(job: dict[str, Any]) -> str:
     meta = session_meta(job)
     return f"""You are deciding whether a Pi session has durable value.
@@ -84,6 +153,7 @@ Rules:
 - Save only if the session contains a stable user preference, enduring project fact, reusable external artifact, decision, procedure, or blocker that will matter in future unrelated sessions.
 - If nothing durable exists, return save=false.
 - If the memory is likely durable, assign the best type and include evidence.
+- Title must be a complete, standalone phrase. Never end it with a dangling connector, article, preposition, or unfinished clause.
 - Output JSON only. No markdown. No extra text.

 Return this schema:
@@ -153,7 +223,7 @@ def main() -> int:
                 "scope": analysis.get("scope") or meta.get("scope") or "global",
                 "status": "inbox" if is_candidate else "active",
                 "source_kind": "manual",
-                "title": analysis.get("title") or "Session insight",
+                "title": normalize_title(analysis.get("title"), analysis.get("content")),
                 "content": analysis.get("content") or "",
                 "confidence": float(analysis.get("confidence") or 0.75),
                 "freshness": float(analysis.get("freshness") or 0.9),


**Considering service options**

I’m thinking about whether I need to mention restarting the service. The analyzer launches the script each time, so no restart is needed if the file path is the same, which is good. I wonder if I should save the memory of that decision, but maybe it’s not necessary. I really want to keep my response concise. It’s fascinating to weigh these aspects carefully!

Fixed.

Change:
- tightened analyzer prompt: title must be complete
- added `normalize_title()` fallback in `scripts/session-memory-analyzer.py`
- if model returns a dangling title, it now repairs from content or falls back safely

Check:
- `python3 -m py_compile scripts/session-memory-analyzer.py` passed

No service restart needed. New session-memory jobs will use it.


what? no. i wanted to change the title of the menu asking to analyze the session


Right. I changed wrong thing.

Menu titles live here:
- `/mnt/rpi/pi-config/extensions/session-memory-dispatcher.ts`

Current titles:
- `Before closing this session`
- `Before forking away from this session`
- `Guardar insights en memoria?`
- `Queue session analysis from this session`

Need exact replacement text.
Give me the new title string, or I can set all prompts to:

`Analyze this session for memory?`


yes. revert the other thing because i dont even know what it was


edit

from pathlib import Path
from typing import Any
from urllib import request
import re

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

def normalize_title(title: Any, content: Any, fallback: str = "Session insight") -> str:
    value = re.sub(r"\s+", " ", str(title or "")).strip(" -:\n\t")
    content_text = re.sub(r"\s+", " ", str(content or "")).strip()

    bad_endings = {
        "a",
        "an",
        "and",
        "as",
        "at",
        "by",
        "de",
        "del",
        "el",
        "en",
        "for",
        "from",
        "has",
        "in",
        "into",
        "la",
        "las",
        "los",
        "of",
        "on",
        "or",
        "para",
        "por",
        "still",
        "the",
        "to",
        "with",
        "without",
        "y",
    }

    def looks_incomplete(text: str) -> bool:
        if not text:
            return True
        words = text.split()
        if len(words) < 3:
            return True
        if words[-1].lower() in bad_endings:
            return True
        if text.endswith(("/", "(", "[", "{", "-", ":")):
            return True
        if text.count("(") > text.count(")"):
            return True
        if text.count("[") > text.count("]"):
            return True
        return False

    if not looks_incomplete(value):
        return value[:140]

    if content_text:
        sentence = re.split(r"(?<=[.!?])\s+|\n+", content_text, maxsplit=1)[0].strip(" -:\n\t")
        sentence = re.sub(r"\s+", " ", sentence)
        if sentence and not looks_incomplete(sentence):
            return sentence[:140]
        if sentence:
            words = sentence.split()
            if len(words) >= 3:
                return " ".join(words[: min(len(words), 12)])[:140]

    return fallback

def build_prompt(job: dict[str, Any]) -> str:

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:

- If the memory is likely durable, assign the best type and include evidence.
- Title must be a complete, standalone phrase. Never end it with a dangling connector, article, preposition, or unfinished clause.
- Output JSON only. No markdown. No extra text.

- If the memory is likely durable, assign the best type and include evidence.
- Output JSON only. No markdown. No extra text.

"title": analysis.get("title") or "Session insight",


     ...
   8 import sys
   9 from pathlib import Path
  10 from typing import Any
  11 from urllib import request
- 12 import re
  13
  14 AGENTS_DB_API = os.environ.get("AGENTS_DB_API", "http://100.116.176.16:8091")
  15 PI_MODEL = os.environ.get("PI_SESSION_MEMORY_MODEL", "gpt-5.4-mini")
  16 PI_THINKING = os.environ.get("PI_SESSION_MEMORY_THINKING", "off")
     ...
  73         return payload
  74     return job
  75
  76
- 77 def normalize_title(title: Any, content: Any, fallback: str = "Session insight") -> str:
- 78     value = re.sub(r"\s+", " ", str(title or "")).strip(" -:\n\t")
- 79     content_text = re.sub(r"\s+", " ", str(content or "")).strip()
- 80
- 81     bad_endings = {
- 82         "a",
- 83         "an",
- 84         "and",
- 85         "as",
- 86         "at",
- 87         "by",
- 88         "de",
- 89         "del",
- 90         "el",
- 91         "en",
- 92         "for",
- 93         "from",
- 94         "has",
- 95         "in",
- 96         "into",
- 97         "la",
- 98         "las",
- 99         "los",
-100         "of",
-101         "on",
-102         "or",
-103         "para",
-104         "por",
-105         "still",
-106         "the",
-107         "to",
-108         "with",
-109         "without",
-110         "y",
-111     }
-112
-113     def looks_incomplete(text: str) -> bool:
-114         if not text:
-115             return True
-116         words = text.split()
-117         if len(words) < 3:
-118             return True
-119         if words[-1].lower() in bad_endings:
-120             return True
-121         if text.endswith(("/", "(", "[", "{", "-", ":")):
-122             return True
-123         if text.count("(") > text.count(")"):
-124             return True
-125         if text.count("[") > text.count("]"):
-126             return True
-127         return False
-128
-129     if not looks_incomplete(value):
-130         return value[:140]
-131
-132     if content_text:
-133         sentence = re.split(r"(?<=[.!?])\s+|\n+", content_text, maxsplit=1)[0].strip(" -:\n\t")
-134         sentence = re.sub(r"\s+", " ", sentence)
-135         if sentence and not looks_incomplete(sentence):
-136             return sentence[:140]
-137         if sentence:
-138             words = sentence.split()
-139             if len(words) >= 3:
-140                 return " ".join(words[: min(len(words), 12)])[:140]
-141
-142     return fallback
-143
-144
 145 def build_prompt(job: dict[str, Any]) -> str:
 146     meta = session_meta(job)
 147     return f"""You are deciding whether a Pi session has durable value.
 148
     ...
 152 - Do NOT save operational notes about this session-memory system, dispatch flow, or local implementation work unless they affect broader workflows beyond this repo.
 153 - Save only if the session contains a stable user preference, enduring project fact, reusable external artifact, decision, procedure, or blocker that will matter in future unrelated sessions.
 154 - If nothing durable exists, return save=false.
 155 - If the memory is likely durable, assign the best type and include evidence.
-156 - Title must be a complete, standalone phrase. Never end it with a dangling connector, article, preposition, or unfinished clause.
 157 - Output JSON only. No markdown. No extra text.
 158
 159 Return this schema:
 160 {{
     ...
 222                 "subtype": analysis.get("subtype"),
 223                 "scope": analysis.get("scope") or meta.get("scope") or "global",
 224                 "status": "inbox" if is_candidate else "active",
 225                 "source_kind": "manual",
-226                 "title": normalize_title(analysis.get("title"), analysis.get("content")),
+156                 "title": analysis.get("title") or "Session insight",
 227                 "content": analysis.get("content") or "",
 228                 "confidence": float(analysis.get("confidence") or 0.75),
 229                 "freshness": float(analysis.get("freshness") or 0.9),
 230                 "importance": float(analysis.get("importance") or 0.5),
     ...


	pi.on("session_before_switch", async (event, ctx) => {
		const result = await runPrompt(ctx, `before_switch:${event.reason}`, { allowCancel: true, title: "Before closing this session" });
		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: "Before forking away from this session" });
		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: "Guardar insights en memoria?", 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: "Guardar insights en memoria?", 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: "Queue session analysis from this session" });
		},
	});

	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?" });
		},
	});


Successfully replaced 1 block(s) in /mnt/rpi/pi-config/extensions/session-memory-dispatcher.ts.

     ...
 139 		}
 140 	}
 141
 142 	pi.on("session_before_switch", async (event, ctx) => {
-143 		const result = await runPrompt(ctx, `before_switch:${event.reason}`, { allowCancel: true, title: "Before closing this session" });
+143 		const result = await runPrompt(ctx, `before_switch:${event.reason}`, { allowCancel: true, title: "Analyze this session for memory?" });
 144 		if (result.cancel) return { cancel: true };
 145 		return undefined;
 146 	});
 147
 148 	pi.on("session_before_fork", async (_event, ctx) => {
-149 		const result = await runPrompt(ctx, "before_fork", { allowCancel: true, title: "Before forking away from this session" });
+149 		const result = await runPrompt(ctx, "before_fork", { allowCancel: true, title: "Analyze this session for memory?" });
 150 		if (result.cancel) return { cancel: true };
 151 		return undefined;
 152 	});
 153
 154 	pi.on("session_before_shutdown", async (event, ctx) => {
-155 		const result = await runPrompt(ctx, `before_shutdown:${event.reason}`, { allowCancel: true, title: "Guardar insights en memoria?", timeoutMs: 12000 });
+155 		const result = await runPrompt(ctx, `before_shutdown:${event.reason}`, { allowCancel: true, title: "Analyze this session for memory?", timeoutMs: 12000 });
 156 		if (result.cancel) return { cancel: true };
 157 		return undefined;
 158 	});
 159
 160 	pi.on("session_shutdown", async (_event, ctx) => {
-161 		await runPrompt(ctx, "shutdown_fallback", { allowCancel: false, title: "Guardar insights en memoria?", timeoutMs: 12000 });
+161 		await runPrompt(ctx, "shutdown_fallback", { allowCancel: false, title: "Analyze this session for memory?", timeoutMs: 12000 });
 162 	});
 163
 164 	pi.registerCommand("remember-session", {
 165 		description: "Queue a session analysis from the current session",
 166 		handler: async (_args, ctx) => {
 167 			promptHandledForCurrentSession = false;
-168 			await runPrompt(ctx, "manual_command", { allowCancel: false, title: "Queue session analysis from this session" });
+168 			await runPrompt(ctx, "manual_command", { allowCancel: false, title: "Analyze this session for memory?" });
 169 		},
 170 	});
 171 }


bash

cd /mnt/rpi/pi-config && python3 -m py_compile scripts/session-memory-analyzer.py && git diff -- scripts/session-memory-analyzer.py extensions/session-memory-dispatcher.ts


diff --git a/extensions/session-memory-dispatcher.ts b/extensions/session-memory-dispatcher.ts
index 6063e7b..9080989 100644
--- a/extensions/session-memory-dispatcher.ts
+++ b/extensions/session-memory-dispatcher.ts
@@ -140,32 +140,32 @@ export default function (pi: ExtensionAPI) {
 	}

 	pi.on("session_before_switch", async (event, ctx) => {
-		const result = await runPrompt(ctx, `before_switch:${event.reason}`, { allowCancel: true, title: "Before closing this session" });
+		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: "Before forking away from this session" });
+		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: "Guardar insights en memoria?", timeoutMs: 12000 });
+		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: "Guardar insights en memoria?", timeoutMs: 12000 });
+		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: "Queue session analysis from this session" });
+			await runPrompt(ctx, "manual_command", { allowCancel: false, title: "Analyze this session for memory?" });
 		},
 	});
 }


Done.

Reverted:
- all analyzer/title logic changes in `scripts/session-memory-analyzer.py`

Changed:
- all session memory prompt titles now:
  - `Analyze this session for memory?`

File:
- `/mnt/rpi/pi-config/extensions/session-memory-dispatcher.ts`

Note:
- if Pi loads extensions at startup, restart/reload Pi session to see it.