fix(projects): Backfill v2 — robuster Match trotz GPS/Barge-In/FILE-Marker

v1 matchte text==content exakt und verfehlte damit Nachrichten, bei denen die
Bridge den Brain-Text anreichert oder cleant:
- User-Turns: _build_core_text PREPENDT [Hinweis: Barge-In]-/[GPS-Position]-
  Bloecke — die stehen in conversation.jsonl, nicht in chat_backup.
- Assistant-Turns: conversation enthaelt [FILE: /shared/uploads/...]-Marker,
  chat_backup hat sie schon rausgecleant.

_norm entfernt jetzt FILE-Marker + fuehrende [..]-Bloecke, kollabiert
Whitespace und vergleicht einen 120-Zeichen-Praefix. Neuer Marker (v2) →
laeuft einmal neu und fuellt die von v1 verbliebenen Luecken. Nicht-destruktiv
(eigene .pre-backfill-v2.bak), bestehende Tags bleiben unangetastet.

Mit Tests gegen GPS-/Barge-In-/FILE-Marker-Faelle verifiziert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 02:34:17 +02:00
co-authored by Claude Opus 4.8
parent 97ee455ab4
commit 1568c25ac4
+23 -5
View File
@@ -25,6 +25,7 @@ from __future__ import annotations
import json import json
import logging import logging
import os import os
import re
from collections import defaultdict, deque from collections import defaultdict, deque
from pathlib import Path from pathlib import Path
@@ -32,13 +33,30 @@ logger = logging.getLogger("aria.migrate.backfill_projectid")
CONVERSATION_FILE = Path(os.environ.get("CONVERSATION_FILE", "/data/conversation.jsonl")) CONVERSATION_FILE = Path(os.environ.get("CONVERSATION_FILE", "/data/conversation.jsonl"))
CHAT_BACKUP_FILE = Path(os.environ.get("CHAT_BACKUP_FILE", "/shared/config/chat_backup.jsonl")) CHAT_BACKUP_FILE = Path(os.environ.get("CHAT_BACKUP_FILE", "/shared/config/chat_backup.jsonl"))
MARKER_FILE = Path("/shared/config/.chat_backup_projectid_backfill_v1") # v2: robusterer Match (Marker-Strip + Praefix). v1 verlangte exakte Gleichheit
# von text==content und verfehlte damit alle Nachrichten bei denen die Bridge
# den Brain-Text anreichert (GPS/Barge-In-Hints prepended) oder cleant
# (FILE-Marker entfernt). Neuer Marker → laeuft einmal neu, fuellt die Luecken.
MARKER_FILE = Path("/shared/config/.chat_backup_projectid_backfill_v2")
# _build_core_text (Bridge) PREPENDT bei User-Nachrichten Hinweis-/GPS-Bloecke
# in eckigen Klammern vor den eigentlichen Text; conversation.jsonl speichert
# diesen angereicherten Text, chat_backup nur den rohen. FILE-Marker stehen in
# conversation-Assistant-Turns, sind in chat_backup aber schon rausgecleant.
_FILE_MARKER_RE = re.compile(r"\[FILE:\s*/shared/uploads/[^\]]+\]", re.IGNORECASE)
_LEADING_BRACKET_RE = re.compile(r"^\s*(?:\[[^\]]*\]\s*)+")
_WS_RE = re.compile(r"\s+")
def _norm(text: str) -> str: def _norm(text: str) -> str:
"""Match-Key: getrimmt + auf 500 Zeichen begrenzt. Reicht um Turns eindeutig """Match-Key: FILE-Marker + fuehrende [Hinweis]/[GPS]-Bloecke entfernen,
zu unterscheiden, ist aber tolerant gegen minimale Trailing-Unterschiede.""" Whitespace kollabieren, auf 120-Zeichen-Praefix kuerzen. Toleriert damit
return (text or "").strip()[:500] die Anreicherungs-/Cleaning-Unterschiede zwischen conversation und backup,
bleibt durch den 120er-Praefix aber spezifisch genug gegen Fehl-Matches."""
t = _FILE_MARKER_RE.sub("", text or "")
t = _LEADING_BRACKET_RE.sub("", t)
t = _WS_RE.sub(" ", t).strip()
return t[:120]
def run() -> dict: def run() -> dict:
@@ -112,7 +130,7 @@ def run() -> dict:
# 4) Sicherung + atomarer Rewrite. # 4) Sicherung + atomarer Rewrite.
try: try:
bak = CHAT_BACKUP_FILE.with_suffix(".jsonl.pre-backfill-v1.bak") bak = CHAT_BACKUP_FILE.with_suffix(".jsonl.pre-backfill-v2.bak")
if not bak.exists(): if not bak.exists():
bak.write_bytes(CHAT_BACKUP_FILE.read_bytes()) bak.write_bytes(CHAT_BACKUP_FILE.read_bytes())
tmp = CHAT_BACKUP_FILE.with_suffix(".jsonl.tmp") tmp = CHAT_BACKUP_FILE.with_suffix(".jsonl.tmp")