Yes, but “free” here is misleading.

What seems true:
- Google still advertises a **Free Tier**
- but your actual API calls are hitting a
  billing state that says:
  `prepayment credits are depleted`

So likely model is:

- free tier exists in principle
- but your key/project is attached to an
  AI Studio / Gemini API billing bucket
  that is exhausted or not eligible
- therefore API returns 429 anyway

Possible reasons:
1. free tier has project/account limits
2. old AI Studio projects moved to
   prepaid billing behavior
3. region/account/project state affects it
4. “free tier” does not mean
   “unlimited no-billing API forever”

So bluntly:
- yes, it was *marketed* as free tier
- no, that does not mean your current
  project can still use it for free now

The error message is the key signal:
- not “invalid key”
- not “model unavailable”
- specifically:
  **credits depleted**

That points to billing state, not config.

Best interpretation:
the free tier is not currently usable
for this specific key/project.


[telegram] check online how pi people do it. pretty sure you still can for free


bash

python3 - <<'PY'
import requests
urls=[
 'https://github.com/mariozechner/pi-coding-agent',
 'https://github.com/mariozechner/pi-coding-agent/blob/main/README.md',
 'https://github.com/mariozechner/pi-coding-agent/search?q=GEMINI_API_KEY&type=code',
 'https://mariozechner.at/posts/2025-02-15-how-i-use-pi/'
]
for u in urls:
    try:
        r=requests.get(u,timeout=25,headers={'User-Agent':'Mozilla/5.0'})
        print('\nURL',u,'STATUS',r.status_code)
        print(r.text[:2000].replace('\n',' '))
    except Exception as e:
        print('ERR',u,e)
PY

python3 - <<'PY'
import requests,re
readme=requests.get('https://raw.githubusercontent.com/mariozechner/pi-coding-agent/main/README.md',timeout=25,headers={'User-Agent':'Mozilla/5.0'}).text
for pat in ['GEMINI_API_KEY','--provider google','free','AI Studio','provider \(default: google\)']:
 m=re.search(pat, readme, re.I)
 print(pat, 'yes' if m else 'no')
if 'GEMINI_API_KEY' in readme:
 idx=readme.find('GEMINI_API_KEY')
 print(readme[idx-500:idx+900])
PY

python3 - <<'PY'
import requests,re
text=requests.get('https://raw.githubusercontent.com/mariozechner/pi-coding-agent/main/README.md',timeout=25,headers={'User-Agent':'Mozilla/5.0'}).text
for kw in ['google', 'gemini', 'free tier', 'ai studio', 'api key']:
    print('\nKW',kw)
    for m in re.finditer(kw, text, re.I):
        s=max(0,m.start()-120); e=min(len(text),m.end()+220)
        print(text[s:e].replace('\n',' '))
        break
PY


URL https://github.com/mariozechner/pi-coding-agent STATUS 404
        <!DOCTYPE html>             <meta charset="utf-8">   <link rel="dns-prefetch" href="https://github.githubassets.com">   <link rel="dns-prefetch" href="https://avatars.githubusercontent.com">   <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com">   <link rel="dns-prefetch" href="https://user-images.githubusercontent.com/">   <link rel="preconnect" href="https://github.githubassets.com" crossorigin>   <link rel="preconnect" href="https://avatars.githubusercontent.com">        <link crossorigin="anonymous" rel="preload" as="script" href="https://github.githubassets.com/assets/global-banner-disable-7d723d7ca7c7142f.js" />    <link rel="preload" href="https://github.githubassets.com/assets/MonaSansVF-wdth-wght-opsz-902d64c7ad02.woff2" as="font" type="font/woff2" crossorigin>     <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/light-4fded0090af0ad58.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/light_high_contrast-cf8e26bc17e62ebc.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/dark-06381ff23d863842.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/dark_high_contrast-9023e6605402defb.css" /><link data-color-theme="light" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light-4fded0090af0ad58.css" /><link data-color-theme="light_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_high_contrast-cf8e26bc17e62ebc.css" /><link data-color-theme="light_colorblind" crossorigin="anonymous" media="all" rel="stylesheet"

URL https://github.com/mariozechner/pi-coding-agent/blob/main/README.md STATUS 404
        <!DOCTYPE html>             <meta charset="utf-8">   <link rel="dns-prefetch" href="https://github.githubassets.com">   <link rel="dns-prefetch" href="https://avatars.githubusercontent.com">   <link rel="dns-prefetch" href="https://github-cloud.s3.amazonaws.com">   <link rel="dns-prefetch" href="https://user-images.githubusercontent.com/">   <link rel="preconnect" href="https://github.githubassets.com" crossorigin>   <link rel="preconnect" href="https://avatars.githubusercontent.com">        <link crossorigin="anonymous" rel="preload" as="script" href="https://github.githubassets.com/assets/global-banner-disable-7d723d7ca7c7142f.js" />    <link rel="preload" href="https://github.githubassets.com/assets/MonaSansVF-wdth-wght-opsz-902d64c7ad02.woff2" as="font" type="font/woff2" crossorigin>     <link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/light-4fded0090af0ad58.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/light_high_contrast-cf8e26bc17e62ebc.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/dark-06381ff23d863842.css" /><link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/dark_high_contrast-9023e6605402defb.css" /><link data-color-theme="light" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light-4fded0090af0ad58.css" /><link data-color-theme="light_high_contrast" crossorigin="anonymous" media="all" rel="stylesheet" data-href="https://github.githubassets.com/assets/light_high_contrast-cf8e26bc17e62ebc.css" /><link data-color-theme="light_colorblind" crossorigin="anonymous" media="all" rel="stylesheet"

URL https://github.com/mariozechner/pi-coding-agent/search?q=GEMINI_API_KEY&type=code STATUS 404
Not Found

URL https://mariozechner.at/posts/2025-02-15-how-i-use-pi/ STATUS 200
  <!DOCTYPE html>        <meta charset='utf-8'>     <meta name='viewport' content='width=device-width, initial-scale=1'>     <link rel='stylesheet' type='text/css' href='/style.css'>      <title>{ Mario Zechner }</title>     <meta name="description" content="developer, coach, speaker" />     <meta property="og:title" content="{ Mario Zechner }" />     <meta property="og:type" content="article" />     <meta property="og:description" content="developer, coach, speaker" />     <meta property="og:image" content="https://mariozechner.at/img/band.png" />     <meta property="og:url" content="https://mariozechner.at" />     <meta name="twitter:card" content="summary_large_image" />     <meta property="twitter:domain" content="mariozechner.at" />     <meta property="twitter:url" content="https://mariozechner.at" />     <meta name="twitter:title" content="{ Mario Zechner }" />     <meta name="twitter:description" content="developer, coach, speaker" />     <meta name="twitter:image" content="https://mariozechner.at/img/band.png" />

