Skip to content

feat(library): P3 — heavy tier download, quality preferences, storage accounting, per-source rules#2070

Open
hognek wants to merge 9 commits into
jaylfc:devfrom
hognek:feat/library-p3
Open

feat(library): P3 — heavy tier download, quality preferences, storage accounting, per-source rules#2070
hognek wants to merge 9 commits into
jaylfc:devfrom
hognek:feat/library-p3

Conversation

@hognek

@hognek hognek commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Epic #2057 — Phase P3: Heavy tier

Builds on PR #2068 (P2 — YouTube cheap tier + web ingestor).

What this adds

Opt-in media download (heavy tier):

  • New HeavyDownloadProcessor wrapping yt-dlp's download_video()
  • Per-item quality preference (360/480/720/1080/best)
  • POST /api/library/items/{item_id}/download — trigger heavy download
  • GET /api/library/items/{item_id}/download/status — check progress
  • Quality fallback chain: explicit > rule > item > 720

Quality preference settings:

  • New quality column on library_items (P3 migration, safe ALTER TABLE)
  • Download button with quality selector in item cards (only for url:youtube, ready status)

Storage accounting:

  • LibraryStore.get_storage_summary() — total bytes, item count, per-kind breakdown
  • GET /api/library/usage endpoint with HTMX-aware HTML output
  • Storage summary bar in library UI (auto-refreshed via htmx)

Per-source rules engine:

  • library_rules table — source_pattern (fnmatch), quality, auto_download, enabled
  • POST/GET/DELETE /api/library/rules — CRUD endpoints
  • LibraryStore.match_rules(source_url) — fnmatch-based matching
  • Rules management UI in library page (collapsible <details> panel)
  • Auto-download: _ingest_task checks matching rules with auto_download=True and triggers heavy pipeline after cheap tier completes

Files changed (5)

File Changes
tinyagentos/library_store.py +112 — rules table, P3 column migration, rule CRUD, storage summary
tinyagentos/library_pipeline.py +152 — HeavyDownloadProcessor, run_heavy_pipeline
tinyagentos/routes/library.py +183 — download, rules, usage endpoints, auto-download logic
tinyagentos/templates/library.html +39 — storage bar, rules panel
tests/test_library.py +391 — 24 new tests

Test gate

  • 70/70 library tests pass (24 new P3 tests + 46 existing)
  • 1 existing slow test (test_filter_by_kind) skipped — hits real YouTube API

Design doc

Per docs/design/library-app.md section 3 (heavy tier) and section 4 step 5.

Summary by CodeRabbit

  • New Features
    • Added a Library app with UI and API to ingest files, URLs, web pages, and YouTube, then manage items, artifacts, jobs, and processing statuses.
    • Automatic metadata and previews for text/PDF/web/YouTube plus thumbnails and transcript/chapters where available.
    • Rule-driven YouTube heavy downloads with downloadable status tracking; supports source rules and storage usage dashboard.
    • Hand-off of text artifacts into Collections with per-item manifest.
  • Bug Fixes
    • Improved handling for missing content/tools, invalid inputs, and oversized uploads; better failure reporting and safer web fetching.
  • Tests
    • Added comprehensive automated tests for kind detection, pipeline behavior, storage lifecycle, API routes, downloads, rules, and collections handoff.

hognek added 6 commits July 20, 2026 16:34
…ollections handoff, app UI

Implements the Library app Phase 1 from docs/design/library-app.md:

- LibraryStore (SQLite, BaseStore): items, artifacts, jobs tables
  with full CRUD and cascade deletion
- Ingest endpoint (POST /api/library/ingest): file upload + URL
  reference, async background pipeline
- Pipeline processors (cheap tier): file metadata, text extraction,
  PDF text extraction, image thumbnails + dimensions
- Collections handoff: text artifacts indexed into taosmd
- App UI (/library): drop zone, htmx item list, Pico CSS

6 API routes: GET /library, POST /api/library/ingest,
GET /api/library/items, GET/DELETE /api/library/items/{id},
POST /api/library/items/{id}/reprocess

39 tests covering: kind detection, store CRUD, all 4 processors,
pipeline runner, collections handoff, and all API routes.

