fix(backend): address workspace file handling bugs

- Fix data loss on overwrite: defer old file deletion until new file is
  written to storage, preventing data loss if the write fails
- Fix GCS download: move return before cleanup so download success isn't
  lost if client.close() fails
- Fix AgentFileInputBlock: use for_block_output instead of
  for_local_processing so files persist in CoPilot workspace

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2026-01-28 21:00:30 -06:00
parent d442ad64bc
commit 6abc70f793
3 changed files with 15 additions and 12 deletions

View File

@@ -471,10 +471,8 @@ class AgentFileInputBlock(AgentInputBlock):
# Determine return format based on user preference
# for_external_api: always returns data URI (base64) - honors "Produce Base64 Output"
# for_local_processing: returns local file path
return_format = (
"for_external_api" if input_data.base_64 else "for_local_processing"
)
# for_block_output: smart format - workspace:// in CoPilot, data URI in graphs
return_format = "for_external_api" if input_data.base_64 else "for_block_output"
yield "result", await store_media_file(
file=input_data.value,

View File

@@ -60,16 +60,21 @@ async def download_with_fresh_session(bucket: str, blob: str) -> bytes:
session = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=10, force_close=True)
)
client: async_gcs_storage.Storage | None = None
try:
client = async_gcs_storage.Storage(session=session)
content = await client.download(bucket, blob)
await client.close()
return content
except Exception as e:
if "404" in str(e) or "Not Found" in str(e):
raise FileNotFoundError(f"File not found: gcs://{bucket}/{blob}")
raise
finally:
if client:
try:
await client.close()
except Exception:
pass # Best-effort cleanup
await session.close()

View File

@@ -196,13 +196,13 @@ class WorkspaceManager:
# Resolve path with session prefix
path = self._resolve_path(path)
# Check if file exists at path
existing = await get_workspace_file_by_path(self.workspace_id, path)
if existing is not None:
if overwrite:
# Delete existing file first
await self.delete_file(existing.id)
else:
# Check if file exists at path (only error for non-overwrite case)
# For overwrite=True, we let the write proceed and handle via UniqueViolationError
# This ensures the new file is written to storage BEFORE the old one is deleted,
# preventing data loss if the new write fails
if not overwrite:
existing = await get_workspace_file_by_path(self.workspace_id, path)
if existing is not None:
raise ValueError(f"File already exists at path: {path}")
# Auto-detect MIME type if not provided