{ Mario Zechner }
                     developer â¢ coach â¢ speaker
                          <theme-toggle class="mb-auto ml-auto"></theme-toggle>
      <color-band class="w-full mt-4 mb-6" style="height: 80px;"></color-band>
Musings <svg style="width: 18px; color: var(--link-color);" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rss-ic


<stdin>:3: SyntaxWarning: invalid escape sequence '\('
GEMINI_API_KEY no
--provider google no
free no
AI Studio no
provider \(default: google\) no


KW google

KW gemini

KW free tier

KW ai studio

KW api key


I checked what I can.

What I found:
- `pi` itself expects Google via:
  `GEMINI_API_KEY`
- local help says provider default is
  `google`
- so the usual setup is just:
  - get Gemini API key
  - export `GEMINI_API_KEY`
  - run `pi --provider google ...`

I did **not** find strong public evidence
of some special hidden setup that avoids
billing/project-credit issues.

And your new key confirms that:
- config path is correct
- API is reachable
- but Google still returns:
  `429 RESOURCE_EXHAUSTED`
  `prepayment credits are depleted`

So this is not:
- wrong env var
- wrong provider flag
- wrong model name

It is still:
- the Google project behind the key

Blunt read:
people using `pi` with Google for free are
probably doing exactly this simple setup,
but on a project/account where the Gemini
API free usage is still actually available.

For you, both tested keys are on projects
that are not usable right now.

If you want the next high-signal move:
1. create a **brand new** AI Studio key
2. ideally from a **different project**
3. then I smoke test immediately

If that still 429s, then we know this is
account/project billing state, not setup.


Summary:
## Goal
- Simplify and automate the memory ingestion pipeline so Telegram-heavy usage does not miss memory creation.
- Add automatic Telegram notifications for high-signal memories that may belong in `AGENTS`.
- Notify via Telegram when periodic memory maintenance causes promotions/archives/conflicts.
- Earlier in session: audit recent memory activity, triage missed session-end memories, and write a cheap-pass set of durable memories.

## Constraints & Preferences
- User prefers concise, low-noise updates.
- User wants automation simplified because current memory flow has “many ways to work and some deprecated even”.
- Do not auto-edit `AGENTS`; only suggest promotion via Telegram and wait for user approval.
- Periodic process must cover Telegram-only stretches where session-end hook may not fire.
- Important always-needed rules should live in `AGENTS`; memory is for situational durable facts.
- User chose “cheap” triage approach rather than exhaustive review.

## Progress
### Done
- [x] Audited recent memory activity: last 30 days had 32 memories; last 14 days had only 3, all manual by `personal-agent`, 0 by `pi-session-memory-analyzer`.
- [x] Counted recent session backlog: 36 session files in last 2 weeks, ~4,132 lines, ~23.2 MB.
- [x] Cheap-triaged recent sessions and concluded only ~4–7 durable memories were worth writing.
- [x] Wrote 4 durable memories manually:
  - `mem_58309c3e69e444d7b15c377d18b6b960` — gastos-europa raw + published app
  - `mem_8008ca82485848cd9f5492c09c236fa4` — rss-offline full local page capture
  - `mem_8c5c0ef5a6e3412289c6fbe2210108b1` — OurVend real-ui migration complete
  - `mem_caa17fc9013941b08d40188047b0958d` — Ballbox html-first accessibility rule
- [x] Found root cause of prior session-memory automation failures from old job logs:
  - `Token refresh failed: 401`
  - `"Your refresh token has already been used to generate a new access token. Please try signing in again."`
  - `No API key for provider: openai-codex`
- [x] Confirmed active services:
  - `agents-database-http.service`
  - `agents-database-maintenance.service`
  - `install-session-memory-dispatcher.service`
  - `pi-telegram.service`
- [x] Added periodic sweeper script: `/home/sebas/pi-config/scripts/session-memory-sweeper.py`
- [x] Added systemd unit files:
  - `/home/sebas/pi-config/scripts/session-memory-sweeper.service`
  - `/home/sebas/pi-config/scripts/session-memory-sweeper.timer`
- [x] Installed timer into `~/.config/systemd/user/` and enabled it:
  - `session-memory-sweeper.timer`
  - `session-memory-sweeper.service`
- [x] Timer cadence set to:
  - `OnBootSec=10m`
  - `OnUnitActiveSec=6h`
- [x] Updated `/home/sebas/pi-config/scripts/session-memory-analyzer.py` to add deterministic `fallback_analysis(...)` when `call_pi(...)` fails, instead of hard-failing memory analysis.
- [x] Fixed analyzer job status updates so top-level `job["status"]` now moves through `running` / `completed` / `failed`.
- [x] Updated `/home/sebas/pi-config/README.md` to describe dispatcher + periodic sweeper + fallback behavior.
- [x] Extended `/home/sebas/pi-config/scripts/session-memory-sweeper.py` to:
  - classify possible `AGENTS` candidates from strong global `profile`/`decision`/`procedural` memories
  - send Telegram notifications via `~/.agents/skills/telegram-notify/telegram-notify`
  - run one-shot maintenance via `/home/sebas/agents-database/scripts/run-maintenance-daemon.sh --once`
  - notify on maintenance promotions/archives/links/conflicts
- [x] User requested AGENTS promotion logic “now”, and assistant reported it implemented.
- [x] Assistant reported one detected AGENTS candidate:
  - `Prefers fallback to free AI models when quota runs out`

### In Progress
- [ ] Validate that periodic sweeper Telegram notifications are not too noisy and that AGENTS-candidate heuristic is accurate.
- [ ] Verify maintenance notification path actually fires on real promotion/archive/conflict events.

### Blocked
- Prior provider-auth path for analyzer was broken (`openai-codex` auth/token issue), but fallback analysis now mitigates this.
- Some older queued jobs remained with stale `status: "queued"` until analyzer status-handling fix.

## Key Decisions
- **Use one reliable periodic catch-up path**: Telegram-heavy usage means session-end hooks are insufficient; periodic sweeping is now the dependable ingestion mechanism.
- **Keep session-end dispatcher but demote it from sole path**: `install-session-memory-dispatcher.service` still exists, but `session-memory-sweeper.py` is the main catch-up mechanism.
- **Do not auto-edit `AGENTS`**: High-signal universal behavior memories should trigger Telegram suggestions only; user will approve promotion explicitly.
- **Fallback instead of hard failure**: `session-memory-analyzer.py` now creates deterministic episode-summary candidates when model/provider auth is unavailable.
- **Notify on maintenance changes**: Promotions/archives/conflicts from periodic maintenance should be surfaced to user via Telegram.
- **Always-needed rules belong in `AGENTS`**: Memory is for durable but situational facts; near-universal operating rules should graduate into `AGENTS`.