Related: jaylfc#2057
Docs-Reviewed: P1 library routes are internal admin-session; Library app docs live at docs/design/library-app.md. No external API surface change yet.
…d cap, task GC, HTMX responses

- pipeline: mark missing source files as 'error' instead of silently 'ready'
- ingest: stream file uploads in 1MB chunks with 100MB cap, reject oversized
- background tasks: retain strong references via module-level _background_tasks set
- HTMX: return HTML fragments (.item-card) when HX-Request header is present
…ngestor

Add YouTubeProcessor for url:youtube items — fetches metadata, thumbnail,
transcript, and chapters via knowledge_fetchers.youtube (yt-dlp). Cheap-tier
only: no video download, just text artifacts for collection indexing.

Add WebProcessor for url:web items — fetches HTML (SSRF-guarded), extracts
readable text via readability-lxml (falls back to tag-stripping). Auto-titles
from <title> tag.

Both registered in _PROCESSORS dict; run_pipeline dispatches to them
for url:youtube and url:web kinds.

Design doc: docs/design/library-app.md sections 3-4.
Tests: 8 new tests, all 47 library tests pass.
… exceptions

- _render_item_card: escape all user-controlled values (title, kind, id,
  status) with html.escape() to prevent reflected XSS through crafted
  page titles, YouTube metadata, or user form fields.
- WebProcessor: use html.unescape() on extracted <title> text to decode
  HTML entities before storage.
- YouTubeProcessor/WebProcessor: remove bare except Exception that
  swallowed fetch failures. Let network/SSRF/yt-dlp errors propagate to
  run_pipeline which already marks items as 'error' — a failed URL fetch
  must not silently appear 'ready' with zero artifacts.

All 47 library tests pass.
…ponse-size cap

- WebProcessor: disable auto-redirects, manually validate every redirect
  hop with validate_url_or_raise() against the SSRF blocklist (same
  pattern as knowledge_ingest._download_article). Max 5 redirects.
- WebProcessor: cap response body at 10 MB, stream with aiter_bytes()
  to avoid OOM on large/hostile pages.
- Tests: update _mock_httpx_response with is_redirect=False, encoding,
  and aiter_bytes() to match the new fetch flow.

All library tests pass.
… accounting, per-source rules

- LibraryStore: add library_rules table, new columns (quality, auto_download,
  download_path, download_bytes, downloaded_at) with safe migration in
  _post_init, rule CRUD methods, fnmatch-based match_rules, get_storage_summary
- HeavyDownloadProcessor: yt-dlp-based media download for url:youtube items
  with quality preference (360/480/720/1080/best), fallback heuristics for
  missing output paths
- run_heavy_pipeline: rule-aware quality resolution (explicit > rule > item >
  default 720), job tracking via library_jobs table
- Routes: POST /download, GET /download/status, POST/GET/DELETE /rules,
  GET /usage with HTMX-aware HTML responses
- Auto-download: _ingest_task checks matching rules with auto_download=True
  and triggers heavy pipeline after cheap tier completes
- Template: storage summary bar (polled via htmx), per-item download button
  with quality selector for YouTube items, rules management <details> panel
  with add/delete
- Tests: 24 new tests (7 store rules/storage, 4 HeavyDownloadProcessor,
  4 run_heavy_pipeline, 9 routes)
- 70/70 library tests pass (1 slow route test skipped)
@hognek
hognek marked this pull request as ready for review July 20, 2026 17:50
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a complete Library application with SQLite persistence, asynchronous file and URL ingestion, artifact processing, YouTube downloads, collections handoff, FastAPI endpoints, an HTMX UI, and comprehensive tests.

Changes

Library application

