Repository navigation
Conversation
…after a reload (#3265) The composer uploaded a file on attach, then cleared its chip when the turn went out, and nothing about the attachment reached the user's row. The bubble showed only the typed text, so the person could not tell whether the file went with the message, and a reload could not show it. The 1:1 send now waits for in-flight uploads, puts the settled files on the message and sends their names. The server keeps a sent file only when that name is in the caller's own uploads to the agent (size and type from there), records a failed upload with its reason, and stores the list on the user row (new nullable `attachments` column, SQLite migration + Alembic 0090). History returns it, so a reload shows it. The bubble renders a thumbnail per image, a chip per other file and a failed chip; clicking downloads through the existing upload route. Thumbnails are fetched once per tab and only when in view, because that route is rate-limited per person. Fixes #3265 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Resolve by running |
|
Resolve by merging |
…3127) — mechanical, per the merge-train note on the PR #3255, #3145 and #3269 each added an 0090 revision off 0089_supersede_queue_flood_backlog, which is the #2068 two-heads fork once two of them land. The tables are disjoint (operator_queue vs chat_sessions), so this is a re-parent: 0090_chat_session_claude_id becomes 0091_chat_session_claude_id with down_revision 0090_platform_alert_subjects. The SQLite entry is keyed by name and needs nothing; only the two doc references to the id change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…) — mechanical, per the merge-train note on the PR `build` was red on roomEscalationAttachments.spec.js "does NOT clear them". The slice ran from `async function send()` to `submitUserText`, so it now read #3265's ordinary-send `clearAttachments()`, which runs only after the escalation branch has returned. Behaviour was right; the pin matched by accident. The slice now ends at `const reply = replyTo.value`, the first line of the ordinary-send tail. Moving a `clearAttachments()` into the escalation branch still turns it red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#3265) — mechanical, per the merge-train note on the PR #3255, #3145 and this PR each added an 0090 revision off 0089_supersede_queue_flood_backlog (the #2068 two-heads fork). The tables are disjoint (operator_queue, chat_sessions, enterprise_portal_messages), so this is a re-parent: 0090_portal_messages_attachments becomes 0092_portal_messages_attachments with down_revision 0091_chat_session_claude_id. The SQLite entry is keyed by name and needs nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train (2026-10-06): riding this train. Mechanical changes pushed to your branch:
Expected until #3145 lands: Follow-up for you (needs your call, not blocking this train): |
|
✅ Alembic head check clear — merging this PR into Previously flagged; resolved. Evaluated on the version line only — this PR also conflicts with Advisory — this check does not block merge. · head_sha: |
AndriiPasternak31
left a comment
There was a problem hiding this comment.
Thanks, the backend design is good. Attachments are stored as names only and checked against the sender's own inbox. The read route is roster-gated, and the path always comes from the caller's own inbox, so nobody can read another person's file by name. Both migration tracks are there.
CI: as vybe's note says, the three reds are only the 0092 → 0091 parent. #3255 is in now, so this goes green once #3145 lands and dev is merged. Locally, re-pointing 0092 at 0089 turns all 7 single-head tests green, so nothing else is hiding behind it.
Two regressions this PR introduces. Please fix them before merge:
-
Double send while uploads finish. The ordinary send() now clears the input and does
await attachmentsSettled()with no guard (PortalConversation.vue ~2513-2531).sendingis only set later, in deliver(). While a big file uploads, the text disappears and no bubble shows. A second Enter with new text gets past the guard at :2459, and two turns run at once. The escalation branch already guards this exact window withescalatingNow(the #2794 note at :2447). Please do the same here, and ideally show the bubble as pending before waiting. -
More than 20 files means the message can't be sent.
PortalChatRequest.attachmentsismax_length=20, but the composer only caps each drop (usePortalFileDrop.js:229). Entries pile up across drops and pastes, so 2×11 files gives a 422, and Retry sends the same list again. Before this PR, the same message sent fine. Cap the total in the composer, and on the server keep the first 20 instead of rejecting the turn. Same forerror(max 300): Pydantic rejects it before the[:300]in resolve_turn_attachments can run.
Non-blocking (in line with vybe's follow-up):
3. An inbox read failure becomes "not in your uploads" for good. _read_inbox returns [] when Docker can't be read, so a file that did upload is stored as failed, and the retry dedupe keeps that row. Telling "couldn't read" apart from "not there" would fix it. If it's cheap, do it here.
4. Two hops have no test. With the kwarg removed at service.py:4051 (start_portal_turn → portal_chat) or service.py:3029 (portal_chat → _persist_user_turn), or with the sync /chat attachments removed at router.py:1293, all 16 tests still pass. I checked each one. One test from the stream route down to the stored row, plus one sync-route test, would cover them.
Smaller:
- Each thumbnail goes through the download route, so it spends the 20/min and 100/h file budget and writes a portal_upload_download audit row. Reloading an image-heavy thread can block Files-tab downloads, and the audit log fills with downloads nobody clicked.
- A refused thumbnail turns the fixed 160×112 box into a small chip, which shifts the layout.
- A re-uploaded file with the same name changes what older messages show.
The frontend unit suite (274 files, ratchets included) and check:tokens pass locally.
* feat(pull): /chat on the durable queue for pull pilots (#3127) On a pull-pilot agent, POST /api/agents/{name}/chat (MCP chat_with_agent, the connector tool and the trinity CLI) is admitted onto the durable queue and claimed by a worker. Every interactive trigger is now pull-owned on a pilot. Memory on a pilot is one Claude conversation per chat_sessions row, i.e. per (agent, user), resumed by id through run_resumable_turn (ResumeLock, persist_session, cold retry on resume-not-found). The id is cached on chat_sessions.cached_claude_session_id (SQLite migration + Alembic 0084) and joins the session reaper's keep set. The post-turn work /chat did inline runs in the caller after the terminal: assistant message, collaboration close, response shape, idempotency complete, timeout receipt. GET /chat/history serves the caller's session from the database on a pilot; DELETE /chat/history also clears the cached ids. execute_task carries chain_depth so a cold-retry row keeps its depth. Non-pilot agents are unchanged. Fixes #3127 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pull): /chat reset on a pilot closes the sessions /chat used (#3127) DELETE /chat/history forgot the cached Claude ids but left the sessions active, so GET /chat/history still showed the old conversation. The sessions that carry a cached id are now closed with it; sessions /chat never used stay open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(security): /cso --diff report for #3127 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(pull): renumber the chat_sessions migration to 0086 after dev's 0085 (#3127) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(pull): renumber the chat_sessions migration to 0089 after dev's 0088 (#3127) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pull): address review on pulled /chat (#3127) - Skill gate runs once: the pulled turn passes gate_checked=True, and the pilot admission branch audits a self-approved gate like the push branch. - Agent-to-agent /chat (trigger agent) on a pilot: run_resumable_turn opts into the claim phase (caller_waiting=True), the claim orders session:-keyed rows with interactive turns, and the collaboration activity rides the queue payload so the pull sink closes it. - Resume lock waits one turn's lock TTL; lock-busy closes the activity and emits the terminal event only on a CAS win, and answers 429 capacity. - Full backlog returns FAILED/CAPACITY and maps to 429 capacity. - Docs: chat_with_agent description, execution.md, three feature flows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(security): CSO report names the 0090 revision (#3127) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * merge-train: re-parent the chat_sessions revision onto #3255's 0090 (#3127) — mechanical, per the merge-train note on the PR #3255, #3145 and #3269 each added an 0090 revision off 0089_supersede_queue_flood_backlog, which is the #2068 two-heads fork once two of them land. The tables are disjoint (operator_queue vs chat_sessions), so this is a re-parent: 0090_chat_session_claude_id becomes 0091_chat_session_claude_id with down_revision 0090_platform_alert_subjects. The SQLite entry is keyed by name and needs nothing; only the two doc references to the id change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(chat): pulled /chat keeps the default 30s resume-lock wait (#3127) A longer wait outlived the #106 no-session sweep: the waiting turn's admission row is running with no Claude session, so the sweep failed it at 60s and the enqueue CAS then refused it, answering 429 for a turn that never ran. Drop the lock_wait plumbing; a test pins the default wait under NO_SESSION_TIMEOUT_SECONDS. chat_with_agent's parallel description notes per-user sessions on a pilot; execution.md says the self-approval audit rides the row's setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.com>
|
merge-train (2026-10-06): ejected at the merge step. Rides the next train once fixed. This was the last member of train #3279; the other six landed. It was held back by @AndriiPasternak31's changes-requested review (18:27), which found two regressions that the train's validation missed:
The branch is in good shape otherwise. It is up to date with |
Summary
Before/after screenshots (light + dark, send → reply → reload): https://claude.ai/artifact/Wswqq7GVLBtjyiHcNaCTHs
How it works
portalMessageAttachments.js::sentAttachments), clears the composer, and sends their names asattachmentson/chat/streamand the sync/chatfallback. Retry resends them.resolve_turn_attachmentsreads the sender's own inbox once. It keeps a sent file only when that filename is there, with size and type taken from the listing, so a request can't put a file it never uploaded on its message.attachments TEXTcolumn onenterprise_portal_messages, on both tracks (SQLiteportal_messages_attachments+ Alembic0090_portal_messages_attachments), plusschema.py/tables.py. Existing rows stayNULL. History returns it onPortalHistoryMessage.attachments.PortalMessageAttachments.vue.Changes
client_portal/{models,service,router,db}.py,db/{schema,tables,migrations}.py,migrations/versions/0090_portal_messages_attachments.pyPortalConversation.vue, newPortalMessageAttachments.vueandportalMessageAttachments.js(separate from the bug(workspace): attachments are dropped when a 1:1 chat is escalated into a room via @mention #2794portalAttachments.js),usePortalFileDrop.js,stores/clientPortal.jsdocs/memory/architecture/workspace.mdtest_ent473_chat_titles.pyandtest_ent551_voice_background_tasks.pypinned the_persist_user_turn(...)call text.portalModelChoice.spec.jshad a 400-character proximity window over the store signature; it's now 480.Test Plan
tests/unit/test_3265_portal_message_attachments.py: 16 passed, on real SQLite through the real history and streaming routes. Mutations all turn tests red:router.py,db.pyormodels.py;src/frontend/tests/unit/portalMessageAttachments.mount.spec.js: 13 passed (mounted). Covers:check:tokensOK.test_ent751_gate_callers.py::TestPaidA2A(3 tests), which fail the same way on unmodifieddev.check_alembic_heads: 1 head.check_alembic_parity origin/dev HEAD: PASS.Before merge
0090sits on today's head0089. fix(operator-queue): platform alerts are conditions — one pending row per subject (#3246, part 1) #3255 and feat(pull): /chat on the durable queue for pull pilots (#3127) #3145 also take0090; whichever lands second renumbers and re-parents.Out of scope / follow-ups
enterprise_room_messages) don't carry attachments yet.Fixes #3265
🤖 Generated with Claude Code