feat(workspace): @mentioning an agent from a 1:1 starts a group discussion (ent#361) - #2148
Conversation
…ssion (ent#361) The AC Eugene made binding on the 12th — "@mentioning an agent from ANY existing chat (including a 1:1) creates a group discussion with them" — was the last piece of ent#361, and the issue has been sitting at status-in-dev without it. There was nothing to build on: `client_portal/` had zero mention handling, so a 1:1 could only ever stay a 1:1. Typing `@other` in a 1:1 now creates a room containing both agents, carries the message into it, and moves the user there. **Why this half is UI-side and the in-room half is not.** Membership inside a room is the engine's concern and agent replies flow through it, so `_join_mentioned_newcomers` lives there (Abilityai/trinity-enterprise#390). The 1:1 escalation is a different question — "should this conversation become a room" — and answering it in OSS would mean importing the private module. Routing it through the existing `POST /api/rooms` instead keeps `create_room`'s per-agent ACL as the single enforcement point rather than growing a second one that can drift from it. **An @name that is not a rostered agent stays plain text, and that is not an error.** Reporting "no such agent" would answer, for any string a user cares to type, whether an agent by that name exists on the instance. The engine already behaves this way; the composer now matches it. The pattern mirrors the engine's `_MENTION_RE` deliberately and is pinned by tests. If the two drift, a user gets a room built around a handle the engine then renders as plain text. Ordering matters and is not incidental: the room is created, then navigated to, then the message is posted. Posting first leaves the user in the old thread while the agents they summoned reply somewhere they cannot see; a failed post leaves them in a room they can retype into, which is recoverable. A failed escalation gives the typed text back rather than swallowing it. Gated on the same rooms capability as the picker (#2128) — without it there is nowhere to escalate to, so an @mention has to keep working as ordinary text. Related to Abilityai/trinity-enterprise#361 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
trinity-enterprise#390 was squash-merged, so the commit this pointer named (fade3cbb, the feature-branch head) is not reachable from enterprise `main` — `git compare` reports it diverged, ahead 1 / behind 1, and the branch has since been deleted. A pointer at an orphaned commit is worse than a stale one: a fresh `git submodule update` cannot fetch it at all, and CI stays green either way because both submodules are `update = none` and are never checked out there. Repointed at dcf0b52, the squash commit now on enterprise main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four conflicts, all additive — both sides added at the same point, so each resolves as a union rather than a choice: * `core-agent.md` — #2147 (ent#360) and this branch both wrote a `### 5.11`. #2147 merged first and keeps the number; ent#361 renumbers to **5.12**. No cross-references to either anchor exist elsewhere in docs/ or src/. * `portalUtils.js` — #2133's budget constants and ent#361's `mentionedAgents` are unrelated exports; both kept. * `PortalConversation.vue` — one import line, merged to name all three symbols. * `portalSidebarIA.spec.js` — two `describe` blocks whose conflict cut through both; ent#361's is closed and #2133's follows it. Verified: 282 frontend tests across 19 files pass, vite build clean, and an unfiltered marker sweep over the whole tree is empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vybe
left a comment
There was a problem hiding this comment.
Validated via /validate-pr. All 4 required checks, all 6 pytest shards and e2e green after the fixes below. No secrets, no new config var, no schema change.
I corrected the submodule pointer before merging — it would have shipped broken. The pin named fade3cbb, the pre-squash head of trinity-enterprise#390. Because #390 was squash-merged (and its branch deleted), that commit is not reachable from enterprise main — compare reports it diverged, ahead 1 / behind 1. A fresh git submodule update could not fetch it at all. Nothing in CI can catch this: both submodules are update = none, so CI never checks them out and stays green either way. Repointed at dcf0b52. I also confirmed dev's prior pin a0ad11c5 is a clean ancestor of enterprise main exactly one commit behind, so after this merge the pointer equals main's tip with nothing skipped.
Conflict resolution against the newly-merged dev — four conflicts, all additive, so each is a union rather than a choice:
core-agent.md— #2147 (ent#360) and this branch both wrote### 5.11. #2147 merged first and keeps it; ent#361 renumbered to §5.12. No cross-references to either anchor exist elsewhere indocs/orsrc/.portalUtils.js— #2133's budget constants and ent#361'smentionedAgentsare unrelated exports; both kept.PortalConversation.vue— one import line naming all three symbols.portalSidebarIA.spec.js— the conflict cut through twodescribeblocks; ent#361's is closed and #2133's follows.
Verified locally on the merge result before pushing: 282 frontend tests across 19 files, vite build clean, and an unfiltered conflict-marker sweep over the whole tree empty.
On the change itself: keeping the 1:1→room decision in OSS and routing it through the existing POST /api/rooms is the right layering — it keeps create_room's per-agent ACL as the single enforcement point instead of growing a second one, and avoids OSS importing the private module. The unresolvable-@name-stays-plain-text rule matches the engine's own behaviour and correctly avoids turning the composer into an existence oracle; mirroring _MENTION_RE with tests pinning it is the right guard against the two sides disagreeing. Create → navigate → post ordering is argued correctly: a failed post is recoverable, a created-but-unreachable room is not.
ent#361 is cross-tracker, so nothing auto-promotes — its enterprise half (#390) is already merged and I'll leave the issue's status to the release close.
No conflicts. Merged rather than left alone because this branch's CI last ran against a `dev` from before #2147, #2148, #2150 and #2151 landed — three of which touch the same portal files (`client_portal/service.py`, `PortalConversation.vue`, `Portal.vue`). A green run against the older base is not evidence the combination works, and it also leaves a stale cancelled pytest shard on the PR. Frontend suite on the merge result: 282 tests across 19 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Finishes ent#361 — the p1 AC added on the 12th: "@mentioning an agent from ANY existing chat (including a 1:1) creates a group discussion with them."
The issue has been sitting at
status-in-devwithout it. There was nothing to build on:client_portal/had zero mention handling, so a 1:1 could only ever stay a 1:1.Typing
@otherin a 1:1 now creates a room with both agents, carries the message into it, and moves you there.Two halves, deliberately in different layers
The in-room half — @mentioning someone not in a room adds them (AC #4) — is engine-side: abilityai/trinity-enterprise#390. Membership is the engine's concern and agent replies flow through it.
This half answers a different question — should this conversation become a room — and answering it in OSS would mean importing the private module. Routing it through the existing
POST /api/roomsinstead keepscreate_room's per-agent ACL as the single enforcement point rather than growing a second one that can drift from it.An unresolvable @name is plain text, not an error
Reporting "no such agent" would answer, for any string a user cares to type, whether an agent by that name exists on the instance. The rooms engine already behaves this way; the composer now matches it.
The pattern mirrors the engine's
_MENTION_REon purpose, and is pinned by tests. Drift there builds a room around a handle the engine then renders as plain text.Ordering is not incidental
Create the room → navigate → then post. Posting first leaves the user staring at the old thread while the agents they just summoned reply somewhere they cannot see. A failed post leaves them in a room they can retype into, which is recoverable; a created-but-unreachable room is not. A failed escalation gives the typed text back rather than swallowing it.
Worth flagging
AC #4's premise was wrong, and this is why ent#361 looked done. The issue says "today a chat can only become multi-agent by @mentioning someone mid-conversation; that path is fine and stays", and asks that it "still works".
resolve_mentionsmatched only names already in the room and documented the intent outright — "a mention can never reach outside". So the AC reads like a regression guard and is actually a feature request. Both halves are new work, not preserved behaviour.Verification
260 frontend tests (18 files) incl. 8 new for the resolver — roster-scoped resolution, the excluded current agent, dedupe, multiple agents in order, hyphen/digit names matching the engine, and the empty/missing-roster cases. Build clean. Enterprise side: 9 new tests, 294 passed vs 285 before (the 29 failures are pre-existing on
main, none in shared_sessions).Gated on the same rooms capability as the picker (#2128) — no rooms, no escalation,
@stays ordinary text.Submodule pin advanced to the enterprise commit; abilityai/trinity-enterprise#390 should merge first.
Related to abilityai/trinity-enterprise#361