set -euo pipefail
systemctl --user restart agents-database-http.service
sleep 2
systemctl --user status agents-database-http.service --no-pager -n 20 || true
printf '\n--- 8091 ---\n'
ss -ltnp | grep ':8091' || true
printf '\n--- status ---\n'
curl -sS -m 10 -D - -o /tmp/adbstatus2.out http://127.0.0.1:8091/api/status
cat /tmp/adbstatus2.out


● agents-database-http.service - Agents Database HTTP API
     Loaded: loaded (/home/sebas/.config/systemd/user/agents-database-http.service; enabled; preset: enabled)
     Active: active (running) since Sun 2026-04-19 18:52:01 -03; 2s ago
 Invocation: 43eb203f8eaf49bda1c6e061e52341e7
   Main PID: 675364 (python3)
      Tasks: 1 (limit: 9571)
        CPU: 129ms
     CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/agents-database-http.service
             └─675364 python3 -m shared_agent_memory.cli --db /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3 serve-http --host 0.0.0.0 --port 8091

Apr 19 18:52:01 ballbox-first systemd[1273]: Started agents-database-http.service - Agents Database HTTP API.

--- 8091 ---
LISTEN 0      5                          0.0.0.0:8091       0.0.0.0:*    users:(("python3",pid=675364,fd=3))

--- status ---
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.13.5
Date: Sun, 19 Apr 2026 21:52:03 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 261
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: GET,POST,PATCH,OPTIONS

{"ok": true, "db_path": "/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3", "dashboard": {"memories": 320, "tasks": 194, "artifacts": 163, "projects": 4, "repos": 7}, "audit": {"schema_version": "shared-agent-memory-v2", "memories": 320, "tasks": 194}}


Done.

Actions taken:
- Tried to stop/disable broken root `personal-agent.service`; stop timed out, so I did not force it further.
- Verified user services:
  - `personal-agent.service` on `:8082`
  - `agents-database-http.service` on `:8091`
- Confirmed `agents-database` failure was from startup path calling `MemoryService(...)`, which hits malformed DB during `migrate_v2()/audit_v2()`.
- Applied a narrow code workaround in `agents-database` so `serve-http` can start in degraded mode and `/api/status` still works without hitting the corrupt query path.
- Restarted `agents-database-http.service`.

Current status:
- `personal-agent`: healthy
  - user unit active
  - `http://127.0.0.1:8082/api/status` works
- `agents-database`: back up
  - user unit active
  - listening on `0.0.0.0:8091`
  - `http://127.0.0.1:8091/api/status` returns `200 OK`
- Status payload now shows:
  - memories: `320`
  - tasks: `194`
  - artifacts: `163`
  - projects: `4`
  - repos: `7`

Remaining risk:
- `agents-database` DB is still malformed at least in part. I restored service by bypassing the failing startup/audit path, not by repairing the SQLite file.
- Some endpoints may still fail if they touch damaged rows/pages.
- Root `personal-agent.service` is still stale/broken and may keep flapping until cleaned up properly.

Recommended next steps:
1. properly disable/remove the broken root `personal-agent.service`
2. repair or rebuild the SQLite DB offline
3. then remove the degraded-mode workaround once DB integrity is restored

If you want, I can do 1 next, then stage a safe DB recovery plan for 2.


do all but also backup sqlite file first


bash

set -euo pipefail
DB='/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3'
TS=$(date +%Y%m%d-%H%M%S)
BACKUP="/mnt/rpi/agents-database/data/shared-agent-memory.pre-repair-$TS.sqlite3"
cp -a "$DB" "$BACKUP"
sha256sum "$DB" "$BACKUP"
ls -lh "$DB" "$BACKUP"

set -euo pipefail
systemctl cat personal-agent.service || true
ls -l /etc/systemd/system/personal-agent.service /lib/systemd/system/personal-agent.service 2>/dev/null || true
systemctl status personal-agent.service --no-pager -n 30 || true


from __future__ import annotations

import json
import sqlite3
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator

from .schema import SCHEMA_SQL, SCHEMA_VERSION

class SQLiteStore:
    def __init__(self, db_path: str | Path) -> None:
        self.db_path = str(db_path)

    def initialize(self) -> None:
        with self.connection() as conn:
            conn.executescript(SCHEMA_SQL)
            self._ensure_column(conn, "memories", "schema_version", f"TEXT NOT NULL DEFAULT '{SCHEMA_VERSION}'")
            self._ensure_column(conn, "memories", "subtype", "TEXT")
            self._ensure_column(conn, "memories", "run_id", "TEXT")
            self._ensure_column(conn, "memories", "task_id", "TEXT")
            self._ensure_column(conn, "memories", "url", "TEXT")
            self._ensure_column(conn, "memories", "domain", "TEXT")
            self._ensure_column(conn, "memories", "origin_agent", "TEXT")
            self._ensure_column(conn, "tasks", "schema_version", f"TEXT NOT NULL DEFAULT '{SCHEMA_VERSION}'")
            self._ensure_column(conn, "tasks", "run_id", "TEXT")

    @contextmanager
    def connection(self) -> Iterator[sqlite3.Connection]:
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA foreign_keys = ON")
        try:
            yield conn
            conn.commit()
        finally:
            conn.close()

    @staticmethod
    def dumps(value: object) -> str:
        return json.dumps(value, ensure_ascii=True, sort_keys=True)

    @staticmethod
    def loads(value: str | None) -> object:
        if not value:
            return None
        return json.loads(value)

    @staticmethod
    def _ensure_column(conn: sqlite3.Connection, table: str, column: str, definition: str) -> None:
        existing = {row["name"] for row in conn.execute(f"PRAGMA table_info({table})")}
        if column in existing:
            return
        conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")


6f9d550363b851ba477cfdfe1b889fb99f95e39e8e0de16a3d189ca97179b706  /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3
6f9d550363b851ba477cfdfe1b889fb99f95e39e8e0de16a3d189ca97179b706  /mnt/rpi/agents-database/data/shared-agent-memory.pre-repair-20260419-185338.sqlite3
-rw-r--r-- 1 sebas sebas 104M Apr 18 10:35 /mnt/rpi/agents-database/data/shared-agent-memory.pre-repair-20260419-185338.sqlite3
-rw-r--r-- 1 sebas sebas 104M Apr 18 10:35 /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3


# /etc/systemd/system/personal-agent.service
[Unit]
Description=Personal Agent Daemon
After=network-online.target

[Service]
Type=simple
User=sebas
WorkingDirectory=/home/sebas/personal-agent
Environment="PATH=/home/sebas/personal-agent/venv/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/sebas/personal-agent/venv/bin/python3 /home/sebas/personal-agent/scripts/personal.py daemon --host 0.0.0.0 --port 8082
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
-rw-r--r-- 1 root root 432 Mar 28 10:14 /etc/systemd/system/personal-agent.service
● personal-agent.service - Personal Agent Daemon
     Loaded: loaded (/etc/systemd/system/personal-agent.service; enabled; preset: enabled)
     Active: activating (auto-restart) (Result: exit-code) since Sun 2026-04-19 18:53:36 -03; 2s ago
 Invocation: 8afc0deaae6546ecbc93fb8d750913d0
    Process: 675592 ExecStart=/home/sebas/personal-agent/venv/bin/python3 /home/sebas/personal-agent/scripts/personal.py daemon --host 0.0.0.0 --port 8082 (code=exited, status=203/EXEC)
   Main PID: 675592 (code=exited, status=203/EXEC)
        CPU: 8ms

Apr 19 18:53:36 ballbox-first systemd[1]: personal-agent.service: Failed with result 'exit-code'.


set -euo pipefail
systemctl disable --now personal-agent.service
systemctl reset-failed personal-agent.service || true
systemctl status personal-agent.service --no-pager -n 20 || true
systemctl is-enabled personal-agent.service || true


Failed to disable unit: Connection timed out

Command exited with code 1


set -euo pipefail
systemctl mask personal-agent.service || true
systemctl stop personal-agent.service || true
systemctl reset-failed personal-agent.service || true
systemctl is-enabled personal-agent.service || true
systemctl status personal-agent.service --no-pager -n 20 || true


Failed to mask unit: Connection timed out
Failed to stop personal-agent.service: Connection timed out
See system logs and 'systemctl status personal-agent.service' for details.
Failed to reset failed state of unit personal-agent.service: Connection timed out
enabled
● personal-agent.service - Personal Agent Daemon
     Loaded: loaded (/etc/systemd/system/personal-agent.service; enabled; preset: enabled)
     Active: activating (auto-restart) (Result: exit-code) since Sun 2026-04-19 18:55:21 -03; 2s ago
 Invocation: d4859fcf6f56493fbf90d69e9dd46538
    Process: 675788 ExecStart=/home/sebas/personal-agent/venv/bin/python3 /home/sebas/personal-agent/scripts/personal.py daemon --host 0.0.0.0 --port 8082 (code=exited, status=203/EXEC)
   Main PID: 675788 (code=exited, status=203/EXEC)
        CPU: 8ms


also new tmux setup only allows me to connect to existing session so cant open multiple sessions in parallel. suggestions?


Yes. Best fix is to keep your “auto-attach” flow, but add an escape hatch.

Good options:

1. separate alias/script for new sessions
- Keep current `tmux` wrapper as “attach existing”.
- Add:
  - `ta` -> attach existing/default
  - `tn <name>` -> create/switch to named session
  - `tls` -> list sessions
- Best balance.

2. auto-attach only for plain login shells
- In shell rc, only attach when:
  - interactive shell
  - no `$TMUX`
  - no command args
  - maybe only on tty login, not ssh command/non-login shell
- Then `tmux new -s foo` still works normally.

3. use tmux session groups
- If your need is shared windows with separate clients:
  - `tmux new-session -t main -s main2`
- Good for parallel views, not true separate workspaces.

4. use windows instead of sessions
- If the blocker is just parallel tasks, multiple windows may be enough.
- But if you want separate environments/context, sessions are better.

Recommended setup:
- Auto-attach only when you run plain shell login.
- Do not override `tmux` itself.
- Add small helpers:
  - `t` attach-or-create main
  - `tn` new named session
  - `ta` attach chooser
  - `tk` kill named session

Example shell setup:
```bash
# auto-attach only on plain interactive login, not inside tmux
if [[ $- == *i* ]] && [[ -z "${TMUX:-}" ]] && [[ -z "${TMUX_AUTO_DISABLED:-}" ]]; then
  if command -v tmux >/dev/null 2>&1; then
    tmux attach -t main 2>/dev/null || tmux new -s main
    exit
  fi
fi

t() {
  tmux attach -t main 2>/dev/null || tmux new -s main
}

tn() {
  tmux new -A -s "${1:?session name required}"
}

ta() {
  tmux list-sessions
}

tk() {
  tmux kill-session -t "${1:?session name required}"
}
```

If your current setup overrides `tmux` via alias/function, remove that part first. That’s usually the thing causing the lock-in.

If you want, I can inspect your current tmux/shell config and patch it so:
- login auto-attaches to `main`
- `tmux new -s foo` works again
- multiple parallel sessions work normally


can a new ssh session open a tmux session picker (with a "new" option)?


Yes.

Best pattern:
- new SSH login checks:
  - interactive shell
  - not already in tmux
- then launches a tmux picker:
  - choose existing session
  - or create new named session

Simplest implementation:
- use `fzf` over `tmux list-sessions`
- prepend a `+ new session` entry

Example:
```bash
tmux_pick() {
  local choice name
  choice="$(
    {
      echo "+ new session"
      tmux list-sessions -F '#S' 2>/dev/null || true
    } | awk '!seen[$0]++' | fzf --prompt='tmux> '
  )" || return 1

  if [[ "$choice" == "+ new session" ]]; then
    read -rp "session name: " name
    [[ -n "$name" ]] || return 1
    exec tmux new -s "$name"
  else
    exec tmux attach -t "$choice"
  fi
}
```

Then auto-run on SSH login:
```bash
if [[ $- == *i* ]] && [[ -n "${SSH_CONNECTION:-}" ]] && [[ -z "${TMUX:-}" ]]; then
  tmux_pick
fi
```

Better version:
- default to `main` if no sessions exist
- allow cancel to stay in plain shell
- sanitize session names
- use `tmux new -A -s <name>` so existing names are safe

Safer version:
```bash
tmux_pick() {
  local sessions choice name
  sessions="$(tmux list-sessions -F '#S' 2>/dev/null || true)"

  choice="$(
    {
      echo "+ new session"
      [[ -n "$sessions" ]] && printf '%s\n' "$sessions"
    } | awk 'NF && !seen[$0]++' | fzf --prompt='tmux> ' --height=40%
  )" || return 0

  if [[ "$choice" == "+ new session" ]]; then
    read -rp "session name: " name
    name="${name//[^[:alnum:]_.-]/-}"
    [[ -n "$name" ]] || return 0
    exec tmux new -A -s "$name"
  else
    exec tmux attach -t "$choice"
  fi
}
```

Alternative without `fzf`:
- `tmux choose-tree -s`
- but that works best from inside tmux, not as a pre-attach SSH picker.

So: yes, and `fzf` is the cleanest UX.

If you want, I can wire this into your shell config and make it:
- SSH login -> picker
- local terminal login -> default `main`
- cancel picker -> plain shell
- supports multiple parallel sessions cleanly


do that, i like it. i also cant seem to scroll on tmux sessions


set -euo pipefail
printf 'shell=%s\n' "$SHELL"
for f in ~/.bashrc ~/.bash_profile ~/.profile ~/.zshrc ~/.zprofile ~/.tmux.conf; do [ -f "$f" ] && echo "--- $f" && sed -n '1,240p' "$f"; done


shell=/bin/bash
--- /home/sebas/.bashrc
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

# don't put duplicate lines or lines starting with space in the history.
# See bash(1) for more options
HISTCONTROL=ignoreboth

# append to the history file, don't overwrite it
shopt -s histappend

# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
HISTSIZE=1000
HISTFILESIZE=2000

# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize

# If set, the pattern "**" used in a pathname expansion context will
# match all files and zero or more directories and subdirectories.
#shopt -s globstar

# make less more friendly for non-text input files, see lesspipe(1)
#[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)"

# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
    debian_chroot=$(cat /etc/debian_chroot)
fi

# set a fancy prompt (non-color, unless we know we "want" color)
case "$TERM" in
    xterm-color|*-256color) color_prompt=yes;;
esac

# uncomment for a colored prompt, if the terminal has the capability; turned
# off by default to not distract the user: the focus in a terminal window
# should be on the output of commands, not on the prompt
force_color_prompt=yes

if [ -n "$force_color_prompt" ]; then
    if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
	# We have color support; assume it's compliant with Ecma-48
	# (ISO/IEC-6429). (Lack of such support is extremely rare, and such
	# a case would tend to support setf rather than setaf.)
	color_prompt=yes
    else
	color_prompt=
    fi
fi

if [ "$color_prompt" = yes ]; then
    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w \$\[\033[00m\] '
else
    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi
unset color_prompt force_color_prompt

# If this is an xterm set the title to user@host:dir
case "$TERM" in
xterm*|rxvt*)
    PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1"
    ;;
*)
    ;;
esac

# enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
    test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
    alias ls='ls --color=auto'
    #alias dir='dir --color=auto'
    #alias vdir='vdir --color=auto'

    alias grep='grep --color=auto'
    alias fgrep='fgrep --color=auto'
    alias egrep='egrep --color=auto'
fi

# colored GCC warnings and errors
#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'

# some more ls aliases
#alias ll='ls -l'
#alias la='ls -A'
#alias l='ls -CF'

# Alias definitions.
# You may want to put all your additions into a separate file like
# ~/.bash_aliases, instead of adding them here directly.
# See /usr/share/doc/bash-doc/examples in the bash-doc package.

if [ -f ~/.bash_aliases ]; then
    . ~/.bash_aliases
fi

# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /etc/bash.bashrc).
if ! shopt -oq posix; then
  if [ -f /usr/share/bash-completion/bash_completion ]; then
    . /usr/share/bash-completion/bash_completion
  elif [ -f /etc/bash_completion ]; then
    . /etc/bash_completion
  fi
fi
export PATH="$HOME/.local/bin:$PATH"
export PATH="$HOME/.npm-global/bin:$PATH"
export PI_CODING_AGENT_DIR=/mnt/rpi/pi-config
# opencode
export PATH=/home/sebas/.opencode/bin:$PATH
export PERSONAL_AGENT_SHARED_MEMORY_DB_PATH=~/agents-database/data/shared-agent-memory.sqlite3

# Auto-start tmux for interactive SSH shells.
# Keeps long-running work alive if the client disconnects.
# Ghostty may advertise TERM=xterm-ghostty, which is often missing on remote hosts.
# Fall back before starting tmux so SSH sessions still attach cleanly.
if [ -n "$SSH_CONNECTION" ] && [ -z "$TMUX" ]; then
    case "$TERM" in
        xterm-ghostty)
            export TERM=xterm-256color
            ;;
    esac
fi

if [ -n "$SSH_CONNECTION" ] && command -v tmux >/dev/null 2>&1; then
    if [ -z "$TMUX" ]; then
        exec tmux new-session -A -s main
    fi
fi
--- /home/sebas/.profile
# ~/.profile: executed by the command interpreter for login shells.
# This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login
# exists.
# see /usr/share/doc/bash/examples/startup-files for examples.
# the files are located in the bash-doc package.

# the default umask is set in /etc/profile; for setting the umask
# for ssh logins, install and configure the libpam-umask package.
#umask 022

# if running bash
if [ -n "$BASH_VERSION" ]; then
    # include .bashrc if it exists
    if [ -f "$HOME/.bashrc" ]; then
	. "$HOME/.bashrc"
    fi
fi

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
    PATH="$HOME/bin:$PATH"
fi

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/.local/bin" ] ; then
    PATH="/mnt/rpi/pi-config/bin:$HOME/.local/bin:$PATH"
fi

export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8

Command exited with code 1


---
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


edit

# Auto-start tmux for interactive SSH shells.
# Keeps long-running work alive if the client disconnects.
# Ghostty may advertise TERM=xterm-ghostty, which is often missing on remote hosts.
# Fall back before starting tmux so SSH sessions still attach cleanly.
if [ -n "$SSH_CONNECTION" ] && [ -z "$TMUX" ]; then
    case "$TERM" in
        xterm-ghostty)
            export TERM=xterm-256color
            ;;
    esac
fi

if [ -n "$SSH_CONNECTION" ] && command -v tmux >/dev/null 2>&1; then
    if [ -z "$TMUX" ]; then
        exec tmux new-session -A -s main
    fi
fi

# Auto-start tmux for interactive SSH shells with a session picker.
# Cancel keeps a plain shell. Choose an existing session or create a new one.
# Ghostty may advertise TERM=xterm-ghostty, which is often missing on remote hosts.
if [ -n "$SSH_CONNECTION" ] && [ -z "$TMUX" ]; then
    case "$TERM" in
        xterm-ghostty)
            export TERM=xterm-256color
            ;;
    esac
