diff --git a/backend/context_builder.py b/backend/context_builder.py index 4f007f4..d06a342 100644 --- a/backend/context_builder.py +++ b/backend/context_builder.py @@ -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, { diff --git a/backend/journal_opening.py b/backend/journal_opening.py index 6170c39..11e2bc1 100644 --- a/backend/journal_opening.py +++ b/backend/journal_opening.py @@ -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 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 diff --git a/backend/retrieval.py b/backend/retrieval.py index 271d35a..4b01d8c 100644 --- a/backend/retrieval.py +++ b/backend/retrieval.py @@ -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, } ) diff --git a/backend/tests/test_journal_opening.py b/backend/tests/test_journal_opening.py index 0976e3d..536f307 100644 --- a/backend/tests/test_journal_opening.py +++ b/backend/tests/test_journal_opening.py @@ -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 diff --git a/docs/architecture/functional/mvp.md b/docs/architecture/functional/mvp.md index dbd21b2..8be989c 100644 --- a/docs/architecture/functional/mvp.md +++ b/docs/architecture/functional/mvp.md @@ -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. --- diff --git a/docs/architecture/functional/mvp_stand_und_abgleich.md b/docs/architecture/functional/mvp_stand_und_abgleich.md index b41dbc5..583b4aa 100644 --- a/docs/architecture/functional/mvp_stand_und_abgleich.md +++ b/docs/architecture/functional/mvp_stand_und_abgleich.md @@ -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.4–5.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.1–13.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. diff --git a/docs/architecture/technical/memory_storage_and_offline.md b/docs/architecture/technical/memory_storage_and_offline.md index 01b3e9c..c5f0a69 100644 --- a/docs/architecture/technical/memory_storage_and_offline.md +++ b/docs/architecture/technical/memory_storage_and_offline.md @@ -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 diff --git a/docs/architecture/technical/mvp_implementation.md b/docs/architecture/technical/mvp_implementation.md index 1e8dcb4..f00f5eb 100644 --- a/docs/architecture/technical/mvp_implementation.md +++ b/docs/architecture/technical/mvp_implementation.md @@ -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.