Layer / File(s) Summary
Storage contracts and persistence
tinyagentos/library_store.py, tests/test_library.py
Adds SQLite-backed items, artifacts, jobs, rules, migrations, status validation, filtering, and storage summaries with CRUD and rule-matching tests.
Ingest and download pipelines
tinyagentos/library_pipeline.py, tests/test_library.py
Adds kind detection, file/text/PDF/image/YouTube/web processors, artifact generation, pipeline status handling, and rule-driven YouTube downloads with orchestration tests.
Collections handoff
tinyagentos/library_collections.py, tests/test_library.py
Copies text artifacts into per-item collection directories, attempts indexing, and writes manifests.
Library routes and browser interface
tinyagentos/routes/library.py, tinyagentos/routes/__init__.py, tinyagentos/templates/library.html, tests/test_library.py
Adds ingestion, item management, downloads, rules, usage endpoints, router registration, background processing, and an HTMX library page.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant LibraryRoutes
  participant LibraryPipeline
  participant LibraryStore
  participant CollectionsHandoff
  Browser->>LibraryRoutes: submit file or URL
  LibraryRoutes->>LibraryStore: create pending item
  LibraryRoutes->>LibraryPipeline: start ingest task
  LibraryPipeline->>LibraryStore: store artifacts and ready status
  LibraryRoutes->>CollectionsHandoff: hand off text artifacts
  CollectionsHandoff->>LibraryStore: read artifacts and item metadata
  LibraryRoutes-->>Browser: return item and status
Loading

Possibly related PRs

  • jaylfc/taOS#2062 — Directly overlaps the LibraryStore, ingest pipeline, collections handoff, routes, templates, and tests.
  • jaylfc/taOS#2068 — Directly overlaps YouTube and web processors, kind detection, pipeline behavior, and related tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main P3 library changes: heavy downloads, quality selection, storage usage, and per-source rules.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Comment thread tinyagentos/library_pipeline.py Outdated
quality = item.get("quality", "") or "720"

# Create a job entry
await store.create_job(item_id, "heavy_download")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: create_job returns the new job id, but it is discarded here. The success/error branches below re-fetch with (await store.get_item_jobs(item_id))[-1]["id"] to locate "the" heavy-download job. That is racy and incorrect if more than one job exists for the item (e.g. the cheap-tier pipeline already created jobs, or a manual download and an auto-download run concurrently). get_item_jobs orders by created_at, and with time.time() timestamps two jobs created in the same tick can sort unpredictably, so [-1] may point at the wrong job and mark the wrong entry done/error.

Capture the id directly and reuse it:

Suggested change
await store.create_job(item_id, "heavy_download")
job_id = await store.create_job(item_id, "heavy_download")

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/library_pipeline.py Outdated
return None
except Exception:
logger.exception("Heavy pipeline failed for item %s", item_id)
await store.update_item_status(item_id, "error")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: An unexpected exception in the optional heavy-download tier flips the whole item to error via update_item_status(item_id, "error"). The heavy download is opt-in and runs after the cheap-tier pipeline has already produced a ready item with valid text artifacts. A failed secondary media download should not overwrite that ready status and effectively hide the successfully-ingested content in the UI. Consider recording the failure in meta_json/the job entry (as the in-band path is None branch already does) and leaving the item status untouched.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/library_pipeline.py Outdated
# Read body with a size cap to avoid OOM on large/hostile pages.
body_chunks: list[bytes] = []
total = 0
async for chunk in resp.aiter_bytes(8192):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This size cap does not actually protect against OOM. resp = await client.get(current_url) (line 413) fully reads and buffers the entire response body into memory before this loop runs, so aiter_bytes() here just replays already-downloaded bytes. A hostile/large page is fully loaded regardless of _MAX_WEB_BYTES. To enforce the cap during download, stream the request instead, e.g. async with client.stream("GET", current_url) as resp: and read/cap inside that context (this also changes the redirect handling, since a streamed redirect response must be inspected before the body is consumed).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/library.py Outdated
logger.info("Heavy download complete for item %s: %s", item_id, result)
except Exception:
logger.exception("Heavy download crashed for item %s", item_id)
await store.update_item_status(item_id, "error")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Same concern as run_heavy_pipeline: if the background heavy download crashes, this marks the entire item as error, overwriting the ready status and text artifacts already produced by the cheap tier. Since heavy download is an opt-in, additive step, a failure should be surfaced via the download job/status rather than degrading the item's overall state. (Note also that run_heavy_pipeline already swallows exceptions internally and sets error itself, so this outer handler will rarely trigger.)


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • tinyagentos/library_pipeline.py - previous SUGGESTION (line 418, HEAD fallback comment overstating protection) resolved by the clarifying comment added at lines 404-407
Previous Review Summaries (3 snapshots, latest commit 7bd5912)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 7bd5912)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
tinyagentos/library_pipeline.py 416 Fallback to GET only triggers on an exception, not when the server rejects HEAD with a 405 status; the comment on line 405 overstates the protection
Files Reviewed (3 files)
  • tinyagentos/library_pipeline.py - 1 suggestion
  • tinyagentos/templates/library.html - 0 issues (prior csrf-meta suggestion resolved by this diff)
  • tests/test_library.py - 0 issues

