Skip to content

feat(asset-store): migrate to agentflare-store documents+blobs - #282

Merged
getappz merged 8 commits into
masterfrom
item-185-asset-store-migration
Jul 20, 2026
Merged

feat(asset-store): migrate to agentflare-store documents+blobs#282
getappz merged 8 commits into
masterfrom
item-185-asset-store-migration

Conversation

@getappz

@getappz getappz commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replaces the hand-rolled agentflare_backend::asset table + raw filesystem storage with agentflare_store::Store (content-addressed blobs + versioned documents), for both the asset and handoff MCP tools.
  • One-time backfill migrates existing legacy assets into the store on first use.
  • Review fixes (item Propagate silently-swallowed errors in credential/profile writes #229): Store::doc_get() now excludes soft-deleted rows (a deleted asset was still get/delete-able, worse once its blob was purged); the legacy backfill no longer silently skips forever for attach-only workflows; a lock().unwrap() in the backfill path now degrades gracefully instead of panicking.

Test plan

  • cargo test --workspace --all-features: 703 passed / 7 failed (7 failures are pre-existing, orchestrator-sandboxed git worktree tests, unrelated to this change)
  • cargo clippy -p agentflare-store -p agentflare --all-targets: no new warnings
  • New regression tests: documents::tests::soft_delete_and_list (extended), asset_get_and_delete_after_delete_return_not_found
  • cargo fmt: this branch has pre-existing formatting drift (12 files) from the original commit, not addressed here — recommend a follow-up fmt-only pass before merge

Closes #179.

Summary by CodeRabbit

  • New Features
    • Assets now store per-item metadata and size, including schema support and preserved version history.
    • Asset and handoff attachments are persisted as unified, blob-backed documents.
    • Legacy assets are automatically backfilled once.
    • Handoff attachment filenames are now unique per upload to prevent collisions.
  • Bug Fixes
    • Deleted assets now consistently return a “not found” result for both subsequent get and repeated delete.
    • Inline asset responses more clearly omit content when blobs are missing or size limits are exceeded.
  • Tests
    • Added post-delete get/delete coverage; updated handoff filename assertions; removed the older content-dedup test.

getappz added 2 commits July 20, 2026 23:00
…ts+blobs (#185)

Replace agentflare-backend::asset's storage with agentflare-store's
documents+blobs (content-addressed blob store with dedup/unref), and
route handoff/asset MCP tool handlers through it.
…ed forever

- Store::doc_get() didn't filter deleted_at, so asset get/delete on an
  already-deleted id returned 200 instead of not-found (regression from
  agentflare_backend::asset::get, worse now that delete also physically
  purges the blob once ref_count hits 0 -- soft-deleted docs pointed at
  already-gone content).
- with_store()'s one-time legacy backfill only fires when backend_db is
  unlocked at call time; attach always nests with_store inside
  with_backend_db so it never got a chance there. Trigger it once,
  unlocked, at the top of attach so first-run migration isn't dependent
  on call order.
- replaced a lock().unwrap() in the backfill path with a graceful skip
  on a poisoned mutex, matching the rest of with_store/with_backend_db.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e029790-cebb-4212-92e6-28c685903db8

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef6315 and 4a19ec2.

📒 Files selected for processing (2)
  • src/asset_store.rs
  • src/mcp_server/asset.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/asset_store.rs
  • src/mcp_server/asset.rs

📝 Walkthrough

Walkthrough

The PR moves MCP asset persistence from backend asset rows and filesystem paths to store documents and blobs. It adds document metadata and size schema support, migrates legacy assets, updates handoff attachments, and adds deletion lifecycle coverage.

Changes

Store-backed asset migration

Layer / File(s) Summary
Document storage contract
crates/agentflare-store/src/documents.rs, crates/agentflare-store/src/migrations.rs, crates/agentflare-backend/src/asset.rs
Document metadata and size fields are persisted across current records and history, with schema migration support and a backend helper for listing non-deleted assets.
Asset store adapters
src/asset_store.rs, src/main.rs
Path parsing, asset JSON conversion, legacy backend migration, and blob-or-inline content retrieval are added and registered.
Store lifecycle wiring
src/mcp_server.rs
MCP server state lazily opens a reusable store, isolates test stores, and performs one-time best-effort legacy backfill.
MCP asset operations
src/mcp_server/asset.rs, src/mcp_server/tests/asset_tests.rs
Attach, get, list, and delete use store documents and blob reference management, with coverage for deleted-asset behavior.
Handoff attachment persistence
src/mcp_server/handoff.rs, src/mcp_server/tests/artifact_tests.rs
Handoff attachments use unique filenames, store blobs and metadata, calculate versions from stored documents, and validate the updated filename format.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant AgentflareMcp
  participant StoreDocuments
  participant BlobStore
  MCPClient->>AgentflareMcp: attach, get, list, or delete asset
  AgentflareMcp->>StoreDocuments: upsert or retrieve asset document
  StoreDocuments->>BlobStore: store, retrieve, or unref blob
  BlobStore-->>StoreDocuments: blob content or hash
  StoreDocuments-->>AgentflareMcp: document result
  AgentflareMcp-->>MCPClient: asset JSON response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR does not implement #179's worktree push/PR flow or the next-step hints for claim/done responses. Add the item(done) push+PR behavior and next-field guidance for claim/done responses, or retarget the linked issue.
Out of Scope Changes check ⚠️ Warning The asset-store migration changes are unrelated to linked issue #179, which is about MCP worktree and PR workflow behavior. Either align the PR to #179's MCP workflow requirements or remove the unrelated issue link.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely describes the main change: migrating asset storage to agentflare-store documents and blobs.
Description check ✅ Passed The description includes a clear summary, test plan, and reviewer notes, with only some non-critical template details left sparse.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch item-185-asset-store-migration

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

@getappz getappz changed the title asset-store: migrate to agentflare-store documents+blobs feat(asset-store): migrate to agentflare-store documents+blobs Jul 20, 2026

@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: 4

🧹 Nitpick comments (1)
src/mcp_server/asset.rs (1)

179-213: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Both asset list and handoff version-counting do a full-workspace doc_list scan. Root cause: the Store API only exposes a whole-workspace doc_list, so every entity-scoped read filters the entire workspace's documents client-side.

  • src/mcp_server/asset.rs#L179-L213: filters all workspace docs by doc_type == "asset" and an optional path prefix on every asset(list) call.
  • src/mcp_server/handoff.rs#L116-L155: fetches all workspace docs just to count siblings under item_attachment/{item.id} for the display "asset_version".

Consider adding a prefix-scoped query (e.g. doc_list_by_prefix) to agentflare-store's document API so both call sites can avoid scanning documents outside their entity/prefix.

🤖 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 `@src/mcp_server/asset.rs` around lines 179 - 213, Add a prefix-scoped document
query to the agentflare-store API, such as doc_list_by_prefix, and implement it
in the Store layer. Update asset list in src/mcp_server/asset.rs lines 179-213
to query only the resolved prefix when one is provided, while preserving
workspace-wide filtering for an empty prefix; update handoff version-counting in
src/mcp_server/handoff.rs lines 116-155 to use the item_attachment/{item.id}
prefix instead of doc_list. Keep the existing asset-type filtering and result
behavior unchanged.
🤖 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 `@src/asset_store.rs`:
- Around line 41-90: Update backfill_legacy_assets to handle read_file,
blob_store, and per-asset doc_upsert_with_opts failures within the loop without
returning early; skip or log the failing asset, continue processing remaining
assets, and still write _asset_backfill_done after the batch completes so
successful assets are not replayed.

In `@src/mcp_server.rs`:
- Around line 627-655: Update with_store’s one-time backfill flow to explicitly
handle poisoned backend_db and store mutexes instead of silently skipping them,
while avoiding deadlock for the nested backend case. Capture
backfill_legacy_assets failures, log or propagate them according to the existing
error-handling conventions, and ensure the backfill completion state is recorded
or otherwise prevents retrying already-processed assets after partial failure,
avoiding duplicate history versions.

In `@src/mcp_server/asset.rs`:
- Around line 66-72: Ensure the initial backfill trigger in the attach flow
opens or initializes backend_db before invoking self.with_store(|_| ())?. Update
the surrounding attach logic, using the existing backend_db
initialization/access mechanism, so the trigger observes a populated connection
on the first attach call while backend_db remains unlocked.
- Around line 214-231: In the "delete" branch of the asset operation, update the
store closure to call doc_delete before blob_unref. Preserve the existing
blob_hash lookup and error mappings, and only release the blob reference after
the document deletion succeeds.

---

Nitpick comments:
In `@src/mcp_server/asset.rs`:
- Around line 179-213: Add a prefix-scoped document query to the
agentflare-store API, such as doc_list_by_prefix, and implement it in the Store
layer. Update asset list in src/mcp_server/asset.rs lines 179-213 to query only
the resolved prefix when one is provided, while preserving workspace-wide
filtering for an empty prefix; update handoff version-counting in
src/mcp_server/handoff.rs lines 116-155 to use the item_attachment/{item.id}
prefix instead of doc_list. Keep the existing asset-type filtering and result
behavior unchanged.
🪄 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: 24a37168-0474-4f36-ad67-260bdd58cbbb

📥 Commits

Reviewing files that changed from the base of the PR and between 2ab2dd0 and dbd386d.

📒 Files selected for processing (10)
  • crates/agentflare-backend/src/asset.rs
  • crates/agentflare-store/src/documents.rs
  • crates/agentflare-store/src/migrations.rs
  • src/asset_store.rs
  • src/main.rs
  • src/mcp_server.rs
  • src/mcp_server/asset.rs
  • src/mcp_server/handoff.rs
  • src/mcp_server/tests/artifact_tests.rs
  • src/mcp_server/tests/asset_tests.rs

Comment thread src/asset_store.rs
Comment thread src/mcp_server.rs
Comment on lines +627 to +655
/// Lock the agentflare-store, lazily opening it on first use.
/// After opening, runs the one-time asset backfill (best-effort,
/// skipped if backend_db is already locked by this thread).
fn with_store<T>(
&self,
f: impl FnOnce(&agentflare_store::Store) -> T,
) -> Result<T, ErrorData> {
self.ensure_store()?;

// One-time backfill: try_lock to avoid deadlock when called from
// within with_backend_db (attach handler nests store inside backend).
// If backend isn't open yet, skip — with_backend_db will handle it.
if let Ok(bg) = self.backend_db.try_lock() {
if let Some(ref conn) = *bg {
let base_path = crate::paths::home().join(".agentflare");
if let Ok(s) = self.store.lock() {
if let Some(ref store) = *s {
let _ = crate::asset_store::backfill_legacy_assets(store, conn, &base_path);
}
}
}
}

let guard = self
.store
.lock()
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
Ok(f(guard.as_ref().expect("ensure_store just initialized it")))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
import os, subprocess, textwrap, json, sys

def sh(cmd):
    print(f"\n$ {cmd}")
    r = subprocess.run(cmd, shell=True, text=True, capture_output=True)
    print(r.stdout)
    if r.stderr:
        print(r.stderr, file=sys.stderr)
    print("exit:", r.returncode)
    return r.returncode, r.stdout, r.stderr

# Locate relevant symbols and read focused slices.
commands = [
    r"rg -n \"fn with_store|try_lock\(|backfill_legacy_assets|_asset_backfill_done|Poison|poison\" src/mcp_server.rs src/asset_store.rs",
    r"sed -n '600,690p' src/mcp_server.rs",
    r"sed -n '1,260p' src/asset_store.rs",
]
for c in commands:
    sh(c)
PY

Repository: getappz/agentflare

Length of output: 7704


🏁 Script executed:

python3 - <<'PY'
import subprocess, sys

def sh(cmd):
    print(f"\n$ {cmd}")
    r = subprocess.run(cmd, shell=True, text=True, capture_output=True)
    print(r.stdout)
    if r.stderr:
        print(r.stderr, file=sys.stderr)
    print("exit:", r.returncode)

for cmd in [
    r"rg -n \"backend_db|with_backend_db|clear_poison|Poisoned|TryLockError\" src/mcp_server.rs src",
    r"sed -n '520,610p' src/mcp_server.rs",
]:
    sh(cmd)
PY

Repository: getappz/agentflare

Length of output: 3792


🏁 Script executed:

python3 - <<'PY'
import subprocess, sys

def sh(cmd):
    print(f"\n$ {cmd}")
    r = subprocess.run(cmd, shell=True, text=True, capture_output=True)
    print(r.stdout)
    if r.stderr:
        print(r.stderr, file=sys.stderr)
    print("exit:", r.returncode)

for cmd in [
    r"rg -n \"doc_upsert_with_opts|struct DocUpsertOpts|fn doc_upsert|version\" .",
    r"rg -n \"_asset_backfill_done\" .",
]:
    sh(cmd)
PY

Repository: getappz/agentflare

Length of output: 586


🏁 Script executed:

python3 - <<'PY'
import subprocess, sys

def sh(cmd):
    print(f"\n$ {cmd}")
    r = subprocess.run(cmd, shell=True, text=True, capture_output=True)
    print(r.stdout)
    if r.stderr:
        print(r.stderr, file=sys.stderr)
    print("exit:", r.returncode)

for cmd in [
    r"rg -n 'doc_upsert_with_opts|fn doc_upsert|struct DocUpsertOpts' src",
    r"rg -n '_asset_backfill_done' src",
]:
    sh(cmd)
PY

Repository: getappz/agentflare

Length of output: 690


🏁 Script executed:

python3 - <<'PY'
import subprocess, sys

def sh(cmd):
    print(f"\n$ {cmd}")
    r = subprocess.run(cmd, shell=True, text=True, capture_output=True)
    print(r.stdout)
    if r.stderr:
        print(r.stderr, file=sys.stderr)
    print("exit:", r.returncode)

for cmd in [
    r"git ls-files | rg '(^|/)(store|agentflare_store|src)/.*\.(rs|toml)$'",
    r"rg -n 'doc_upsert_with_opts|fn doc_upsert|upsert_with_opts|history|version' .",
]:
    sh(cmd)
PY

Repository: getappz/agentflare

Length of output: 50375


🏁 Script executed:

python3 - <<'PY'
import subprocess, sys

def sh(cmd):
    print(f"\n$ {cmd}")
    r = subprocess.run(cmd, shell=True, text=True, capture_output=True)
    print(r.stdout)
    if r.stderr:
        print(r.stderr, file=sys.stderr)
    print("exit:", r.returncode)

for cmd in [
    r"sed -n '100,190p' crates/agentflare-store/src/documents.rs",
    r"sed -n '730,810p' crates/agentflare-store/src/documents.rs",
]:
    sh(cmd)
PY

Repository: getappz/agentflare

Length of output: 6247


Handle poisoned mutexes and backfill failures src/mcp_server.rs:627-655

  • try_lock() drops Poisoned, so a poisoned backend_db will keep skipping this backfill.
  • backfill_legacy_assets(...) errors are ignored, and _asset_backfill_done is only written on success; a partial failure will rerun the whole import on the next call. Since doc_upsert_with_opts() increments existing docs, that can add duplicate history versions for already-migrated assets.
🤖 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 `@src/mcp_server.rs` around lines 627 - 655, Update with_store’s one-time
backfill flow to explicitly handle poisoned backend_db and store mutexes instead
of silently skipping them, while avoiding deadlock for the nested backend case.
Capture backfill_legacy_assets failures, log or propagate them according to the
existing error-handling conventions, and ensure the backfill completion state is
recorded or otherwise prevents retrying already-processed assets after partial
failure, avoiding duplicate history versions.

Comment thread src/mcp_server/asset.rs
Comment thread src/mcp_server/asset.rs
getappz added 3 commits July 21, 2026 00:31
…-features clippy errors

Agentflare-Agent: claude-code_2-1-215_agent
Agentflare-Branch: item-185-asset-store-migration
- backfill_legacy_assets: don't abort the whole batch on one bad legacy
  asset; skip and log it, migrate the rest, and always write the
  completion marker. Previously a single unreadable asset meant the
  marker never got written, so every later with_store() call replayed
  the entire batch, re-upserting already-migrated docs and bumping
  their version/history each time.
- attach: open backend_db (and release it) before triggering the
  backfill check, so a totally fresh instance's very first attach call
  can actually backfill, not just the second one onward.
- delete: soft-delete the document before unref'ing its blob, so a
  failure between the two steps can't leave the last reference's
  content purged while the asset still reads as live.

Agentflare-Agent: claude-code_2-1-215_agent
Agentflare-Branch: item-185-asset-store-migration
@getappz

getappz commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Addressed all 4 CodeRabbit findings in 4a19ec2:

  • asset_store.rs: backfill now skips/logs a failing legacy asset instead of aborting the batch, and always writes the completion marker so a bad asset can't cause the whole batch to replay forever.
  • mcp_server.rs / asset.rs: attach opens backend_db before triggering the backfill check, so the very first attach call (not just the second) can backfill.
  • asset.rs delete: soft-deletes the document before releasing the blob ref, so a failure between the two can't leave a purged blob behind a still-live asset.

Added a regression test (asset_store::tests::backfill_skips_bad_asset_and_still_marks_done) covering the resilience fix.

Agentflare-Agent: claude-code_2-1-215_agent
Agentflare-Branch: item-185-asset-store-migration
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.

1 participant