feat(chat): invalidate saved chats on sources sync — versioned stamps, stale marker, Regenerate against the new index
This commit is contained in:
+48
-8
@@ -45,6 +45,16 @@ debug runs never burn a ``lite`` call, and a ``lite`` failure only
|
||||
reports ``overview=failed`` on the summary line: the import's exit code
|
||||
is about files, and the previous outline stays (an old outline is better
|
||||
than none).
|
||||
|
||||
A run that **changed** the knowledge base (added + updated + pruned > 0
|
||||
— phase 53, task 02) also advances the single-row ``sources_meta``
|
||||
version exactly once (``sources_version=<n>`` on the summary line): the
|
||||
generation saved chats are stamped against, so a sync can no longer
|
||||
silently invalidate a stored answer. The gate is deliberately broader
|
||||
than the overview's — a pruned document can invalidate a saved answer
|
||||
that cited it — and ``--limit`` debug runs (an incomplete walk is
|
||||
debug-only, mirroring the ``--limit`` overview skip) and unchanged
|
||||
re-runs never bump (the line carries ``sources_version=skipped``).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -64,6 +74,7 @@ from app.rag.git_sources import effective_sources
|
||||
from app.rag.importer import ImportSummary, import_sources
|
||||
from app.rag.llm import LLMClient
|
||||
from app.rag.overview import regenerate_overview
|
||||
from app.rag.sources_meta import bump_sources_version
|
||||
from scripts.git_sync import GitSyncError, clone_or_pull
|
||||
|
||||
logger = logging.getLogger("scripts.import_docs")
|
||||
@@ -211,8 +222,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
llm = LLMClient()
|
||||
|
||||
async def _run() -> tuple[ImportSummary, str]:
|
||||
"""Import, then (change-gated) refresh the stored KB overview.
|
||||
async def _run() -> tuple[ImportSummary, str, str]:
|
||||
"""Import, then (change-gated) advance the sources version and
|
||||
refresh the stored KB overview.
|
||||
|
||||
One event loop, one ``LLMClient`` (phase 31, task 04): the
|
||||
outline that every chat prompt injects as ``<knowledge_base>`` is
|
||||
@@ -224,31 +236,59 @@ def main(argv: list[str] | None = None) -> int:
|
||||
unchanged re-imports never burn a ``lite`` call, and a ``lite``
|
||||
failure only flips the status token (``failed``) — the import's
|
||||
exit code is unchanged.
|
||||
|
||||
The sources version (phase 53, task 02) advances exactly once
|
||||
per run that changed the KB — change-gated on
|
||||
``added + updated + pruned > 0`` (a pruned document can
|
||||
invalidate a saved answer that cited it, so the gate is
|
||||
deliberately broader than the overview's). ``--limit`` debug
|
||||
runs and unchanged re-runs never bump. The returned token is
|
||||
the new version, or ``"skipped"``.
|
||||
"""
|
||||
summary = await import_sources(sources, llm, prune=args.prune, limit=args.limit)
|
||||
if args.limit is not None:
|
||||
# An incomplete walk is debug-only — it must never advance
|
||||
# the generation (mirrors the --limit overview skip below).
|
||||
sources_version = "skipped"
|
||||
elif summary.added + summary.updated + summary.pruned > 0:
|
||||
# The KB changed — advance the saved-chat invalidation
|
||||
# marker exactly once, in its own short session (the
|
||||
# best-effort overview below runs in a separate one, so a
|
||||
# failed outline never rolls the bump back).
|
||||
session = SessionLocal()
|
||||
try:
|
||||
new_version = bump_sources_version(session)
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
sources_version = str(new_version)
|
||||
logger.info("sources: version bumped to %d", new_version)
|
||||
else:
|
||||
sources_version = "skipped"
|
||||
logger.info("sources: version bump skipped (KB unchanged)")
|
||||
if args.limit is not None:
|
||||
logger.info("overview: skipped (--limit)")
|
||||
return summary, "skipped"
|
||||
return summary, "skipped", sources_version
|
||||
if summary.added + summary.updated == 0:
|
||||
if summary.files == 0:
|
||||
logger.info("overview: skipped (nothing imported)")
|
||||
return summary, "skipped"
|
||||
return summary, "skipped", sources_version
|
||||
if _overview_row_exists():
|
||||
logger.info("overview: skipped (KB unchanged)")
|
||||
return summary, "skipped"
|
||||
return summary, "skipped", sources_version
|
||||
# No outline yet after an unchanged re-import (e.g. the first
|
||||
# run after migration 0005) — fall through and generate one.
|
||||
ok = await regenerate_overview(llm)
|
||||
return summary, "updated" if ok else "failed"
|
||||
return summary, "updated" if ok else "failed", sources_version
|
||||
|
||||
summary, overview_status = asyncio.run(_run())
|
||||
summary, overview_status, sources_version = asyncio.run(_run())
|
||||
print(
|
||||
f"import_docs: files={summary.files} added={summary.added} "
|
||||
f"updated={summary.updated} unchanged={summary.unchanged} "
|
||||
f"pruned={summary.pruned} errors={summary.errors} chunks={summary.chunks} "
|
||||
f"embed_batches={summary.embed_batches} summaries={summary.summaries} "
|
||||
f"summary_errors={summary.summary_errors} formats={summary.format_counts()} "
|
||||
f"overview={overview_status}"
|
||||
f"overview={overview_status} sources_version={sources_version}"
|
||||
)
|
||||
# Non-zero if any file failed, so cron/CI notice — the rest of the KB
|
||||
# was imported and the failed files are retried on the next run. The
|
||||
|
||||
Reference in New Issue
Block a user