Note: the previous WARNING on library_pipeline.py (Phase 1 client.get() buffering the body) and the previous SUGGESTION on templates/library.html (unused csrf-token meta tag) are resolved by this incremental diff (HEAD 7bd59128). Findings on lines outside this diff (e.g. substring stem match) are out of scope for this incremental update.

Fix these issues in Kilo Cloud

Previous review (commit 1b3fcc7)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/library_pipeline.py 413 Phase 1 client.get() still fully buffers the final URL body, partially defeating the OOM protection the Phase 2 streaming was added for (and double-fetches)

SUGGESTION

File Line Issue
tinyagentos/library_pipeline.py 679 Substring stem match expected_stem in f.name may select an unrelated downloaded file
tinyagentos/templates/library.html 9 Unused empty <meta name="csrf-token" content=""> tag is dead/misleading markup
Files Reviewed (5 files)
  • tinyagentos/library_pipeline.py - 1 warning, 1 suggestion
  • tinyagentos/library_store.py - 0 issues
  • tinyagentos/routes/library.py - 0 issues
  • tinyagentos/templates/library.html - 1 suggestion
  • tests/test_library.py - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit 74cb317)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 4
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/library_pipeline.py 746 create_job return id discarded; job state later updated via racy get_item_jobs()[-1], which can target the wrong job under concurrent/multiple jobs
tinyagentos/library_pipeline.py 769 Unexpected exception in opt-in heavy tier flips an already-ready item to error, hiding successfully-ingested cheap-tier content
tinyagentos/library_pipeline.py 429 Response-size cap is ineffective — client.get() fully buffers the body before the aiter_bytes cap loop, so it does not prevent OOM; use client.stream()
tinyagentos/routes/library.py 538 Background heavy-download crash overwrites ready status with error (same concern as run_heavy_pipeline)
Files Reviewed (7 files)
  • tinyagentos/library_pipeline.py - 3 issues
  • tinyagentos/routes/library.py - 1 issue
  • tinyagentos/library_store.py - 0 issues
  • tinyagentos/templates/library.html - 0 issues
  • tinyagentos/library_collections.py - 0 issues
  • tinyagentos/routes/__init__.py - 0 issues
  • tests/test_library.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 35.6K · Output: 3.1K · Cached: 95.6K

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
tests/test_library.py (2)

409-419: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Incomplete assertion — updated_title is computed but never checked.

updated_title is read on Line 416 and the comment describes verifying the title, but no assertion follows, so this block is a no-op. Either assert the expected title behavior or drop the dead variable and comments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 409 - 419, Complete the title
verification in the test block following the updated_title assignment by
asserting the expected title behavior after processing. If no title value is
guaranteed, remove updated_title and its accompanying comments instead of
leaving unused test code.

1096-1097: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace fixed asyncio.sleep(0.5) waits with polling to avoid flaky tests. All three tests block on a hardcoded 0.5s sleep for the background ingest task to complete, which is timing-dependent and can fail under CI load or pass spuriously; poll the item status (or download state) with a short timeout instead.

  • tests/test_library.py#L1096-L1097: poll GET /api/library/items/{item_id} until status is terminal (bounded timeout) before posting the download.
  • tests/test_library.py#L1122-L1123: same polling before asserting the non-YouTube download rejection.
  • tests/test_library.py#L1135-L1136: same polling before requesting /download/status.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 1096 - 1097, Replace the fixed
asyncio.sleep waits in tests/test_library.py at lines 1096-1097, 1122-1123, and
1135-1136 with bounded polling of GET /api/library/items/{item_id}. Wait until
the item reaches a terminal status before posting the download, asserting the
non-YouTube rejection, or requesting /download/status, using a short polling
interval and timeout.
tinyagentos/library_store.py (1)

