phase: 187_sync_peak_memory_line
Build and Push Containers / build-and-push-app (push) Successful in 1m47s
Build and Push Containers / build-and-push-db (push) Successful in 13s

**Phase 187 — final verification pass: all criteria met, repo 100% green.**

Verified (all four tasks already in `complete/`)
- `app/core/memory.py`: `rss_mb` / `rss_peak_mb` / `cgroup_peak_mb` / `fmt_mb`, pure stdlib, never-raise, **100%** coverage, 24 branch tests.
- Lifecycle lines append-only: `sync: started … rss_mb=`, all three terminals `… rss_mb= rss_peak_mb=` (failed = sanitized one-liner beside the surviving `logger.exception`), `import: summary … rss_mb=`; prefixes byte-identical.
- `GET /api/sync/status`: additive `rss_mb`/`rss_peak_mb` (`int|null`), keyed off the ONE `_sample_rss` (run start, phase transitions, forced terminals, 5 s-throttled hook), `peak >= current` invariant, anonymous 403.
- `sources.js` `fmtSyncPeakClause`: ` · peak <N> MB` trailing the counts line / "Sync cancelled", nothing when null, no new control/live region.

Gates (exact commands)
- `uv run pytest --cov=app --cov-report=term-missing` → **4570 passed**, TOTAL **99%**, `app/api/sync.py` **100%**, `app/core/memory.py` **100%**
- `uv run ruff check .` → clean · `uv run pyright` → **0 errors**
- `uv run pytest tests/e2e/test_sync_peak_memory_visible.py -v --no-cov` → **5 passed in isolation** (ran twice)
- All 27 touched E2E families re-run **in isolation**, all green (progress_box 8, cancel 3, status_freshness 4, summary_visibility 3, sync_button 3, session_reset 4, git_freeze 2, git_log_stream_dates 5, PDF/image suites, …)

Completion criteria: every checkbox ✅ except the commit + phase-dir move, which belong to the harness.

Documentation
- README reviewed in full (pages, commands, config tables cross-checked against `app/config.py` — 85 env rows, all defaults match; phase-187 ops section, greps, process-vs-cgroup caveat): consistent, no changes needed.
- `AGENTS.md`: **fixed one pre-existing inconsistency** — rule 6 claimed the no-CDN test covers "the index page"; it actually parametrizes 10 pages (all but `/doc-edit.html`).

Notable
- Two failures early in the pass were self-inflicted by running the E2E module concurrently with the full suite on the shared Postgres (TRUNCATE deadlock + one order-dependent `test_overview` flake); both vanish under the documented serial isolation — no code defect, no change made.
- **PLAN §9 still needs the owner's reconciliation** (phase 187 adds the sync memory fields + two status keys; PLAN is never agent-edited — AGENTS rule 10 carries the spec). `TODO.md` holds an unrelated owner note, left untouched.

Next pending phase: `188_chunks_hnsw_index`.
This commit is contained in:
2026-10-16 15:22:51 -04:00
parent 644f7de696
commit c054b47df5
72 changed files with 3826 additions and 96 deletions
+55 -3
View File
@@ -457,6 +457,21 @@
* rules stop walking a full tree — folderKnownIn() only claims what
* the HELD levels prove — and treeStats() prefers kbTree.totals (the
* walk is the pre-phase-167 fallback only).
*
* Phase 187 (task 03, D4/A4) — the sync result line carries the run's
* PEAK MEMORY. The two additive `rss_mb` / `rss_peak_mb` keys on
* GET /api/sync/status feed ONE clause builder (fmtSyncPeakClause),
* which APPENDS ` · peak 512 MB` to the result text the settle already
* wrote (the fmtSyncResult counts line, "Sync cancelled") — no new
* control, no new state string, no new live region, and the failed
* terminal's empty box is untouched (the banner + modal keep the error
* copy byte-identical, phase 150). Null — idle, a run that predates the
* phase, a platform that cannot know, and the upload job (whose status
* has no memory keys at all) — renders NOTHING, never "peak ? MB". The
* clause rides the single box writer, so the sole aria-live region
* announces today's wording plus the clause, and the number is the
* PROCESS high-water mark app/core/memory.py reports (never the host's
* cgroup figure — that one counts page cache).
*/
import { fetchIsAdmin } from "./header.js";
@@ -1103,6 +1118,37 @@ export async function mount(root) {
return { main, count, chunk, image };
}
/* Phase 187 (task 03, D4/A4): the run's PEAK-memory clause — the one
* number the 2026-10-15 incident had to reconstruct from three boots
* of systemd teardown lines, now on the page that started the run.
* It is a CLAUSE on the result line that already exists (the
* fmtSyncResult counts line, "Sync cancelled"), never a new control,
* never a state string of its own, and never a replacement: the
* existing copy keeps its exact wording and the clause trails it
* (" · peak 512 MB"). The value is the status's rss_peak_mb (the
* process high-water mark, the number the incident was about) or,
* where only a current reading exists, rss_mb — both PROCESS numbers
* from app/core/memory.py, never the host's cgroup figure (that one
* counts page cache; the caveat owns app/core/memory.py).
* NULL IS THE ABSENCE CONTRACT: both keys null (idle, a run predating
* this phase, a platform that cannot know) renders NOTHING — never
* "peak ? MB", never "peak null MB", never 0. The clause rides the
* SAME string the single box writer (setSyncProgress) lands on
* #sync-progress-text, so the box — the sole sync text surface AND
* the sole aria-live region (phase 150) — announces today's wording
* plus the clause, with no second announcement and no new region. It
* appears in the TERMINAL renders only (success + cancelled): the
* running label keeps its byte-identical shape, the failed terminal's
* box stays empty by the phase-150 contract (the banner + modal carry
* the error, byte-identical), and the upload settle never has the keys
* at all — which is the null case again. Text wraps inside the
* existing line (no clip, no .cell-clip, no layout shift at 375px). */
function fmtSyncPeakClause(status) {
const peak = status ? status.rss_peak_mb ?? status.rss_mb : null;
if (typeof peak !== "number" || !Number.isFinite(peak) || peak < 0) return "";
return ` · peak ${peak} MB`;
}
function fmtSyncResult(detail) {
const d = detail || {};
const added = d.added || 0;
@@ -1284,8 +1330,11 @@ export async function mount(root) {
// Phase 150 (task 02): the settle's counts line IS the box's
// result line (the sole live region) — the string builder is
// unchanged (A2: the "Synced <stamp>" label stays on the button;
// the stamp is date-qualified, phase 154).
setSyncProgress(fmtSyncResult(status.detail));
// the stamp is date-qualified, phase 154). Phase 187 (task 03)
// APPENDS the peak-memory clause to that same string — the counts
// line keeps its exact wording, the clause trails it, and a null
// peak contributes nothing (fmtSyncPeakClause).
setSyncProgress(fmtSyncResult(status.detail) + fmtSyncPeakClause(status));
hideSyncError();
emitSyncStatus(status);
// Refresh the catalog live — the KB just changed.
@@ -1322,7 +1371,10 @@ export async function mount(root) {
* else. */
function applySyncCancelled(status) {
settleSyncButton("Sync sources");
setSyncProgress("Sync cancelled"); // the box's result line (phase 150)
// The box's result line (phase 150) + the phase-187 peak clause —
// a cancelled run's memory is still the number the operator asked
// for ("Sync cancelled · peak 512 MB", nothing when it is null).
setSyncProgress("Sync cancelled" + fmtSyncPeakClause(status));
hideSyncError();
emitSyncStatus(status);
}