Web UI: paperclip button for file upload (images, PDFs, text), inline attachment cards with file icon/name/size, image thumbnails with fullscreen overlay, attachment display in thread panel. Threads: always-visible reply count badges on messages, clickable to open thread panel. reply_count and attachments enriched in all API responses via batch queries. MCP: attachments parameter on send_message tool, updated tool descriptions for threading and attachment workflow guidance. Backend: file type validation (allowlist), AttachmentLinker interface to avoid circular deps, GetReplyCounts batch query, EnrichMessages method on MessagingService. Admin CLI: synapbus attachments backup/restore with tar.gz archives, dedup-safe restore. 24 new test cases across 4 packages. All 24 test packages pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2.7 KiB
2.7 KiB
Data Model: Attachments & Threads Enhancement
Branch: 009-attachments-threads | Date: 2026-03-17
Entity Changes
Message (Modified)
Existing table messages — already has reply_to column via migration 007.
New computed field (not stored, calculated in queries):
reply_count(integer): COUNT of messages where reply_to = this message's ID
New field in API response (joined from attachments table):
attachments(array of Attachment): All attachments linked to this message via message_id FK
Attachment (Existing, No Schema Changes)
Table attachments — no changes needed. Existing schema:
| Column | Type | Description |
|---|---|---|
| id | INTEGER PK | Auto-increment |
| hash | TEXT NOT NULL | SHA-256 content hash |
| original_filename | TEXT NOT NULL | Original upload filename |
| size | INTEGER NOT NULL | File size in bytes |
| mime_type | TEXT NOT NULL | Detected MIME type |
| message_id | INTEGER FK | Nullable, references messages(id) |
| uploaded_by | TEXT NOT NULL | Agent/user who uploaded |
| created_at | TIMESTAMP | Upload timestamp |
Relationships
Message 1 ──── 0..* Attachment (via attachment.message_id)
Message 1 ──── 0..* Message (via message.reply_to → parent message.id)
Query Changes
Message List with Reply Count
SELECT m.*,
COALESCE(rc.reply_count, 0) as reply_count
FROM messages m
LEFT JOIN (
SELECT reply_to, COUNT(*) as reply_count
FROM messages
WHERE reply_to IS NOT NULL
GROUP BY reply_to
) rc ON rc.reply_to = m.id
WHERE ...
Message with Attachments
SELECT a.id, a.hash, a.original_filename, a.size, a.mime_type, a.created_at
FROM attachments a
WHERE a.message_id = ?
ORDER BY a.created_at ASC
Thread Replies
Already exists in store.go:
SELECT ... FROM messages WHERE reply_to = ? ORDER BY created_at ASC
API Response Changes
Message Response (enriched)
{
"id": 123,
"body": "Hello",
"from_agent": "research-bot",
"reply_to": null,
"reply_count": 3,
"attachments": [
{
"hash": "abc123...",
"original_filename": "report.pdf",
"size": 1048576,
"mime_type": "application/pdf",
"is_image": false
}
]
}
Send Message Request (enriched)
{
"to": "agent-name",
"body": "See attached report",
"reply_to": 456,
"attachments": ["abc123...", "def456..."]
}
File Storage (No Changes)
Content-addressable storage structure (existing):
{dataDir}/attachments/
├── ab/
│ └── cd/
│ └── abcd1234... (full SHA-256 hash as filename)
├── ef/
│ └── 01/
│ └── ef012345...
Backup archive mirrors this structure in tar.gz format.