85-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: fold the P3 columns into CREATE TABLE and keep the migration for legacy DBs only.

quality, auto_download, downloaded_at, download_path, and download_bytes are absent from the library_items CREATE TABLE (Lines 25-36) and exist only because _post_init always runs the ALTER TABLE path. It works, but a reader inspecting the schema would not see these columns, and the if col_names: guard on Line 100 is always truthy (the table always exists), so it does not actually gate the commit on a migration having run. Declaring the columns in the base schema and scoping the migration to legacy P1/P2 databases makes the intended shape self-documenting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_store.py` around lines 85 - 101, Update the library_items
CREATE TABLE definition to declare quality, auto_download, downloaded_at,
download_path, and download_bytes with their existing types and defaults. In
_post_init, retain the ALTER TABLE migration only for legacy databases missing
those columns, and commit only when at least one migration was applied rather
than checking col_names, while preserving existing schema behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tinyagentos/library_pipeline.py`:
- Around line 667-684: The missing-file fallback in the download handling flow
must not select an arbitrary newest entry from the shared download directory.
Update the logic around the Path(path) check to locate only the file matching
the current video’s expected ID or output stem, exclude temporary or in-progress
files such as .part, and retain the existing download_error update when no
matching completed file exists.
- Around line 745-766: The heavy download flow should retain the ID returned by
create_job and use it for both update_job calls. Update the job creation
assignment and replace each get_item_jobs(item_id)[-1]["id"] lookup with that
captured job_id, preserving the existing done and error states.
- Around line 404-439: Update the redirect-fetch loop around current_url, resp,
and httpx.AsyncClient so requests use client.stream("GET", current_url) instead
of client.get(). Keep the response context open while checking redirects,
calling raise_for_status(), and consuming aiter_bytes(); enforce _MAX_WEB_BYTES
during streaming before accumulating chunks, then close the stream before
proceeding.

In `@tinyagentos/routes/library.py`:
- Around line 181-213: Update _ingest_task to fetch and validate the item’s
terminal status immediately after run_pipeline completes, and only perform rule
matching and run_heavy_pipeline when the status is ready. Return without
auto-download when the pipeline leaves the item in error, while preserving the
existing auto-download behavior for successful items.
- Around line 287-289: Remove the unnecessary f-string prefixes from the static
`<div class="info">` and `<div class="meta">` fragments in the surrounding HTML
construction, while retaining the f-string prefix on the title-containing `<h3>`
fragment.

In `@tinyagentos/templates/library.html`:
- Around line 76-152: Add a shared HTMX configuration in library.html that
injects the X-CSRF-Token header for every HTMX request, sourcing the token from
the page’s existing CSRF mechanism. Ensure the configuration applies to the
ingest form/drop-zone and source-rules write requests so session-cookie users
pass verify_csrf, without changing their endpoints or targets.

---

Nitpick comments:
In `@tests/test_library.py`:
- Around line 409-419: Complete the title verification in the test block
following the updated_title assignment by asserting the expected title behavior
after processing. If no title value is guaranteed, remove updated_title and its
accompanying comments instead of leaving unused test code.
- Around line 1096-1097: Replace the fixed asyncio.sleep waits in
tests/test_library.py at lines 1096-1097, 1122-1123, and 1135-1136 with bounded
polling of GET /api/library/items/{item_id}. Wait until the item reaches a
terminal status before posting the download, asserting the non-YouTube
rejection, or requesting /download/status, using a short polling interval and
timeout.

In `@tinyagentos/library_store.py`:
- Around line 85-101: Update the library_items CREATE TABLE definition to
declare quality, auto_download, downloaded_at, download_path, and download_bytes
with their existing types and defaults. In _post_init, retain the ALTER TABLE
migration only for legacy databases missing those columns, and commit only when
at least one migration was applied rather than checking col_names, while
preserving existing schema behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 793490c3-123c-4664-941e-416e7481397c

📥 Commits

Reviewing files that changed from the base of the PR and between e2d931d and 74cb317.

