Skip to content

docs: define channel image ingestion strategy (#266)#327

Merged
yacosta738 merged 9 commits into
mainfrom
feat/channel-image-ingestion-strategy
Mar 26, 2026
Merged

docs: define channel image ingestion strategy (#266)#327
yacosta738 merged 9 commits into
mainfrom
feat/channel-image-ingestion-strategy

Conversation

@yacosta738
Copy link
Copy Markdown
Contributor

@yacosta738 yacosta738 commented Mar 26, 2026

Summary

  • Complete SDD cycle for channel image ingestion strategy
  • Add observability section (REQ-9) to design doc
  • Update tasks with Linear issue references (DALLAY-192 through DALLAY-195)
  • Sync delta spec to openspec/specs/channel-image-ingestion/
  • Archive change to openspec/changes/archive/
  • Fix cargo fmt in discord.rs

Follow-up issues created in Linear

Issue Priority Description
DALLAY-192 High Discord image ingestion
DALLAY-193 High Slack image ingestion
DALLAY-194 Medium Startup temp file reaper
DALLAY-195 Low Multi-image per turn

SDD Artifacts

Codify the canonical 5-step ingestion pipeline (parse → emit → fetch →
validate → stage) for multimodal image input across channels.

- 9 requirements covering MVP channels, config gating, staging, cleanup
- 9 Given/When/Then scenarios including rejection and edge cases
- Channel-specific contracts for Telegram, WhatsApp, Discord, Slack
- 5 ADRs: shared validation, magic-byte sniffing, RAII cleanup,
  fail-closed default, single dispatch point
- Follow-up issue definitions for Discord, Slack, reaper, multi-image
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Discord image-attachment ingestion: parses Discord attachments into image parts, downloads and streams-validated bytes with magic-byte MIME checks and size limits, computes SHA-256, writes staged temp files, and wires Discord into the centralized staging dispatch and multimodal allowlist.

Changes

Cohort / File(s) Summary
Discord Image Ingestion
clients/agent-runtime/src/channels/discord.rs
Adds DiscordChannel::fetch_and_stage_image(...), streaming download with content-length and per-chunk size checks, magic-byte MIME validation, SHA-256 hashing, temp-file staging returning media::StagedImage; adds parse_image_attachments() and updates MESSAGE_CREATE handling to include image parts and mention-only gating; includes unit tests for parsing and gating.
Pipeline Dispatch & Config
clients/agent-runtime/src/channels/mod.rs, clients/agent-runtime/src/config/schema.rs
Extends stage_channel_images() to dispatch "discord" to new Discord staging flow; updates MVP_VALID_MULTIMODAL_CHANNELS to include "discord" and adjusts validation tests/messages.
Specification & Design Documentation
openspec/specs/channel-image-ingestion/spec.md, openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/*
Adds comprehensive spec, proposal, design, exploration, tasks, follow-up issues, and verification docs defining the 5-step ingestion pipeline, staging semantics/RAII, naming conventions, observability (ImageIngressEvent), and Phase 2 work items (Slack, reaper, multi-image).

Sequence Diagram(s)

sequenceDiagram
    participant Gateway as Discord Gateway
    participant Channel as DiscordChannel
    participant CDN as Discord CDN
    participant Media as media::Validator
    participant FS as Temp File System
    participant Dispatcher as stage_channel_images()

    Gateway->>Channel: MESSAGE_CREATE with attachments
    Channel->>Channel: parse_image_attachments() -> ContentPart::Image
    Channel->>CDN: GET attachment URL (stream)
    CDN-->>Channel: stream bytes (chunks)
    Channel->>Media: validate mime & size (per-chunk + total)
    Media-->>Channel: validation result
    Channel->>FS: write temp file, compute SHA-256
    FS-->>Channel: StagedImage(path, metadata)
    Channel->>Dispatcher: return StagedImage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: multimodal image input mvp #324: Introduced the foundational multimodal image-staging pipeline (StagedImage, media validation helpers) that this PR extends with Discord-specific ingestion and config allowlisting.

Suggested labels

area:docs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change—defining channel image ingestion strategy—and uses proper Conventional Commit format (docs prefix) within the 72-character limit.
Description check ✅ Passed The description includes a clear Summary section explaining the SDD cycle completion, documents follow-up issues with priorities and Linear references, itemizes SDD artifacts, and explicitly closes #266 as required.
Linked Issues check ✅ Passed The PR fully addresses all acceptance criteria in #266: initial channel list (Telegram, WhatsApp, Discord, Slack) is defined; channel-specific ingest behavior specified per-channel; file staging/retention expectations documented; and follow-up issues (DALLAY-192 through DALLAY-195) created cleanly.
Out of Scope Changes check ✅ Passed All changes are within scope: spec/design/task documentation, observability section, archive sync, delta spec sync, and the mentioned cargo fmt fix in discord.rs all relate to #266 channel image ingestion strategy.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/channel-image-ingestion-strategy

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 and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 26, 2026

✅ Contributor Report

User: @yacosta738
Status: Passed (12/13 metrics passed)

Metric Description Value Threshold Status
PR Merge Rate PRs merged vs closed 89% >= 30%
Repo Quality Repos with ≥100 stars 0 >= 0
Positive Reactions Positive reactions received 9 >= 1
Negative Reactions Negative reactions received 0 <= 5
Account Age GitHub account age 3072 days >= 30 days
Activity Consistency Regular activity over time 108% >= 0%
Issue Engagement Issues with community engagement 0 >= 0
Code Reviews Code reviews given to others 456 >= 0
Merger Diversity Unique maintainers who merged PRs 2 >= 0
Repo History Merge Rate Merge rate in this repo 91% >= 0%
Repo History Min PRs Previous PRs in this repo 178 >= 0
Profile Completeness Profile richness (bio, followers) 90 >= 0
Suspicious Patterns Spam-like activity detection 1 N/A

Contributor Report evaluates based on public GitHub activity. Analysis period: 2025-03-26 to 2026-03-26

@sentry
Copy link
Copy Markdown

sentry Bot commented Mar 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/2026-03-26-channel-image-ingestion-strategy/design.md`:
- Line 12: The fenced code blocks in design.md lack language specifiers which
hinders syntax highlighting and tooling; update the three blocks referenced (the
architecture diagram block starting at the diagram that begins with
"┌────────────────", the file-naming example block showing
"{temp_dir}/corvus-{channel_abbrev}-img-{sha256_prefix}.{ext}", and the sequence
diagram block that starts "User                Telegram API       
TelegramChannel       media.rs        Provider") by changing their opening
backtick fences from ``` to ```text (or another appropriate language like
```bash/```text for diagrams) so each fenced block includes a language
specifier.

In
`@openspec/changes/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md`:
- Line 175: The phrase "PNG image" is redundant; update the text that currently
reads 'When a WhatsApp user sends a PNG image with no caption' to use 'PNG' (or
'PNG file') instead — e.g., change it to 'When a WhatsApp user sends a PNG with
no caption' — to remove the redundant wording while preserving meaning.
- Line 91: Update the wording to American English by changing the sentence that
mentions "The `multimodal.max_image_bytes` config field MAY override
`MAX_IMAGE_BYTES` in future." to read "The `multimodal.max_image_bytes` config
field MAY override `MAX_IMAGE_BYTES` in the future." — locate the sentence
referencing `multimodal.max_image_bytes` and `MAX_IMAGE_BYTES` and insert "the"
before "future".

In
`@openspec/changes/2026-03-26-channel-image-ingestion-strategy/verify-report.md`:
- Around line 61-74: The scenario audit table is missing Scenario 9; add a new
table row for "9: Image rejected — vision route misconfigured" with the same
Given/When/Then/And status format (mark as ✅ Correct if it follows the format),
update the summary count at the end from "All 8 scenarios" to "All 9 scenarios",
and remove or update the SUGGESTION that asks to add Scenario 9 (the existing
SUGGESTION referencing Scenario 9 should be deleted or marked resolved); locate
references by the scenario title "Image rejected — vision route misconfigured"
and the SUGGESTION block mentioning Scenario 9 to make the edits.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 078c7cc6-24d1-4e89-80e8-0879a36b820f

📥 Commits

Reviewing files that changed from the base of the PR and between 31172e7 and ea62d21.

📒 Files selected for processing (6)
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/design.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/exploration.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/proposal.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/verify-report.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: pr-checks
  • GitHub Check: sonar
  • GitHub Check: pr-checks
  • GitHub Check: Cloudflare Pages
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{md,mdx}

⚙️ CodeRabbit configuration file

**/*.{md,mdx}: Verify technical accuracy and that docs stay aligned with code changes.
For user-facing docs, check EN/ES parity or explicitly note pending translation gaps.

Files:

  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/exploration.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/proposal.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/design.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
**/*

⚙️ CodeRabbit configuration file

**/*: Security first, performance second.
Validate input boundaries, auth/authz implications, and secret management.
Look for behavioral regressions, missing tests, and contract breaks across modules.

Files:

  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/exploration.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/proposal.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/design.md
  • openspec/changes/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
🪛 LanguageTool
openspec/changes/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md

[locale-violation] ~91-~91: The phrase ‘in future’ is British English. Did you mean: “in the future”?
Context: ...ig field MAY override MAX_IMAGE_BYTES in future. ### REQ-5: Config Gating Image inges...

(IN_FUTURE)


[style] ~175-~175: This phrase is redundant (‘G’ stands for ‘graphic’). Use simply “PNG”.
Context: ...app"]` When a WhatsApp user sends a PNG image with no caption Then the channel em...

(ACRONYM_TAUTOLOGY)

🪛 markdownlint-cli2 (0.22.0)
openspec/changes/2026-03-26-channel-image-ingestion-strategy/design.md

[warning] 12-12: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 134-134: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (13)
openspec/changes/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md (6)

1-25: LGTM! Clear definitions and scope.

The overview and definitions section provides a solid foundation for the specification. The 5-step pipeline definition (parse → emit → fetch → validate → stage) is clear and aligns with the design document.


94-109: LGTM! Config gating requirements align with implementation.

The config validation requirements match the existing validate_multimodal_config() implementation perfectly (as shown in context snippets). The fail-closed semantics and startup validation rules are correctly specified.


128-134: LGTM! RAII cleanup semantics correctly specified.

The StagedImageGuard RAII cleanup requirements match the existing implementation. The best-effort cleanup on all exit paths (success, error, timeout, early return) is correctly documented.


236-276: LGTM! Channel contracts are accurate and complete.

The channel-specific contracts correctly document:

  • Telegram and WhatsApp contracts match their existing implementations (verified against context snippets)
  • Discord and Slack are properly marked as Wave 2 with contract-only status
  • Auth mechanisms (bot token in URL for Telegram, Bearer token for WhatsApp, etc.) are correctly specified
  • Metadata fields align with each channel's API capabilities

189-210: ⚠️ Potential issue | 🟡 Minor

Ensure all rejection scenarios explicitly assert ImageIngressEvent emission.

REQ-8 and REQ-9 require that all rejection cases emit an ImageIngressEvent. However, Scenarios 4 (oversized), 5 (unsupported format), and 6 (multimodal disabled) do not explicitly include "And an ImageIngressEvent with outcome Rejected and reason [X] is emitted" assertions, unlike Scenarios 1, 3, 8, and 9.

For completeness and to match the requirement, add explicit ImageIngressEvent assertions to these scenarios.

📝 Suggested additions

Add to Scenario 4 (after line 194):

 **And** the user receives "That image is too large to process"
+**And** an `ImageIngressEvent` with outcome `Rejected` and reason `Oversize` is emitted

Add to Scenario 5 (after line 202):

 **And** the user receives "That image format is not supported"
+**And** an `ImageIngressEvent` with outcome `Rejected` and reason `MimeRejected` is emitted

Add to Scenario 6 (after line 210):

 **And** the user receives "Image input is currently disabled"
+**And** an `ImageIngressEvent` with outcome `Rejected` and reason `Disabled` is emitted
			> Likely an incorrect or invalid review comment.

115-121: No action needed. The spec documentation is correct: providers do base64-encode images for InlineBytes transport. Both OpenAiCompatibleProvider (compatible.rs:531) and GeminiProvider (gemini.rs:390) read from temp_path and explicitly call base64_encode() before passing to providers. The enum documentation "Raw bytes inlined in the provider request" refers to inline format (vs. URL references), not encoding state.

openspec/changes/2026-03-26-channel-image-ingestion-strategy/exploration.md (1)

1-124: LGTM! Thorough exploration that grounds the specification.

This exploration document provides excellent context by:

  • Documenting the current implementation state for Telegram and WhatsApp
  • Identifying which channels are in scope vs. deferred
  • Analyzing staging/retention behavior and risks
  • Closing key questions that inform the spec requirements

The identified risks (orphaned temp files, multi-image support, GIF rejection) align with the design's future considerations and verify-report findings.

openspec/changes/2026-03-26-channel-image-ingestion-strategy/tasks.md (1)

1-57: LGTM! Clear task breakdown supporting implementation.

The task structure is well-organized:

  • Phase 1 (spec-only) correctly marked complete
  • Phase 2 follow-up issues are discrete and actionable
  • Each channel implementation (Discord, Slack) includes parsing, staging, config updates, and tests
  • Acceptance criteria mapping provides traceability back to requirements

This will make it straightforward to create follow-up implementation issues.

openspec/changes/2026-03-26-channel-image-ingestion-strategy/proposal.md (1)

1-73: LGTM! Concise proposal with clear scope boundaries.

The proposal effectively:

  • Defines intent to codify existing Telegram/WhatsApp patterns
  • Sets clear in-scope vs. out-of-scope boundaries (including explicit exclusion of implementation, GIF, multi-image, video/audio)
  • Identifies affected modules accurately
  • Notes this is spec-only with simple rollback
  • Lists reasonable risks with appropriate mitigations
openspec/changes/2026-03-26-channel-image-ingestion-strategy/verify-report.md (1)

136-153: LGTM! Valuable verification with actionable warnings.

The verification report correctly identifies:

  1. Missing ImageIngressEvent assertions in scenarios 4 and 5 (WARNING - valid, I flagged this in spec review)
  2. Deduplication scope ambiguity (SUGGESTION - reasonable)
  3. Placeholder issue numbers in tasks (SUGGESTION - acceptable for now)

The PASS WITH WARNINGS verdict is appropriate for a spec-only change.

openspec/changes/2026-03-26-channel-image-ingestion-strategy/design.md (3)

7-62: LGTM! Clear architecture with excellent separation of concerns.

The pipeline architecture diagram effectively shows:

  • Per-channel parsing layer producing ContentPart::Image
  • Centralized config gating
  • Single dispatch point to per-channel staging implementations
  • Fail-closed default for unimplemented channels
  • Provider handoff with RAII cleanup

This design supports the spec requirements cleanly.


64-112: LGTM! Well-justified ADRs addressing key design decisions.

All 5 ADRs have clear rationale:

  • ADR-1: Channel-specific fetch with shared validation (security-critical)
  • ADR-2: Magic-byte sniffing over declared MIME (prevents MIME confusion attacks)
  • ADR-3: RAII cleanup (reliable on all exit paths)
  • ADR-4: Fail-closed default (security-first)
  • ADR-5: Single dispatch point (auditability and consistency)

These align perfectly with the spec requirements and security-first approach.


162-172: LGTM! Future considerations align with exploration risks.

The future considerations section appropriately flags:

  • Multi-image support (matches tasks Phase 2)
  • GIF support (noted as rejected in spec)
  • Startup reaper (matches exploration risks)
  • URL-based transport optimization for Discord
  • Deduplication potential via SHA-256 naming

These provide good guidance for follow-up work.

Comment thread openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md Outdated
### Scenario 2: WhatsApp image without caption

**Given** multimodal is enabled with `allowed_channels: ["whatsapp"]`
**When** a WhatsApp user sends a PNG image with no caption
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor: Remove redundant "PNG image" phrasing.

"PNG" already stands for "Portable Network Graphics" (where "graphic" means image), so "PNG image" is technically redundant. However, this is extremely minor and can be ignored if the team prefers the more explicit phrasing for clarity.

🧰 Tools
🪛 LanguageTool

[style] ~175-~175: This phrase is redundant (‘G’ stands for ‘graphic’). Use simply “PNG”.
Context: ...app"]` When a WhatsApp user sends a PNG image with no caption Then the channel em...

(ACRONYM_TAUTOLOGY)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md`
at line 175, The phrase "PNG image" is redundant; update the text that currently
reads 'When a WhatsApp user sends a PNG image with no caption' to use 'PNG' (or
'PNG file') instead — e.g., change it to 'When a WhatsApp user sends a PNG with
no caption' — to remove the redundant wording while preserving meaning.

Comment on lines +61 to +74
### Scenario Format Audit

| Scenario | Given/When/Then | Status |
|----------|-----------------|--------|
| 1: Telegram photo accepted | Given/And/When/Then/And(x5) | ✅ Correct |
| 2: WhatsApp image without caption | Given/When/Then/And(x3) | ✅ Correct |
| 3: Image rejected — channel not allowed | Given/When/Then/And(x2) | ✅ Correct |
| 4: Image rejected — oversized | Given/When/Then/And | ✅ Correct |
| 5: Image rejected — unsupported format | Given/When/Then/And | ✅ Correct |
| 6: Image rejected — multimodal disabled | Given/When/Then/And | ✅ Correct |
| 7: Fail-closed for unimplemented channel | Given/When/Then/And | ✅ Correct |
| 8: Temp file cleanup on timeout | Given/When/Then/And | ✅ Correct |

All 8 scenarios follow Given/When/Then format per openspec config rules.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update scenario count to match spec.

The scenario format audit lists 8 scenarios, but the spec (lines 159-235) actually contains 9 scenarios. Scenario 9 ("Image rejected — vision route misconfigured") is present in the spec but missing from this table.

Additionally, the SUGGESTION on line 146-148 asks for a Scenario 9 covering vision route rejection, but this scenario already exists in the spec as Scenario 9 (lines 227-234).

📝 Proposed fix

Add Scenario 9 to the table after line 72:

 | 7: Fail-closed for unimplemented channel | Given/When/Then/And | ✅ Correct |
 | 8: Temp file cleanup on timeout | Given/When/Then/And | ✅ Correct |
+| 9: Image rejected — vision route misconfigured | Given/When/Then/And(x2) | ✅ Correct |

And update the summary on line 74:

-All 8 scenarios follow Given/When/Then format per openspec config rules.
+All 9 scenarios follow Given/When/Then format per openspec config rules.

Remove or update the SUGGESTION on lines 146-148 since Scenario 9 already exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/2026-03-26-channel-image-ingestion-strategy/verify-report.md`
around lines 61 - 74, The scenario audit table is missing Scenario 9; add a new
table row for "9: Image rejected — vision route misconfigured" with the same
Given/When/Then/And status format (mark as ✅ Correct if it follows the format),
update the summary count at the end from "All 8 scenarios" to "All 9 scenarios",
and remove or update the SUGGESTION that asks to add Scenario 9 (the existing
SUGGESTION referencing Scenario 9 should be deleted or marked resolved); locate
references by the scenario title "Image rejected — vision route misconfigured"
and the SUGGESTION block mentioning Scenario 9 to make the edits.

Implement Discord channel image ingestion following the canonical
5-step pipeline defined in the channel image ingestion strategy.

Code changes:
- Parse Discord message attachments into ContentPart::Image
- Implement DiscordChannel::fetch_and_stage_image() (CDN download)
- Add "discord" dispatch arm in stage_channel_images()
- Add "discord" to MVP_VALID_MULTIMODAL_CHANNELS in config validation
- Deduplicate inline parsing by calling parse_image_attachments()
- 11 new tests for image attachment parsing

Spec refinements:
- Add observability assertions to Scenarios 4, 5, 8
- Add Scenario 9 (vision route misconfiguration)
- Add REQ-10 (dedup explicitly out of scope for MVP)
- Update verify report to PASS
- Add follow-up issue definitions for Slack, reaper, multi-image
- Add observability section (REQ-9) to design doc
- Update tasks with Linear issue refs (DALLAY-192 through DALLAY-195)
- Sync delta spec to openspec/specs/channel-image-ingestion/
- Archive change to openspec/changes/archive/
- Full SDD cycle: explore → propose → spec → design → tasks → verify → archive
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 26, 2026

Deploying corvus with  Cloudflare Pages  Cloudflare Pages

Latest commit: b6e9edf
Status: ✅  Deploy successful!
Preview URL: https://3fa55fe9.corvus-42x.pages.dev
Branch Preview URL: https://feat-channel-image-ingestion.corvus-42x.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (1)
clients/agent-runtime/src/config/schema.rs (1)

6033-6049: 🧹 Nitpick | 🔵 Trivial

Add a runtime-validation success case for "discord".

The new allowlist value is only exercised by parsing. validate_multimodal_config() is still never asserted to accept "discord", so a regression in MVP_VALID_MULTIMODAL_CHANNELS would slip past these tests.

➕ Suggested test
+    #[test]
+    fn multimodal_validation_accepts_discord_when_enabled() {
+        let config = Config {
+            multimodal: MultimodalConfig {
+                enabled: true,
+                allowed_channels: vec!["discord".into()],
+                vision_model_hint: Some("vision".into()),
+                max_image_bytes: None,
+            },
+            model_routes: vec![make_vision_route()],
+            ..Config::default()
+        };
+
+        assert!(config.validate_multimodal_config().is_ok());
+    }

As per coding guidelines, **/*: look for behavioral regressions, missing tests, and contract breaks across modules.

Also applies to: 6164-6178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/agent-runtime/src/config/schema.rs` around lines 6033 - 6049, Add a
runtime-validation assertion that the parsed multimodal config accepts
"discord": after parsing the TOML into parsed (the multimodal struct) call and
assert validate_multimodal_config(&parsed.multimodal).unwrap_or(…) succeeds (or
returns Ok) to exercise the validate_multimodal_config function and its
MVP_VALID_MULTIMODAL_CHANNELS allowlist; ensure MVP_VALID_MULTIMODAL_CHANNELS
contains "discord" if validation checks that constant. This will pair the
parsing assertions (parsed.multimodal.allowed_channels contains "discord") with
a runtime validation check to catch regressions in
validate_multimodal_config/MVP_VALID_MULTIMODAL_CHANNELS.
♻️ Duplicate comments (2)
openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md (1)

91-91: ⚠️ Potential issue | 🟡 Minor

Use American English phrasing for consistency.

Line 91 should read “in the future” instead of “in future”.

As per coding guidelines **/*.{md,mdx}: "Verify technical accuracy and that docs stay aligned with code changes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md`
at line 91, Update the phrasing for consistency with American English by
changing the sentence that references the multimodal.max_image_bytes config
field (the line mentioning "The `multimodal.max_image_bytes` config field MAY
override `MAX_IMAGE_BYTES` in future.") to read "in the future" instead of "in
future", ensuring the clause around `multimodal.max_image_bytes` and
`MAX_IMAGE_BYTES` remains otherwise unchanged.
openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md (1)

12-62: ⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks.

The diagram/example fences (Line 12, Line 149, Line 168) still omit language tags (```text recommended), which keeps triggering markdownlint MD040.

As per coding guidelines **/*: "Look for behavioral regressions, missing tests, and contract breaks across modules."

Also applies to: 149-157, 168-194

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md`
around lines 12 - 62, The fenced diagrams in design.md are missing language
identifiers and trigger markdownlint MD040; update each triple-backtick fence
(the large channel diagram and the two smaller fences referenced around the
diagram) to use a language tag such as ```text (i.e., replace ``` with ```text)
so markdownlint recognizes them as code blocks; this affects the diagram block
containing Channel Layer/Config Gating/Fetch & Stage/Provider Dispatch and the
two other fences noted in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clients/agent-runtime/src/channels/discord.rs`:
- Around line 473-479: The mention_only check is being bypassed for
attachment-only messages because the guard uses && !content.is_empty(); remove
the content non-empty condition so the block always enforces mention-only when
self.mention_only is true (i.e., change the condition to if self.mention_only &&
!contains_bot_mention(content, &bot_user_id) { continue; }), and add a
regression test exercising an image/attachment-only Discord message (empty
content, attachment present) to assert that an unmentioned upload is ignored
when mention_only=true; reference the mention_only flag, contains_bot_mention
function, and bot_user_id variable to locate the change and the handler code in
discord.rs.
- Around line 100-114: The staging path currently uses only the hash prefix
(temp_path derived from sha256[..16]) so concurrent identical images collide;
generate a per-stage unique nonce (e.g., let nonce =
uuid::Uuid::new_v4().to_string() or similar) and include it in the filename
(e.g., format!("corvus-dc-img-{}-{}.{}", &sha256[..16], nonce, ext)) before
calling tokio::fs::write, and use that same temp_path variable for any
subsequent cleanup to ensure each turn has its own file.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md`:
- Around line 37-42: Update the architecture doc to reflect that Discord image
staging is implemented: change the line in the stage_channel_images() dispatch
list that currently reads `"discord"  → DiscordChannel::fetch_and_stage_image()
[Wave2]` to indicate current/implemented status (e.g., remove `[Wave2]` or
replace with `[implemented]` / `current-state`), ensuring the reference to
DiscordChannel::fetch_and_stage_image() and stage_channel_images() remains
unchanged so the doc aligns with the PR's implementation.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md`:
- Around line 32-37: Update the spec to mark Discord as implemented for MVP:
change the Discord row in the channel status table (the "Channel | Status |
Priority" table showing Discord as "Planned | Wave 2") to "Implemented | MVP",
and also update the later section that describes Discord as
"planned/contract-only" (the paragraph around the Discord implementation
details) to reflect that Discord ingestion is implemented and behaves as MVP;
ensure wording and any examples or notes align with implemented behavior.
- Around line 115-121: The fenced code block showing the runtime handoff
(starting with "ChannelMessage.parts: [ContentPart::Image ...]" and including
stage_channel_images(), Vec<StagedImage> and Provider::chat()) is missing a
fence language and triggers markdownlint MD040; update that fence to include an
explicit language token (e.g., add "text" after the opening ```), re-save the
spec.md so the block becomes ```text ... ```, and ensure related references
(ChannelMessage.parts, ContentPart::Image, stage_channel_images(),
Vec<StagedImage>, Provider::chat(), and StagedImage fields like sha256,
mime_type, temp_path, transport_form, channel_origin) remain unchanged.
- Around line 218-221: The scenario uses allowed_channels: ["matrix"] which
violates REQ-5 (allowed_channels must be valid MVP channel names) making the
test unreachable; update the scenario to use a valid-but-unimplemented MVP
channel name (e.g., replace "matrix" with an actual MVP channel identifier used
elsewhere in the spec) or alternatively revise REQ-5 to allow "matrix" so the
scenario becomes valid, and keep the expected behavior referencing
stage_channel_images() and the rejection message unchanged.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md`:
- Around line 29-56: The task checklist items under the "Issue: Slack image
ingestion", "Issue: Startup temp file reaper", and "Issue: Multi-image per turn"
sections are incorrectly marked as completed; update the checklist entries
(e.g., items 2.6–2.11, 2.12–2.15, 2.16–2.19) to use unchecked boxes "[ ]"
instead of "[x]" or alternatively rename the section header to clarify that
"[x]" denotes "issue created" (not "implementation complete"); ensure the
changes are applied to the corresponding markdown headings ("Issue: Slack image
ingestion", "Issue: Startup temp file reaper", "Issue: Multi-image per turn") so
the documentation accurately reflects that these are tracked follow-up tasks
rather than shipped features.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md`:
- Around line 5-6: Update the verify-report.md artifact to reflect actual PR
changes: remove the "spec-only / no code modified" claim and either (a) scope
the report explicitly to archived spec artifacts only, or (b) document the
runtime validation that was run for the Rust changes in clients/agent-runtime
(specifically mention channels::discord.rs and config::schema.rs changes);
include the updated task IDs DALLAY-192 through DALLAY-195 instead of `#next`
placeholders; and for any modified clients/agent-runtime/**/*.rs files, add or
update explicit lines stating the status of cargo fmt, cargo clippy, and cargo
test (or explain why any were skipped). Ensure markdown files **/*.{md,mdx}
mentioned in the PR are updated to remain aligned with these code changes.

In `@openspec/specs/channel-image-ingestion/spec.md`:
- Around line 107-110: REQ-5 and Scenario 7 conflict: when `enabled=true` REQ-5
requires all `allowed_channels` to be valid MVP channel names but Scenario 7
uses `["matrix"]`; either make the scenario use an MVP channel or loosen REQ-5.
Fix by updating Scenario 7’s `allowed_channels` from `["matrix"]` to a valid MVP
channel (e.g., `"slack"`), or alternatively update the REQ-5 text to permit
non-MVP channels (adjust the validation sentence that mentions
`allowed_channels` and `valid MVP channel names`) and update any startup
validation logic that enforces REQ-5 accordingly; ensure `vision_model_hint` and
`enabled=true` rules remain satisfied.
- Around line 32-37: Update the spec to reflect that Discord is implemented at
runtime: change the Channel status table row for "Discord" from "Planned" to
"Implemented" (and Priority from "Wave 2" to "MVP" if applicable), and update
the narrative in the sections around the old contract-only description (the
paragraph covering the Discord channel and the block referencing the "discord"
dispatch) to indicate that stage_channel_images now routes a live "discord"
path; reference the runtime symbol stage_channel_images and the existence of a
"discord" dispatch when editing the sections previously claiming Discord was
planned/contract-only.
- Around line 115-121: The fenced handoff block (the code fence showing
ChannelMessage.parts → stage_channel_images → Vec<StagedImage> → Provider
behavior) is untyped and triggers markdownlint MD040; fix it by adding a
language tag (e.g., change the opening ``` to ```text) so the fence becomes a
typed block (```text) and the rest of the content remains unchanged.

---

Outside diff comments:
In `@clients/agent-runtime/src/config/schema.rs`:
- Around line 6033-6049: Add a runtime-validation assertion that the parsed
multimodal config accepts "discord": after parsing the TOML into parsed (the
multimodal struct) call and assert
validate_multimodal_config(&parsed.multimodal).unwrap_or(…) succeeds (or returns
Ok) to exercise the validate_multimodal_config function and its
MVP_VALID_MULTIMODAL_CHANNELS allowlist; ensure MVP_VALID_MULTIMODAL_CHANNELS
contains "discord" if validation checks that constant. This will pair the
parsing assertions (parsed.multimodal.allowed_channels contains "discord") with
a runtime validation check to catch regressions in
validate_multimodal_config/MVP_VALID_MULTIMODAL_CHANNELS.

---

Duplicate comments:
In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md`:
- Around line 12-62: The fenced diagrams in design.md are missing language
identifiers and trigger markdownlint MD040; update each triple-backtick fence
(the large channel diagram and the two smaller fences referenced around the
diagram) to use a language tag such as ```text (i.e., replace ``` with ```text)
so markdownlint recognizes them as code blocks; this affects the diagram block
containing Channel Layer/Config Gating/Fetch & Stage/Provider Dispatch and the
two other fences noted in the comment.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md`:
- Line 91: Update the phrasing for consistency with American English by changing
the sentence that references the multimodal.max_image_bytes config field (the
line mentioning "The `multimodal.max_image_bytes` config field MAY override
`MAX_IMAGE_BYTES` in future.") to read "in the future" instead of "in future",
ensuring the clause around `multimodal.max_image_bytes` and `MAX_IMAGE_BYTES`
remains otherwise 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: ASSERTIVE

Plan: Pro

Run ID: 54392901-b695-4216-bc8a-d510d7cb179d

📥 Commits

Reviewing files that changed from the base of the PR and between ea62d21 and c305b92.

📒 Files selected for processing (11)
  • clients/agent-runtime/src/channels/discord.rs
  • clients/agent-runtime/src/channels/mod.rs
  • clients/agent-runtime/src/config/schema.rs
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/exploration.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/follow-up-issues.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/proposal.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • openspec/specs/channel-image-ingestion/spec.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: pr-checks
  • GitHub Check: pr-checks
  • GitHub Check: sonar
  • GitHub Check: Cloudflare Pages
🧰 Additional context used
📓 Path-based instructions (7)
clients/agent-runtime/src/channels/**/*.rs

📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)

Implement Channel trait in src/channels/ with consistent send, listen, and health_check semantics and cover auth/allowlist/health behavior with tests

Files:

  • clients/agent-runtime/src/channels/mod.rs
  • clients/agent-runtime/src/channels/discord.rs
clients/agent-runtime/src/**/*.rs

📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)

clients/agent-runtime/src/**/*.rs: Never log secrets, tokens, raw credentials, or sensitive payloads in any logging statements
Avoid unnecessary allocations, clones, and blocking operations to maintain performance and efficiency

Files:

  • clients/agent-runtime/src/channels/mod.rs
  • clients/agent-runtime/src/config/schema.rs
  • clients/agent-runtime/src/channels/discord.rs
clients/agent-runtime/**/*.rs

📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)

Run cargo fmt --all -- --check, cargo clippy --all-targets -- -D warnings, and cargo test for code validation, or document which checks were skipped and why

Files:

  • clients/agent-runtime/src/channels/mod.rs
  • clients/agent-runtime/src/config/schema.rs
  • clients/agent-runtime/src/channels/discord.rs
**/*.rs

⚙️ CodeRabbit configuration file

**/*.rs: Focus on Rust idioms, memory safety, and ownership/borrowing correctness.
Flag unnecessary clones, unchecked panics in production paths, and weak error context.
Prioritize unsafe blocks, FFI boundaries, concurrency races, and secret handling.

Files:

  • clients/agent-runtime/src/channels/mod.rs
  • clients/agent-runtime/src/config/schema.rs
  • clients/agent-runtime/src/channels/discord.rs
**/*

⚙️ CodeRabbit configuration file

**/*: Security first, performance second.
Validate input boundaries, auth/authz implications, and secret management.
Look for behavioral regressions, missing tests, and contract breaks across modules.

Files:

  • clients/agent-runtime/src/channels/mod.rs
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/proposal.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • clients/agent-runtime/src/config/schema.rs
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/exploration.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/follow-up-issues.md
  • clients/agent-runtime/src/channels/discord.rs
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
**/*.{md,mdx}

⚙️ CodeRabbit configuration file

**/*.{md,mdx}: Verify technical accuracy and that docs stay aligned with code changes.
For user-facing docs, check EN/ES parity or explicitly note pending translation gaps.

Files:

  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/proposal.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/exploration.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/follow-up-issues.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
clients/agent-runtime/src/{security,gateway,tools,config}/**/*.rs

📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)

Do not silently weaken security policy or access constraints; keep default behavior secure-by-default with deny-by-default where applicable

Files:

  • clients/agent-runtime/src/config/schema.rs
🧠 Learnings (1)
📚 Learning: 2026-02-17T12:31:17.076Z
Learnt from: CR
Repo: dallay/corvus PR: 0
File: clients/agent-runtime/AGENTS.md:0-0
Timestamp: 2026-02-17T12:31:17.076Z
Learning: Applies to clients/agent-runtime/src/channels/**/*.rs : Implement `Channel` trait in `src/channels/` with consistent `send`, `listen`, and `health_check` semantics and cover auth/allowlist/health behavior with tests

Applied to files:

  • clients/agent-runtime/src/channels/mod.rs
  • clients/agent-runtime/src/config/schema.rs
  • clients/agent-runtime/src/channels/discord.rs
🪛 LanguageTool
openspec/specs/channel-image-ingestion/spec.md

[locale-violation] ~91-~91: The phrase ‘in future’ is British English. Did you mean: “in the future”?
Context: ...ig field MAY override MAX_IMAGE_BYTES in future. ### REQ-5: Config Gating Image inges...

(IN_FUTURE)


[style] ~179-~179: This phrase is redundant (‘G’ stands for ‘graphic’). Use simply “PNG”.
Context: ...app"]` When a WhatsApp user sends a PNG image with no caption Then the channel em...

(ACRONYM_TAUTOLOGY)

openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md

[locale-violation] ~91-~91: The phrase ‘in future’ is British English. Did you mean: “in the future”?
Context: ...ig field MAY override MAX_IMAGE_BYTES in future. ### REQ-5: Config Gating Image inges...

(IN_FUTURE)


[style] ~179-~179: This phrase is redundant (‘G’ stands for ‘graphic’). Use simply “PNG”.
Context: ...app"]` When a WhatsApp user sends a PNG image with no caption Then the channel em...

(ACRONYM_TAUTOLOGY)

🪛 markdownlint-cli2 (0.22.0)
openspec/specs/channel-image-ingestion/spec.md

[warning] 115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md

[warning] 12-12: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 149-149: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 168-168: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md

[warning] 115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (1)
clients/agent-runtime/src/channels/mod.rs (1)

1043-1048: Discord staging dispatch is consistent with existing channel routing.

This new match arm follows the same fetch-and-stage contract as Telegram/WhatsApp and keeps the fail-closed default path intact.

Comment thread clients/agent-runtime/src/channels/discord.rs Outdated
Comment thread clients/agent-runtime/src/channels/discord.rs
Comment thread openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md Outdated
Comment thread openspec/specs/channel-image-ingestion/spec.md
Comment thread openspec/specs/channel-image-ingestion/spec.md
Comment thread openspec/specs/channel-image-ingestion/spec.md Outdated
- Fix mention_only bypass for attachment-only Discord messages
- Add UUID nonce to staging path to prevent concurrent collision
- Add regression tests for mention_only + image-only messages
- Add discord multimodal validation test in schema.rs
- Update Discord status from Planned/Wave2 to Implemented/MVP in specs
- Fix Scenario 7: use valid MVP channel (slack) instead of matrix
- Add language tags to fenced code blocks (MD040)
- Uncheck Slack/reaper/multi-image tasks (not yet implemented)
- Update verify-report with actual build/test status and Linear refs
- Fix 'in future' → 'in the future' wording
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (5)
openspec/specs/channel-image-ingestion/spec.md (2)

109-110: ⚠️ Potential issue | 🟠 Major

Scenario 7 conflicts with REQ-5 channel validation.

Line 109 requires MVP channel names only, but Line 218 uses Slack (non-MVP in Line 37). This scenario is not reachable under the current requirement set.

As per coding guidelines **/*.{md,mdx}: "Verify technical accuracy and that docs stay aligned with code changes."

Also applies to: 218-221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/specs/channel-image-ingestion/spec.md` around lines 109 - 110,
Scenario 7 conflicts with REQ-5 because allowed_channels is restricted to MVP
channel names but the scenario uses Slack (a non-MVP channel); update the
scenario or the requirement so they align: either change Scenario 7 (and the
entries at lines referenced 218-221) to use a valid MVP channel name instead of
"Slack", or modify REQ-5/allowed_channels to explicitly allow Slack and document
the exception; ensure references to "allowed_channels", "REQ-5", and "Scenario
7" are updated consistently.

10-13: ⚠️ Potential issue | 🟠 Major

Discord status remains inconsistent across this spec.

Line 10-Line 13 and Line 261-Line 270 still frame Discord as future/contract-only while Line 36 lists it as Implemented MVP. Please unify these sections to the same rollout state.

As per coding guidelines **/*.{md,mdx}: "Verify technical accuracy and that docs stay aligned with code changes."

Also applies to: 261-270

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/specs/channel-image-ingestion/spec.md` around lines 10 - 13, The
spec contains conflicting rollout status for Discord: update all mentions so
they match (choose either "Implemented MVP" or "Contract-only") to keep
documentation consistent; specifically change the Discord status in the opening
summary block (currently lines 10–13), the "Implemented MVP" list (around line
36) and the later status block (around lines 261–270) so they all state the same
rollout state and wording, and ensure any related changelog/notes or bullets
referencing Discord use the identical term and tense.
openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md (2)

109-110: ⚠️ Potential issue | 🟠 Major

Scenario 7 is unreachable under REQ-5 as written.

Line 109 requires allowed_channels to be valid MVP channel names, but Line 218 uses ["slack"] while Slack is not MVP in Line 37. Either relax REQ-5 + runtime validation, or rewrite/remove Scenario 7 to match current enforceable behavior.

As per coding guidelines **/*.{md,mdx}: "Verify technical accuracy and that docs stay aligned with code changes."

Also applies to: 218-221

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md`
around lines 109 - 110, REQ-5 currently mandates that allowed_channels entries
be valid MVP channel names, but Scenario 7 uses ["slack"] which is not an MVP
channel; update the spec so it is internally consistent by either (a) relaxing
REQ-5 to allow non-MVP channel names with runtime validation and document the
new behavior in REQ-5 and the runtime validation rules, or (b) change Scenario
7’s allowed_channels to use an actual MVP channel name (e.g., replace ["slack"]
with a valid MVP channel like ["email"] or another MVP entry) or remove Scenario
7 entirely; locate references to allowed_channels, REQ-5 and "Scenario 7" in the
spec and update the text so the requirement and the scenario are aligned.

10-13: ⚠️ Potential issue | 🟠 Major

Discord status is still contradictory across the spec.

Line 10-Line 13 and Line 261-Line 270 still describe Discord as future/contract-only, but Line 36 marks Discord as Implemented for MVP. Please align these sections to one status.

Suggested doc patch
-This specification defines the canonical strategy for how Corvus messaging channels ingest user-sent
-images, validate them, stage them to disk, and hand them off to the runtime's provider pipeline. It
-codifies the patterns already implemented for Telegram and WhatsApp, and defines contracts for
-future channel implementations.
+This specification defines the canonical strategy for how Corvus messaging channels ingest user-sent
+images, validate them, stage them to disk, and hand them off to the runtime's provider pipeline. It
+codifies the patterns already implemented for Telegram, WhatsApp, and Discord, and defines contracts
+for future channel implementations.
@@
-### Discord (Wave 2 — contract only)
+### Discord (Implemented — MVP)

As per coding guidelines **/*.{md,mdx}: "Verify technical accuracy and that docs stay aligned with code changes."

Also applies to: 261-270

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md`
around lines 10 - 13, The spec contains contradictory Discord status entries:
update all occurrences of "Discord" in this document so they use a single status
(pick either "Implemented for MVP" or "future/contract-only") and match the
chosen wording and tense. Specifically, change the Discord mention in the
opening summary paragraph (the block around the first few lines), the status
entry at the section near line 36 where it currently says "Implemented for MVP",
and the later Discord subsection (around lines 261–270) so they all use
identical status text and any follow-up wording (e.g., "implemented", "will
implement", "contract") is consistent.
openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md (1)

36-38: ⚠️ Potential issue | 🟠 Major

Verification report still contains stale “spec-only” and placeholder statements.

Line 36-Line 38 conflicts with Line 6 and Line 26-Line 31, and Line 99-Line 100 / Line 149-Line 151 are outdated against the current artifacts. Please refresh these sections so the report has one consistent truth.

As per coding guidelines **/*.{md,mdx}: "Verify technical accuracy and that docs stay aligned with code changes."

Also applies to: 99-100, 149-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md`
around lines 36 - 38, The verification report contains stale "spec-only" and
placeholder statements (e.g., the paragraph starting "Not applicable — this is a
spec-only change. No implementation to validate against scenarios.") that
conflict with other sections (lines referenced in the review: 6, 26-31, 99-100,
149-151); update the verify-report.md so all assertions are consistent: remove
or replace the "spec-only" placeholder, reconcile the Behavioral validation
statement with the content at lines 6 and 26-31, refresh the outdated sections
around lines 99-100 and 149-151 to reflect current artifacts, and ensure the
file follows the docs guidance for **/*.{md,mdx} (technical accuracy and
alignment with code changes).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clients/agent-runtime/src/channels/discord.rs`:
- Around line 79-86: The stream error closure currently discards the diagnostic
by using |_|; change the map_err closure in the loop that processes
stream.next().await to capture the original error (e.g., |err|), call the future
sanitize_error(err) helper (or sanitize_error(&err) when implemented) to produce
a sanitized string/message, log that sanitized message with tracing::warn!
alongside the existing context, and then map to
media::ImageRejectionReason::FetchFailed so behavior is unchanged; update the
closure surrounding chunk_result.map_err(...) in the loop that handles
chunk_result to perform these steps.
- Around line 61-64: The current map_err closure in the Discord image download
(the get/send call inside the method that assigns dl_resp) logs the raw Reqwest
error which can include Discord CDN query tokens; update that error handling to
sanitize the error before logging (reuse or mirror the Telegram implementation's
sanitize_error() helper), e.g. call sanitize_error(&e) and log the sanitized
message instead of the raw error, then still return
media::ImageRejectionReason::FetchFailed so the rest of the flow is unchanged.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md`:
- Around line 115-117: The text incorrectly states that all ImageIngressEvent
emissions occur at stage_channel_images(); update the wording to say that
ImageIngressEvent emissions occur both at pre-dispatch gating points (e.g.,
channel-not-allowed/disabled rejections emitted from channels::mod before
staging) and at the single dispatch point (stage_channel_images()) for
post-staging outcomes (validation success, provider errors, etc.), and reference
the Observer trait as receiving events from both pre-dispatch gates and the
staging/provider path so the doc accurately reflects where each rejection or
success is emitted.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md`:
- Around line 15-19: Update the section header "Phase 2: Follow-Up
Implementation Issues (to be created)" to reflect that these issues are already
created/tracked (e.g., "Phase 2: Follow-Up Implementation Issues
(created/tracked)" or remove the parenthetical), and ensure the text under the
"Issue: Discord image ingestion ([DALLAY-192])" entry accurately indicates the
issue exists in Linear; verify the change follows the project's docs guideline
for markdown files so the doc and linked Linear items remain technically
accurate and aligned with the codebase.
- Around line 62-63: Update the acceptance mapping for REQ-1 so it reflects that
Discord is implemented in the MVP: edit the table cell labeled "Initial channel
list is defined" (the REQ-1 mapping) to replace "Discord, Slack Wave 2" with a
current mapping such as "Discord included in MVP; Slack Wave 2" or "Telegram,
WhatsApp, Discord (MVP); Slack Wave 2" to keep the "REQ-1" mapping consistent
with the completed Discord implementation.

---

Duplicate comments:
In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md`:
- Around line 109-110: REQ-5 currently mandates that allowed_channels entries be
valid MVP channel names, but Scenario 7 uses ["slack"] which is not an MVP
channel; update the spec so it is internally consistent by either (a) relaxing
REQ-5 to allow non-MVP channel names with runtime validation and document the
new behavior in REQ-5 and the runtime validation rules, or (b) change Scenario
7’s allowed_channels to use an actual MVP channel name (e.g., replace ["slack"]
with a valid MVP channel like ["email"] or another MVP entry) or remove Scenario
7 entirely; locate references to allowed_channels, REQ-5 and "Scenario 7" in the
spec and update the text so the requirement and the scenario are aligned.
- Around line 10-13: The spec contains contradictory Discord status entries:
update all occurrences of "Discord" in this document so they use a single status
(pick either "Implemented for MVP" or "future/contract-only") and match the
chosen wording and tense. Specifically, change the Discord mention in the
opening summary paragraph (the block around the first few lines), the status
entry at the section near line 36 where it currently says "Implemented for MVP",
and the later Discord subsection (around lines 261–270) so they all use
identical status text and any follow-up wording (e.g., "implemented", "will
implement", "contract") is consistent.

In
`@openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md`:
- Around line 36-38: The verification report contains stale "spec-only" and
placeholder statements (e.g., the paragraph starting "Not applicable — this is a
spec-only change. No implementation to validate against scenarios.") that
conflict with other sections (lines referenced in the review: 6, 26-31, 99-100,
149-151); update the verify-report.md so all assertions are consistent: remove
or replace the "spec-only" placeholder, reconcile the Behavioral validation
statement with the content at lines 6 and 26-31, refresh the outdated sections
around lines 99-100 and 149-151 to reflect current artifacts, and ensure the
file follows the docs guidance for **/*.{md,mdx} (technical accuracy and
alignment with code changes).

In `@openspec/specs/channel-image-ingestion/spec.md`:
- Around line 109-110: Scenario 7 conflicts with REQ-5 because allowed_channels
is restricted to MVP channel names but the scenario uses Slack (a non-MVP
channel); update the scenario or the requirement so they align: either change
Scenario 7 (and the entries at lines referenced 218-221) to use a valid MVP
channel name instead of "Slack", or modify REQ-5/allowed_channels to explicitly
allow Slack and document the exception; ensure references to "allowed_channels",
"REQ-5", and "Scenario 7" are updated consistently.
- Around line 10-13: The spec contains conflicting rollout status for Discord:
update all mentions so they match (choose either "Implemented MVP" or
"Contract-only") to keep documentation consistent; specifically change the
Discord status in the opening summary block (currently lines 10–13), the
"Implemented MVP" list (around line 36) and the later status block (around lines
261–270) so they all state the same rollout state and wording, and ensure any
related changelog/notes or bullets referencing Discord use the identical term
and tense.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 325e2753-6d41-48a8-a62d-1b0bcb1d3e75

📥 Commits

Reviewing files that changed from the base of the PR and between c305b92 and 43441c7.

📒 Files selected for processing (7)
  • clients/agent-runtime/src/channels/discord.rs
  • clients/agent-runtime/src/config/schema.rs
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • openspec/specs/channel-image-ingestion/spec.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: sonar
  • GitHub Check: pr-checks
  • GitHub Check: pr-checks
  • GitHub Check: Cloudflare Pages
🧰 Additional context used
📓 Path-based instructions (7)
clients/agent-runtime/src/**/*.rs

📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)

clients/agent-runtime/src/**/*.rs: Never log secrets, tokens, raw credentials, or sensitive payloads in any logging statements
Avoid unnecessary allocations, clones, and blocking operations to maintain performance and efficiency

Files:

  • clients/agent-runtime/src/config/schema.rs
  • clients/agent-runtime/src/channels/discord.rs
clients/agent-runtime/**/*.rs

📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)

Run cargo fmt --all -- --check, cargo clippy --all-targets -- -D warnings, and cargo test for code validation, or document which checks were skipped and why

Files:

  • clients/agent-runtime/src/config/schema.rs
  • clients/agent-runtime/src/channels/discord.rs
clients/agent-runtime/src/{security,gateway,tools,config}/**/*.rs

📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)

Do not silently weaken security policy or access constraints; keep default behavior secure-by-default with deny-by-default where applicable

Files:

  • clients/agent-runtime/src/config/schema.rs
**/*.rs

⚙️ CodeRabbit configuration file

**/*.rs: Focus on Rust idioms, memory safety, and ownership/borrowing correctness.
Flag unnecessary clones, unchecked panics in production paths, and weak error context.
Prioritize unsafe blocks, FFI boundaries, concurrency races, and secret handling.

Files:

  • clients/agent-runtime/src/config/schema.rs
  • clients/agent-runtime/src/channels/discord.rs
**/*

⚙️ CodeRabbit configuration file

**/*: Security first, performance second.
Validate input boundaries, auth/authz implications, and secret management.
Look for behavioral regressions, missing tests, and contract breaks across modules.

Files:

  • clients/agent-runtime/src/config/schema.rs
  • openspec/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md
  • clients/agent-runtime/src/channels/discord.rs
**/*.{md,mdx}

⚙️ CodeRabbit configuration file

**/*.{md,mdx}: Verify technical accuracy and that docs stay aligned with code changes.
For user-facing docs, check EN/ES parity or explicitly note pending translation gaps.

Files:

  • openspec/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md
clients/agent-runtime/src/channels/**/*.rs

📄 CodeRabbit inference engine (clients/agent-runtime/AGENTS.md)

Implement Channel trait in src/channels/ with consistent send, listen, and health_check semantics and cover auth/allowlist/health behavior with tests

Files:

  • clients/agent-runtime/src/channels/discord.rs
🧠 Learnings (4)
📚 Learning: 2026-02-17T12:31:17.076Z
Learnt from: CR
Repo: dallay/corvus PR: 0
File: clients/agent-runtime/AGENTS.md:0-0
Timestamp: 2026-02-17T12:31:17.076Z
Learning: Applies to clients/agent-runtime/src/channels/**/*.rs : Implement `Channel` trait in `src/channels/` with consistent `send`, `listen`, and `health_check` semantics and cover auth/allowlist/health behavior with tests

Applied to files:

  • clients/agent-runtime/src/config/schema.rs
  • openspec/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • clients/agent-runtime/src/channels/discord.rs
📚 Learning: 2026-02-17T12:31:17.076Z
Learnt from: CR
Repo: dallay/corvus PR: 0
File: clients/agent-runtime/AGENTS.md:0-0
Timestamp: 2026-02-17T12:31:17.076Z
Learning: Applies to clients/agent-runtime/src/main.rs : Preserve CLI contract unless change is intentional and documented; prefer explicit errors over silent fallback for unsupported critical paths

Applied to files:

  • openspec/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
  • clients/agent-runtime/src/channels/discord.rs
📚 Learning: 2026-02-17T12:31:17.076Z
Learnt from: CR
Repo: dallay/corvus PR: 0
File: clients/agent-runtime/AGENTS.md:0-0
Timestamp: 2026-02-17T12:31:17.076Z
Learning: Applies to clients/agent-runtime/**/*.rs : Run `cargo fmt --all -- --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test` for code validation, or document which checks were skipped and why

Applied to files:

  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md
  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
📚 Learning: 2026-02-17T12:31:17.076Z
Learnt from: CR
Repo: dallay/corvus PR: 0
File: clients/agent-runtime/AGENTS.md:0-0
Timestamp: 2026-02-17T12:31:17.076Z
Learning: Applies to clients/agent-runtime/src/{security,gateway,tools,config}/**/*.rs : Do not silently weaken security policy or access constraints; keep default behavior secure-by-default with deny-by-default where applicable

Applied to files:

  • openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/verify-report.md
🪛 LanguageTool
openspec/specs/channel-image-ingestion/spec.md

[style] ~179-~179: This phrase is redundant (‘G’ stands for ‘graphic’). Use simply “PNG”.
Context: ...app"]` When a WhatsApp user sends a PNG image with no caption Then the channel em...

(ACRONYM_TAUTOLOGY)

openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/specs/channel-image-ingestion/spec.md

[style] ~179-~179: This phrase is redundant (‘G’ stands for ‘graphic’). Use simply “PNG”.
Context: ...app"]` When a WhatsApp user sends a PNG image with no caption Then the channel em...

(ACRONYM_TAUTOLOGY)

🔇 Additional comments (6)
clients/agent-runtime/src/config/schema.rs (2)

274-275: Allowlist expansion and runtime validation are consistent.

The "discord" MVP addition and the validation error update stay aligned with fail-closed channel gating.

Also applies to: 3155-3158


6035-6044: Good test coverage for the new allowlist behavior.

You added both a positive Discord case and a negative unsupported-channel case, which guards this contract well.

Also applies to: 6164-6194

clients/agent-runtime/src/channels/discord.rs (4)

106-111: UUID nonce addresses temp-path collision.

The per-stage UUID nonce ensures concurrent downloads of identical images don't overwrite each other. This properly addresses the previously raised concern about hash-only paths.


478-483: mention_only bypass for attachment-only messages fixed.

The guard now correctly enforces the mention check regardless of whether content is empty, closing the access-control gap for image-only uploads. The regression tests at lines 1177-1205 confirm the expected behavior.


571-608: parse_image_attachments is well-structured.

The function correctly:

  • Filters by image/* content-type prefix
  • Validates against the allowlist via AllowedImageMime::from_mime_str
  • Skips attachments with empty URLs
  • Extracts optional metadata (filename, size)

This matches the patterns used by Telegram and WhatsApp channels.


984-1205: Good test coverage for image attachment parsing and mention_only regression.

Tests cover:

  • Single/multiple image attachments
  • Non-image and unsupported MIME filtering
  • Missing/empty fields edge cases
  • Text+image and image-only message composition
  • Regression tests for mention_only with attachment-only messages

Comment thread clients/agent-runtime/src/channels/discord.rs
Comment thread clients/agent-runtime/src/channels/discord.rs Outdated
Comment thread openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/design.md Outdated
Comment thread openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md Outdated
Comment thread openspec/changes/archive/2026-03-26-channel-image-ingestion-strategy/tasks.md Outdated
- Sanitize CDN URLs in Discord error logs to prevent token leaks
- Capture and log stream read errors instead of discarding with |_|
- Loosen REQ-5: non-MVP channels warn at config time, fail-closed at runtime
- Update validate_multimodal_config to warn instead of reject non-MVP channels
- Fix design.md observability section: events fire at pre-dispatch and staging
- Update Discord status to Implemented/MVP in overview and contract sections
- Fix tasks.md: header reflects issues are tracked, REQ-1 mapping includes Discord
- Remove stale spec-only claims from verify-report, add runtime validation details
…ategy' into feat/channel-image-ingestion-strategy
…266)

- Add stream_validate_and_stage() to media.rs for shared post-download flow
- Refactor discord.rs and whatsapp.rs to use shared helper (~120 lines deduped)
- Add 6 tests for the shared helper (status, size, mime, staging, cleanup)
- WhatsApp now also uses UUID nonce in staging filenames (consistency)
- Add http as dev-dependency for test mocking
@sonarqubecloud
Copy link
Copy Markdown

@yacosta738 yacosta738 merged commit 091168a into main Mar 26, 2026
13 checks passed
@yacosta738 yacosta738 deleted the feat/channel-image-ingestion-strategy branch March 26, 2026 19:28
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.

Define channel ingestion strategy for multimodal image input

1 participant