All green. Final report:
**Phase 129 final verification pass — 100% green** (previous work intact; this pass re-verified everything and fixed two pre-existing README inconsistencies)
- Verified on-spec: `LLMClient.embed` per-batch `on_progress` hook; importer `on_chunk_progress` plumbing (text/image/PDF branches; summary embed excluded); `SyncStatus.current_chunk/chunks_total` reset+clear contract; UI `.sync-label-chunk` pinned part with untruncated `title`/`#sync-result`
- `uv run pytest --cov=app -q`: exit 0, ~3041 passed, 0 failed, `app/` coverage **99%** (>90% gate)
- `uv run pytest tests/e2e/test_sync_chunk_progress.py -v --no-cov` (isolated, DB up): **2 passed** (endpoint chunk climb with true `chunk_document` total + UI `· chunk n/m` part)
- `uv run ruff check .`: all checks passed · `uv run pyright`: 0 errors, 0 warnings
- Completion criteria: all 5 ✅ (incl. no behavior change in completed phases — full suite green, CLI/chat no-op paths byte-identical)
- Docs: README.md fixed 2 pre-existing inconsistencies (upload section claimed "unpacks + scans immediately" — phase 90 made it unpack+register only, scan deferred to Sync; troubleshooting claimed "120s client timeout" — actual guard is the 300 s pre-token `TURN_TIMEOUT_MS`). Phase-129 label behavior already documented. AGENTS.md re-verified consistent — no changes
- Removed stray untracked `NOTES.md` (8-byte artifact); no live-infra changes; no commits — all changes left in working tree for the harness
- Next pending phase: `130_chat_answer_clusters`
7.1 KiB
Phase 129 — The sync label shows what chunk we're on out of how many
Source: TODO.md L5 — "The Syncing… name/of/file/here (2/3) needs to show what chunk we're on out of how many" (ASSUMPTION A6, owner-confirmed 2026-09-27: "chunk" = the current file's EMBEDDING CHUNKS; position reported at BATCH granularity — n = first chunk of the in-flight token-budgeted embed batch — because the transport embeds batches per HTTP call and per-chunk calls would be a latency regression; the summary-chunk embed is excluded from the counter; label Syncing… <file> (2/3) · chunk 14/38).
Story: n/a (TODO-derived phase, Protocol B append; a NEW feature — distinct from phase 128's mobile fix, per the owner).
Context: app/rag/llm.py — LLMClient.embed(texts) (L346: iterates self._batch_texts(texts) — token-budgeted batches, BOR_EMBED_BATCH_SIZE cap, _REQUEST_TOKEN_BUDGET ≈ 2800 chars), _embed_batch (L289: the halving backstop on _TooLarge), self.embed_batches counter (L249); app/rag/importer.py — import_sources (L408, the per-file progress(source, rel, done, total) hook fired BEFORE each _index_file), _index_file (L647 — text branch → _replace_content_chunks, image branch → _index_image_file, PDF branch → _index_pdf_file), _replace_content_chunks (L603 — chunks the content, llm.embed([c.content for c in doc.chunks]) in ONE call, token-cap re-chunk retry loop), _store_summary (embeds the ONE summary chunk — EXCLUDED from the counter); app/api/sync.py — SyncStatus (the phase-64/98 progress fields: current_file, files_done/total, phase, current_summary, summaries_done/total; _run_sync's _hook closure convention; terminal-state clearing); frontend/assets/sources.js — the phase-128 part builders (setSyncLabel, .sync-label-main / .sync-label-count); tests/e2e/slow_llm.py + tests/e2e/test_sync_summary_visibility.py — the slow-proxy sampling pattern.
Objective
While a file is being imported, the sync status (endpoint + button label) shows the file's embedding-chunk position: Syncing… <file> (2/3) · chunk 14/38 — 14 = the first chunk of the embed batch in flight, 38 = the file's total content chunks. The position climbs through the file's batches and is cleared when the file (or the run) moves on. Everything that passes no callback stays a zero-cost no-op (the CLI, the chat path).
Dependencies
128_sync_label_mobile(todo) — the label's part structure (.sync-label-main+ pinned count span) is the carrier; this phase adds the pinned chunk span.
Design (shared by all tasks — the executor reads this, not the chat)
- The embed hook (task 01):
LLMClient.embed(texts, on_progress: Callable[[int, int], None] | None = None)— before EACH top-level batch request,on_progress(first_1based, len(texts))wherefirst_1based= 1 + the cumulative chunk counts of the already-sent batches.None(every existing caller — the chat retrieval path,embed_one, the summarizer, the CLI) is a zero-cost guarded no-op. The_embed_batchhalving backstop does NOT sub-report — the top-level loop reports batch starts; a halved batch means the position is approximate (document in the docstring; the backstop is rare by construction). - Importer plumbing (task 02):
import_sources(..., on_chunk_progress: Callable[[int, int], None] | None = None)— a new keyword-only param, plumbed:import_sources→_index_file(..., on_chunk=...)(all three branches: text, image, PDF — the PDF branch's content embed is the same_replace_content_chunkscall; the image branch's ONE description-chunk embed reports1/1— acceptable, it IS the file's only chunk) →_replace_content_chunks(..., on_chunk=...)→llm.embed([...], on_progress=on_chunk). The token-cap re-chunk retry re-embeds with the NEW (smaller-target) chunk list — the hook naturally reports against the new total (no special case)._store_summary's single summary-chunk embed NEVER receives the hook (the counter is content chunks only — no1/1noise after the real progress ends). Thescripts/import_docs.pyCLI passes nothing — byte-identical. - Sync status (task 03):
SyncStatusgainscurrent_chunk: int = 0andchunks_total: int = 0(documented: the current file's embed-batch position — 0/0 before the first batch and in terminal states)._run_sync: reset to 0/0 at run start (with the other progress fields); a closure_chunk_hook(chunk, total)(the file's_hookconvention) sets them; passed ason_chunk_progresstoimport_sources. CLEARED (0/0) when the import phase ends — set at thephase = "overview"andphase = "summaries"transitions AND in both terminal states (thecurrent_fileclearing points — same convention, same spots).GET /api/sync/statusexposes both fields (0/0 idle — the dataclass defaults). TheSyncStatusdocstring + module docstring get the chunk-position paragraph (the phase-64/98 style). - UI (task 04):
sources.js— the import-phase label gains a THIRD pinned part.sync-label-chunk(markup inindex.html's#sync-label,hiddenby default): whenstatus.chunks_total > 0the import fall-through emits· chunk ${status.current_chunk}/${status.chunks_total}into it; every other phase/state leaves it hidden/empty (the summaries phase has its own(n/m)— no chunk part; the prelude has no file yet). The UNTRUNCATED concatenation (main + count + chunk part) rides the buttontitle+#sync-result(A4). The pinned unit tests (tests/unit/test_frontend_sync_upload.py) gain the chunk-part pins.
Tasks
01_llm_embed_hook.md—LLMClient.embed's optional per-batchon_progress.02_importer_chunk_progress.md—import_sources→_index_file→_replace_content_chunks→embedplumbing.03_sync_status_chunk_fields.md—SyncStatusfields, the closure, clearing, the endpoint.04_ui_chunk_part.md— the label's chunk part + unit pins.05_e2e_chunk_progress.md— dedicated Playwright E2E, run in isolation.
Testing & Quality
- Unit/integration: the embed hook's call sequence (batch boundaries,
Noneno-op, halving-approximate documented); the importer plumbing (hook fires per file with its own total; the CLI path unchanged; the summary-chunk embed never fires it); the sync status (fields in the endpoint JSON, reset per run, cleared at phase transitions + terminal, 0/0 idle). - Coverage: >90% on new/modified code (
app/). - E2E: one dedicated Playwright file, run in isolation, slow-LLM sampling.
Completion Criteria
GET /api/sync/statusreportscurrent_chunkclimbing 1 → N withchunks_total= the file's true content-chunk count while a file imports (E2E, sampled)- the rendered mobile/desktop label shows
· chunk n/mduring the import phase only; the untruncated text ridestitle+#sync-result uv run pytest --cov=app --cov-report=term-missinggreen,app/coverage >90%uv run pytest tests/e2e/test_sync_chunk_progress.py -v --no-covgreen in isolation- no behavior change in completed phases (chat embed path, CLI import, the phase-64/98 label contract minus the new part)