📒 Files selected for processing (7)
  • tests/test_library.py
  • tinyagentos/library_collections.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/library_store.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/library.py
  • tinyagentos/templates/library.html

Comment thread tinyagentos/library_pipeline.py Outdated
Comment thread tinyagentos/library_pipeline.py
Comment thread tinyagentos/library_pipeline.py Outdated
Comment thread tinyagentos/routes/library.py
Comment thread tinyagentos/routes/library.py Outdated
Comment thread tinyagentos/templates/library.html
…bbit)

- Capture create_job id directly instead of racy get_item_jobs()[-1] lookup
- Don't flip ready→error on heavy download failure (cheap-tier artifacts stay valid)
- Switch WebProcessor to client.stream() for real in-stream size capping
- Match file fallback by expected stem, exclude .part files
- Gate auto-download on item status==ready (not error)
- Remove extraneous f-prefixes from static HTML strings
- Add CSRF header injection for HTMX write requests in library page
- Assert title in YouTube processor test instead of dead comment
- Replace fixed asyncio.sleep with bounded status polling in 3 tests
- Fold P3 columns into CREATE TABLE, gate migration commit on actual ALTERs
- Update httpx mock to support both client.get() and client.stream()
@hognek

hognek commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Bot-fix round pushed — addresses all 10 inline findings (4 Kilo WARNING + 6 CodeRabbit actionable) plus 3 CodeRabbit nitpicks:

Kilo (4 WARNING):

  1. Capture create_job id directly instead of racy get_item_jobs()[-1]
  2. Don't flip ready→error on heavy download failure (record on job instead)
  3. Switch WebProcessor to client.stream() for real in-stream size capping
  4. Don't overwrite ready→error in background heavy download crash handler

