Files
brain-of-reese/AGENTS.md
T
ducoterra 065304b52f
Build and Push Containers / build-and-push-app (push) Successful in 1m53s
Build and Push Containers / build-and-push-db (push) Successful in 13s
phase: 184_sync_bounded_session
All gates green; no code changes were needed — the prior failure was a missing final report, not a test failure.

**Verified / implemented (phase 184 final pass)**
- All 4 tasks present in `.agents/phases/complete/184_sync_bounded_session/`; confirmed their output in the worktree: `sync_session_reset_files=200` / `sync_prune_batch=500` on `Settings` (+ non-negative validators), both `SettingMeta` rows under `"Import & sources"`, `.env.example` + `README.md` entries.
- `import_sources` expunges at **exactly two** sites (per-file loop top L972, source boundary L1012) — grep proves no third `expunge`/`expire_all` anywhere in `app/`; `app/db.py:30` untouched.
- `_prune` uses `load_only(id, source, path, is_image, image_path, page_count).order_by(Document.id)` + per-batch commits, with the phase-122/126 guards and `changed_paths` intact.

**Tests / lint / coverage (all 100% green)**
- `uv run pytest --cov=app --cov-report=term-missing` → **4506 passed**, `app/` TOTAL **99%** (>90%).
- `uv run pytest tests/integration/test_import_session_retention.py -v --no-cov` → **12 passed**.
- `uv run pytest tests/e2e/test_sync_session_reset.py -v --no-cov` → **4 passed in isolation**.
- Regression sweep of neighbouring sync suites (each isolated): `test_sync_changed_only` 3, `test_sync_summary_visibility` 3, `test_prune_cascade_cleanup` 3, `test_sync_git_freeze` 2 — all passed.
- `uv run ruff check .` → All checks passed. `uv run pyright` → 0 errors, 0 warnings.

**Completion criteria**
1. Settings + CATALOG + `.env.example` + README; `test_settings_catalog.py` → ✅ green
2. Retention test green with the post-fix bound and both measured numbers in the docstring (pre-fix 12-row monotone staircase / ≈265 MB extrapolated; post-fix ends at 0, add-path peak 14 vs 21→49) → ✅
3. Expunge only at the two documented-safe points; light-row batched prune with guards intact → ✅
4. Every existing importer/sync/removal test green with unchanged expectations → ✅
5. Full suite green, coverage >90%, ruff + pyright clean → ✅
6. Phase E2E green in isolation → ✅
7. README + AGENTS accurate/consistent → ✅
8. Atomic commit — left to the harness (I ran no git write commands).

**Documentation** — Full README (1886 lines) + AGENTS.md (189 lines) review: **verified consistent — no changes needed.** Programmatic cross-checks: every `Settings` field appears in `.env.example` and in the README config reference; every `.env.example` var is documented in that reference; every `tests/…` and `app/…` path cited in README/AGENTS exists; all 10 README page URLs are registered in `app/main.py`; documented E2E port vars (`E2E_APP_PORT_SYNCRESET[_ZERO]`) exist in the test. The only reference-var without a `Settings` field is `BOR_THEME`, which the README explicitly documents as retired (correct as written).

**Notable decisions/deviations** — None. No code, test, or doc edits were made this pass; changes remain entirely in the working tree for the harness to stage. `00_phase.md` still sits in `todo/` while tasks 01–04 are in `complete/` — harness-owned, so I left it untouched.

**Next pending phase:** `185_sync_defer_content_fast_path`.
2026-10-15 23:19:27 -04:00

12 KiB
Raw Blame History