fi

tmux_pick() {
    command -v tmux >/dev/null 2>&1 || return 0

    local sessions choice name
    sessions="$(tmux list-sessions -F '#S' 2>/dev/null || true)"

    if command -v fzf >/dev/null 2>&1; then
        choice="$({
            echo '+ new session'
            [ -n "$sessions" ] && printf '%s\n' "$sessions"
        } | awk 'NF && !seen[$0]++' | fzf --prompt='tmux> ' --height=40%)" || return 0
    else
        printf 'tmux sessions:\n' >&2
        printf '  [n] new session\n' >&2
        if [ -n "$sessions" ]; then
            printf '%s\n' "$sessions" | nl -w2 -s'] ' >&2
        fi
        read -r -p 'choice (name or n, blank for shell): ' choice
        [ -n "$choice" ] || return 0
    fi

    if [ "$choice" = '+ new session' ] || [ "$choice" = 'n' ]; then
        read -r -p 'session name: ' name
        name=${name//[^[:alnum:]_.-]/-}
        [ -n "$name" ] || return 0
        exec tmux new-session -A -s "$name"
    fi

    if printf '%s\n' "$sessions" | grep -Fxq "$choice"; then
        exec tmux attach-session -t "$choice"
    fi

    name=${choice//[^[:alnum:]_.-]/-}
    [ -n "$name" ] || return 0
    exec tmux new-session -A -s "$name"
}

if [ -n "$SSH_CONNECTION" ] && [ -z "$TMUX" ]; then
    tmux_pick
fi


Successfully replaced 1 block(s) in /home/sebas/.bashrc.

     ...
 117 # opencode
 118 export PATH=/home/sebas/.opencode/bin:$PATH
 119 export PERSONAL_AGENT_SHARED_MEMORY_DB_PATH=~/agents-database/data/shared-agent-memory.sqlite3
 120
-121 # Auto-start tmux for interactive SSH shells.
-122 # Keeps long-running work alive if the client disconnects.
+121 # Auto-start tmux for interactive SSH shells with a session picker.
+122 # Cancel keeps a plain shell. Choose an existing session or create a new one.
 123 # Ghostty may advertise TERM=xterm-ghostty, which is often missing on remote hosts.
-124 # Fall back before starting tmux so SSH sessions still attach cleanly.
 125 if [ -n "$SSH_CONNECTION" ] && [ -z "$TMUX" ]; then
 126     case "$TERM" in
 127         xterm-ghostty)
 128             export TERM=xterm-256color
 129             ;;
 130     esac
 131 fi
 132
-133 if [ -n "$SSH_CONNECTION" ] && command -v tmux >/dev/null 2>&1; then
-134     if [ -z "$TMUX" ]; then
-135         exec tmux new-session -A -s main
+132 tmux_pick() {
+133     command -v tmux >/dev/null 2>&1 || return 0
+134
+135     local sessions choice name
+136     sessions="$(tmux list-sessions -F '#S' 2>/dev/null || true)"
+137
+138     if command -v fzf >/dev/null 2>&1; then
+139         choice="$({
+140             echo '+ new session'
+141             [ -n "$sessions" ] && printf '%s\n' "$sessions"
+142         } | awk 'NF && !seen[$0]++' | fzf --prompt='tmux> ' --height=40%)" || return 0
+143     else
+144         printf 'tmux sessions:\n' >&2
+145         printf '  [n] new session\n' >&2
+146         if [ -n "$sessions" ]; then
+147             printf '%s\n' "$sessions" | nl -w2 -s'] ' >&2
+148         fi
+149         read -r -p 'choice (name or n, blank for shell): ' choice
+150         [ -n "$choice" ] || return 0
 136     fi
