Use local pinned metadata for deletion checks instead of L2 API

- Add is_pinned(), pin(), _load_meta(), _save_meta() to L1CacheManager
- Update can_delete() and can_discard_activity() to check local pinned status
- Update run deletion endpoints (API and UI) to check pinned metadata
- Remove L2 shared check fallback from run deletion
- Fix L2SharedChecker to return True on error (safer - prevents accidental deletion)
- Update tests for new pinned behavior

When items are published to L2, the publish flow marks them as pinned
locally. This ensures items remain non-deletable even if L2 is unreachable,
and both outputs AND inputs of published runs are protected.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-08 02:44:18 +00:00
parent f61c82dd51
commit f23a721816
3 changed files with 85 additions and 44 deletions

View File

@@ -444,6 +444,17 @@ async def discard_run(run_id: str, username: str = Depends(get_required_user)):
# Failed runs can always be deleted (no output to protect)
if run.status != "failed":
# Check if any items are pinned (published or input to published)
items_to_check = list(run.inputs or [])
if run.output_hash:
items_to_check.append(run.output_hash)
for content_hash in items_to_check:
meta = load_cache_meta(content_hash)
if meta.get("pinned"):
pin_reason = meta.get("pin_reason", "published")
raise HTTPException(400, f"Cannot discard run: item {content_hash[:16]}... is pinned ({pin_reason})")
# Check if activity exists for this run
activity = cache_manager.get_activity(run_id)
@@ -457,15 +468,6 @@ async def discard_run(run_id: str, username: str = Depends(get_required_user)):
success, msg = cache_manager.discard_activity(run_id)
if not success:
raise HTTPException(500, f"Failed to discard: {msg}")
else:
# Legacy run without activity record - check L2 shared status manually
items_to_check = list(run.inputs or [])
if run.output_hash:
items_to_check.append(run.output_hash)
for content_hash in items_to_check:
if cache_manager.l2_checker.is_shared(content_hash):
raise HTTPException(400, f"Cannot discard run: item {content_hash[:16]}... is published to L2")
# Remove from Redis
redis_client.delete(f"{RUNS_KEY_PREFIX}{run_id}")
@@ -491,6 +493,17 @@ async def ui_discard_run(run_id: str, request: Request):
# Failed runs can always be deleted
if run.status != "failed":
# Check if any items are pinned (published or input to published)
items_to_check = list(run.inputs or [])
if run.output_hash:
items_to_check.append(run.output_hash)
for content_hash in items_to_check:
meta = load_cache_meta(content_hash)
if meta.get("pinned"):
pin_reason = meta.get("pin_reason", "published")
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Cannot discard: item {content_hash[:16]}... is pinned ({pin_reason})</div>'
# Check if activity exists for this run
activity = cache_manager.get_activity(run_id)
@@ -502,15 +515,6 @@ async def ui_discard_run(run_id: str, request: Request):
success, msg = cache_manager.discard_activity(run_id)
if not success:
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Failed to discard: {msg}</div>'
else:
# Legacy run - check L2 shared status
items_to_check = list(run.inputs or [])
if run.output_hash:
items_to_check.append(run.output_hash)
for content_hash in items_to_check:
if cache_manager.l2_checker.is_shared(content_hash):
return f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Cannot discard: item {content_hash[:16]}... is published to L2</div>'
# Remove from Redis
redis_client.delete(f"{RUNS_KEY_PREFIX}{run_id}")