## Next Steps
1. Inspect actual Telegram notifications produced by `session-memory-sweeper.py` and tune message volume/content if needed.
2. Verify that `run_maintenance_once()` in `/home/sebas/pi-config/scripts/session-memory-sweeper.py` correctly parses output from `/home/sebas/agents-database/scripts/run-maintenance-daemon.sh --once`.
3. Confirm real maintenance events produce Telegram notices for promoted/archived/linked/conflict counts.
4. Tighten `classify_agents_candidates(...)` heuristic if false positives appear.
5. Optionally remove or deprecate older/confusing memory paths even more aggressively, as previously suggested.
6. Later: implement user-approved AGENTS patch proposal flow when user replies yes to a promotion suggestion.

## Critical Context
- Recent files changed:
  - `/home/sebas/pi-config/scripts/session-memory-sweeper.py`
  - `/home/sebas/pi-config/scripts/session-memory-sweeper.service`
  - `/home/sebas/pi-config/scripts/session-memory-sweeper.timer`
  - `/home/sebas/pi-config/scripts/session-memory-analyzer.py`
  - `/home/sebas/pi-config/README.md`
- Installed user units:
  - `/home/sebas/.config/systemd/user/session-memory-sweeper.service`
  - `/home/sebas/.config/systemd/user/session-memory-sweeper.timer`
- Existing related services:
  - `/home/sebas/.config/systemd/user/install-session-memory-dispatcher.service`
  - `/home/sebas/.config/systemd/user/agents-database-maintenance.service`
- Relevant existing code/services:
  - `/home/sebas/pi-config/scripts/session-memory-dispatcher.py`
  - `/home/sebas/pi-config/extensions/session-memory-dispatcher.ts`
  - `/home/sebas/agents-database/scripts/run-maintenance-daemon.sh`
  - `/home/sebas/agents-database/src/shared_agent_memory/service.py`
  - `/home/sebas/agents-database/src/shared_agent_memory/engine.py`
- Important functions/paths added or edited:
  - `fallback_analysis(job, error)` in `session-memory-analyzer.py`
  - `classify_agents_candidates(analysis)`
  - `maybe_send_telegram(message)`
  - `run_maintenance_once()`
  - `main()` in `session-memory-sweeper.py`
- Sweeper behavior:
  - scans recent session files under `/home/sebas/pi-config/sessions`
  - skips obvious loop noise using prefixes:
    - `"you are iteration "`
    - `"continue the bounded ralph-loop"`
  - defaults:
    - `LOOKBACK_DAYS=21`
    - `MAX_SESSIONS_PER_RUN=12`
    - `MIN_TOTAL_MESSAGES=6`
    - `MIN_ASSISTANT_TEXT=120`
- Timer status previously showed active waiting, and service had run successfully.
- There was a temporary syntax error in `session-memory-analyzer.py` from stray `+` lines; it was fixed and `python3 -m py_compile` then passed.
- One analyzer rerun showed successful completion and saved 5 memories, e.g. top-level output included:
  - `"status": "completed"`
  - `"saved_count": 5`
- Earlier RSS finding:
  - current feed list: 17
  - `feed-sources.json` last changed `2026-05-15`
  - `/var/www/rss-offline/rss-offline-latest.pdf` timestamp `2026-05-24 04:34:04 -0300`
  - conclusion: 0 new feeds since last merged PDF generation.

---

**Turn Context (split turn):**

## Original Request
[What did the user ask for in this turn?]
- User asked to check whether Google models are still free.
- User also asked to upgrade the memory “Prefers fallback to free AI models when quota runs out” so it specifically means: only use Google models as fallback, because other free providers/models are too prone to timeouts/429s.

## Early Progress
- Checked Google AI pricing/docs via web requests to `ai.google.dev`.
- Found repeated “Free Tier” entries on the pricing page, including around Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 2.5 Flash-Lite, and some related variants, suggesting Google still offers free-tier access for at least some Gemini models.
- Queried shared memory DB and found existing memory:
  - `Prefers fallback to free AI models when quota runs out`
  - Current content was generic, not restricted to Google.

## Context for Suffix
- The assistant was gathering enough evidence to answer “yes, Google models still have a free tier.”
- The next needed action was to refine/update the existing memory so fallback preference is narrowed to free Google models only, excluding other free providers due to reliability issues (timeouts, 429s).
- Repo docs search (`README.md`/`AGENTS.md`) did not yet show a specific fallback rule to reuse; only a general explicit provider/model default note was found.

<read-files>
/home/sebas/.agents/skills/memory-read/SKILL.md
/home/sebas/.agents/skills/memory-write/SKILL.md
/home/sebas/.agents/skills/telegram-notify/SKILL.md
/home/sebas/agents-database/scripts/run-maintenance-daemon.sh
/home/sebas/agents-database/src/shared_agent_memory/engine.py
/home/sebas/agents-database/src/shared_agent_memory/service.py
/home/sebas/pi-config/AGENTS.md
/home/sebas/pi-config/docs/bb-004-rss-offline-status-2026-05-14.md
/home/sebas/pi-config/extensions/session-memory-dispatcher.ts
/home/sebas/pi-config/scripts/session-memory-dispatcher.py
/home/sebas/pi-config/sessions/--home-sebas--/2026-05-15T11-21-13-095Z_019e2b5e-7c86-743e-a727-efe867ba7d5f.jsonl
/home/sebas/pi-config/sessions/--home-sebas--/2026-05-17T07-48-17-216Z_019e34e8-42bf-7188-b802-c22ee31bc60c.jsonl
/home/sebas/pi-config/sessions/--home-sebas--/2026-05-21T12-06-23-509Z_019e4a6e-0014-76c5-a9f2-fa668d9f2f44.jsonl
/home/sebas/pi-config/sessions/--home-sebas-work-projects-ballbox--/2026-05-17T16-35-27-825Z_019e36ca-e7d0-70e9-b2d1-ed456cbe4f7d.jsonl
/home/sebas/pi-config/sessions/--home-sebas-work-projects-vending-status-pages--/2026-05-22T15-06-13-702Z_019e5039-0145-746b-a4be-37c5c81d82aa.jsonl
/home/sebas/pi-config/sessions/session-memory-jobs/20260507T202641961301Z.json
/home/sebas/pi-config/sessions/session-memory-jobs/20260507T202641961301Z.log
/home/sebas/pi-config/sessions/session-memory-jobs/20260507T210856564296Z.log
/home/sebas/pi-config/sessions/session-memory-jobs/20260524T091449627951Z.json
/home/sebas/runtime/rss-offline-build.log
/home/sebas/work/projects/rss-offline/feed-sources.json
</read-files>

