Keep journal openings from treating finished days as open plans.

A 400-character entry prefix plus plan regex could reopen a completed yesterday as if it were still pending. Openings now take plans only from yesterday's user dialogue, never from saved entries, and skip space recency in the first call.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-09-07 08:25:00 +02:00
parent e42f751fdc
commit ec0f8bad55
8 changed files with 108 additions and 19 deletions

View File

@ -70,6 +70,7 @@ def build_internal_context(
space_id: str | None = None,
journal_day_id: str | None = None,
purpose: str = "dialogue_turn",
include_space_recency: bool = True,
include_existing: bool = False,
conversation_ids: list[str] | None = None,
existing_text: str = "",
@ -118,7 +119,7 @@ def build_internal_context(
day_messages = read("conversation_messages", profile_id=profile_id, context={"conversation_id": conversation_id})
items.append({"type": "day_messages", "messages": day_messages})
if purpose == "dialogue_turn" and space_id:
if purpose == "dialogue_turn" and space_id and include_space_recency:
prior = retrieve(
profile_id,
{

View File

@ -1,12 +1,15 @@
"""Journal-specific first impulse. Not a generic continuation or intent engine.
Uses the existing Privacy Gateway, dialogue prompt, guards and request-scoped
trace. Attested plans and open day points come only from user-role sources.
Assistant text is conversation context, never a user fact. Recency is not a pattern.
trace. Attested plans come only from yesterday's user-role dialogue. Open day
points stay on the same journal day. Saved journal entries are completed
narrative, never an opening plan. Assistant text is conversation context, never
a user fact. Recency is not a pattern.
"""
from __future__ import annotations
import re
from datetime import date, timedelta
from context_builder import assemble_text, build_internal_context
from debug_store import persist_engine_error, persist_step
@ -20,11 +23,14 @@ from retrieval import retrieve
NEUTRAL_OPENING = "Wenn du magst, fang einfach an ich höre zu."
OPENING_POLICY = (
"Dies ist der erste Impuls dieses Gesprächs. Es gibt hier noch keine Nutzerzeile. "
"Nur belegte Vorhaben aus user-Quellen oder offene user-Punkte desselben Journal Day aufgreifen. "
"Nur belegte Vorhaben aus dem user-Dialog des unmittelbar vorigen Kalendertags "
"oder offene user-Punkte desselben Journal Day aufgreifen. "
"Ein gespeicherter Journal-Eintrag ist abgeschlossene Erzählung, kein offenes Vorhaben. "
"Ein Recency-Treffer ist kein bewiesenes wiederkehrendes Muster. "
"Nicht erwähnt ist nicht geschehen. Ein Plan ist kein Vollzug. "
"Assistententext ist Gesprächskontext, kein Nutzerfakt. "
"Keine Behauptung, ein Vorhaben sei ausgeführt worden. "
"Keine bereits erzählte, abgeschlossene Episode eines früheren Tags als offenen Faden führen."
)
FUTURE_INTENT = re.compile(
@ -46,6 +52,22 @@ UNEARNED_PATTERN = re.compile(
)
def parse_calendar_date(value: str | None) -> date | None:
raw = (value or "").strip()
if not raw:
return None
try:
return date.fromisoformat(raw)
except ValueError:
return None
def is_previous_calendar_day(source: date | None, current: date | None) -> bool:
if source is None or current is None:
return False
return source == current - timedelta(days=1)
def is_attested_plan(text: str) -> bool:
"""User wording that marks a plan or intention. Not past completion, not overlap."""
body = (text or "").strip()
@ -88,7 +110,8 @@ def collect_opening_context(profile_id: str, conversation_id: str) -> dict:
journal_day_id = conversation.get("journal_day_id")
if not space_id or not journal_day_id:
raise StoreError("not_journal_conversation", "Erster Impuls nur für einen Journal-Dialog.")
get_day(profile_id, journal_day_id)
day = get_day(profile_id, journal_day_id)
current_date = parse_calendar_date(day.get("calendar_date"))
day_conversations = list_conversations_for_day(profile_id, journal_day_id)
open_points: list[str] = []
@ -114,6 +137,7 @@ def collect_opening_context(profile_id: str, conversation_id: str) -> dict:
},
)
recency_user: list[str] = []
yesterday_user: list[str] = []
for entry in prior_entries:
excerpt = (entry.get("excerpt") or "").strip()
if excerpt:
@ -122,15 +146,19 @@ def collect_opening_context(profile_id: str, conversation_id: str) -> dict:
excerpt = (source.get("excerpt") or "").strip()
if excerpt:
recency_user.append(excerpt)
if excerpt and is_previous_calendar_day(
parse_calendar_date(source.get("calendar_date")),
current_date,
):
yesterday_user.append(excerpt)
user_pool = [*open_points, *recency_user]
plans = attested_plans_from_user_texts(user_pool)
plans = attested_plans_from_user_texts(yesterday_user)
has_relevant = bool(plans or open_points)
return {
"space_id": space_id,
"journal_day_id": journal_day_id,
"conversation_id": conversation_id,
"user_texts": user_pool,
"user_texts": [*open_points, *yesterday_user],
"assistant_texts": day_assistant,
"attested_plans": plans,
"open_day_points": open_points,
@ -230,13 +258,15 @@ def start_journal_opening(profile_id: str, conversation_id: str) -> dict:
journal_day_id=conversation.get("journal_day_id"),
purpose="dialogue_turn",
conversation_ids=day_ids,
include_space_recency=False,
)
assembled = assemble_text(context)
assembled = dict(assembled)
assembled["opening_hint"] = format_opening_hint(facts)
assembled["register_hint"] = (
"Erster Impuls. Noch keine Nutzerzeile in diesem Gespräch. "
"Nur belegte Vorhaben oder offene user-Punkte. Kein Muster aus Recency."
"Nur Vorhaben aus dem user-Dialog von gestern oder offene user-Punkte dieses Tags. "
"Kein Muster aus Recency. Keine abgeschlossene frühere Episode wieder öffnen."
)
prompt = load_active_prompt("mvp.dialogue_turn")
calls = 0

View File

@ -190,8 +190,9 @@ def _space_recent_sources(profile_id: str, spec: dict[str, Any]) -> list[dict]:
with get_db() as conn:
conversations = conn.execute(
f"""
SELECT c.id, c.updated, c.journal_day_id
SELECT c.id, c.updated, c.journal_day_id, d.calendar_date
FROM conversations c
LEFT JOIN journal_days d ON d.id = c.journal_day_id AND d.profile_id = c.profile_id
WHERE c.profile_id = ? AND c.space_id = ? {extra}
ORDER BY c.updated DESC, c.created DESC
LIMIT ?
@ -217,6 +218,7 @@ def _space_recent_sources(profile_id: str, spec: dict[str, Any]) -> list[dict]:
{
"conversation_id": conv["id"],
"journal_day_id": conv["journal_day_id"],
"calendar_date": conv["calendar_date"],
"excerpt": excerpt,
}
)

View File

@ -6,6 +6,8 @@ import sys
import tempfile
from pathlib import Path
from datetime import date
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
@ -23,6 +25,8 @@ from journal_opening import (
attested_plans_from_user_texts,
collect_opening_context,
is_attested_plan,
is_previous_calendar_day,
parse_calendar_date,
)
from journal_reconstruct import assign_source_ids
from retrieval import pair_user_priority_messages
@ -58,6 +62,9 @@ def main() -> None:
expect(not is_attested_plan("Heute war der Hafen ruhig. Kirschen am Stand."), "past report is not a plan")
expect(not is_attested_plan("Wir standen vor dem Hafen und aßen Kirschen."), "vor without haben is not a plan")
expect(not is_attested_plan("Hafen Kirschen Wind"), "word overlap is not a plan")
expect(parse_calendar_date("2026-08-29") == date(2026, 8, 29), "calendar date parses")
expect(is_previous_calendar_day(date(2026, 8, 29), date(2026, 8, 30)), "yesterday is previous")
expect(not is_previous_calendar_day(date(2026, 8, 29), date(2026, 9, 3)), "older than yesterday is not previous")
plans = attested_plans_from_user_texts(
["Heute war der Hafen ruhig.", "Morgen wollen wir nach Cres fahren.", "Hafen Kirschen"]
)
@ -157,6 +164,48 @@ def main() -> None:
expect(overlap_open.json().get("opening", {}).get("kind") == "local_neutral", "overlap falls back to neutral")
expect("Muster" not in (overlap_open.json().get("opening") or {}).get("assistant", {}).get("body", ""), "neutral impulse does not claim a pattern")
done_space, done_day = open_space_day(client, headers, "Abgeschlossen", "2026-08-20")
done_conv = start_conv(client, headers, done_day["day"]["id"])
client.post(
f"/api/journal/conversations/{done_conv.json()['id']}/turn",
headers=headers,
json={"body": "Nachmittag am Strand, abends Eis, dann schlafen."},
)
client.post(
"/api/journal/entries",
headers=headers,
json={
"journal_day_id": done_day["day"]["id"],
"title": "Hitze",
"body": (
"Schon um 6 Uhr schlug mir eine drückende Luft entgegen. "
"Ich hatte überlegt, die Roller auf den nächsten Tag zu schieben. "
"Wir haben sie dann doch geholt und sind den Tag gefahren."
),
"origin": "user_edit",
},
)
later_done = client.post(
f"/api/journal/spaces/{done_space['id']}/days",
headers=headers,
json={"calendar_date": "2026-08-21"},
)
done_open = start_conv(client, headers, later_done.json()["day"]["id"])
done_facts = collect_opening_context(setup.json()["profile_id"], done_open.json()["id"])
expect(not done_facts["attested_plans"], "completed journal entry is not an opening plan")
expect(not done_facts["has_relevant_context"], "a closed earlier day does not keep the next opening relevant")
expect(done_open.json().get("opening", {}).get("kind") == "local_neutral", "completed story falls back to neutral")
stale = client.post(
f"/api/journal/spaces/{plan_space['id']}/days",
headers=headers,
json={"calendar_date": "2026-08-04"},
)
stale_open = start_conv(client, headers, stale.json()["day"]["id"])
stale_facts = collect_opening_context(setup.json()["profile_id"], stale_open.json()["id"])
expect(not any("Cres" in item for item in stale_facts["attested_plans"]), "plan from two days ago is no longer an opening mandate")
expect(stale_open.json().get("opening", {}).get("kind") == "local_neutral", "stale plan uses local neutral")
asst_space, asst_day = open_space_day(client, headers, "Assistent", "2026-08-05")
asst_conv = start_conv(client, headers, asst_day["day"]["id"])
from dialogue_store import append_message

View File

@ -388,7 +388,11 @@ Der erste Gesprächsimpuls unterliegt derselben Dialog-Policy wie spätere Folge
**Status: Code vorhanden, journalspezifisch, keine universelle Continuation-Engine.**
Ein neu gestarteter Journal-Dialog erhält einen ersten Kanshō-Impuls. Belegte Vorhaben und offene user-Punkte desselben Journal Day dürfen den Einstieg stützen. Recency allein ist kein Muster. Assistententext ist Kontext, kein Nutzerfakt. Ohne relevanten Kontext: kurzer neutraler lokaler Einstieg. Fehlgeschlagener Modelloutput speichert keine beschädigte Nachricht.
Ein neu gestarteter Journal-Dialog erhält einen ersten Kanshō-Impuls. Belegte Vorhaben dürfen den Einstieg stützen, wenn sie aus dem **user-Dialog des unmittelbar vorigen Kalendertags** stammen. Offene user-Punkte gelten nur für denselben Journal Day. Recency allein ist kein Muster. Ein gespeicherter Journal-Eintrag ist abgeschlossene Erzählung, kein offenes Vorhaben; ein 400-Zeichen-Entry-Ausschnitt darf keinen Folgetag als offenen Faden führen. Assistententext ist Kontext, kein Nutzerfakt. Ohne relevanten Kontext: kurzer neutraler lokaler Einstieg. Fehlgeschlagener Modelloutput speichert keine beschädigte Nachricht.
**Additiv 2026-09-03:** Der Opening-Pfad lädt Space-Recency nicht in den Modellkontext. Gestern angekündigte Vorhaben bleiben ein Folgetag lang zulässig; ältere oder nur im Entry vorkommende Formulierungen („auf den nächsten Tag“) erzeugen den neutralen Einstieg.
**Additiv 2026-09-07 (Fehlerbild, ohne Thread-Engine):** Ein nachgetragener Morgenanker (späte user-Zeile mit früher Uhrzeit) kann im Journal am Textanfang landen. Ein festes Prefix (400 Zeichen) liest dann Überlegung ohne späteren Vollzug und erzeugt tagelang denselben Einstieg. Das ist kein Primär-Thread, sondern Recency plus Plan-Regex. **Bewusst offen:** dieselben 400-Zeichen-Ausschnitte bleiben im *laufenden* Dialogzug (`dialogue_turn`) als Hinweis geladen; sie sind kein Opening-Mandat mehr. Entfernen oder Relevanz statt Prefix ist Slice-2-/Memory-Thema, nicht Opening-Regression.
---

View File

@ -439,10 +439,10 @@ Foundation-Verletzungen, die ein Refactor erzwingen würden, sind **nicht** gefu
| Kein Fragebogen; Reflexionspartner | **Teilweise** | Prompt und Stimme zielen darauf. Qualität ist modellabhängig. Lokale Wächter gegen Recap, erfundenen Vollzug, erfundene Empfindung, Systemfloskeln, Schluss-Wiederöffnung. `local_hold` bittet um Fortsetzung statt Floskel. |
| Sechs Operationen, keine Intent-Engines | **Erfüllt** | Ein Call, JSON `{operation, impulse}`; sichtbar ist `impulse` |
| Register lokal, kein zweites Modell | **Erfüllt** | `infer_register` |
| Bekannter Tagesplan (Cres-Beispiel) | **Teilweise / Ungenauigkeit** | Plan kann aus begrenzten Recency-Ausschnitten **gespeicherter Entries** oder **jüngerer Original-Conversations** desselben Space kommen. Kein Plan-Objekt, keine zeitliche Vor/Nach-Logik, kein semantisches Retrieval. |
| Bekannter Tagesplan (Cres-Beispiel) | **Teilweise / Ungenauigkeit** | **Opening (seit 2026-09-03):** nur user-Dialog des vorigen Kalendertags, nicht gespeicherte Entries. **Laufender Dialogzug:** Recency-Ausschnitte (Entries und jüngere Original-Conversations) bleiben Hinweis. Kein Plan-Objekt, keine Vor/Nach-Logik über den Folgetag hinaus, kein semantisches Retrieval. |
| Wiederkehrende Aktivität (Japanisch-Beispiel) | **Gap** | Keine Zählung über Morgenbeschreibungen. Recency kann den Treffer zufällig enthalten oder nicht. |
| Keine triviale Gefühlsfrage nach selbst benannter Emotion | **Teilweise** | Prompt + `is_unearned_stance`. Kein Dialogzustand „bereits beantwortet“. |
| Kontextsensitiver Einstieg statt „Erzähl von deinem Tag“ | **Erfüllt** als begrenzte Keimzelle | Erster Impuls aus belegtem Plan oder offenem Tagesfaden; sonst neutral. Keine Pattern-Engine. |
| Kontextsensitiver Einstieg statt „Erzähl von deinem Tag“ | **Erfüllt** als begrenzte Keimzelle | Erster Impuls aus belegtem *gestrigem* user-Plan oder offenem Tagesfaden; sonst neutral. Entries und Prefix-Recency sind kein Einstieg. Keine Pattern-Engine. |
| Unterbrechen und Fortsetzen am selben Day | **Erfüllt** | Transcript bleibt. **Teilweise:** keine lokale Karte „welche Tagesabschnitte schon da sind“. |
| Folgetag-Fortsetzung dem ursprünglichen Day zuordnen | **Erfüllt** | Nutzer öffnet den Day; Message-Zeit ist unabhängig. |
@ -518,7 +518,7 @@ Foundation-Verletzungen, die ein Refactor erzwingen würden, sind **nicht** gefu
1. **`mvp.md` §1 und §5.1** versprechen „relevante frühere Aussagen“ und „wiederkehrende Aktivitäten“. Der Code liefert Recency-Ausschnitte finaler Entries plus begrenzte Original-Ausschnitte jüngerer Space-Conversations. Der klarstellende Absatz in §5.1 und §8.2 ist der gültige Implementierungsstand; die Beispiele §5.45.5 und Case C bleiben **schärfer als der Code** (kein Plan-Objekt, keine Wiederholungszählung).
2. **`mvp.md` §17** sprach vom „nächsten Schritt: technischen Plan ableiten“. Der Slice ist gebaut. Dieses Dokument ist der Abgleich; die Formulierung in §17 bleibt historisch.
3. **`mvp.md` §4.1 Konsolidierung** beschreibt narrative Kriterien. Der Slice nutzt lokale Signale derselben Dimensionen, keine LLM-Typklassifikation.
4. **`mvp.md` §6 Einstieg** verlangt einen kontextsensitiven ersten Impuls. Seit 2026-08-25: journalspezifischer Opening-Adapter; ohne belegten Plan oder offenen Tagesfaden lokal neutral.
4. **`mvp.md` §6 Einstieg** verlangt einen kontextsensitiven ersten Impuls. Seit 2026-08-25: journalspezifischer Opening-Adapter; ohne belegten Plan oder offenen Tagesfaden lokal neutral. **Additiv 2026-09-03/07:** Plan nur noch aus dem user-Dialog des unmittelbar vorigen Kalendertags, nicht aus Journal-Entries oder älterer Recency. Opening-Call ohne Space-Recency. Ein 400-Zeichen-Entry-Prefix (typisch: nachgetragener Morgenanker ohne späteren Vollzug) ist kein Opening-Mandat. Der laufende Dialogzug lädt Recency-Ausschnitte weiterhin; das ist kein Slice-Gap des Openings, sondern bewusst offene Heuristik bis zu Relevanz/Memory.
5. **`mvp.md` §8.2 Punkt 2** Space-Kontext bei der Generierung: Generate sieht Prior-Entries nicht. Für diesen Freeze bewusst so begrenzt (`mvp.md` §8.2.1).
6. **`mvp.md` §9.3** listet Lernsignale, von denen nur ein Teil heuristisch existiert.
7. **`mvp.md` §11.3 / §13.113.2** Papierkorb-UI und Entry-Liste sind vorhanden. Dialog-Medien bleiben bewusst aus (`mvp.md` §10.2).
@ -549,6 +549,7 @@ Zielmodell und Non-Goals, nicht „vergessen im MVP“:
- Contextual Continuation als Ziel-IA (der Freeze hat nur eine journalspezifische Startkeimzelle)
- Browser-Zurück als harter Unsaved-Blocker (Data-Router)
- Dialogmedien als direkte Dialogquelle
- Prefix-Recency (400 Zeichen) als Hinweis im *laufenden* Dialogzug; Relevanz statt Prefix bleibt Zielmodell (`resurfacing_and_saturation.md`)
---
@ -558,7 +559,7 @@ Keine neue Horizontal-Konzeption. Getrennt halten, sonst baut ein Agent das Targ
## 8.1 Distanz zum **freigegebenen MVP-Text** (`mvp.md`)
1. **Case C Wiederholung** wiederkehrende Aktivität bleibt Recency, keine Zählung. Der erste Impuls greift nur belegte Pläne oder offene Tagesfäden auf.
1. **Case C Wiederholung** wiederkehrende Aktivität bleibt Recency, keine Zählung. Der erste Impuls greift nur belegte Pläne aus dem user-Dialog von gestern oder offene Tagesfäden desselben Journal Day auf; Journal-Entries nicht.
2. **Generate und Space-Ausschnitte** für diesen Freeze entschieden: Day-Conversations plus Verified Artifact, keine Prior-Entries als Generate-Fakten (`mvp.md` §8.2.1).
3. **Dialog-Medien** bewusst gestrichen für den Slice (`mvp.md` §10.2); Editor-Medien bleiben.
4. **Lokales Detect-Modell** Klartext an externe Detect-Provider ist in Production blockiert; Ollama als Ablösung der Dev/Test-Phase bleibt offen.
@ -597,6 +598,7 @@ Ein Agent, der 8.2 in diesem Slice „schließt“, verletzt Foundation-Regeln 2
|---|---|
| `backend/privacy_gateway.py`, `entity_detect.py`, `pronoun_bind.py`, `identity_store.py` | Egress, Maskierung, Dialog-Pronomen |
| `backend/context_builder.py`, `retrieval.py`, `conversation_signals.py` | Interner Kontext, Selection-Specs, lokale Dialogsignale |
| `backend/journal_opening.py` | Erster Journal-Impuls; Plan nur gestriger user-Dialog; Opening ohne Space-Recency |
| `backend/dialogue_turn.py` | Ein Call, Operation/Impuls, lokale Wächter; kein Writing-Profile-Kernel |
| `backend/journal_generate.py`, `journal_shape.py`, `journal_body.py` | Entwurf, Shape, Titel/Markdown |
| `backend/journal_store.py`, `journal_policy.py` | Entries, Versionen, Scratch, explizites Generate |
@ -618,6 +620,7 @@ Ein Agent, der 8.2 in diesem Slice „schließt“, verletzt Foundation-Regeln 2
| `backend/tests/test_journal_shape.py` | Keine Pronomenersetzung nach Demask; akzeptierter Wortlaut bleibt | Modellqualität, Stil |
| `backend/tests/test_writing_profile.py` | Quellenpriorität | „klingt wie die Person“ |
| `backend/tests/test_frame.py` | Auth, Prompts, Rahmen | Journal-IA |
| `backend/tests/test_journal_opening.py` | Neutral, Folgetag-Plan, abgeschlossener Entry kein Mandat, Plan älter als gestern entfällt | Opening-Qualität des Modells |
| `frontend/src/journal/document.test.js` | Markdown-Runde | Editor-UX, Dirty-Flag |
Laufzeit lokal: Frontend Port **5188**, Backend **8018**, SQLite `backend/data/kansho.sqlite`. Prompt-Templates in der DB werden bei unangetasteten Seeds durch `init_db` aktualisiert; nach Prompt-Änderungen Backend neu starten.

View File

@ -166,7 +166,7 @@ Zusätzlich zu Layer 0:
`derived_records` bleibt die generische Layer-0-Hülle und ist nicht der Journal-Store. `handoffs.target='journal'` ist kein Schreibpfad für Einträge.
Nicht enthalten: Threads-UI, Related Spaces, semantisches Retrieval, Session-Summaries, Voice, Space Content Memory. Der Dialogzug sieht denselben Journal Day vollständig und dazu die letzten Journal Entries des Spaces als Recency-Ausschnitte. Die Journalgenerierung bekommt denselben Tagesdialog inklusive Kanshō-Impulse als Gesprächskontext, nicht als Erlebtes.
Nicht enthalten: Threads-UI, Related Spaces, semantisches Retrieval, Session-Summaries, Voice, Space Content Memory. Der Dialogzug sieht denselben Journal Day vollständig und dazu die letzten Journal Entries des Spaces als Recency-Ausschnitte. **Additiv 2026-09-03:** Der *erste* Impuls eines neuen Gesprächs nutzt diese Ausschnitte nicht als Vorhaben und lädt sie nicht in den Opening-Prompt. Die Journalgenerierung bekommt denselben Tagesdialog inklusive Kanshō-Impulse als Gesprächskontext, nicht als Erlebtes.
## 7. Offene Fragen

View File

@ -37,7 +37,7 @@ Lokal, eine Instanz, ein Profil-Nutzer nach Setup:
Proxy: Vite leitet `/api` an 8018. CORS erlaubt `localhost:5188`.
**Abweichung vom technischen Zielrahmen:** `product_frame_and_stack.md` nennt PostgreSQL. Der Slice läuft auf SQLite. Lokal zulässig; Prod-Pfad offen. Siehe Fit-Gap.
**Abweichung vom technischen Zielrahmen:** `product_frame_and_stack.md` nennt PostgreSQL. Der Slice läuft auf SQLite. Lokal zulässig; Prod-Pfad offen. Transfer der Urlaubsinstanz: zuerst SQLite-Restore auf dem Heimrechner, danach Compose/Postgres. Siehe `environment_handover.md` und `runtime_and_deploy.md` §7.2.
Health: `GET /api/health`.
@ -278,7 +278,7 @@ Additiv zum Slice. Fachliche Abnahme: `../functional/mvp_freeze_candidate.md`.
- Lokaler Testeinstieg `scripts/test-mvp.ps1` (UTF-8, isolierte Temp-Daten, keine Live-Keys; Fake nur in den Suites, die ihn setzen).
- Backup: `backend/local_backup.py`, Einstieg `scripts/backup-local.ps1`. SQLite-Backup-API, Medien, Manifest, keine Secrets. Restore bestätigt, prüft Checksummen, legt Sicherheitsbackup an, bricht bei geöffneter DB ab.
- Papierkorb: `GET /spaces/{id}/entries`, `GET /spaces/{id}/trash`, `POST /entries/{id}/undelete`, `POST /entries/{id}/purge`.
- Erster Impuls: `journal_opening.py`, derselbe Gateway-Pfad, `{{opening_hint}}`.
- Erster Impuls: `journal_opening.py`, derselbe Gateway-Pfad, `{{opening_hint}}`. **Additiv 2026-09-03:** Vorhaben nur aus dem user-Dialog des vorigen Kalendertags (`calendar_date` an `space_recent_sources`); Journal-Entries und ältere Recency sind kein Opening-Mandat. Der Opening-Call setzt `include_space_recency=False`. Tests: abgeschlossener Entry mit „nächsten Tag“ und Plan von vorgestern → `local_neutral`; „Morgen wollen wir …“ nur am Folgetag → Modellpfad. **Additiv 2026-09-07:** Prefix-Recency (5×400 Entries, 3×400 Source-Conversations) bleibt im *laufenden* Dialogzug; das ist kein Opening-Mandat. Ein Nachsatz mit Morgenuhrzeit darf den Folgetag nicht über den Entry-Anfang steuern.
- Home: `GET /continuity`.
Bewusst nicht: Docker, Postgres, Ollama, Verschlüsselung, Dialogmedien als Quelle, Space-Entries als Generate-Fakten.