phase: 96_oneshot_resilience
All checks complete. Final report: **Phase 96 (one-shot resilience) — final verification pass, all green** (all 4 task files already in `complete/`; verified the working-tree implementation against the design) - `LLMClient.chat()` empty-content retry (D1–D3) via `_chat_once` + `_EmptyContentError` (carries `finish_reason`), under `BOR_LLM_RETRIES`/`BOR_LLM_RETRY_DELAY` — verified in diff - `missing_folder_summaries()` + `generate_folder_summaries(only_missing=…)` — verified; `folder_summary_table_empty` deleted, both sync gates switched to the gap probe - `.env.example` comments updated (chat-turn stream + one-shot summary calls) **Test / lint / coverage results** - `uv run pytest --cov=app --cov-report=term-missing` → **1988 passed**, coverage **99%** (gate >90%) - `uv run pytest tests/e2e/test_oneshot_llm_retry.py -v --no-cov` → **2 passed** (isolated) - Regressions, each isolated: `test_ls_tree_drilldown` 3 passed, `test_sync_button` 3 passed, `test_local_directory_sources` 3 passed, `test_llm_retry` 4 passed - `uv run ruff check . && uv run pyright` → clean (0 errors) **Completion criteria:** retry-then-recover unit-pinned ✓ · exhaustion + `BOR_LLM_RETRIES=0` byte-identical ✓ · streaming path untouched ✓ · gap-fill both sync paths, other rows byte-identical incl. `updated_at` ✓ · no-gap zero-burn ✓ · phase E2E green ✓ · regression E2Es green ✓ · full suite + >90% + lint/types ✓ · no completed-phase behavior change (full suite green) ✓. Commit left to the harness per executor rules (working tree, 16 files). **Deviations:** none. **Next pending phase:** `97_kb_tree_catalog`.
This commit is contained in:
+40
-20
@@ -44,13 +44,17 @@ decisions):
|
||||
(phase 31 trigger, best-effort inside) — and ``generate_
|
||||
folder_summaries`` (phase 94, task 02) regenerates the stored folder
|
||||
summaries (the drill-down ``ls``'s per-level descriptions): its gate
|
||||
is the same change trigger **plus** an empty ``folder_summaries``
|
||||
table (the first sync after migration 0017 — the KB may predate the
|
||||
table). It is per-folder fail-soft (a ``lite`` outage keeps the
|
||||
failed folders' previous rows and never flips the run to
|
||||
``failed``) and only flushes — this run's own short-lived session
|
||||
commits (the phase-53 convention), so a folder failure never blocks
|
||||
step 6's bump;
|
||||
is the same change trigger (full regeneration) **plus**, on an
|
||||
unchanged re-sync, a GAP probe — a candidate folder (at least 2
|
||||
docs) with no stored row (``missing_folder_summaries``, phase 96,
|
||||
task 03): a gap fills ONLY the missing rows (``only_missing=True``;
|
||||
the old table-empty first-sync trigger is subsumed exactly — an
|
||||
empty table leaves every candidate missing), a complete table burns
|
||||
zero ``lite`` calls. It is per-folder fail-soft (a ``lite`` outage
|
||||
keeps the failed folders' previous rows — or leaves the row absent
|
||||
— and never flips the run to ``failed``) and only flushes — this
|
||||
run's own short-lived session commits (the phase-53 convention), so
|
||||
a folder failure never blocks step 6's bump;
|
||||
6. when the import changed the KB (added + updated + pruned > 0 — the
|
||||
saved-chat invalidation gate, phase 53 task 02: a pruned document
|
||||
can invalidate a saved answer that cited it, deliberately broader
|
||||
@@ -88,7 +92,7 @@ from app.config import get_settings
|
||||
from app.core.auth import require_admin
|
||||
from app.core.errors import sanitize_error as _sanitize_error
|
||||
from app.db import SessionLocal
|
||||
from app.rag.folder_summaries import folder_summary_table_empty, generate_folder_summaries
|
||||
from app.rag.folder_summaries import generate_folder_summaries, missing_folder_summaries
|
||||
from app.rag.git_sources import effective_sources
|
||||
from app.rag.importer import ImportSummary, import_sources
|
||||
from app.rag.llm import LLMClient, check_models
|
||||
@@ -261,24 +265,40 @@ async def _run_sync() -> None:
|
||||
overview = False
|
||||
if summary.added + summary.updated > 0:
|
||||
overview = await regenerate_overview(llm)
|
||||
# Phase 94 (task 02): the folder summaries — the drill-down
|
||||
# ls's per-level descriptions. Same change gate as the overview
|
||||
# (added + updated > 0), plus the table-empty first-run trigger
|
||||
# (the first sync after migration 0017 — the KB may have been
|
||||
# imported by the CLI before the table landed). The generator is
|
||||
# per-folder fail-soft (a lite outage never flips the run to
|
||||
# failed) and only flushes: this run's own short-lived session
|
||||
# commits (the phase-53 convention), so the step-6 bump stays
|
||||
# change-gated on the KB, not on the summaries. No status-surface
|
||||
# change: the stats are log-only (the detail shape is untouched).
|
||||
# Phase 94 (task 02), phase 96 (task 03): the folder
|
||||
# summaries — the drill-down ls's per-level descriptions. A
|
||||
# changed KB (added + updated > 0) is a full regeneration
|
||||
# (today's behavior, byte-identical); an unchanged re-sync
|
||||
# takes the GAP probe instead of the old table-empty one — a
|
||||
# candidate folder (at least 2 docs) with no stored row: a gap
|
||||
# fills ONLY the missing rows (only_missing=True — the
|
||||
# subsumed table-empty first-sync trigger included, where
|
||||
# every candidate is missing), a complete table burns zero
|
||||
# lite calls. The generator is per-folder fail-soft (a lite
|
||||
# outage never flips the run to failed) and only flushes: this
|
||||
# run's own short-lived session commits (the phase-53
|
||||
# convention), so the step-6 bump stays change-gated on the KB,
|
||||
# not on the summaries. No status-surface change: the stats
|
||||
# are log-only (the detail shape is untouched).
|
||||
fs_db = SessionLocal()
|
||||
try:
|
||||
if summary.added + summary.updated > 0 or folder_summary_table_empty(fs_db):
|
||||
if summary.added + summary.updated > 0:
|
||||
folder_stats = await generate_folder_summaries(fs_db, llm)
|
||||
fs_db.commit()
|
||||
logger.info("sync: folder_summaries stats=%s", folder_stats)
|
||||
else:
|
||||
logger.info("sync: folder_summaries skipped (KB unchanged)")
|
||||
missing = missing_folder_summaries(fs_db)
|
||||
if missing:
|
||||
folder_stats = await generate_folder_summaries(
|
||||
fs_db, llm, only_missing=True
|
||||
)
|
||||
fs_db.commit()
|
||||
logger.info(
|
||||
"sync: folder_summaries gap-fill missing=%d stats=%s",
|
||||
len(missing), folder_stats,
|
||||
)
|
||||
else:
|
||||
logger.info("sync: folder_summaries skipped (KB unchanged)")
|
||||
finally:
|
||||
fs_db.close()
|
||||
# Phase 53 (task 02): a sync that changed the KB advances the
|
||||
|
||||
Reference in New Issue
Block a user