<modified-files>
/home/sebas/pi-config/README.md
/home/sebas/pi-config/scripts/session-memory-analyzer.py
/home/sebas/pi-config/scripts/session-memory-sweeper.py
/home/sebas/pi-config/scripts/session-memory-sweeper.service
/home/sebas/pi-config/scripts/session-memory-sweeper.timer
</modified-files>


[telegram] then remove entirely the memory that tries to use free llms. no free llm is usable besides google and that is not available anymore


python3 - <<'PY'
import requests, json
q='free llm fallback google quota free-tier Gemini fallback'
url='http://100.116.176.16:8091/api/search'
r=requests.get(url, params={'q': q, 'limit': 10}, timeout=20)
r.raise_for_status()
for m in r.json().get('results', []):
    print(m.get('id'), '|', m.get('title'))
    print(m.get('content','')[:300].replace('\n',' '))
    print('---')
PY


None | None

---
None | None

---
None | None

---
None | None

---
None | None

---
None | None

---
None | None

---
None | None

---
None | None

---
None | None

---


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


# Personal Agent

## Communication
- Caveman full. Active every response.
- Short, direct, low-noise.
- No filler, pleasantries, hedging, verbal tics.
- Avoid repetition at all costs. Do not restate the same point with different wording unless repetition materially improves clarity or safety.
- Keep tech terms exact. Fragments OK.
- Ask only if scope, risk, or success changes.
- No drift unless user says `normal mode` or `stop caveman`.
- Code, commands, commits, PR text: normal technical form.

## Intent
- Optimize business impact.
- Chase real objective, not surface ask.
- Once clear, execute end to end.
- Routine choices: decide and move.
- Follow-up instructions inside an active task go into the todo list first; do not derail into immediate side work unless the user explicitly changes priority.
- Pause only for blockers, contradictions, irreversible actions, or real scope shift.
- Use subagents for recon, parallel work, or context isolation.
- Prefer `ralph-loop-run` when the work is a bounded multi-pass audit, migration, or hardening pass on one repo and can be driven through shared `TASKS.md` / `STATE.md` handoff.
- Good `ralph-loop-run` cases: accessibility sweeps, focused polish passes, test/backfill passes, real-backend migrations, and other cumulative review loops where each pass should pick up exactly where the previous one stopped.
- Do not define Ralph loops mainly by a fixed number of passes. Default objective is to run until one goal gate is true: `done`, `blocked_with_reason`, or `needs_user_decision`.
- Goal gates that express user intent, business outcome, exclusions, or what counts as truly done should come from the user when unclear. Do not invent intent gates by yourself when doubt remains; ask and lock them first.
- You may derive technical checklists, feature states, and validator markers after intent gates are clear, but not substitute your own guessed intent for the user's done criteria.
- Every Ralph loop must carry a binary checklist for visible success criteria. Validator tail checks that list item by item; no soft "mostly done" verdicts.
- Use explicit feature states in loop artifacts for every visible feature: `real_working`, `real_read_only`, `blocked_hidden`, or `unknown_do_not_show`.
- Hard rule: if a feature depends on business truth, never present fake/local placeholder backend as real. Validator must fail immediately on that condition.
- Default `ralph-loop-run` expectation: open passes continue only while they make material progress. Material progress means at least one of: code change, state change, evidence gain, risk reduction, or unblock path.
- If a pass makes no material progress, the next pass must not continue normally. It must replan, switch tools/approach, or end in `blocked_with_reason` / `needs_user_decision`.
- Reserve the tail of the loop for validation. The closing sequence is validator -> bounded repair if safe -> mandatory revalidation. Stop only after clean revalidation, explicit blocked state, or explicit user decision.
- The validator should report-only instead of self-repair only when the fix changes scope, raises meaningful risk, or needs user judgment.
- Let validator prune visible scope when needed. If a feature has no confirmed real endpoint or truthful implementation, hide or disable it rather than leaving it half-working.
- Leave this visible in loop artifacts. `STATE.md` should name whether the closing pass was `validator`, `repair`, or `revalidation`, and should also track `candidate_done`, `validator_status`, `repair_applied`, and `revalidation_status`.
- Avoid `ralph-loop-run` for one-shot tasks, broad open-ended exploration, or work that needs rich parallel branching more than strict sequential handoff.
- Use Agents Database as durable shared memory.
- Prefer saving durable memory at session end via session analysis, not mid-conversation.
- Save mid-conversation only when the user explicitly asks, handoff risk is high, or the information would likely be lost before session end.
- Save high-signal prefs, decisions, project facts, reusable artifacts, and brief handoff context.
- Prefer few strong memories over many noisy ones.
- If Codex quota/rate/auth becomes unavailable and a fallback model is needed, prefer Google free-tier Gemini models only. Do not fall back by default to other free providers because they are too prone to timeouts, 429s, or instability.
- This Pi config repo is the place to evolve these rules.

## Rules
- Irreversible actions need explicit confirmation.
- Never merge or push to main without user confirmation.
- Stay direct and low-noise.
- Call out contradictions when they matter.
- Code, commands, commits, PR text: normal technical form.

## Machine Context
- Machine: `ballbox-first`.
- Do not SSH into `ballbox-first` for normal local work.
- This Pi is workstation + server for Sebas.
- "the server" usually means this machine unless user says other.

## Server Details
- Hostname: `ballbox-first.emperor-ratio.ts.net`
- Tailscale IP: `100.116.176.16`
- Runs 24/7
- Nginx on `:80` routes local services
- `agents-database`: `http://100.116.176.16:8091/api/status`

## Web Exposure / Tailscale
- For local web apps, prefer nginx on `localhost:80`, then `tailscale serve`.
- Prefer subpaths like `/cli/` before `/` when portal/home exists.
- If app only lives in Tailscale, disable local auth when it adds no value and tailnet already gates access.
- If you share any link with Sebas, prefer the full clickable URL, not relative paths, route fragments, or bare local file paths.
- If you generate a file Sebas may open from another device, return the full clickable URL when possible.
- Prefer: `https://ballbox-first.emperor-ratio.ts.net/files/...`
- Do not expose the local username in shared file URLs unless required.
- Preferred path mapping: `/home/sebas/...` -> `/files/...`, `/mnt/...` -> `/files/mnt/...`.
- Legacy compatibility: old `/files/home/...` links should redirect to `/files/...`.

## tmux / TUI
- tmux/persistence architecture: `/home/sebas/pi-config/SERVICES-ARCHITECTURE.md`.
- That doc is source of truth for `pitmux`, `piresume`, scroll, bindings, persistence model.
- If you touch tmux or `pi` launcher, update that file first.