AGENTS.md — Operating Rules for All Agents on This Repo

  1. Always read .agents/PLAN.md first. It contains the architecture, the LOCKED DECISIONS, the UI/UX standards, the data model, and the roadmap. Do not design anything that contradicts it.
  2. Follow the phased execution protocol in .agents/phases/. Work through .agents/phases/todo/ in numeric order. When a phase is complete, move its directory to .agents/phases/complete/.
  3. Strictly adhere to the LOCKED DECISIONS (PLAN §2). If you believe a LOCKED decision is wrong, stop and flag it — do not silently deviate.
  4. "One Story, One Phase": each user story in .agents/user_stories/ corresponds to a distinct execution phase, and every phase — story-driven or owner-directed — includes its own dedicated Playwright E2E test suite (one test file for the phase, run in isolation).
  5. "UI Structure Check": before finalizing any UI component, verify that it follows the layout principles in .agents/PLAN.md §7 (proper container usage, the 72rem centered reading column — every page rides the 72rem container at every viewport; phase 100 supersedes §7.1's 46rem chat column, full-width tables on Sources — no skinny wasted-space lists; the catalogue tables (RAG, Sources, History) are bounded scroll panels — max-height: min(70vh, 64rem) — with a sticky header row; a clipped action label shows an ellipsis hint (.cell-clip) with the full text kept reachable via the control's aria-label (phase 156); an in-flight turn never paints an empty answer bubble — the box is created empty as the reasoning anchor and .msg .bubble:empty { display: none } hides it until the first answer token (phase 166); an answer's inline [text](url), ![alt](src) and ####+ heading render through the ONE shared renderer into an allowlist-gated a.md-link (underlined, never a javascript:/data: element), a container-capped img.md-img (max-width: 100% — no horizontal overflow at 375px) and an h5/h6 sized into the existing type scale — never printed reference text (phase 180); an SVG document paints as a graphic — the viewer's image block adds .doc-image-img--vector (width: 100%, object-fit: contain) when format is svg so a viewBox-only drawing spans the .doc-image card instead of the 300×150 default box, while the chat figure and Sources thumbnail keep their fixed caps and the bytes stay an <img> src (never inlined) (phase 181); the document viewer's page strip is CLASS-scoped — the primary class shows the strip in place of the text, the additive class never hides the text: Text/Pages toggle group (aria-pressed, Text default) with the strip behind it (phase 163); a lazily-drilled KB-tree level keeps the level the user is ON rendered while its node fetch runs, and its ONE in-flight row (role="status") / alert + Retry row (role="alert", a real <button>) sits inside that same bounded scroll panel — and the stat cards read the server's totals, never a walk of a depth-limited tree (phase 167); an answer's code block is EITHER verbatim-and-badged OR plainly the model's — never a silently-substituted hybrid: the ```bor locator fence renders through the ONE shared renderer as .verbatim[data-bor-locator] with the model's body DROPPED (renderMarkdown(md, { verbatim: true }), off by default so every other surface is byte-identical), styled from palette variables only with a min(40rem, 70vh) scroll body, a sticky source/path caption and a user-select: none line-number gutter, while the plain .code-block wrapper carries a Copy button and no badge (phase 182); those placeholders hydrate against the frame's answer_text through the ONE panel client (frontend/assets/verbatim.js — a classic script loaded AFTER markdown.js on the chat AND shared pages, no second locator grammar, a plain string compare against the server's canonical locator, the caption's numbers always the SERVER's extent, an unresolvable locator painting the pinned sentence I could not verify this against the knowledge base with no badge, no body and none of the model's bytes, no panel left at "Retrieving the exact text…" after a terminal frame (the error frame, the 300 s guard, Stop and the failure funnel all settle it), the Copy button's clipboard content equal to the server string exactly, and the shared page's panel Copy button + plain /document.html link the ONE owner-directed exception to that page's zero-controls rule) — phase 182 task 07) and meets WCAG 2.1 AA basics (semantic landmarks, labels, contrast ≥4.5:1, focus-visible, aria-live for the stream).
  6. "No CDN Rule": all CSS, JS, fonts, and images must be served statically from within the FastAPI application. No <script src="https://…"> or <link href="https://…"> tags in HTML templates unless bundled locally. An integration test enforces this on the index page.
  7. "Debugpy Check": any new entrypoint must keep the conditional import — debugpy is imported only when DEBUGPY=1 (see app/core/debugging.py); default off, never imported otherwise.
  8. Git protocol: Git is mandatory. One atomic, professional commit per completed phase (Conventional Commits, e.g. feat(rag): stream RAG answers over SSE with source citations). Always append --no-gpg-sign (the repo also sets commit.gpgsign=false). .agents/ is tracked and committed with the phase — only runtime artifacts (.agents/phase-sessions/, .agents/pipeline.log) are gitignored.
  9. Test gates are non-negotiable: a phase is complete only when unit + integration tests pass, app/ coverage is >90% (uv run pytest --cov=app --cov-report=term-missing), and the phase's Playwright E2E file passes in isolation (uv run pytest tests/e2e/test_<story>.py -v --no-cov).
  10. Amply log, visibly feed back: server-side, log the required per-turn line (PLAN §9 — phase 182 appends verbatim_blocks=N after scaffold_stripped=N: the verbatim display blocks actually SENT on the done frame, 0 on every turn without them, deflected included) and the per-response byte-budget line (app/core/response_budget.py — one api method=… path=… status=… bytes=… enc=… duration_ms=… line per /api/* response, INFO at/over BOR_LOG_LARGE_RESPONSE_BYTES, read it with grep ' :: api ', audit it with uv run python -m scripts.api_payload_audit, phase 167); UI-side, honor the "never stale" feedback contract (PLAN §7.4).
  11. Never block the event loop: anything blocking (a subprocess, synchronous file/network I/O, a heavy walk) reached from an async handler or a spawned background coroutine goes through asyncio.to_thread (or create_subprocess_exec). The sync's git calls do (phase 179: a long git clone used to park the single uvicorn loop and freeze the whole HTTP surface, the 2 s GET /api/sync/status poll included — pinned by tests/integration/test_sync_git_freeze.py with a 3 s stub and by tests/e2e/test_sync_git_freeze.py with a real ~9 s file:// clone). A cooperative cancel unblocks the run at the to_thread await; the worker thread itself runs to completion (threads are not interruptible) while the loop keeps serving — pinned by the same E2E module's cancel-during-clone test.
  12. Never hydrate what you delete: Document.chunks is passive_deletes=True (phase 183), so deleting a document removes its chunk rows in the database (chunks_document_id_fkey is ON DELETE CASCADE, alembic 0001) and a mass prune costs O(documents). The importer's prune and the source-removal endpoint both rely on it. Do not remove the keyword, and do not add a delete path that SELECTs chunks (or a Chunk.embedding) on its way to deleting them — one hydrated 768-dim embedding is ~24.8 KiB of Python, which is how the 2026-10-15 sync's 3,008-document prune reached 4 GB and locked up the VM. Pinned by tests/integration/test_prune_cascade_statements.py (statement count, zero orphan chunk rows, and a live-FK drift guard that fails the suite if the cascade clause ever disappears — because then passive_deletes=True would silently orphan rows). The doc.chunks = [...] re-embed path is the deliberate, documented exception (collection replacement legitimately needs the loaded collection; changed files only).
  13. Release the sync session at the proven-safe points only: an import run holds one session and expire_on_commit=False (app/db.py) means a commit releases nothing, so _unchanged_doc's file_hash stamp keeps every walked Document — content included — resident in the session's own strong dirty set (not collectable): memory grew with the KB. The cadence is therefore session.expunge_all() every BOR_SYNC_SESSION_RESET_FILES files at the TOP of the per-file loop and once per source before the two post-passes (phase 184; 0 = never, the pre-phase rollback). expunge_all DISCARDS unflushed changes, so never move it into _index_file, _unchanged_doc, _store_summary or a post-pass — their locals are live across statements — and never add a reset before a file's per-file commit. Pinned by tests/integration/test_import_session_retention.py (bounded with the cadence on, unbounded with it 0, byte-identical summaries and DB) and through the UI by tests/e2e/test_sync_session_reset.py (two module apps, one per cadence: every document exactly once, the all-unchanged re-sync, and the 0-cadence run byte-identical to the 3-cadence one).
  14. The prune reads six columns and commits in batches: _prune selects its candidates through load_only(id, source, path, is_image, image_path, page_count) order_by(Document.id) — the exact set the phase-122 images-off guard, the phase-126 parent-PDF rule and the image/page copy cleanup read, and NOT content (252 MB of markdown per full KB walk) — and commits every BOR_SYNC_PRUNE_BATCH deletes (0 = the pre-phase single commit; phase 184). Never read another Document attribute inside that loop (a deferred column costs one lazy SELECT per document and unbounds the walk — no raiseload paper-over, add the column instead), and never swap the per-document session.delete(doc) for a bulk delete() (the guards, changed_paths and the log line depend on the per-document shape). A non-zero batch deliberately trades the all-or-nothing prune for a prefix (LOCKED A2): seen is recomputed from disk, so the next sync converges. Pinned by tests/integration/test_import_session_retention.py (the captured SELECT's column list, exactly one document-wide SELECT per prune (a second one is a per-row lazy load), the commit counts at 10 and 0, both guards on light rows) and tests/e2e/test_sync_session_reset.py (the five-document prune at batch 2 through the Sources page: both folders cleared, the survivors' chunks intact, zero orphan chunk rows).

Quick reference

podman compose up -d db                      # start Postgres 17 + pgvector
cp .env.example .env                         # once; then edit
uv run alembic upgrade head                  # apply migrations
uv run uvicorn app.main:app --reload         # dev server (add DEBUGPY=1 to debug)
uv run pytest                                # unit + integration
uv run pytest --cov=app --cov-report=term-missing
uv run playwright install chromium           # once
uv run pytest tests/e2e/test_smoke.py -v --no-cov
uv run ruff check . && uv run pyright        # lint + types
uv run python -m scripts.eval_retrieval --from-file tests/fixtures/retrieval_battery.txt   # retrieval regression battery (phases 119/162)
uv run python -m scripts.api_payload_audit                      # endpoint byte audit vs the 256 KB budget (phase 167; app must be running)