Skip to content

feat(sources): acquire live Drive attachment bytes into the blob store#3073

Merged
Sinity merged 3 commits into
masterfrom
feature/attachments/drive-live-byte-acquisition
Jul 18, 2026
Merged

feat(sources): acquire live Drive attachment bytes into the blob store#3073
Sinity merged 3 commits into
masterfrom
feature/attachments/drive-live-byte-acquisition

Conversation

@Sinity

@Sinity Sinity commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the Drive sub-case of polylogue-83u.2: live Drive-hosted attachment references (driveDocument(s)/driveImage/driveAudio/driveVideo) are now fetched during ingest and land in the content-addressed blob store with a true SHA-256, instead of never being acquired.

Problem

A 2026-07-08 investigation (recorded on the bead) found the only code that ever called DriveSourceClient.download_bytes for an attachment — _apply_drive_attachments + iter_drive_sessions — had zero live callers. The real ingest path (iter_drive_raw_data → generic dispatch parse) never wired attachment acquisition in at all. Even if that dead code had been revived, it wrote bytes to attachment.path/local disk rather than ParsedAttachment.inline_bytes, so it would not have reached the existing true-hash blob-publish path anyway (schema v13's acquisition_status).

Solution

iter_drive_raw_data (polylogue/sources/drive/__init__.py) already owns the one live DriveSourceAPI client instance per file, inside the iterator scope, before the acquire and parse stages decouple for memory-bounded streaming. A new module, polylogue/sources/drive/attachment_fetch.py, walks the raw session JSON for Drive-hosted doc/media references at that exact point, downloads each via the same live client, and injects the fetched bytes as base64 under a sidecar key (DRIVE_LIVE_FETCH_DATA_KEY, defined in polylogue/sources/parsers/drive_support_attachments.py). The existing chunk parser (attachment_from_doc) decodes that sidecar into ParsedAttachment.inline_bytes — no other parser changes — so the already-shipped publish-then-write path (ingest_batch/_core.py) picks it up as a real content-addressed blob with acquisition_status='acquired', with zero downstream changes needed.

Fetch failures and oversize files (50MB cap) leave the attachment honestly upload_origin="drive" with no inline_bytes — never a fabricated hash.

Removed _apply_drive_attachments/iter_drive_sessions (and the now-dead sources/__init__.py re-export) — the obsolete, zero-caller path this change replaces, per surgical-renewal doctrine.

Non-goals (explicitly out of scope)

polylogue-83u.2 also named export-zip-member and local-path attachment sub-cases. Both were re-confirmed to have zero current producer — no parser has ever emitted an attachment whose bytes live as a sibling zip member or a real local filesystem path. There is nothing live to un-bypass for either, so this PR does not touch them; noted on parent polylogue-83u rather than filing speculative follow-up beads with no defined producer.

Verification

  • devtools test tests/unit/sources/test_drive_ops.py tests/unit/sources/test_drive_attachment_fetch.py tests/unit/storage/test_attachment_acquisition.py tests/unit/sources/test_source_laws.py143 passed
  • devtools verify --quickexit 0 (ruff format/check, mypy --strict, render all --check, topology, layering, closure-matrix, manifests, ci-workflows, doc-commands, docs-coverage, test-infra-currency, test-clock-hygiene, pytest-timeout-overrides, degrade-loudly)
  • devtools test tests/unit/sources/1705 passed, 2 pre-existing failures in test_chatgpt_normalization_survivors.py confirmed unrelated (reproduce identically with this change stashed out)

Ref polylogue-83u.2

Co-Authored-By: Claude noreply@anthropic.com

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Sinity, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 881e5197-bc7f-4d23-87b3-5742643d256b

📥 Commits

Reviewing files that changed from the base of the PR and between 05fbaa8 and 74ba3f4.

📒 Files selected for processing (12)
  • .beads/interactions.jsonl
  • .beads/issues.jsonl
  • docs/plans/topology-target.yaml
  • docs/topology-status.md
  • polylogue/sources/__init__.py
  • polylogue/sources/drive/__init__.py
  • polylogue/sources/drive/attachment_fetch.py
  • polylogue/sources/parsers/drive_support_attachments.py
  • polylogue/sources/providers/gemini_message.py
  • tests/unit/sources/test_drive_attachment_fetch.py
  • tests/unit/sources/test_drive_ops.py
  • tests/unit/sources/test_parsers_drive.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/attachments/drive-live-byte-acquisition

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2e0f2bb17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return {"id": doc, DRIVE_LIVE_FETCH_DATA_KEY: data_b64}
if isinstance(doc, dict):
updated = dict(doc)
updated[DRIVE_LIVE_FETCH_DATA_KEY] = data_b64

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Strip fetched bytes before block metadata

When a live fetch succeeds, this sidecar remains inside the driveDocument/driveImage object. The typed Gemini parser accepts that object and _drive_media_content_block copies it wholesale into ContentBlock.raw, which parsed_blocks_from_meta then persists as block metadata; every acquired attachment can therefore be duplicated as base64 metadata in the index, up to the 50 MB cap, and exposed through block metadata surfaces instead of only the blob path. Strip DRIVE_LIVE_FETCH_DATA_KEY before constructing blocks/metadata after decoding it into inline_bytes.

Useful? React with 👍 / 👎.

if isinstance(doc, str):
return doc or None
if isinstance(doc, dict):
for key in ("id", "fileId", "driveId"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use fileId when present for Drive downloads

For Gemini/Drive attachment records that carry both id and fileId, the existing parser treats id as the provider attachment id and fileId as the actual Drive file id, while DriveSourceClient.download_bytes ultimately calls Drive get_media(fileId=...). This loop tries id first, so those payloads request the attachment handle instead of the Drive file and the new acquisition path logs a failure and leaves an otherwise fetchable attachment unfetched. Prefer fileId over id, and avoid falling back to driveId as a file id.

Useful? React with 👍 / 👎.

Comment thread polylogue/sources/drive/__init__.py Outdated
exc,
)
continue
raw_bytes = _inject_live_drive_attachment_bytes(raw_bytes, drive_client, file_meta)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Process cached Drive payloads through the injector

Because this injector is only called in the download branch, any existing Drive cache file takes the cache_path.exists() branch above and is blob-stored unchanged. In archives with cache files from earlier ingests, re-acquiring the source will still parse the old JSON without the sidecar, so Drive-hosted attachments remain unfetched until the user manually deletes the cache. Run cached bytes through the same injection/upgrade path, or invalidate legacy cache entries when the sidecar is absent.

Useful? React with 👍 / 👎.

@Sinity

Sinity commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed all 3 Codex findings in 74da2f3:

  • P1 (sidecar leaking into block metadata): _drive_media_content_block now strips DRIVE_LIVE_FETCH_DATA_KEY before building ContentBlock.raw, mirroring _without_inline_bytes's existing treatment of inlineData/inlineFile/inlineImage. Covered by a new end-to-end test (test_live_fetched_drive_attachment_bytes_reach_inline_bytes_not_block_metadata).
  • P2 (fileId vs id precedence): _doc_file_id now tries fileId before id (matching attachment_from_doc's own refactor(schema): first-class attachment native identifiers (#864 slice B) #1252 precedence) and never falls back to driveId (a shared-drive container id, not a file id). Covered by two new tests.
  • P2 (cached payloads bypass the injector): the injector now runs on every read (cache hit or fresh download), refreshing the cache file only when something new was actually resolved. Covered by a new test seeding a stale pre-injector cache file and proving the next run backfills it.

200 focused tests pass (test_drive_ops.py, test_drive_attachment_fetch.py, test_parsers_drive.py, test_attachment_acquisition.py, test_source_laws.py, test_gemini_chunked_prompt_contract.py), devtools verify --quick + public-claims green.

Re: ci/circleci: quick-gate failing — confirmed pre-existing/unrelated: it also fails on 4053787ab (master itself, PR #3064, already merged), consistently ~18s in, well before any real lint/mypy/render step would complete. Not something this PR introduced.

Sinity and others added 3 commits July 18, 2026 05:20
Problem: Drive-hosted attachment references (driveDocument/driveImage/
driveAudio/driveVideo) were never fetched by the live ingest pipeline.
The only code that called DriveSourceClient.download_bytes for an
attachment (_apply_drive_attachments + iter_drive_sessions) had zero
callers — the real path (iter_drive_raw_data -> generic dispatch parse)
never wired it in, and that dead code wrote to attachment.path/local
disk rather than inline_bytes, so even if revived it would not reach
the existing true-hash blob-publish path.

Solution: iter_drive_raw_data already owns the one live DriveSourceAPI
client instance per file, inside the iterator scope, before the acquire
and parse stages decouple for memory-bounded streaming. A new module
(sources/drive/attachment_fetch.py) walks the raw session JSON for
Drive-hosted doc/media references *at that point*, downloads each via
the same live client, and injects the fetched bytes as base64 under a
sidecar key (DRIVE_LIVE_FETCH_DATA_KEY). attachment_from_doc (the
existing chunk parser) decodes that sidecar into
ParsedAttachment.inline_bytes with no other parser changes, so the
already-shipped publish-then-write path (ingest_batch/_core.py) picks
it up as a real content-addressed blob with a true SHA-256 and
acquisition_status='acquired' — zero downstream changes needed. Fetch
failures/oversize files leave the attachment honestly unfetched, never
fabricating a hash. Removed the dead _apply_drive_attachments/
iter_drive_sessions path they replace (zero live callers).

Scope: this lands only the Drive sub-case of polylogue-83u.2. The
export-zip-member and local-path sub-cases have no current producer
(no parser ever emits a zip-embedded or local-filesystem attachment) —
tracked as a parent-epic note rather than speculative beads.

Verification: devtools test tests/unit/sources/test_drive_ops.py
tests/unit/sources/test_drive_attachment_fetch.py
tests/unit/storage/test_attachment_acquisition.py
tests/unit/sources/test_source_laws.py -> 143 passed. devtools verify
--quick -> exit 0 (ruff format/check, mypy --strict, render all
--check, topology, layering, closure-matrix, manifests, ci-workflows,
doc-commands, docs-coverage, test-infra-currency, test-clock-hygiene,
pytest-timeout-overrides, degrade-loudly all green). devtools test
tests/unit/sources/ -> 1705 passed, 2 pre-existing failures in
test_chatgpt_normalization_survivors.py confirmed unrelated (same
failures reproduce with this change stashed out).

Ref polylogue-83u.2

Co-Authored-By: Claude <noreply@anthropic.com>
Drive live attachment byte acquisition (this branch, commit above) is
the Drive sub-case of polylogue-83u.2. Export-zip-member and
local-path sub-cases have no current producer (no parser has ever
emitted an attachment with real zip-member or local-filesystem bytes)
-- noted on parent polylogue-83u as inapplicable rather than deferred.

Hand-merged the two touched records from a fresh `bd export` rather
than copying the whole file: bd resolves its export target independent
of invocation cwd, so it also carries unrelated concurrent-session bead
activity that doesn't belong on this branch.

Co-Authored-By: Claude <noreply@anthropic.com>
Problem: Codex/CodeRabbit review of the Drive live attachment byte
acquisition change (previous commit) found three real gaps.

- P1: the live-fetch sidecar (DRIVE_LIVE_FETCH_DATA_KEY) survived
  unstripped into ContentBlock.raw for driveDocument/driveImage/
  driveAudio/driveVideo blocks (gemini_message.py's
  _drive_media_content_block), unlike inlineData/inlineFile/inlineImage
  which already strip their `data` payload via _without_inline_bytes.
  Every acquired Drive attachment would duplicate its full base64 bytes
  into block metadata -- persisted in index.db, which is not
  content-addressed, up to the 50MB cap.
- P2: the live-fetch file-id resolution tried `id` before `fileId`
  (and fell back to `driveId`, a shared-drive CONTAINER id, never a
  file id), the reverse of attachment_from_doc's own native-id
  precedence (#1252: `fileId` is the real Drive file id
  DriveSourceClient.download_bytes needs; `id` is a provider
  attachment handle). Payloads carrying both fields would request the
  wrong handle and silently fail live.
- P2: the injector only ran on a fresh download; a cache_path.exists()
  hit skipped it entirely, so a cache file written before this feature
  (or where a Drive-hosted attachment failed/was oversize at the time)
  stayed unfetched forever even on repeated re-ingest.

What changed:
- _drive_media_content_block strips DRIVE_LIVE_FETCH_DATA_KEY from the
  record before building ContentBlock.raw, mirroring
  _without_inline_bytes's existing treatment of inline data.
- _doc_file_id now tries `fileId` before `id` and never considers
  `driveId`.
- _inject_live_drive_attachment_bytes now returns (bytes, mutated) and
  iter_drive_raw_data runs it unconditionally on every read (cache hit
  or fresh download), refreshing the cache file only when something
  new was actually resolved.

Verification: devtools test tests/unit/sources/test_drive_ops.py
tests/unit/sources/test_drive_attachment_fetch.py
tests/unit/sources/test_parsers_drive.py
tests/unit/storage/test_attachment_acquisition.py
tests/unit/sources/test_source_laws.py
tests/unit/sources/test_gemini_chunked_prompt_contract.py -> 200
passed (4 new tests covering each finding). devtools verify --quick +
public-claims -> exit 0. devtools test tests/unit/sources/ -> 1709
passed (same 2 pre-existing unrelated failures).

Co-Authored-By: Claude <noreply@anthropic.com>
@Sinity
Sinity force-pushed the feature/attachments/drive-live-byte-acquisition branch from 74da2f3 to 74ba3f4 Compare July 18, 2026 03:22
@Sinity
Sinity merged commit 1501536 into master Jul 18, 2026
1 of 3 checks passed
@Sinity
Sinity deleted the feature/attachments/drive-live-byte-acquisition branch July 18, 2026 03:22
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