## Storage Layout
- Canonical layout doc: `/home/sebas/pi-config/docs/system-layout.md`.
- Prefer canonical home-first roots in `/home/sebas` for config, runtime, work trees, outputs, and docs.
- Runtime plane: `/home/sebas/runtime`.
- Work plane: `/home/sebas/work`.
- Use `/mnt/rpi` only when it has an explicit special-storage role, not as the default agent plane.

## Task System
- Canonical task-system doc: `/home/sebas/pi-config/docs/task-system.md`.
- Use `BACKLOG.md` for shaping/research, `TASKS.md` for actionable work, `DONE.md` for completions, and session `todo` for live execution state.
- Concrete tasks should have a workspace folder under `/home/sebas/work/tasks/T-xxx/` for notes and small artifacts.
- Repo-local task systems are allowed; global docs track cross-session outcome/priority, repo-local docs track implementation detail.

## /tmp Cleanup
- Use `bin/tmp-cleanup` for top-level `/tmp` files older than 3 days.
- Install via `scripts/install-tmp-cleanup-cron.sh`.

## Laptop Helpers
- CLI helpers exist for Sebas's laptop via Tailnet SSH-over-TCP.
- Default host: `brag-pipe-fur-kneel.emperor-ratio.ts.net`.
- Default port: `2222`.
- Default key: `~/.ssh/id_ed25519_laptop_agent`.
- Helpers:
  - `laptop-read <remote-path>`: remote cat over SSH.
  - `laptop-run <cmd ...>`: remote command over SSH.
  - `laptop-rsync <src> <dest> [extra args]`: rsync via SSH.
  - `laptop-mount`, `laptop-umount`, `laptop-write`: access/copy flows.
- If you need laptop IP/host, use `tailscale status --json` and look for MacBook peers.
- If task says "files on the laptop", use these helpers instead of manual SSH.

## Telegram
- Vars:
  - `TELEGRAM_BOT_TOKEN`
  - `TELEGRAM_CHAT_ID`
- Notify:
```bash
~/.agents/skills/telegram-notify/telegram-notify "Message"
```
- If a Telegram user sends literal `/new`, do not treat it as normal chat. Acknowledge briefly and run `/home/sebas/pi-config/bin/pi-telegram-new-session` to rotate the tmux-backed Telegram Pi session.


**Searching for delete API**

I need to find a delete API, which likely means checking to see if the service has a specific endpoint available. It seems like I’ll have to use the right request generation (rg) to communicate with the API effectively. Once I identify the appropriate service endpoint, I can proceed further. It's all about ensuring I have the right tools in place to accomplish what I need.

rg -n "@app\.(delete|post|get)|/api/.+delete|delete_memory|tombstone|status.*deleted|/memories/|DELETE" /home/sebas/agents-database -S | head -200

python3 - <<'PY'
import requests, json
for q in [
 'Prefers fallback to free AI models when quota runs out',
 'Fallback non-Codex model preference is Google free-tier only',
 'Google free-tier Gemini models only',
 'free providers timeout 429 instability',
 'fallback only to Google free-tier Gemini models'
]:
    r=requests.get('http://100.116.176.16:8091/api/search', params={'q':q,'limit':5}, timeout=20)
    print('\nQUERY',q,'STATUS',r.status_code)
    try:
        data=r.json()
    except Exception:
        print(r.text[:500]); continue
    print(type(data).__name__)
    print(json.dumps(data, indent=2)[:2000])
PY


/home/sebas/agents-database/src/shared_agent_memory/schema.py:186:    FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
/home/sebas/agents-database/src/shared_agent_memory/schema.py:201:    FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
/home/sebas/agents-database/src/shared_agent_memory/http_api.py:137:                if parsed.path.startswith("/api/memories/"):
/home/sebas/agents-database/src/shared_agent_memory/service.py:1534:        conn.execute("DELETE FROM memories_fts WHERE memory_id = ?", (record["id"],))
/home/sebas/agents-database/tests/test_memory_service.py:294:        status_code, payload = _request_json(f"{base_url}/api/memories/mem_lookup_1")
/home/sebas/agents-database/docs/http-api.md:16:- `GET /api/memories/<id>`
/home/sebas/agents-database/docs/http-api.md:49:- Use `GET /api/memories/<id>` for direct memory lookup by id.


QUERY Prefers fallback to free AI models when quota runs out STATUS 200
dict
{
  "retrieval_id": "ret_842d4ee6117445e784a37eabd8328000",
  "results": [
    {
      "memory": {
        "id": "mem_def0c389189143f28f37df63cebabe3c",
        "type": "profile",
        "scope": "global",
        "status": "active",
        "project_id": null,
        "repo_id": null,
        "agent_id": null,
        "source_kind": "manual",
        "title": "Prefers fallback to free AI models when quota runs out",
        "content": "When daily Codex usage hits quota limits, wants a practical fallback to free models with enough intelligence to finish the task.",
        "summary": "When daily Codex usage hits quota limits, wants a practical fallback to free models with enough intelligence to finish the task.",
        "confidence": 0.98,
        "freshness": 0.99,
        "created_at": "2026-05-24T09:31:18.030792+00:00",
        "updated_at": "2026-05-24T10:08:47.357726+00:00",
        "observed_at": "2026-05-24T09:31:18.030792+00:00",
        "source_ref": "session:019e07be-7ba4-74de-bcff-17f9df09a627",
        "evidence_ref": "last_user_text",
        "schema_version": "shared-agent-memory-v2",
        "subtype": "workflow_preference",
        "run_id": null,
        "task_id": null,
        "url": null,
        "domain": null,
        "origin_agent": "pi-session-memory-analyzer",
        "metadata": {
          "analysis_reason": "One clear durable preference, one implementation requirement, and one concise handoff episode were present. No other session facts looked reusable.",
          "assistant_messages": 1,
          "candidate_reason": "Stable workflow preference with clear future reuse value.",
          "cwd": "/home/sebas",
          "job_file": "/home/sebas/pi-config/sessions/session-memory-jobs/20260524T093107505404Z.json",
          "promotion_candidate": true,
          "reason": "periodic_sweep",
          "session_candidate_index": 1,
          "session_file": "/home/sebas/pi-config/sessions/--home-sebas--/2026-05-08T13-19-44-549Z_019e07be-7ba4

QUERY Fallback non-Codex model preference is Google free-tier only STATUS 200
dict
{
  "retrieval_id": "ret_2f4dd6a02ad44dd9af14db13ce72a97e",
  "results": [
    {
      "memory": {
        "id": "mem_716699746ab8484e9311cbaeb33f516c",
        "type": "profile",
        "scope": "global",
        "status": "active",
        "project_id": null,
        "repo_id": null,
        "agent_id": null,
        "source_kind": "manual",
        "title": "Fallback non-Codex model preference is Google free-tier only",
        "content": "When Codex quota, auth, or rate limits block work, Sebas prefers fallback only to Google free-tier Gemini models. Avoid other free providers as default fallback because they are too prone to timeouts, 429s, and instability.",
        "summary": "When Codex quota, auth, or rate limits block work, Sebas prefers fallback only to Google free-tier Gemini models. Avoid other free providers as default fallback because they are t\u2026",
        "confidence": 1.0,
        "freshness": 1.0,
        "created_at": "2026-05-24T09:34:52.270248+00:00",
        "updated_at": "2026-05-24T09:34:52.270248+00:00",
        "observed_at": "2026-05-24T09:34:52.270248+00:00",
        "source_ref": "conversation:2026-05-24-google-fallback",
        "evidence_ref": null,
        "schema_version": "shared-agent-memory-v2",
        "subtype": "working_preference",
        "run_id": null,
        "task_id": null,
        "url": null,
        "domain": null,
        "origin_agent": "personal-agent",
        "metadata": {},
        "embedding": null
      },
      "score": 0.39999999999999997,
      "score_breakdown": {
        "facet": 0.0,
        "lexical": 1.0,
        "semantic": 0.0,
        "confidence": 1.0,
        "freshness": 1.0
      },
      "matched_scope": "global",
      "evidence_ref": null,
      "explanation": "facets=0.00 lexical=1.00 semantic=0.00 confidence=1.00 freshness=1.00"
    },
    {
      "memory": {
        "id": "legacy_run_7e6517b3-759f-4063-8d98-121d9cba45c5",
        "type": "episode",
        "scope": "global",

QUERY Google free-tier Gemini models only STATUS 200
dict
{
  "retrieval_id": "ret_5a73f9f0e6714e019e98d4bb7084cbbb",
  "results": [
    {
      "memory": {
        "id": "mem_716699746ab8484e9311cbaeb33f516c",
        "type": "profile",
        "scope": "global",
        "status": "active",
        "project_id": null,
        "repo_id": null,
        "agent_id": null,
        "source_kind": "manual",
        "title": "Fallback non-Codex model preference is Google free-tier only",
        "content": "When Codex quota, auth, or rate limits block work, Sebas prefers fallback only to Google free-tier Gemini models. Avoid other free providers as default fallback because they are too prone to timeouts, 429s, and instability.",
        "summary": "When Codex quota, auth, or rate limits block work, Sebas prefers fallback only to Google free-tier Gemini models. Avoid other free providers as default fallback because they are t\u2026",
        "confidence": 1.0,
        "freshness": 1.0,
        "created_at": "2026-05-24T09:34:52.270248+00:00",
        "updated_at": "2026-05-24T09:34:52.270248+00:00",
        "observed_at": "2026-05-24T09:34:52.270248+00:00",
        "source_ref": "conversation:2026-05-24-google-fallback",
        "evidence_ref": null,
        "schema_version": "shared-agent-memory-v2",
        "subtype": "working_preference",
        "run_id": null,
        "task_id": null,
        "url": null,
        "domain": null,
        "origin_agent": "personal-agent",
        "metadata": {},
        "embedding": null
      },
      "score": 0.39999999999999997,
      "score_breakdown": {
        "facet": 0.0,
        "lexical": 1.0,
        "semantic": 0.0,
        "confidence": 1.0,
        "freshness": 1.0
      },
      "matched_scope": "global",
      "evidence_ref": null,
      "explanation": "facets=0.00 lexical=1.00 semantic=0.00 confidence=1.00 freshness=1.00"
    },
    {
      "memory": {
        "id": "mem_ballbox_alias_volvox",
        "type": "profile",
        "scope": "project",
        "status": "active",

QUERY free providers timeout 429 instability STATUS 200
dict
{
  "retrieval_id": "ret_13a9f56a6a174373b76c5400ea28ebdc",
  "results": [
    {
      "memory": {
        "id": "mem_716699746ab8484e9311cbaeb33f516c",
        "type": "profile",
        "scope": "global",
        "status": "active",
        "project_id": null,
        "repo_id": null,
        "agent_id": null,
        "source_kind": "manual",
        "title": "Fallback non-Codex model preference is Google free-tier only",
        "content": "When Codex quota, auth, or rate limits block work, Sebas prefers fallback only to Google free-tier Gemini models. Avoid other free providers as default fallback because they are too prone to timeouts, 429s, and instability.",
        "summary": "When Codex quota, auth, or rate limits block work, Sebas prefers fallback only to Google free-tier Gemini models. Avoid other free providers as default fallback because they are t\u2026",
        "confidence": 1.0,
        "freshness": 1.0,
        "created_at": "2026-05-24T09:34:52.270248+00:00",
        "updated_at": "2026-05-24T09:34:52.270248+00:00",
        "observed_at": "2026-05-24T09:34:52.270248+00:00",
        "source_ref": "conversation:2026-05-24-google-fallback",
        "evidence_ref": null,
        "schema_version": "shared-agent-memory-v2",
        "subtype": "working_preference",
        "run_id": null,
        "task_id": null,
        "url": null,
        "domain": null,
        "origin_agent": "personal-agent",
        "metadata": {},
        "embedding": null
      },
      "score": 0.39999999999999997,
      "score_breakdown": {
        "facet": 0.0,
        "lexical": 1.0,
        "semantic": 0.0,
        "confidence": 1.0,
        "freshness": 1.0
      },
      "matched_scope": "global",
      "evidence_ref": null,
      "explanation": "facets=0.00 lexical=1.00 semantic=0.00 confidence=1.00 freshness=1.00"
    },
    {
      "memory": {
        "id": "mem_d33f08e0d2f1468fbb47a61f10c8cfb9",
        "type": "project",
        "scope": "global",
        "status": "

QUERY fallback only to Google free-tier Gemini models STATUS 200
dict
{
  "retrieval_id": "ret_34d36bf15dae4da28089684c1cefd6f1",
  "results": [
    {
      "memory": {
        "id": "mem_716699746ab8484e9311cbaeb33f516c",
        "type": "profile",
        "scope": "global",
        "status": "active",
        "project_id": null,
        "repo_id": null,
        "agent_id": null,
        "source_kind": "manual",
        "title": "Fallback non-Codex model preference is Google free-tier only",
        "content": "When Codex quota, auth, or rate limits block work, Sebas prefers fallback only to Google free-tier Gemini models. Avoid other free providers as default fallback because they are too prone to timeouts, 429s, and instability.",
        "summary": "When Codex quota, auth, or rate limits block work, Sebas prefers fallback only to Google free-tier Gemini models. Avoid other free providers as default fallback because they are t\u2026",
        "confidence": 1.0,
        "freshness": 1.0,
        "created_at": "2026-05-24T09:34:52.270248+00:00",
        "updated_at": "2026-05-24T09:34:52.270248+00:00",
        "observed_at": "2026-05-24T09:34:52.270248+00:00",
        "source_ref": "conversation:2026-05-24-google-fallback",
        "evidence_ref": null,
        "schema_version": "shared-agent-memory-v2",
        "subtype": "working_preference",
        "run_id": null,
        "task_id": null,
        "url": null,
        "domain": null,
        "origin_agent": "personal-agent",
        "metadata": {},
        "embedding": null
      },
      "score": 0.39999999999999997,
      "score_breakdown": {
        "facet": 0.0,
        "lexical": 1.0,
        "semantic": 0.0,
        "confidence": 1.0,
        "freshness": 1.0
      },
      "matched_scope": "global",
      "evidence_ref": null,
      "explanation": "facets=0.00 lexical=1.00 semantic=0.00 confidence=1.00 freshness=1.00"
    },
    {
      "memory": {
        "id": "legacy_run_7e6517b3-759f-4063-8d98-121d9cba45c5",
        "type": "episode",
        "scope": "global",


# Agents Database HTTP API

Remote-first topology:

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

## Endpoints

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

## Run locally

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

## Server service

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

## Memory semantics

- This HTTP API is the correct remote boundary for shared memory. Avoid routing memory CRUD/search through `personal-agent` unless the runtime actually owns that behavior.
- Use `GET /api/memories/<id>` for direct memory lookup by id.
- `consolidate_candidates()` and maintenance jobs live behind `MemoryService`; the HTTP API exposes the store, not a separate memory subsystem.
- `episode` and `inbox` records are candidates, not canonical truth, until consolidation or explicit promotion.

## Gotchas

- The existing `SERVICES-ARCHITECTURE.md` on the server may be stale relative to live routing. Verify real HTTP reachability with `curl`.


                                memory_type=_first(query, "type"),
                                subtype=_first(query, "subtype"),
                                project_id=_first(query, "project_id"),
                                repo_id=_first(query, "repo_id"),
                                source_ref=_first(query, "source_ref"),
                                evidence_ref=_first(query, "evidence_ref"),
                                run_id=_first(query, "run_id"),
                                task_id=_first(query, "task_id"),
                                origin_agent=_first(query, "origin_agent"),
                                url=_first(query, "url"),
                                domain=_first(query, "domain"),
                                metadata=_metadata_filters(query),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/memories/"):
                    self._send_json(service.get_memory(parsed.path.split("/")[3]))
                    return
                if parsed.path == "/api/tasks":
                    requires_human_input = None
                    if "requires_human_input" in query:
                        requires_human_input = _parse_bool(_first(query, "requires_human_input"))
                    self._send_json(
                        {
                            "items": service.list_tasks(
                                status=_first(query, "status"),
                                owner_agent=_first(query, "owner_agent"),
                                requires_human_input=requires_human_input,
                                run_id=_first(query, "run_id"),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/tasks/") and parsed.path.endswith("/bundle"):
                    self._send_json(service.task_bundle(parsed.path.split("/")[3]))
                    return
                if parsed.path.startswith("/api/tasks/"):
                    self._send_json(service.get_task(parsed.path.split("/")[3]))
                    return
                if parsed.path == "/api/task-runs":
                    self._send_json(
                        {
                            "items": service.list_task_runs(
                                task_id=_first(query, "task_id"),
                                status=_first(query, "status"),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/task-runs/"):
                    self._send_json(service.get_task_run(parsed.path.split("/")[3]))
                    return
                if parsed.path == "/api/artifacts":
                    self._send_json(
                        {
                            "items": service.list_artifacts(
                                task_id=_first(query, "task_id"),
                                limit=_first_int(query, "limit", 50),
                            )
                        }
                    )
                    return
                if parsed.path.startswith("/api/artifacts/"):
                    self._send_json(service.get_artifact(parsed.path.split("/")[3]))
                    return
            except KeyError as exc:
                self.send_error(HTTPStatus.NOT_FOUND, str(exc))
                return
            except ValueError as exc:
                self.send_error(HTTPStatus.BAD_REQUEST, str(exc))
                return
            except Exception as exc:
                self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
                return

            self.send_error(HTTPStatus.NOT_FOUND)

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

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

[106 more lines in file. Use offset=260 to continue.]


**Considering database methods**

I’m contemplating that there might not be a delete endpoint available. Instead, maybe I should look into directly using SQLite or a service script. That could be a more efficient solution. I'll need to search for the "tombstone method" in the service as well, to see if that aligns with what I'm looking for or provides the best approach. Let’s see where this takes me!

rg -n "def .*memory|update_memory|delete_memory|archive_memory|status=.*deleted|deleted'|tombstone|soft delete|mark.*deleted" /home/sebas/agents-database/src/shared_agent_memory/service.py -S | head -120


71:    def create_memory(self, memory_input: dict[str, Any]) -> dict[str, Any]:
128:    def ingest(self, memory_input: dict[str, Any]) -> dict[str, Any]:
540:    def get_memory(self, memory_id: str) -> dict[str, Any]:
1174:    def _memory_filter_sql(self, sql: list[str], filters: dict[str, Any]) -> list[Any]:
1184:    def _facet_score(self, memory: dict[str, Any], filters: dict[str, Any]) -> float:
1192:    def _migrate_memory_row(self, row: dict[str, Any]) -> dict[str, Any] | None:
1502:    def _memory_subtype(self, metadata: dict[str, Any]) -> str | None:
1540:    def _row_to_memory(self, row: Any) -> dict[str, Any]:
1604:    def _semantic_score(self, query_embedding: list[float], memory_embedding: list[float] | None) -> float:
1659:    def _record_conflict(self, conn: Any, memory_id: str, conflicting_id: str, reason: str) -> None:


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

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


**Considering sqlite edits**

I'm thinking that a direct SQLite edit might be the easiest approach. I need to check out the schema to understand the different statuses, like active or archived. Maybe I’ll look for a way to search for the memory's status to ensure I’m addressing the right entries. It’s a good idea to be thorough here so I don’t accidentally mess things up. Let's see how I can make this happen!

rg -n "status.*memory|memory.*status|status in \(|active|archived|superseded|deleted" /home/sebas/agents-database/src/shared_agent_memory -S | head -200

python3 - <<'PY'
import sqlite3, glob, os
for p in glob.glob('/home/sebas/agents-database/**/*.sqlite3', recursive=True):
    print(p)
PY


/home/sebas/agents-database/src/shared_agent_memory/http_api.py:118:                                status=_first(query, "status") if "status" in query else "active",
/home/sebas/agents-database/src/shared_agent_memory/cli.py:106:    list_memories.add_argument("--status", default="active")
/home/sebas/agents-database/src/shared_agent_memory/service.py:86:            "status": memory_input.get("status", "active"),
/home/sebas/agents-database/src/shared_agent_memory/service.py:172:            params.append("active")
/home/sebas/agents-database/src/shared_agent_memory/service.py:225:        profile_facts = self.list_memories(status="active", scope="global", memory_type="profile", limit=8)
/home/sebas/agents-database/src/shared_agent_memory/service.py:227:        active_decisions = self.list_memories(
/home/sebas/agents-database/src/shared_agent_memory/service.py:228:            status="active",
/home/sebas/agents-database/src/shared_agent_memory/service.py:236:            status="active",
/home/sebas/agents-database/src/shared_agent_memory/service.py:243:            status="active",
/home/sebas/agents-database/src/shared_agent_memory/service.py:250:            status="active",
/home/sebas/agents-database/src/shared_agent_memory/service.py:261:            for item in [*profile_facts, *project_memories, *active_decisions, *recent_episodes]
/home/sebas/agents-database/src/shared_agent_memory/service.py:267:            "active_decisions": active_decisions,
/home/sebas/agents-database/src/shared_agent_memory/service.py:287:                      AND status = 'active'
/home/sebas/agents-database/src/shared_agent_memory/service.py:311:                    conn.execute("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", ("archived", utcnow(), memory_id))
/home/sebas/agents-database/src/shared_agent_memory/service.py:314:                conn.execute("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", ("active", utcnow(), memory_id))
/home/sebas/agents-database/src/shared_agent_memory/service.py:322:            "archived": 0,
/home/sebas/agents-database/src/shared_agent_memory/service.py:344:                    OR (type = 'episode' AND status = 'active')
/home/sebas/agents-database/src/shared_agent_memory/service.py:360:                    WHERE status = 'active'
/home/sebas/agents-database/src/shared_agent_memory/service.py:388:                        ("archived", utcnow(), candidate["id"]),
/home/sebas/agents-database/src/shared_agent_memory/service.py:390:                    stats["archived"] += 1
/home/sebas/agents-database/src/shared_agent_memory/service.py:398:                        ("archived", utcnow(), candidate["id"]),
/home/sebas/agents-database/src/shared_agent_memory/service.py:400:                    stats["archived"] += 1
/home/sebas/agents-database/src/shared_agent_memory/service.py:441:                        ("active", utcnow(), candidate["id"]),
/home/sebas/agents-database/src/shared_agent_memory/service.py:444:                elif candidate["status"] == "active" and candidate["type"] in {"episode", "artifact", "decision", "project", "procedural", "semantic"} and candidate.get("confidence", 0.0) >= promote_threshold:
/home/sebas/agents-database/src/shared_agent_memory/service.py:445:                    # Normalize qualifying active records as already-promoted durable memories.
/home/sebas/agents-database/src/shared_agent_memory/service.py:550:        status: str | None = "active",
/home/sebas/agents-database/src/shared_agent_memory/service.py:858:        status: str = "active",
/home/sebas/agents-database/src/shared_agent_memory/service.py:911:        active_tasks = self.list_tasks(owner_agent=owner_agent, limit=50)
/home/sebas/agents-database/src/shared_agent_memory/service.py:912:        active_tasks = [task for task in active_tasks if task["status"] in {"open", "draft", "in_progress"}][:20]
/home/sebas/agents-database/src/shared_agent_memory/service.py:917:            "active_tasks": active_tasks,
/home/sebas/agents-database/src/shared_agent_memory/service.py:1059:                            "archived" if approval["status"] != "pending" else "active",
/home/sebas/agents-database/src/shared_agent_memory/service.py:1097:                            "active",


**Planning memory updates**

I see that the user wants to remove certain statuses entirely, not just archive them. I could think about deleting rows directly from sqlite and FTS using sqlite3. Additionally, there's a note about reverting the AGENTS line. Should I create a memory regarding no free LLM fallback? Probably not unless the user asks. Still, it could be useful. However, since the user wants to eliminate that memory, I’ll focus on both deleting memories and editing AGENTS.

edit

- Prefer few strong memories over many noisy ones.
- If Codex quota/rate/auth becomes unavailable and a fallback model is needed, prefer Google free-tier Gemini models only. Do not fall back by default to other free providers because they are too prone to timeouts, 429s, or instability.
- This Pi config repo is the place to evolve these rules.

- Prefer few strong memories over many noisy ones.
- This Pi config repo is the place to evolve these rules.

bash

python3 - <<'PY'
import sqlite3
DB='/home/sebas/agents-database/data/shared-agent-memory.sqlite3'
ids=[
 'mem_def0c389189143f28f37df63cebabe3c',
 'mem_716699746ab8484e9311cbaeb33f516c',
]
conn=sqlite3.connect(DB)
cur=conn.cursor()
for mid in ids:
    cur.execute('DELETE FROM memories_fts WHERE memory_id=?',(mid,))
    cur.execute('DELETE FROM memories WHERE id=?',(mid,))
conn.commit()
for mid in ids:
    row=cur.execute('SELECT id FROM memories WHERE id=?',(mid,)).fetchone()
    print(mid, 'deleted' if row is None else 'still_present')
conn.close()
PY


Successfully replaced 1 block(s) in /home/sebas/pi-config/AGENTS.md.

     ...
  37 - Prefer saving durable memory at session end via session analysis, not mid-conversation.
  38 - Save mid-conversation only when the user explicitly asks, handoff risk is high, or the information would likely be lost before session end.
  39 - Save high-signal prefs, decisions, project facts, reusable artifacts, and brief handoff context.
  40 - Prefer few strong memories over many noisy ones.
- 41 - If Codex quota/rate/auth becomes unavailable and a fallback model is needed, prefer Google free-tier Gemini models only. Do not fall back by default to other free providers because they are too prone to timeouts, 429s, or instability.
  42 - This Pi config repo is the place to evolve these rules.
  43
  44 ## Rules
  45 - Irreversible actions need explicit confirmation.
     ...


mem_def0c389189143f28f37df63cebabe3c deleted
mem_716699746ab8484e9311cbaeb33f516c deleted


Done.

Removed:
- `mem_def0c389189143f28f37df63cebabe3c`
- `mem_716699746ab8484e9311cbaeb33f516c`

Also removed the free-Google fallback rule
from `AGENTS.md`.

Current state:
- no memory says to use free LLM fallback
- no AGENTS rule says to use it either


/new


queued new Telegram Pi session: pi-telegram


Rotated Telegram session.