phase: 96_oneshot_resilience
Build and Push Containers / build-and-push-app (push) Successful in 1m34s
Build and Push Containers / build-and-push-db (push) Successful in 10s

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:
2026-09-11 13:16:20 -04:00
parent bcaef800c5
commit a49be80b8e
42 changed files with 2893 additions and 143 deletions
+40 -20
View File
@@ -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