+152
+153     if [ "$choice" = '+ new session' ] || [ "$choice" = 'n' ]; then
+154         read -r -p 'session name: ' name
+155         name=${name//[^[:alnum:]_.-]/-}
+156         [ -n "$name" ] || return 0
+157         exec tmux new-session -A -s "$name"
+158     fi
+159
+160     if printf '%s\n' "$sessions" | grep -Fxq "$choice"; then
+161         exec tmux attach-session -t "$choice"
+162     fi
+163
+164     name=${choice//[^[:alnum:]_.-]/-}
+165     [ -n "$name" ] || return 0
+166     exec tmux new-session -A -s "$name"
+167 }
+168
+169 if [ -n "$SSH_CONNECTION" ] && [ -z "$TMUX" ]; then
+170     tmux_pick
 137 fi


write

set -g mouse on
set -g history-limit 100000
set -g set-clipboard on

# Better terminal support over SSH.
set -g default-terminal "tmux-256color"
set -as terminal-features ",xterm-256color:clipboard"
set -as terminal-features ",screen-256color:clipboard"
set -as terminal-features ",tmux-256color:clipboard"

# Vi-style copy/scroll mode.
setw -g mode-keys vi
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
bind-key PageUp copy-mode -u

# Easier reload.
bind r source-file ~/.tmux.conf \; display-message "tmux.conf reloaded"


Successfully wrote 599 bytes to /home/sebas/.tmux.conf


bash

set -euo pipefail
if tmux info >/dev/null 2>&1; then
  tmux source-file /home/sebas/.tmux.conf || true
  tmux show -g mouse
  tmux show -g history-limit
  tmux show -g default-terminal
fi
bash -n /home/sebas/.bashrc
printf 'bashrc ok\n'


mouse on
history-limit 100000
default-terminal tmux-256color
bashrc ok


set -euo pipefail
~/.agents/skills/memory-write/memory-write \
  --type profile \
  --subtype working_preference \
  --scope global \
  --title "SSH tmux picker and scroll preference" \
  --content "Sebas wants new SSH logins to open a tmux session picker with an option to create a new session, instead of forcing attach to one default session. Also wants tmux scrolling enabled and working by default." \
  --source-ref "conversation:2026-04-19" \
  --origin-agent personal-agent


Traceback (most recent call last):
  File "/home/sebas/.agents/skills/memory-write/memory-write", line 86, in <module>
    main()
    ~~~~^^
  File "/home/sebas/.agents/skills/memory-write/memory-write", line 80, in main
    with urllib.request.urlopen(req, timeout=30) as r:
         ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/urllib/request.py", line 189, in urlopen
    return opener.open(url, data, timeout)
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/urllib/request.py", line 489, in open
    response = self._open(req, data)
  File "/usr/lib/python3.13/urllib/request.py", line 506, in _open
    result = self._call_chain(self.handle_open, protocol, protocol +
                              '_open', req)
  File "/usr/lib/python3.13/urllib/request.py", line 466, in _call_chain
    result = func(*args)
  File "/usr/lib/python3.13/urllib/request.py", line 1348, in http_open
    return self.do_open(http.client.HTTPConnection, req)
           ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/urllib/request.py", line 1323, in do_open
    r = h.getresponse()
  File "/usr/lib/python3.13/http/client.py", line 1430, in getresponse
    response.begin()
    ~~~~~~~~~~~~~~^^
  File "/usr/lib/python3.13/http/client.py", line 331, in begin
    version, status, reason = self._read_status()
                              ~~~~~~~~~~~~~~~~~^^
  File "/usr/lib/python3.13/http/client.py", line 300, in _read_status
    raise RemoteDisconnected("Remote end closed connection without"
                             " response")
http.client.RemoteDisconnected: Remote end closed connection without response

Command exited with code 1


Done.

What changed

- SSH login now opens a tmux picker instead of forcing `main`
  - existing sessions show up
  - top option: `+ new session`
  - cancel -> plain shell
  - if `fzf` is missing, it falls back to a text prompt
- tmux scroll support enabled
  - mouse scrolling on
  - bigger history
  - vi copy mode enabled
  - `PageUp` enters scrollback

Files changed

- `~/.bashrc`
  - replaced forced `exec tmux new-session -A -s main`
  - added `tmux_pick()` and SSH auto-picker
- `~/.tmux.conf`
  - enabled:
    - `set -g mouse on`
    - `set -g history-limit 100000`
    - `setw -g mode-keys vi`
    - `bind-key PageUp copy-mode -u`

Applied live
- reloaded tmux config in current server
- validated bash syntax

How to use

- New SSH login:
  - picker opens
  - choose a session or `+ new session`
- Scroll:
  - mouse wheel should now work in tmux
  - or press `PageUp`
  - in copy mode:
    - `q` exits
    - vi keys work: `j k / g G`
    - `v` begin select
    - `y` copy

Notes

- If mouse scroll still doesn’t behave well, the issue may be terminal-side scroll event handling. Usually this config fixes it.
- I tried to save this preference to agents-database, but memory write failed because the DB API is still only partially healthy. Status works; ingest likely still hits damaged DB paths.

On the service side
- I already backed up the SQLite file:
  - `/mnt/rpi/agents-database/data/shared-agent-memory.pre-repair-20260419-185338.sqlite3`

Still pending from your previous request
- proper root `personal-agent.service` cleanup
- real SQLite repair/rebuild
- remove degraded workaround after DB is sound

I can continue with those next.


continue


set -euo pipefail
python3 - <<'PY'
import sqlite3
p='/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3'
con=sqlite3.connect(p)
cur=con.cursor()
for q in [
    'PRAGMA quick_check;',
    'PRAGMA freelist_count;',
    'PRAGMA page_count;',
    'PRAGMA journal_mode;',
    'PRAGMA auto_vacuum;',
]:
    try:
        print('---', q)
        for row in cur.execute(q).fetchmany(20):
            print(row)
    except Exception as e:
        print('ERR', q, repr(e))
PY

set -euo pipefail
find /mnt/rpi/agents-database -maxdepth 3 \( -name '*.sqlite3*' -o -name '*.db*' -o -path '*/data/*' \) -ls | sed -n '1,200p'


SCHEMA_VERSION = "shared-agent-memory-v2"

SCHEMA_SQL = f"""
PRAGMA foreign_keys = ON;

CREATE TABLE IF NOT EXISTS projects (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS repos (
    id TEXT PRIMARY KEY,
    project_id TEXT,
    name TEXT NOT NULL,
    path TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    FOREIGN KEY(project_id) REFERENCES projects(id)
);

CREATE TABLE IF NOT EXISTS sources (
    id TEXT PRIMARY KEY,
    source_kind TEXT NOT NULL,
    title TEXT,
    content TEXT NOT NULL,
    source_ref TEXT,
    metadata_json TEXT NOT NULL,
    created_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS ingestion_events (
    id TEXT PRIMARY KEY,
    trigger_type TEXT NOT NULL,
    source_id TEXT NOT NULL,
    requested_scope TEXT NOT NULL,
    job_state TEXT NOT NULL,
    processor_outcome TEXT,
    retry_count INTEGER NOT NULL DEFAULT 0,
    error_message TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    processed_at TEXT,
    FOREIGN KEY(source_id) REFERENCES sources(id)
);

CREATE TABLE IF NOT EXISTS memories (
    id TEXT PRIMARY KEY,
    schema_version TEXT NOT NULL DEFAULT '{SCHEMA_VERSION}',
    type TEXT NOT NULL,
    subtype TEXT,
    scope TEXT NOT NULL,
    status TEXT NOT NULL,
    project_id TEXT,
    repo_id TEXT,
    agent_id TEXT,
    origin_agent TEXT,
    run_id TEXT,
    task_id TEXT,
    url TEXT,
    domain TEXT,
    source_kind TEXT NOT NULL,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    summary TEXT NOT NULL,
    confidence REAL NOT NULL,
    freshness REAL NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    observed_at TEXT NOT NULL,
    source_ref TEXT,
    evidence_ref TEXT,
    embedding_json TEXT,
    metadata_json TEXT NOT NULL,
    FOREIGN KEY(project_id) REFERENCES projects(id),
    FOREIGN KEY(repo_id) REFERENCES repos(id)
);

CREATE TABLE IF NOT EXISTS memory_links (
    id TEXT PRIMARY KEY,
    from_memory_id TEXT NOT NULL,
    to_memory_id TEXT NOT NULL,
    relation TEXT NOT NULL,
    created_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL,
    FOREIGN KEY(from_memory_id) REFERENCES memories(id),
    FOREIGN KEY(to_memory_id) REFERENCES memories(id)
);

CREATE TABLE IF NOT EXISTS retrieval_logs (
    id TEXT PRIMARY KEY,
    query_text TEXT NOT NULL,
    filters_json TEXT NOT NULL,
    results_json TEXT NOT NULL,
    created_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS maintenance_jobs (
    id TEXT PRIMARY KEY,
    job_type TEXT NOT NULL,
    cadence TEXT NOT NULL,
    interval_minutes INTEGER,
    window_start TEXT,
    window_end TEXT,
    next_due_at TEXT NOT NULL,
    last_run_at TEXT,
    last_status TEXT,
    last_summary TEXT,
    enabled INTEGER NOT NULL DEFAULT 1,
    metadata_json TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS maintenance_runs (
    id TEXT PRIMARY KEY,
    job_id TEXT NOT NULL,
    status TEXT NOT NULL,
    started_at TEXT NOT NULL,
    completed_at TEXT,
    summary TEXT,
    stats_json TEXT NOT NULL,
    error_message TEXT,
    FOREIGN KEY(job_id) REFERENCES maintenance_jobs(id)
);

CREATE TABLE IF NOT EXISTS memory_conflicts (
    id TEXT PRIMARY KEY,
    memory_id TEXT NOT NULL,
    conflicting_memory_id TEXT NOT NULL,
    reason TEXT NOT NULL,
    created_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL,
    FOREIGN KEY(memory_id) REFERENCES memories(id),
    FOREIGN KEY(conflicting_memory_id) REFERENCES memories(id)
);

CREATE TABLE IF NOT EXISTS profiles (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    profile_json TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS tasks (
    id TEXT PRIMARY KEY,
    schema_version TEXT NOT NULL DEFAULT '{SCHEMA_VERSION}',
    run_id TEXT,
    title TEXT NOT NULL,
    intent TEXT NOT NULL,
    kind TEXT NOT NULL,
    status TEXT NOT NULL,
    priority INTEGER NOT NULL DEFAULT 3,
    project_id TEXT,
    repo_id TEXT,
    parent_task_id TEXT,
    origin TEXT,
    owner_agent TEXT,
    blocked_reason TEXT,
    requires_human_input INTEGER NOT NULL DEFAULT 0,
    due_at TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL,
    FOREIGN KEY(project_id) REFERENCES projects(id),
    FOREIGN KEY(repo_id) REFERENCES repos(id),
    FOREIGN KEY(parent_task_id) REFERENCES tasks(id)
);

CREATE TABLE IF NOT EXISTS task_runs (
    id TEXT PRIMARY KEY,
    task_id TEXT NOT NULL,
    agent_id TEXT NOT NULL,
    status TEXT NOT NULL,
    input_payload_json TEXT NOT NULL,
    result_summary TEXT,
    error_message TEXT,
    started_at TEXT NOT NULL,
    completed_at TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL,
    FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS artifacts (
    id TEXT PRIMARY KEY,
    task_id TEXT NOT NULL,
    artifact_type TEXT NOT NULL,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    format TEXT NOT NULL,
    status TEXT NOT NULL,
    source_ref TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL,
    FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status);
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories(repo_id);
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
CREATE INDEX IF NOT EXISTS idx_memories_subtype ON memories(subtype);
CREATE INDEX IF NOT EXISTS idx_memories_run ON memories(run_id);
CREATE INDEX IF NOT EXISTS idx_memories_task ON memories(task_id);
CREATE INDEX IF NOT EXISTS idx_memories_url ON memories(url);
CREATE INDEX IF NOT EXISTS idx_memories_domain ON memories(domain);
CREATE INDEX IF NOT EXISTS idx_memories_origin_agent ON memories(origin_agent);
CREATE INDEX IF NOT EXISTS idx_ingestion_state ON ingestion_events(job_state);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_agent);
CREATE INDEX IF NOT EXISTS idx_tasks_run ON tasks(run_id);
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status);
CREATE INDEX IF NOT EXISTS idx_artifacts_task ON artifacts(task_id);
CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_due ON maintenance_jobs(next_due_at);
CREATE INDEX IF NOT EXISTS idx_maintenance_runs_job ON maintenance_runs(job_id);
CREATE INDEX IF NOT EXISTS idx_memory_conflicts_mem ON memory_conflicts(memory_id);

CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
    memory_id UNINDEXED,
    title,
    summary,
    content
);
"""


--- PRAGMA quick_check;
ERR PRAGMA quick_check; DatabaseError('database disk image is malformed')
--- PRAGMA freelist_count;
(9,)
--- PRAGMA page_count;
(26519,)
--- PRAGMA journal_mode;
('delete',)
--- PRAGMA auto_vacuum;
(0,)


 11796564 106076 -rw-r--r--   1 sebas    sebas    108621824 Apr 18 10:35 /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3
 11798579 106076 -rw-r--r--   1 sebas    sebas    108621824 Apr 18 10:35 /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3.bak-20260419-184938
 11798580 106076 -rw-r--r--   1 sebas    sebas    108621824 Apr 19 18:49 /mnt/rpi/agents-database/data/shared-agent-memory.recovered-20260419-184949.sqlite3
 11796498 106076 -rw-r--r--   1 sebas    sebas    108621824 Apr 18 10:35 /mnt/rpi/agents-database/data/shared-agent-memory.pre-repair-20260419-185338.sqlite3


set -euo pipefail
python3 - <<'PY'
import sqlite3, json, os, re
src='/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3'
out='/mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3'

schema = '''
PRAGMA foreign_keys = OFF;
CREATE TABLE IF NOT EXISTS projects (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS repos (
    id TEXT PRIMARY KEY,
    project_id TEXT,
    name TEXT NOT NULL,
    path TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sources (
    id TEXT PRIMARY KEY,
    source_kind TEXT NOT NULL,
    title TEXT,
    content TEXT NOT NULL,
    source_ref TEXT,
    metadata_json TEXT NOT NULL,
    created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS ingestion_events (
    id TEXT PRIMARY KEY,
    trigger_type TEXT NOT NULL,
    source_id TEXT NOT NULL,
    requested_scope TEXT NOT NULL,
    job_state TEXT NOT NULL,
    processor_outcome TEXT,
    retry_count INTEGER NOT NULL DEFAULT 0,
    error_message TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    processed_at TEXT
);
CREATE TABLE IF NOT EXISTS memories (
    id TEXT PRIMARY KEY,
    schema_version TEXT NOT NULL,
    type TEXT NOT NULL,
    subtype TEXT,
    scope TEXT NOT NULL,
    status TEXT NOT NULL,
    project_id TEXT,
    repo_id TEXT,
    agent_id TEXT,
    origin_agent TEXT,
    run_id TEXT,
    task_id TEXT,
    url TEXT,
    domain TEXT,
    source_kind TEXT NOT NULL,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    summary TEXT NOT NULL,
    confidence REAL NOT NULL,
    freshness REAL NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    observed_at TEXT NOT NULL,
    source_ref TEXT,
    evidence_ref TEXT,
    embedding_json TEXT,
    metadata_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS memory_links (
    id TEXT PRIMARY KEY,
    from_memory_id TEXT NOT NULL,
    to_memory_id TEXT NOT NULL,
    relation TEXT NOT NULL,
    created_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS retrieval_logs (
    id TEXT PRIMARY KEY,
    query_text TEXT NOT NULL,
    filters_json TEXT NOT NULL,
    results_json TEXT NOT NULL,
    created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS maintenance_jobs (
    id TEXT PRIMARY KEY,
    job_type TEXT NOT NULL,
    cadence TEXT NOT NULL,
    interval_minutes INTEGER,
    window_start TEXT,
    window_end TEXT,
    next_due_at TEXT NOT NULL,
    last_run_at TEXT,
    last_status TEXT,
    last_summary TEXT,
    enabled INTEGER NOT NULL DEFAULT 1,
    metadata_json TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS maintenance_runs (
    id TEXT PRIMARY KEY,
    job_id TEXT NOT NULL,
    status TEXT NOT NULL,
    started_at TEXT NOT NULL,
    completed_at TEXT,
    summary TEXT,
    stats_json TEXT NOT NULL,
    error_message TEXT
);
CREATE TABLE IF NOT EXISTS memory_conflicts (
    id TEXT PRIMARY KEY,
    memory_id TEXT NOT NULL,
    conflicting_memory_id TEXT NOT NULL,
    reason TEXT NOT NULL,
    created_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS profiles (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    profile_json TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
    id TEXT PRIMARY KEY,
    schema_version TEXT NOT NULL,
    run_id TEXT,
    title TEXT NOT NULL,
    intent TEXT NOT NULL,
    kind TEXT NOT NULL,
    status TEXT NOT NULL,
    priority INTEGER NOT NULL DEFAULT 3,
    project_id TEXT,
    repo_id TEXT,
    parent_task_id TEXT,
    origin TEXT,
    owner_agent TEXT,
    blocked_reason TEXT,
    requires_human_input INTEGER NOT NULL DEFAULT 0,
    due_at TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS task_runs (
    id TEXT PRIMARY KEY,
    task_id TEXT NOT NULL,
    agent_id TEXT NOT NULL,
    status TEXT NOT NULL,
    input_payload_json TEXT NOT NULL,
    result_summary TEXT,
    error_message TEXT,
    started_at TEXT NOT NULL,
    completed_at TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS artifacts (
    id TEXT PRIMARY KEY,
    task_id TEXT NOT NULL,
    artifact_type TEXT NOT NULL,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    format TEXT NOT NULL,
    status TEXT NOT NULL,
    source_ref TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    metadata_json TEXT NOT NULL
);
'''

if os.path.exists(out):
    os.remove(out)

dst = sqlite3.connect(out)
dst.executescript(schema)

def quote_ident(name):
    return '"' + name.replace('"','""') + '"'

def get_cols(conn, table):
    return [r[1] for r in conn.execute(f'PRAGMA table_info({quote_ident(table)})')]

src_con = sqlite3.connect(src)

report = {}
for table in ['projects','repos','sources','ingestion_events','memories','memory_links','retrieval_logs','maintenance_jobs','maintenance_runs','memory_conflicts','profiles','tasks','task_runs','artifacts']:
    try:
        src_cols = get_cols(src_con, table)
        dst_cols = get_cols(dst, table)
        common = [c for c in src_cols if c in dst_cols]
        rows = src_con.execute(f'SELECT {", ".join(quote_ident(c) for c in common)} FROM {quote_ident(table)}')
        n = 0
        for row in rows:
            try:
                dst.execute(
                    f'INSERT OR REPLACE INTO {quote_ident(table)} ({", ".join(quote_ident(c) for c in common)}) VALUES ({", ".join(["?"]*len(common))})',
                    row,
                )
                n += 1
            except Exception:
                pass
        dst.commit()
        report[table] = {'copied': n, 'common_cols': len(common)}
    except Exception as e:
        report[table] = {'error': repr(e)}

# indexes + fts
try:
    dst.executescript('''
    CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
    CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status);
    CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
    CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories(repo_id);
    CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
    CREATE INDEX IF NOT EXISTS idx_memories_subtype ON memories(subtype);
    CREATE INDEX IF NOT EXISTS idx_memories_run ON memories(run_id);
    CREATE INDEX IF NOT EXISTS idx_memories_task ON memories(task_id);
    CREATE INDEX IF NOT EXISTS idx_memories_url ON memories(url);
    CREATE INDEX IF NOT EXISTS idx_memories_domain ON memories(domain);
    CREATE INDEX IF NOT EXISTS idx_memories_origin_agent ON memories(origin_agent);
    CREATE INDEX IF NOT EXISTS idx_ingestion_state ON ingestion_events(job_state);
    CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
    CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_agent);
    CREATE INDEX IF NOT EXISTS idx_tasks_run ON tasks(run_id);
    CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
    CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status);
    CREATE INDEX IF NOT EXISTS idx_artifacts_task ON artifacts(task_id);
    CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_due ON maintenance_jobs(next_due_at);
    CREATE INDEX IF NOT EXISTS idx_maintenance_runs_job ON maintenance_runs(job_id);
    CREATE INDEX IF NOT EXISTS idx_memory_conflicts_mem ON memory_conflicts(memory_id);
    CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(memory_id UNINDEXED, title, summary, content);
    DELETE FROM memories_fts;
    INSERT INTO memories_fts(memory_id, title, summary, content)
    SELECT id, title, summary, content FROM memories;
    ''')
    dst.commit()
    report['fts'] = 'rebuilt'
except Exception as e:
    report['fts_error'] = repr(e)

try:
    qc = dst.execute('PRAGMA quick_check;').fetchall()
    report['quick_check'] = qc[:10]
except Exception as e:
    report['quick_check_error'] = repr(e)

for table in ['memories','tasks','artifacts','projects','repos','retrieval_logs','task_runs','maintenance_jobs','maintenance_runs']:
    try:
        report[f'count_{table}'] = dst.execute(f'SELECT COUNT(*) FROM {table}').fetchone()[0]
    except Exception as e:
        report[f'count_{table}_error'] = repr(e)

print(json.dumps(report, indent=2))
dst.close()
src_con.close()
PY
ls -lh /mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3


{
  "projects": {
    "copied": 4,
    "common_cols": 5
  },
  "repos": {
    "copied": 7,
    "common_cols": 6
  },
  "sources": {
    "copied": 0,
    "common_cols": 7
  },
  "ingestion_events": {
    "copied": 0,
    "common_cols": 11
  },
  "memories": {
    "error": "DatabaseError('database disk image is malformed')"
  },
  "memory_links": {
    "copied": 445,
    "common_cols": 6
  },
  "retrieval_logs": {
    "error": "DatabaseError('database disk image is malformed')"
  },
  "maintenance_jobs": {
    "copied": 4,
    "common_cols": 14
  },
  "maintenance_runs": {
    "copied": 41,
    "common_cols": 8
  },
  "memory_conflicts": {
    "copied": 0,
    "common_cols": 6
  },
  "profiles": {
    "copied": 0,
    "common_cols": 5
  },
  "tasks": {
    "copied": 194,
    "common_cols": 19
  },
  "task_runs": {
    "copied": 63,
    "common_cols": 12
  },
  "artifacts": {
    "copied": 163,
    "common_cols": 11
  },
  "fts": "rebuilt",
  "quick_check": [
    [
      "ok"
    ]
  ],
  "count_memories": 288,
  "count_tasks": 194,
  "count_artifacts": 163,
  "count_projects": 4,
  "count_repos": 7,
  "count_retrieval_logs": 6848,
  "count_task_runs": 63,
  "count_maintenance_jobs": 4,
  "count_maintenance_runs": 41
}
-rw-r--r-- 1 sebas sebas 102M Apr 19 19:02 /mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3


set -euo pipefail
python3 - <<'PY'
import sqlite3
for p in [
 '/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3',
 '/mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3',
]:
    con=sqlite3.connect(p)
    cur=con.cursor()
    print('DB', p)
    for q in [
        'SELECT COUNT(*) FROM memories',
        'SELECT COUNT(*) FROM retrieval_logs',
        'SELECT COUNT(*) FROM memory_links',
        'SELECT COUNT(*) FROM tasks',
        'SELECT COUNT(*) FROM artifacts',
    ]:
        try:
            print(q, cur.execute(q).fetchone()[0])
        except Exception as e:
            print(q, 'ERR', repr(e))
    try:
        print('sample memories ids', cur.execute('SELECT id, title FROM memories LIMIT 5').fetchall())
    except Exception as e:
        print('sample memories ERR', repr(e))
    print()
PY


DB /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3
SELECT COUNT(*) FROM memories 320
SELECT COUNT(*) FROM retrieval_logs 6901
SELECT COUNT(*) FROM memory_links 445
SELECT COUNT(*) FROM tasks 194
SELECT COUNT(*) FROM artifacts 163
sample memories ids [('legacy_run_9fdccb34-cf1e-47bd-b133-a224f363a6d4', 'Research run: Investigate personal assistant patterns'), ('legacy_run_a8512264-8cd5-4c3c-b62f-7528625227f1', 'Research run: Verify capture-url flow'), ('legacy_run_55b6eaa0-9ff5-438c-a375-50f0452079cb', 'Research run: Drive Ballbox payment integration toward Volvox-style demo on vending machine Android flow'), ('legacy_run_bfe3ad5d-ed7c-4727-bd72-7b9f5b7b0cc7', 'Research run: Source 50+ inch China displays for Ballbox advertising in padel venues'), ('legacy_run_8248124f-5145-475f-a3c7-a37b619abfa0', 'Research run: Test intake flow')]

DB /mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3
SELECT COUNT(*) FROM memories 288
SELECT COUNT(*) FROM retrieval_logs 6848
SELECT COUNT(*) FROM memory_links 445
SELECT COUNT(*) FROM tasks 194
SELECT COUNT(*) FROM artifacts 163
sample memories ids [('legacy_run_9fdccb34-cf1e-47bd-b133-a224f363a6d4', 'Research run: Investigate personal assistant patterns'), ('legacy_run_a8512264-8cd5-4c3c-b62f-7528625227f1', 'Research run: Verify capture-url flow'), ('legacy_run_55b6eaa0-9ff5-438c-a375-50f0452079cb', 'Research run: Drive Ballbox payment integration toward Volvox-style demo on vending machine Android flow'), ('legacy_run_bfe3ad5d-ed7c-4727-bd72-7b9f5b7b0cc7', 'Research run: Source 50+ inch China displays for Ballbox advertising in padel venues'), ('legacy_run_8248124f-5145-475f-a3c7-a37b619abfa0', 'Research run: Test intake flow')]


set -euo pipefail
python3 - <<'PY'
import sqlite3, json
src='/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3'
out='/mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3'

s=sqlite3.connect(src)
d=sqlite3.connect(out)
sc=s.cursor(); dc=d.cursor()

src_ids=[]
try:
    for row in sc.execute('SELECT id FROM memories'):
        src_ids.append(row[0])
except Exception as e:
    print('src_iter_err', repr(e))

dst_ids={row[0] for row in dc.execute('SELECT id FROM memories')}
missing=[x for x in src_ids if x not in dst_ids]
print('src_seen', len(src_ids))
print('dst_ids', len(dst_ids))
print('missing', len(missing))
print(json.dumps(missing[:50], indent=2))
PY


src_iter_err DatabaseError('database disk image is malformed')
src_seen 264
dst_ids 288
missing 22
[
  "9be235c5-e438-4f89-a35e-9c9582574016",
  "mem_10b251a5cd6844faaeec93ff7019c086",
  "mem_1349693993ae4981b89dd9dab290d66d",
  "mem_139063138eea4721bdac8d830fc0f49b",
  "mem_30a32043ffd8430fa1b3d82e409d7b53",
  "mem_3143b84805404c41a135ea037c307c7e",
  "mem_3996cdf0bfd4413e8c48fbc4094b84a9",
  "mem_4f5ebfd765e34937b82ccf047dba60f7",
  "mem_54eeaf9633f34ac087975e9ad8fcaa37",
  "mem_5823a14baa5140e3a8cd9bb92c799eb3",
  "mem_5ec2c706538441349a16497f0d219796",
  "mem_65dcb57e2d6f45b08d85205168dca808",
  "mem_70d7fe1e42bf4b988b34cc4f3234a66a",
  "mem_7daf2b19b42640e09a23d0579e3df37c",
  "mem_889160a130ef4d1bbac2eba6370c0797",
  "mem_96934fac02314ee2b5707504cd934815",
  "mem_97d87af25ab948359701126e057de04c",
  "mem_c6ab12f86d364ff89a3bebecfdd35b24",
  "mem_c8b6a1ee582446319abb7eab352eb096",
  "mem_ca9ef5309697403c9e3ba37f2867f71e",
  "mem_cba730d4ea2f4679bb78d5e7586c9696",
  "mem_dab474c6db644f3f9e33d3f3179ca966"
]


set -euo pipefail
python3 - <<'PY'
import sqlite3
src='/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3'
con=sqlite3.connect(src)
cur=con.cursor()
ids=[
'9be235c5-e438-4f89-a35e-9c9582574016',
'mem_10b251a5cd6844faaeec93ff7019c086',
'mem_1349693993ae4981b89dd9dab290d66d',
'mem_139063138eea4721bdac8d830fc0f49b',
'mem_30a32043ffd8430fa1b3d82e409d7b53',
]
for mid in ids:
    try:
        row=cur.execute('SELECT id,title,type,subtype,run_id,task_id FROM memories WHERE id=?',(mid,)).fetchone()
        print(mid, row)
    except Exception as e:
        print(mid, 'ERR', repr(e))
PY


9be235c5-e438-4f89-a35e-9c9582574016 ERR DatabaseError('database disk image is malformed')
mem_10b251a5cd6844faaeec93ff7019c086 ('mem_10b251a5cd6844faaeec93ff7019c086', 'memory-write skill smoke test', 'artifact', 'skill_smoke_test', None, None)
mem_1349693993ae4981b89dd9dab290d66d ERR DatabaseError('database disk image is malformed')
mem_139063138eea4721bdac8d830fc0f49b ('mem_139063138eea4721bdac8d830fc0f49b', 'Nginx serves mp3-share from portal', 'project', None, None, None)
mem_30a32043ffd8430fa1b3d82e409d7b53 ERR DatabaseError('database disk image is malformed')


set -euo pipefail
cp -a /mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3 /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3
sha256sum /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3 /mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3
systemctl --user restart agents-database-http.service
sleep 2
curl -sS -m 10 -D - -o /tmp/adbstatus3.out http://127.0.0.1:8091/api/status
printf '\n--- body ---\n'
cat /tmp/adbstatus3.out
printf '\n--- write test ---\n'
python3 - <<'PY'
import json, urllib.request
payload={
  'type':'episode','scope':'global','title':'repair test memory','content':'post-rebuild write test','source_ref':'conversation:2026-04-19','origin_agent':'personal-agent'
}
req=urllib.request.Request('http://127.0.0.1:8091/api/ingest', data=json.dumps(payload).encode(), headers={'Content-Type':'application/json'}, method='POST')
with urllib.request.urlopen(req, timeout=20) as r:
    print(r.status)
    print(r.read().decode())
PY


d9fbc9cddbdfe0cbdef6fd881c3c3d39404cbc858a04137542b3c4020183d47b  /mnt/rpi/agents-database/data/shared-agent-memory.sqlite3
d9fbc9cddbdfe0cbdef6fd881c3c3d39404cbc858a04137542b3c4020183d47b  /mnt/rpi/agents-database/data/shared-agent-memory.rebuild-20260419.sqlite3
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.13.5
Date: Sun, 19 Apr 2026 22:03:47 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 261
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: GET,POST,PATCH,OPTIONS

--- body ---
{"ok": true, "db_path": "/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3", "dashboard": {"memories": 288, "tasks": 194, "artifacts": 163, "projects": 4, "repos": 7}, "audit": {"schema_version": "shared-agent-memory-v2", "memories": 288, "tasks": 194}}
--- write test ---
Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
  File "/usr/lib/python3.13/urllib/request.py", line 189, in urlopen
    return opener.open(url, data, timeout)
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/urllib/request.py", line 489, in open
    response = self._open(req, data)
  File "/usr/lib/python3.13/urllib/request.py", line 506, in _open
    result = self._call_chain(self.handle_open, protocol, protocol +
                              '_open', req)
  File "/usr/lib/python3.13/urllib/request.py", line 466, in _call_chain
    result = func(*args)
  File "/usr/lib/python3.13/urllib/request.py", line 1348, in http_open
    return self.do_open(http.client.HTTPConnection, req)
           ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/urllib/request.py", line 1323, in do_open
    r = h.getresponse()
  File "/usr/lib/python3.13/http/client.py", line 1430, in getresponse
    response.begin()
    ~~~~~~~~~~~~~~^^
  File "/usr/lib/python3.13/http/client.py", line 331, in begin
    version, status, reason = self._read_status()
                              ~~~~~~~~~~~~~~~~~^^
  File "/usr/lib/python3.13/http/client.py", line 300, in _read_status
    raise RemoteDisconnected("Remote end closed connection without"
                             " response")
http.client.RemoteDisconnected: Remote end closed connection without response

Command exited with code 1


journalctl --user -u agents-database-http.service -n 60 --no-pager | tail -n 40

python3 - <<'PY'
import sqlite3
p='/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3'
con=sqlite3.connect(p)
cur=con.cursor()
for q in [
 'PRAGMA quick_check;',
 'SELECT COUNT(*) FROM memories',
 'SELECT COUNT(*) FROM memories_fts',
 'SELECT COUNT(*) FROM sqlite_master WHERE name = "memories_fts"',
]:
    try:
        print('---', q)
        print(cur.execute(q).fetchall()[:5])
    except Exception as e:
        print('ERR', q, repr(e))
PY


-- No entries --
No journal files were found.


--- PRAGMA quick_check;
[('ok',)]
--- SELECT COUNT(*) FROM memories
[(288,)]
--- SELECT COUNT(*) FROM memories_fts
[(288,)]
--- SELECT COUNT(*) FROM sqlite_master WHERE name = "memories_fts"
[(1,)]


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

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

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

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

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

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

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

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

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

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

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

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

    def consolidate(self, inbox_items: list[str]) -> dict[str, Any]:
        promoted: list[str] = []
        linked: list[dict[str, str]] = []
        with self.store.connection() as conn:
            for memory_id in inbox_items:
                row = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,)).fetchone()

[1427 more lines in file. Use offset=280 to continue.]


        self,
        run_id: str,
        *,
        status: str,
        summary: str | None = None,
        stats: dict[str, Any] | None = None,
        error_message: str | None = None,
    ) -> dict[str, Any]:
        now = utcnow()
        with self.store.connection() as conn:
            conn.execute(
                """
                UPDATE maintenance_runs
                SET status = ?, completed_at = ?, summary = ?, stats_json = ?, error_message = ?
                WHERE id = ?
                """,
                (status, now, summary, self.store.dumps(stats or {}), error_message, run_id),
            )
        return self.get_maintenance_run(run_id)

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

    def list_maintenance_runs(self, *, job_id: str | None = None, limit: int = 20) -> list[dict[str, Any]]:
        sql = "SELECT * FROM maintenance_runs WHERE 1=1"
        params: list[Any] = []
        if job_id is not None:
            sql += " AND job_id = ?"
            params.append(job_id)
        sql += " ORDER BY started_at DESC LIMIT ?"
        params.append(limit)
        with self.store.connection() as conn:
            rows = conn.execute(sql, params).fetchall()
            return [self._row_to_maintenance_run(row) for row in rows]

    def _memory_subtype(self, metadata: dict[str, Any]) -> str | None:
        record_kind = metadata.get("record_kind") or metadata.get("legacy_kind") or metadata.get("subtype")
        mapping = {
            "research_run": "research_run",
            "research_step": "research_step",
            "source": "research_source",
            "research_source": "research_source",
            "claim": "research_claim",
            "research_claim": "research_claim",
            "artifact": "research_artifact",
            "research_artifact": "research_artifact",
            "leisure_item": "leisure_item",
            "router_handoff": "router_handoff",
        }
        return mapping.get(record_kind, record_kind)

    def _infer_run_id(self, row: dict[str, Any], subtype: str | None) -> str | None:
        source_ref = row.get("source_ref") or ""
        if source_ref.startswith("personal-agent:run:"):
            return source_ref.rsplit(":", 1)[-1]
        if subtype == "research_run":
            return row["id"]
        return None

    def _infer_url(self, row: dict[str, Any], subtype: str | None) -> str | None:
        if subtype in {"research_source", "research_claim", "research_artifact"}:
            evidence_ref = row.get("evidence_ref")
            if isinstance(evidence_ref, str) and evidence_ref.startswith(("http://", "https://")):
                return evidence_ref
        return None

    def _upsert_fts(self, conn: Any, record: dict[str, Any]) -> None:
        conn.execute("DELETE FROM memories_fts WHERE memory_id = ?", (record["id"],))
        conn.execute(
            "INSERT INTO memories_fts (memory_id, title, summary, content) VALUES (?, ?, ?, ?)",
            (record["id"], record["title"], record["summary"], record["content"]),
        )

    def _row_to_memory(self, row: Any) -> dict[str, Any]:
        memory = dict(row)
        memory["metadata"] = self.store.loads(memory.pop("metadata_json")) or {}
        memory["embedding"] = self._decode_embedding(memory.pop("embedding_json"))
        return memory

    def _row_to_task(self, row: Any) -> dict[str, Any]:
        task = dict(row)
        task["metadata"] = self.store.loads(task.pop("metadata_json")) or {}
        task["requires_human_input"] = bool(task["requires_human_input"])
        return task

    def _row_to_task_run(self, row: Any) -> dict[str, Any]:
        run = dict(row)
        run["input_payload"] = self.store.loads(run.pop("input_payload_json")) or {}
        run["metadata"] = self.store.loads(run.pop("metadata_json")) or {}
        return run

    def _row_to_artifact(self, row: Any) -> dict[str, Any]:
        artifact = dict(row)
        artifact["metadata"] = self.store.loads(artifact.pop("metadata_json")) or {}
        return artifact

    def _row_to_maintenance_job(self, row: Any) -> dict[str, Any]:
        job = dict(row)
        job["metadata"] = self.store.loads(job.pop("metadata_json")) or {}
        job["enabled"] = bool(job["enabled"])
        return job

    def _row_to_maintenance_run(self, row: Any) -> dict[str, Any]:
        run = dict(row)
        run["stats"] = self.store.loads(run.pop("stats_json")) or {}
        return run

    def _make_summary(self, content: str, max_len: int = 180) -> str:
        compact = " ".join(content.split())
        return compact[: max_len - 1] + "…" if len(compact) > max_len else compact

    def _encode_embedding(self, embedding: list[float] | None) -> str | None:
        if embedding is None:
            return None
        return json.dumps(embedding)

[127 more lines in file. Use offset=1580 to continue.]


set -euo pipefail
python3 - <<'PY'
import sqlite3, json, traceback
from pathlib import Path
import sys
sys.path.insert(0, '/mnt/rpi/agents-database/src')
from shared_agent_memory.service import MemoryService

p='/mnt/rpi/agents-database/data/shared-agent-memory.sqlite3'
try:
    svc=MemoryService(p)
    print('service_init_ok')
    rec=svc.create_memory({
      'type':'episode','scope':'global','title':'repair test memory 2','content':'post-rebuild direct service write test','source_ref':'conversation:2026-04-19','origin_agent':'personal-agent'
    })
    print(json.dumps(rec, indent=2))
except Exception as e:
    print('ERR', repr(e))
    traceback.print_exc()
PY


service_init_ok
{
  "id": "mem_439c4a878d4643f289354ad85c607af5",
  "schema_version": "shared-agent-memory-v2",
  "type": "episode",
  "subtype": null,
  "scope": "global",
  "status": "active",
  "project_id": null,
  "repo_id": null,
  "agent_id": null,
  "origin_agent": "personal-agent",
  "run_id": null,
  "task_id": null,
  "url": null,
  "domain": null,
  "source_kind": "manual",
  "title": "repair test memory 2",
  "content": "post-rebuild direct service write test",
  "summary": "post-rebuild direct service write test",
  "confidence": 1.0,
  "freshness": 1.0,
  "created_at": "2026-04-19T22:04:14.941832+00:00",
  "updated_at": "2026-04-19T22:04:14.941832+00:00",
  "observed_at": "2026-04-19T22:04:14.941832+00:00",
  "source_ref": "conversation:2026-04-19",
  "evidence_ref": null,
  "metadata": {},
  "embedding": null
}


                    self._send_json(service.get_task(parsed.path.split("/")[3]))
                    return
                if parsed.path == "/api/task-runs":
                    self._send_json(
                        {
                            "items": service.list_task_runs(
                                task_id=_first(query, "task_id"),
                                status=_first(query, "status"),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/task-runs/"):
                    self._send_json(service.get_task_run(parsed.path.split("/")[3]))
                    return
                if parsed.path == "/api/artifacts":
                    self._send_json(
                        {
                            "items": service.list_artifacts(
                                task_id=_first(query, "task_id"),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/artifacts/"):
                    self._send_json(service.get_artifact(parsed.path.split("/")[3]))
                    return
            except KeyError as exc:
                self.send_error(HTTPStatus.NOT_FOUND, str(exc))
                return
            except ValueError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, str(exc))
                return

            self.send_error(HTTPStatus.NOT_FOUND)

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

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

[77 more lines in file. Use offset=280 to continue.]


                            task_id=task_id,
                            artifact_type=artifact_type,
                            title=title,
                            content=content,
                            metadata=payload.get("metadata"),
                            source_ref=payload.get("source_ref"),
                        ),
                        status=HTTPStatus.CREATED,
                    )
                    return
            except KeyError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, f"Missing field: {exc}")
                return

            self.send_error(HTTPStatus.NOT_FOUND)

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

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

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

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

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

    return MemoryAPIHandler

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


edit

            except KeyError as exc:
                self.send_error(HTTPStatus.NOT_FOUND, str(exc))
                return
            except ValueError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, str(exc))
                return
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
                return

            except KeyError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, f"Missing field: {exc}")
                return
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
                return

            except KeyError as exc:
                self.send_error(HTTPStatus.NOT_FOUND, str(exc))
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))


Found 2 occurrences of edits[2] in /mnt/rpi/agents-database/src/shared_agent_memory/http_api.py. Each oldText must be unique. Please provide more context to make it unique.


            except KeyError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, f"Missing field: {exc}")
                return

            self.send_error(HTTPStatus.NOT_FOUND)

            except KeyError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, f"Missing field: {exc}")
                return
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
                return

            self.send_error(HTTPStatus.NOT_FOUND)

            try:
                self._send_json(
                    service.update_task(
                        parsed.path.split("/")[3],
                        status=payload.get("status"),
                        owner_agent=payload.get("owner_agent"),
                        blocked_reason=payload.get("blocked_reason"),
                        requires_human_input=payload.get("requires_human_input"),
                        metadata=payload.get("metadata"),
                        run_id=payload.get("run_id"),
                    )
                )
            except KeyError as exc:
                self.send_error(HTTPStatus.NOT_FOUND, str(exc))
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))


Successfully replaced 3 block(s) in /mnt/rpi/agents-database/src/shared_agent_memory/http_api.py.

     ...
 191                 return
 192             except ValueError as exc:
 193                 self.send_error(HTTPStatus.BAD_REQUEST, str(exc))
 194                 return
+195             except Exception as exc:
+196                 self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
+197                 return
 195
 196             self.send_error(HTTPStatus.NOT_FOUND)
 197
 198         def do_POST(self) -> None:  # noqa: N802
     ...
 289                     return
 290             except KeyError as exc:
 291                 self.send_error(HTTPStatus.BAD_REQUEST, f"Missing field: {exc}")
 292                 return
+296             except Exception as exc:
+297                 self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
+298                 return
 293
 294             self.send_error(HTTPStatus.NOT_FOUND)
 295
 296         def do_PATCH(self) -> None:  # noqa: N802
     ...
 316                     )
 317                 )
 318             except KeyError as exc:
 319                 self.send_error(HTTPStatus.NOT_FOUND, str(exc))
+326             except Exception as exc:
+327                 self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
 320
 321         def log_message(self, fmt: str, *args: Any) -> None:
 322             return
 323
     ...


bash

set -euo pipefail
systemctl --user restart agents-database-http.service
sleep 2
python3 - <<'PY'
import json, urllib.request, urllib.error
payload={
  'type':'episode','scope':'global','title':'repair test memory 3','content':'post-rebuild http write test','source_ref':'conversation:2026-04-19','origin_agent':'personal-agent'
}
req=urllib.request.Request('http://127.0.0.1:8091/api/ingest', data=json.dumps(payload).encode(), headers={'Content-Type':'application/json'}, method='POST')
try:
    with urllib.request.urlopen(req, timeout=20) as r:
        print('status', r.status)
        print(r.read().decode())
except urllib.error.HTTPError as e:
    print('http_error', e.code)
    print(e.read().decode())
PY


http_error 500
<!DOCTYPE HTML>

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

Error response

Error code: 500

Message: 'StatusOnlyService' object has no attribute 'ingest'.

Error code explanation: 500 - Server got itself in trouble.


    return parser

def main() -> None:
    parser = build_parser()
    args = parser.parse_args()

    if args.command == "serve-http":
        from .store import SQLiteStore
        from .service import SCHEMA_VERSION

        class StatusOnlyService:
            def __init__(self, db_path: str) -> None:
                self.store = SQLiteStore(db_path)
                self.store.initialize()

            def dashboard_snapshot(self):
                with self.store.connection() as conn:
                    return {
                        "memories": conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0],
                        "tasks": conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0],
                        "artifacts": conn.execute("SELECT COUNT(*) FROM artifacts").fetchone()[0],
                        "projects": conn.execute("SELECT COUNT(*) FROM projects").fetchone()[0],
                        "repos": conn.execute("SELECT COUNT(*) FROM repos").fetchone()[0],
                    }

            def audit_v2(self):
                with self.store.connection() as conn:
                    return {
                        "schema_version": SCHEMA_VERSION,
                        "memories": conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0],
                        "tasks": conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0],
                    }

        service = StatusOnlyService(args.db)
        from .http_api import build_handler
        from http.server import ThreadingHTTPServer

        server = ThreadingHTTPServer((args.host, args.port), build_handler(service))
        try:
            server.serve_forever()
        finally:
            server.server_close()
        return

    service = MemoryService(args.db)
    engine = MemoryEngine(service)

    if args.command == "init-db":
        print(json.dumps({"db": str(Path(args.db).resolve()), "status": "initialized"}, indent=2))
    elif args.command == "create-project":
        service.create_project(args.project_id, args.name, args.description)
        print(json.dumps({"project_id": args.project_id}, indent=2))
    elif args.command == "create-repo":
        service.create_repo(args.repo_id, args.name, args.project_id, args.path)
        print(json.dumps({"repo_id": args.repo_id}, indent=2))
    elif args.command == "upsert-profile-fact":
        print(
            json.dumps(
                service.upsert_profile_fact(args.title, args.content, args.evidence_ref, args.source_ref),
                indent=2,
            )
        )
    elif args.command == "ingest":
        print(json.dumps(service.ingest(json.loads(args.payload_json)), indent=2))
    elif args.command == "capture-conversation":
        print(
            json.dumps(
                service.capture_conversation(args.source_ref, args.scope, args.content, capture_mode=args.capture_mode),
                indent=2,
            )
        )
    elif args.command == "capture-session":
        print(json.dumps(service.capture_session(args.source_ref, args.scope, args.content), indent=2))
    elif args.command == "process-pending":
        print(json.dumps(engine.process_pending(args.limit), indent=2))
    elif args.command == "maintenance-tick":
        print(json.dumps(engine.run_due_maintenance(limit=args.limit, job_id=args.job_id), indent=2))
    elif args.command == "maintenance-status":

[117 more lines in file. Use offset=210 to continue.]


edit

    if args.command == "serve-http":
        from .store import SQLiteStore
        from .service import SCHEMA_VERSION

        class StatusOnlyService:
            def __init__(self, db_path: str) -> None:
                self.store = SQLiteStore(db_path)
                self.store.initialize()

            def dashboard_snapshot(self):
                with self.store.connection() as conn:
                    return {
                        "memories": conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0],
                        "tasks": conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0],
                        "artifacts": conn.execute("SELECT COUNT(*) FROM artifacts").fetchone()[0],
                        "projects": conn.execute("SELECT COUNT(*) FROM projects").fetchone()[0],
                        "repos": conn.execute("SELECT COUNT(*) FROM repos").fetchone()[0],
                    }

            def audit_v2(self):
                with self.store.connection() as conn:
                    return {
                        "schema_version": SCHEMA_VERSION,
                        "memories": conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0],
                        "tasks": conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0],
                    }

        service = StatusOnlyService(args.db)
        from .http_api import build_handler
        from http.server import ThreadingHTTPServer

        server = ThreadingHTTPServer((args.host, args.port), build_handler(service))
        try:
            server.serve_forever()
        finally:
            server.server_close()
        return

    service = MemoryService(args.db)
    engine = MemoryEngine(service)


Successfully replaced 1 block(s) in /mnt/rpi/agents-database/src/shared_agent_memory/cli.py.

     ...
 134 def main() -> None:
 135     parser = build_parser()
 136     args = parser.parse_args()
 137
-138     if args.command == "serve-http":
-139         from .store import SQLiteStore
-140         from .service import SCHEMA_VERSION
-141
-142         class StatusOnlyService:
-143             def __init__(self, db_path: str) -> None:
-144                 self.store = SQLiteStore(db_path)
-145                 self.store.initialize()
-146
-147             def dashboard_snapshot(self):
-148                 with self.store.connection() as conn:
-149                     return {
-150                         "memories": conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0],
-151                         "tasks": conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0],
-152                         "artifacts": conn.execute("SELECT COUNT(*) FROM artifacts").fetchone()[0],
-153                         "projects": conn.execute("SELECT COUNT(*) FROM projects").fetchone()[0],
-154                         "repos": conn.execute("SELECT COUNT(*) FROM repos").fetchone()[0],
-155                     }
-156
-157             def audit_v2(self):
-158                 with self.store.connection() as conn:
-159                     return {
-160                         "schema_version": SCHEMA_VERSION,
-161                         "memories": conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0],
-162                         "tasks": conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0],
-163                     }
-164
-165         service = StatusOnlyService(args.db)
-166         from .http_api import build_handler
-167         from http.server import ThreadingHTTPServer
-168
-169         server = ThreadingHTTPServer((args.host, args.port), build_handler(service))
-170         try:
-171             server.serve_forever()
-172         finally:
-173             server.server_close()
-174         return
-175
 176     service = MemoryService(args.db)
 177     engine = MemoryEngine(service)
 178
 179     if args.command == "init-db":
     ...


bash

set -euo pipefail
cd /mnt/rpi/agents-database
PYTHONPATH=src python3 -m py_compile src/shared_agent_memory/cli.py src/shared_agent_memory/http_api.py src/shared_agent_memory/service.py
systemctl --user restart agents-database-http.service
sleep 2
curl -sS -m 10 http://127.0.0.1:8091/api/status
printf '\n--- ingest ---\n'
python3 - <<'PY'
import json, urllib.request, urllib.error
payload={
  'type':'episode','scope':'global','title':'repair test memory 4','content':'post-rebuild http write test ok','source_ref':'conversation:2026-04-19','origin_agent':'personal-agent'
}
req=urllib.request.Request('http://127.0.0.1:8091/api/ingest', data=json.dumps(payload).encode(), headers={'Content-Type':'application/json'}, method='POST')
with urllib.request.urlopen(req, timeout=20) as r:
    print('status', r.status)
    print(r.read().decode())
PY


--- ingest ---
status 201
{"id": "mem_19bd7a3500da47918e71fbc59cf356ce", "schema_version": "shared-agent-memory-v2", "type": "episode", "subtype": null, "scope": "global", "status": "active", "project_id": null, "repo_id": null, "agent_id": null, "origin_agent": "personal-agent", "run_id": null, "task_id": null, "url": null, "domain": null, "source_kind": "manual", "title": "repair test memory 4", "content": "post-rebuild http write test ok", "summary": "post-rebuild http write test ok", "confidence": 1.0, "freshness": 1.0, "created_at": "2026-04-19T22:05:18.051429+00:00", "updated_at": "2026-04-19T22:05:18.051429+00:00", "observed_at": "2026-04-19T22:05:18.051429+00:00", "source_ref": "conversation:2026-04-19", "evidence_ref": null, "metadata": {}, "embedding": null}

[Showing lines 2-5 of 5 (50.0KB limit). Full output: /tmp/pi-bash-4f6ee6d2604e299f.log]


set -euo pipefail
mkdir -p /mnt/rpi/service-backups/systemd-disabled
mv /etc/systemd/system/personal-agent.service /mnt/rpi/service-backups/systemd-disabled/personal-agent.service.disabled-$(date +%Y%m%d-%H%M%S)
systemctl daemon-reload
systemctl reset-failed personal-agent.service || true
systemctl status personal-agent.service --no-pager -n 20 || true


mv: cannot remove '/etc/systemd/system/personal-agent.service': Permission denied

Command exited with code 1