CodeRabbit (6 actionable):
5. Stream response before size cap (merged with Kilo #3)
6. Match file fallback by expected stem, exclude .part files
7. Use returned job ID (merged with Kilo #1)
8. Gate auto-download on item status==ready (not error)
9. Remove extraneous f-prefixes from static HTML strings
10. Add CSRF header injection for HTMX write requests

CodeRabbit (3 nitpicks):
11. Assert title in YouTube processor test
12. Replace asyncio.sleep(0.5) with bounded polling
13. Fold P3 columns into CREATE TABLE, fix migration commit guard

Comment thread tinyagentos/library_pipeline.py Outdated
timeout=httpx.Timeout(30),
follow_redirects=False,
) as client:
resp = await client.get(current_url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Phase 1 client.get() still fully buffers the final URL's body, partially defeating the OOM protection the Phase 2 streaming was added to provide.

This loop uses client.get() (line 413), which downloads and buffers the entire response body. When the final URL is reached (no redirect), the full page is buffered in memory here, then discarded, and Phase 2 re-downloads it via client.stream(). The _MAX_WEB_BYTES cap therefore only guards the second fetch; a hostile or oversized final page can still exhaust memory in Phase 1. Since Phase 1 only needs redirect detection, prefer client.stream() and read only the headers (or issue a streamed/HEAD request) so the body is never buffered here. This also removes the redundant double download.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

candidates = sorted(
[
f for f in download_dir.glob("*")
if expected_stem in f.name and not f.suffix == ".part"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Substring stem match may select an unrelated downloaded file.

expected_stem in f.name matches any file whose name contains the expected stem, so a concurrent or similarly-named download (e.g. myvideo vs myvideo_part2) could be picked instead of the intended one. Consider an exact-stem (or extension-aware) match, e.g. f.stem == expected_stem, to avoid mis-selection.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/templates/library.html Outdated
<title>Library — TinyAgentOS</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<script src="https://unpkg.com/htmx.org@2"></script>
<meta name="csrf-token" content="">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The <meta name="csrf-token" content=""> tag is unused dead markup.

The CSRF token is actually read from the csrf_token cookie (lines 14-17) and injected via hx-headers; this empty meta tag is never populated or read, which is misleading. Remove it, or wire it up if a meta-based token is intended.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_library.py (1)

1089-1096: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract duplicated pipeline polling logic into a shared helper function. The bounded polling loop to wait for the pipeline to reach a terminal status is identically duplicated across three tests, and it currently fails to assert that a terminal status was actually reached before proceeding. If a timeout occurs, tests might fail downstream with confusing error messages.

  • tests/test_library.py#L1089-L1096: replace this polling loop with a call to a shared helper function that asserts a terminal status was reached.
  • tests/test_library.py#L1121-L1127: replace this polling loop with the shared helper call.
  • tests/test_library.py#L1139-L1145: replace this polling loop with the shared helper call.
♻️ Proposed refactor (add helper and replace)

Add a helper function to the test module (or test class):

async def _wait_for_terminal_status(client, item_id, timeout_sec=5.0):
    import asyncio
    import pytest
    iterations = int(timeout_sec / 0.25)
    for _ in range(iterations):
        resp = await client.get(f"/api/library/items/{item_id}")
        if resp.status_code == 200 and resp.json().get("status") in ("ready", "error"):
            return resp.json()["status"]
        await asyncio.sleep(0.25)
    pytest.fail(f"Timeout waiting for pipeline terminal status on item {item_id}")

Then replace the duplicated loops in your tests with:

        await _wait_for_terminal_status(client, item_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 1089 - 1096, Extract the duplicated
terminal-status polling into a shared async helper, such as
_wait_for_terminal_status, that polls for ready or error within the bounded
timeout and explicitly fails when no terminal status is reached. Replace the
loops at tests/test_library.py lines 1089-1096, 1121-1127, and 1139-1145 with
calls to this helper; all three sites require the same direct change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_library.py`:
- Around line 1089-1096: Extract the duplicated terminal-status polling into a
shared async helper, such as _wait_for_terminal_status, that polls for ready or
error within the bounded timeout and explicitly fails when no terminal status is
reached. Replace the loops at tests/test_library.py lines 1089-1096, 1121-1127,
and 1139-1145 with calls to this helper; all three sites require the same direct
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 08deba4a-d024-49f9-aa21-d724194bd929

📥 Commits

Reviewing files that changed from the base of the PR and between 74cb317 and 1b3fcc7.

📒 Files selected for processing (5)
  • tests/test_library.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/library_store.py
  • tinyagentos/routes/library.py
  • tinyagentos/templates/library.html
🚧 Files skipped from review as they are similar to previous changes (4)
  • tinyagentos/templates/library.html
  • tinyagentos/library_store.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/routes/library.py

Phase 1 now uses client.head() for redirect detection, falling back to
client.get() if the server rejects HEAD. This prevents the final URL's
body from being buffered in memory during redirect resolution — only
Phase 2's client.stream() reads the body.

Also remove the unused <meta name="csrf-token"> tag; the CSRF token
is read from the cookie directly.
) as client:
try:
resp = await client.head(current_url)
except Exception:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Fallback to GET only triggers on an exception, not when the server rejects HEAD with a 405 status.

The comment on line 405 says the code "falls back to GET if the server rejects HEAD", but except Exception here only catches errors raised by client.head() (network failures, timeouts). A server that answers HEAD with 405 Method Not Allowed returns a normal, non-raising response, so resp.is_redirect is False, the loop breaks, and the redirect for that source is silently never followed. Either tighten the comment to "falls back to GET on request error", or also fall back to GET when resp.status_code == 405 (or more generally when the response is neither a redirect nor a 2xx).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@hognek

hognek commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Round 2 pushed — Kilo WARNING resolved (HEAD for redirect resolution).

Round 2 bot review: 0 CRITICAL, 0 WARNING. Only 1 SUGGESTION (clarified HEAD comment) + 1 CodeRabbit nitpick (extract polling helper — deferred as low-value refactor).

All 71 targeted library tests pass. Ready for maintainer review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tinyagentos/library_pipeline.py (1)

404-419: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use a streamed GET for the fallback to prevent buffering large payloads.

The current fallback uses client.get(), which reads the entire response body into memory. If a malicious or oversized site drops HEAD requests to force this fallback, it bypasses the _MAX_WEB_BYTES protection completely, potentially causing an out-of-memory (OOM) error. Additionally, catching a blind Exception suppresses potential logical errors, and servers that return 405 Method Not Allowed for HEAD are not handled—breaking redirect traversal for those sites.

Catch httpx.RequestError instead of a blind Exception (which resolves the static analysis warning) and use a streamed GET to fetch headers without buffering the body. Explicitly trigger this fallback on 405 responses as well.

🔒 Proposed fix to properly stream the GET fallback
-        # Phase 1: resolve redirects using HEAD to avoid buffering
-        # bodies.  Falls back to GET on connection/network errors, but
-        # a server that rejects HEAD (e.g. 405) won't be a redirect
-        # response either — so the loop exits and Phase 2 streams.
+        # Phase 1: resolve redirects using HEAD to avoid buffering bodies.
+        # Falls back to a streamed GET on connection/network errors or if
+        # the server rejects HEAD (e.g. 405) to ensure redirects can still
+        # be followed without fully buffering large malicious payloads.
         current_url = source_url
         for _hop in range(_MAX_WEB_REDIRECTS + 1):
             validate_url_or_raise(current_url)
 
             async with httpx.AsyncClient(
                 timeout=httpx.Timeout(30),
                 follow_redirects=False,
             ) as client:
-                try:
-                    resp = await client.head(current_url)
-                except Exception:
-                    resp = await client.get(current_url)
+                try:
+                    resp = await client.head(current_url)
+                except httpx.RequestError:
+                    resp = None
+
+                if resp is None or resp.status_code == 405:
+                    async with client.stream("GET", current_url) as resp:
+                        pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_pipeline.py` around lines 404 - 419, Update the
redirect-resolution loop around the `client.head` call to catch only
`httpx.RequestError`, and trigger the fallback for both request failures and
`405 Method Not Allowed` responses. Replace the fallback `client.get` with a
streamed GET that reads only the response headers or redirect metadata, closes
the response, and preserves `_MAX_WEB_BYTES` enforcement for the later body
download.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tinyagentos/library_pipeline.py`:
- Around line 404-419: Update the redirect-resolution loop around the
`client.head` call to catch only `httpx.RequestError`, and trigger the fallback
for both request failures and `405 Method Not Allowed` responses. Replace the
fallback `client.get` with a streamed GET that reads only the response headers
or redirect metadata, closes the response, and preserves `_MAX_WEB_BYTES`
enforcement for the later body download.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 12f46456-f528-4924-a05e-869f4048c38d

📥 Commits

Reviewing files that changed from the base of the PR and between 1b3fcc7 and 5b8fac3.

📒 Files selected for processing (3)
  • tests/test_library.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/templates/library.html
💤 Files with no reviewable changes (1)
  • tinyagentos/templates/library.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_library.py

@jaylfc

jaylfc commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Library P1 is merged (#2062, dev sha 4498855). Three rounds of folds, all closed, including a data-loss fix inside an hour. Good work.

This PR is now CONFLICTING against dev and needs a rebase before it can be reviewed. Both P2 and P3 touch the four files P1 substantially rewrote:

  • tinyagentos/library_collections.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/library_store.py
  • tests/test_library.py

Please rebase onto current dev rather than merging dev in, so the diff stays reviewable. Read the merged P1 before resolving, because several things changed underneath you and a mechanical conflict resolution will reintroduce defects that were just fixed:

  1. Collections handoff now calls the real taosmd contract. Create returns {"collection": {...}} with the id nested, index is 202-then-poll rather than synchronous, and the poll response is nested the same way. If your branch still has the older shape anywhere, take P1's version.
  2. Reprocess must never unlink the item's storage_path. The pipeline records the source upload as a metadata artifact, so deleting every artifact path destroys the user's original file. P1 added an explicit guard. Do not let a conflict resolution drop it.
  3. Status transitions go through the compare-and-swap (try_update_item_status in library_store.py), not a read-then-write.
  4. Artifact-count assertions in tests are exact now, not tolerance-based. If your branch has an abs(...) <= N style assertion, replace it: a tolerance cannot distinguish correct behaviour from total loss, which is exactly how the data-loss bug stayed hidden.
  5. File modes are 0o640 for files and 0o2750 for directories, which the cross-user setup on the Pi depends on.

The canonical envelope reference is taosmd docs/collections.md, Response shapes section. Prefer it over anything in my earlier fold lists.

No rush on ordering: P2 first makes sense since P3 builds on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants