Repository navigation
feat(workspace): the Workspace Inbox — Action, Unread, All, a reading pane (trinity-enterprise#610 PR A) - #3054
Conversation
…PR A0) The requirement (security.md §26.8), the endpoint note and the agent-page flow state the new contract before the code: 503 asks_unavailable on a queue or roster read fault, and a store that keeps the last good list.
…er [] (ent#610 PR A0) list_asks caught every error and returned [], and _on_roster turned an unreadable roster into "not on the roster" — both made the Workspace say "nothing needs you" during an outage (the #2915 class). The list now raises AsksUnavailable (strict roster mode in list only) and the route answers 503. A clean off-roster agent is still dropped; answer_ask keeps its uniform 404. test_ent428's unreadable-roster case pinned the old == [] and is reversed deliberately.
…so (ent#610 PR A0) fetchAsks treated every error as absence: it cleared the list and set asksAvailable=false, blanking every PortalAsks surface and zeroing the badge on a 5xx. Now only 404/403 are absence; any other failure sets asksFailed, keeps the list and leaves asksAvailable alone. asksLoaded latches on the first success and asksLoadedAt feeds a stale banner. workspaceAsks.spec.js's "clears the list rather than showing stale asks" pinned the old behaviour and is reversed deliberately (ent#253).
Keeping the last good list on a failed read made it session-scoped state, but signOut() never cleared it, so on a shared browser the next client's first failed read showed the previous client's asks and badge. signOut now resets asks, asksAvailable, asksLoaded, asksFailed and asksLoadedAt. A 404 also resets asksLoaded: a surface that no longer exists has no verdict. Found by /cso and /review. Mutations (drop the sign-out reset; drop the 404 reset) each turn one spec red.
…ent#610 PR A0) It passes on the pre-fix code too, since the old list_asks returned []. The flow doc, registry entry and docstring now say so. The flow doc also names the sign-out reset.
…(ent#610 PR A0) The #2915 entry's Files and Tests lines now name the asks router, the store, and both A0 test files. The A0 bullet also names the sign-out reset. Found by /validate-pr.
…box (ent#610 PR A) Written before the code (Rule #1): core-agent.md §5.40, workspace.md's Inbox section, the report-landing rule in observability.md, the frontend paragraph, the chat-state and report-publish endpoint rows, the new workspace-inbox flow and its index row, the sidebar/deliverables/agents-at-the-centre/operating-room flow deltas, and Inbox variants on journeys J05 and J11.
…(ent#610 PR A) count_unread_by_session stays the only unread function and gains a second arm over one _UNREAD_ARRIVALS fragment: a report addressed to the viewer, stamped to a session the viewer owns, after that chat's cursor or the account baseline. Plain equality on a lowercased bind, so arm (ii) reads idx_agent_reports_audience (EXPLAIN pinned). The #557 fixtures gain agent_reports. Mutations (join, addressee, >=, lower()) each go red.
…essee (ent#610 PR A) report_service.resolve_report_session keeps the publishing turn's chat only if the addressee owns it, else stamps the addressee's Main (ensure_main_session) and touches it with added=0, so a report-only Main is listed and Reset still reads it as untouched. The router calls it; _resolve_portal_session moves with it (Invariant #1). A report addressed to X during Y's turn no longer lands in Y's chat, where nobody's reader showed it. The completion writer stamps source=completion:done|failed from the same status that picks its wording — the Inbox's outcome pill reads that, never the body. ensure_main_session's caller list is corrected. The ent365 and ent457 tests that addressed the moved helper / the old writer signature are retargeted. Mutations (audience=None, owner check, touch, added, marker) red.
…s read (ent#610 PR A)
portalInbox.js holds every Inbox rule as a pure function: the bootstrap
landing target, the Action/Unread/All item builders (archived chats stay in
Unread, rooms excluded, All windowed to 30 days and bounded to 50 with the
total stated, ended asks for 7 days), item keys and the two counts. The
counts read the same sidebarThreads projection the sidebar sums, so the
pinned row and the agent rows cannot disagree; a seeded property test over
500 fleets pins came == totalUnread == sum of unreadByAgent == sum of the
Unread rows. Mutation: dropping archived chats from Unread goes red.
The store's fetchChatState({previews}) returns {state, previews} (the no-arg
call is unchanged) and markChatReadStrict rethrows so Mark all read can count
its failures. /workspace/inbox is a new route on the same shell;
WORKSPACE_ROOT is unchanged. Badge wording moves from replies to new because
deliverables now count.
…10 PR A) PortalAsks gains askIds (one ask, drawn in place once it ends) and testidPrefix over every id it emits, the static ones included; the default prefix keeps every existing id byte-identical. A mount test puts the Work tab's instance next to an inbox-ask one for the same asks and asserts the id sets are disjoint. PortalConversation carries data-message-id on its message wrappers and honours a one-shot ?anchor=m:<id>|d:<reportId> through useConversationAnchor: detach stick-to-bottom, scroll the target into view and outline it, or fall back to the bottom with a 'further up' notice; the anchor key is stripped either way. PortalDeliverables emits loaded and marks its cards with data-report-id. useStickToBottom gains detach(). Mutations (always-prefixed sync badge, a static root id, a no-op detach) each go red.
… (ent#610 PR A)
GET /chat-state?previews=true adds latest {kind, id, at, excerpt, outcome,
title?, display_hint?} and first_unread_message_id to each chat with
arrivals. The read lives in the new client_portal/chat_previews.py;
db.unread_arrivals_with_latest windows the SAME _UNREAD_ARRIVALS fragment the
count groups, and the counts it produces are passed into
service.get_chat_state(unread=...) so a badge and its preview come from one
statement. service.py gains only that optional parameter.
Previews attach only to roster agents (include_owned for platform users),
the 100 most recent. The excerpt is credential-sanitised, markdown-stripped
and capped at 160 chars; outcome comes only from the platform-written
completion:* source marker, never from the body. PortalChatArrival has no
cost and no execution id. Without the flag the payload is the old shape.
Tests: property n == count, latest iff n > 0, latest.at == max; no cost key
anywhere; off-roster; the marker through the real completion writer; a body
starting **Finished** without the marker gives no outcome. Mutations (roster
filter, unread passthrough, cost, latest order, body parsing, first-unread
order, redaction) each go red.
…g pane (ent#610 PR A) PortalInbox is the container: header with a ghost Mark all read (strict per-chat reads under allSettled, the failed count named), Action/Unread/All tabs with counts, list plus pane, and a phone collapse with Back and Esc that returns focus to the row. Every tab has its own honest state: a skeleton until roster, threads and asks have loaded, LoadFailed with no data, a stale banner over data, and empty copy only after a successful read. Tab and selection live in ?tab=&item=. PortalInboxList rows are buttons with a stated total; a read chat and an ended ask stay in place until the selection changes. PortalInboxPane renders an ask through PortalAsks (inbox-ask ids), or a chat from its first unread message within a 50-message window plus its deliverables through ReportRenderer with the ReportSummary fallback; a failed deliverables read is LoadFailed, not empty (fetchSessionDeliverablesStrict). No cost or execution id is rendered. Desktop auto-selects the first row without marking it read. The store gains asksAbsent so an instance without asks does not skeleton forever. Open canvas is not built: the shell cannot cheaply know.
…0 PR A) Portal.vue gains the Inbox stage branch after the rooms and before the conversation, rendered only on a ready stage, so a roster error or an empty roster still shows its own copy (the ent#253 class). bootstrap() reads the landing target before its first await and awaits the replace inside the try, so bare /workspace never flashes agents[0]'s chat. On the Inbox route the active agent comes from the selected item and activeAgentName is never written, so selecting does not mint a Main. threadsLoaded latches on the first good session read; previews are fetched only on the Inbox route; the sidebar filter is now sidebarThreadsOf, the one projection both counts read. The sidebar's head badges move to a pinned PortalInboxRow (brand links to /workspace/inbox), keeping sidebar-ask-count and adding sidebar-unread-count, white on a 700 ground. Four source pins changed on purpose, each with its reason in the spec. e2e: a new @smoke inbox spec; workspace-rail-reserved opens /workspace?agent=<first agent>; the contrast ratchet measures /workspace?new=1 against its frozen baseline and holds /workspace/inbox at zero.
…s loud on a roster outage (ent#610 PR A) Two route cases the build left unpinned: GET /chat-state?previews=true must pass the caller's own principal kind to the roster read (a hard-coded kind went green before), and an unreadable roster must be a 5xx, never a 200 with the previews silently dropped. Each mutation (hard-coded kind, a swallowing roster read) goes red.
The docs commit was written ahead of the code. Corrections: the landing replace is the first await; selection lives in ?item= (auto-select on desktop is never a read); the pinned row is PortalInboxRow; anchors run through useConversationAnchor; the pane's deliverables read is strict; excerpts are credential-sanitised and a deliverable's excerpt is its title; a chat without a preview omits the keys; a roster outage fails the previews read; Open canvas is deferred. The flow's Files and Tests tables now list every file.
…t#610 PR A) On All a read chat stays listed, so holdSelected returned the live row and the next refresh rebuilt it without a preview: the pane fell back to the last five messages under the reader and Open in chat lost its m: anchor. While the selection is unchanged the live row now carries the snapshot it was opened with (first unread message id and latest); another selection releases it. Mutation (return the live list unmerged) goes red in the pure and the mount spec.
…t#610 PR A) refreshThreads swallowed a failed chat-state read, so on the Inbox (where the previews ride that read, and a roster outage deliberately fails it) the list kept stale counts and previews with no signal. A previewsFailed flag now joins the thread verdict the Inbox renders: before any good read it is LoadFailed and threadsLoaded does not latch; after one it is the stale banner, cleared by the next good read. Reset on sign-out. Mutation (drop the flag from the verdict) goes red.
…e's Main (ent#610 PR A)
/cso finding. POST /agents/{name}/reports is gated by AuthorizedAgent, so
every human the agent is shared with can publish as it. With the D4 stamp,
one sharer could address a report to another person on the roster and mint
and touch THAT person's Main: a badge, an excerpt and a deliverable card in
their Inbox that the agent never produced.
resolve_report_session gains a required allow_main keyword (no default, so
every call site states the gate); the route passes
current_user.agent_name == name. A human publish keeps the addressee-owned
in-flight chat and otherwise stays NULL, as before #610. Mutations: forcing
allow_main=True in the route, and dropping the gate in the service, each go
red.
… (ent#610 PR A) /review finding. resolve_report_session minted AND touched the addressee's Main before the report insert; the touch sets last_message_at, which is what lists a report-only Main in the sidebar, so a failed insert left an empty Main listed. Resolving now only mints (the id is needed for the column); the route calls the new report_service.touch_report_session (added=0, fail-soft with a WARNING) after create_report returns. Mutation: moving the touch back before the insert goes red.
…d selection is held (ent#610 PR A) Back and Esc focused the row on a tick before the navigation had shown the list column again, so in a real browser focus() hit a display:none element and landed on the body; jsdom ignores the hidden class, so the mount spec passed. Back now awaits the query replace and a tick, then focuses the row, the row now in its place when the read chat has left Unread, else the list column (tabindex=-1). A selection restored from the URL (reload, deep link) is now held exactly as a clicked one, so an answered ask keeps its row in place until the selection changes (D8). Tests record at each focus() call whether the target sat under a hidden column. Mutations: an un-awaited replace, and no URL hold, each go red. Verified in Chromium at 375 in both themes.
…nt#610 PR A) At 375px Back plus Reply in chat and Open in chat left the chat title about four characters. The header now wraps: the title keeps at least 12rem and the two actions move to their own line; from sm up they stay on one line. Measured in Chromium, both themes: title 261px at 375, one line at 1280.
…ts report row (ent#610 PR A)
fetchAgentReport returns the whole report ({id, title, payload, row_meta});
the pane handed that to ReportRenderer, so ReportSummary dumped Id / Agent
name / Report type / Title instead of the content. Found on a live stack.
The pane now unwraps full?.payload exactly as PortalDeliverables does. A
mount test asserts the renderer receives the payload; reverting the unwrap
goes red.
…#610 PR A) The list states its total on a line above the rows, and that line appeared only with the rows, so the list moved ~36px down when the reads settled (measured live by the e2e arm). The line's box is now reserved in the loading, failed and empty states too. A mount test pins the reserve in each state; dropping it goes red. The e2e arm now skips without a roster agent, as arm 3 does: with no roster the Inbox never renders (D9), so there is no list to measure. The payload test from the previous commit is in this spec file too.
# Conflicts: # tests/registry.json
…ent#610 PR A) /validate-pr §3.3: the new flow lacked User Story, Entry Points, the Frontend/Backend Layer split, Side Effects, Error Handling, Security Considerations, a Testing status and Related Flows. Content is reorganised under those headings with file:line anchors; the error table and the security notes (roster scoping, no cost, the agent-only Main stamp, URL params) state what the code does.
…gn-off) The chat and the room render the shell's PortalThemeSwitch through their header's `header-end` slot; the Inbox header (new in PR A) had no such slot, so the Workspace's landing was the one stage with no theme control. The Inbox exposes the same seam and the shell fills it. The theme-switch placement guard now counts three fills (conversation, room, Inbox). Live: the switch renders in the Inbox header and choosing Dark sets `.dark` on <html>.
|
✅ Nightly unit-suite clean when this PR is merged into |
Brings in 58 commits from dev. Eight conflicts resolved:
- client_portal/asks/{service,router}.py — #3059's paged read (limit/cursor,
X-Total-Count, SQL-side roster set) combined with A0's fail-loud contract:
the roster check stays strict, and an unreadable queue or roster raises
AsksUnavailable -> 503 asks_unavailable instead of dev's empty page (which
the client would render as "nothing is waiting"). Mutation-checked: dev's
`return AsksPage()` turns 4 A0 tests red.
- PortalAsks.vue — the markdown body (sign-off) plus #3028's QueueProposal.
- PortalConversation.vue — useStickToBottom's `enabled` option (7c1c6dc)
plus the Inbox anchor's `detach`.
- Portal.vue — #3060's railSizedOpen, with the Inbox placeholder rail counted
as visible.
- api-endpoints.md / operating-room.md / tests/registry.json — both sides kept;
the asks row now states the 503.
Test updates the merge requires:
- test_ent610_asks_unavailable.py faults all three queue reads #3059 split
the read into (the first runs before any early return).
- test_3064_portal_unread_index.py builds agent_reports too: ent#610 D3's
unread count reads it, so the fresh track could not run the real query.
All 4 pass, including its negative control.
Verified: backend unit 19074 passed / 42 failed — 40 are the known
order-dependent set (ent477 x2, ent666, retention_floor; files identical to
dev, 119/119 in isolation) and the 2 test_3064 are fixed above; frontend 4090
passed + build; alembic single head; live stack: every sign-off behaviour
re-probed, a reply's quote confirmed in the agent's prompt.
… in the card's ink (ent#610 PR A, sign-off) It was an inline button followed by the inline "Got it", so the two ran together on one line; and it was action-primary-600 with no dark half on the amber ask card — low contrast, fighting the card. It is now a block box as wide as its words (`flex w-fit`), in the card's own ink, underlined, with a trailing arrow; the focus ring keeps the token. Live, both themes: link bottom 274px, "Got it" top 282px, no overlap; ink gray-100 dark / gray-900 light. One component, so every surface (chat, Work tab, Inbox) gets it.
|
Resolve by merging |
…ility-chip guard (ent#610 PR A) The row-layout assertion scrubbed PortalSidebar.vue's comments with one `replace(/<!--[\s\S]*?-->/g, '')`, which leaves a `<!--` assembled from the halves of two removed spans. Use the index walk portalComposerAlignment.spec.js already uses: no residue, and it clears CodeQL alert #379 (js/incomplete-multi-character-sanitization) on this PR.
…left open doesn't jump on load (Abilityai#3060) Abilityai#2711 reserves the rail column while the Workspace stage loads, so the conversation lands on the footprint it keeps. But the column's width comes from useColumnResize, which was told the rail is open only when it is VISIBLE, and the rail is never visible while the stage loads. A rail the person left open was reserved at the 48px collapsed width and then jumped to its open width (384px by default) the moment the stage was ready, moving the conversation's right edge 336px in one frame. That is the "collapses and re-opens" in Abilityai#3060. - portalRail.railSizedOpen({open, visible, reserved}) is a pure rule: size the column open when the persisted state is open AND the rail is visible OR its column is reserved. It reads booleans strictly. - Portal.vue feeds it to useColumnResize in place of `open && visible`. enforceFit only runs from onMounted, so the auto-collapse on a narrow viewport now decides during the load too, before the rail lands. - The collapsed default is unchanged (48px reserved, 48px landed). An unfulfilled reservation is still given back in one frame. Measured (1440x900, /workspace?agent=… and bare /workspace, rail persisted open): before, the column went 48 → 384px and the stage's right edge 1377 → 1041px. After, it is 384px from the first reserved frame, with the right edge fixed at 1041px. Tests: - The railSizedOpen unit cases were red before the fix. - A new e2e arm persists the rail open and asserts the column width and the stage edge are each a single value across the load (relative geometry only). It fails with the fix reverted ("48 → 384px") and passes with it; the other eight Abilityai#2711 arms still pass. The Inbox skeleton and the late tab badges in Abilityai#3060 live on the Inbox route that Abilityai#3054 adds and are not on dev. They are handed to that PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ityai#3059) GET /api/enterprise/client-portal/asks stopped at 200 rows and returned no total. A viewer with more than 200 asks silently lost the oldest ended ones; past 200 pending, pending ones went too. Nothing on screen said the list was cut. Non-breaking: the body stays a list of WorkspaceAsk. Paging is opt-in: `limit` (1–200, default 200) plus an opaque `cursor`. Every response carries X-Total-Count, and X-Next-Cursor while more remain. A bad cursor is a named 422 invalid_cursor. The total is honest because every visibility filter now runs in SQL before the limit. The visible kinds (a new `types` filter) and the read-time roster re-check used to run in Python on the result, so a page of 200 rows could render fewer, and no SQL count could be the viewer's real total. The roster predicate itself is unchanged: `_on_roster` is asked once per agent the viewer's asks span, fails closed, and its answer becomes the SQL access set. db/operator_queue.py: - the filter conditions move into one `_list_conditions` helper shared by list_items and the new count_items / list_item_agent_names, so a page, its total and its agents can't drift apart; - `id` is the last sort key, so rows that tie page without overlap or skip. Pending asks still sort first, so none is dropped while ended ones are shown. Rule Abilityai#1: requirements (security.md OPS-001 surfaces) and a new api-endpoints row are updated. Tests: test_3059_asks_paging covers: - 230 pending asks: 200 + 30, every ask visited exactly once; - invisible kinds, other addressees and off-roster agents neither take a page slot nor inflate the total; - pending before ended on page 1; - tie-break stability with identical timestamps; - the roster asked once per agent; - list_asks keeps its list contract; - the route's headers, the 422 cursors and the limit bounds. It was red before the change (13 of 14). The operator-queue and asks suites (53 files, 1877 tests) pass. The Inbox "Show more" and the removal of Abilityai#3054's honest-cap footer belong to Abilityai#3054, which can now read X-Total-Count / X-Next-Cursor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and All don't slide (#3060) OverflowTabs gains an opt-in per-tab `badgeSlot`: the badge span is drawn from the first frame at a tabular-nums min-width, `invisible` until the count lands, in the visible row and the measuring mirror. The dropdown's badge/signal chain is untouched (#2794), and a tab without the field renders exactly what it did (pinned by class string). The Inbox sets it on Action and Unread through a shared INBOX_TAB_SHELL. Measured: Unread's left edge went 77 -> 106px when the counts landed; now 111 -> 111.
…ox's footprint (#3060) While the stage has no verdict, the Inbox route (and bare /workspace, which lands there) drew the conversation skeleton. PortalInboxSkeleton draws the Inbox frame instead: its header and theme switch, the same tab strip with the badge slots reserved, the list column at the width PortalInbox picks (inboxLayout over the same container width and rail allowance), and the pane block. PortalInbox's own loading arm uses the same rows and pane placeholder, so neither hand-off moves. useContainerWidth now reads the content width at attach instead of waiting for the observer's first report; the Inbox used to draw one frame as a 320px split list before stacking, on every mount under 720px. A mount spec compares the skeleton's frame classes with the Inbox's, and e2e arms sample every animation frame of a delayed load (container-relative, the #2711 method). Each arm goes red with its fix reverted.
dev carried PR A0's final commits (#3049: the 503 Retry-After, a 401 ends the session, a late read is dropped) that this branch predated. Resolved as the union: A0-final plus the Inbox's asksAbsent verdict, excludeIds/askIds/ testidPrefix props and the session-list sign-out reset. The A0 unit test takes dev's parametrized every-queue-read version.
|
merge-train: not on this train. It rides the next one once the two items below are settled. /validate-pr and /review found no criticals, the ratchets are exact, and the portal-capability invariant holds. Both items change behaviour, so they're your call.
Mechanical, to fold in at the same time:
|
Clears the 8 conflicts #3054 had against dev @ b09d454. Resolutions: - client_portal/service.py: ent#661's `turn_prefix` and PR A's reply-to `reply_prefix` both ride both arms — delta/history → turn context → canvas → manifest → reply quote → message. test_ent555 and dev's test_ent661 wiring pins updated to the union. - PortalAsks.vue: #3115's AskMarkdown is THE ask renderer (title inline, body block) — it replaces PR A's PortalMarkdown body; PR A's testid prefix is kept on both (`${prefix}-title-`/`-question-`), so the Inbox pane's copy never shares an address with the chat's. - PortalConversation.vue: PR A's chat-scoped strip stays (splitChatAsks + one closed row + pinnedAskIds). It is the ent#610 ruling #3115 stood in for: `pinnedAskIds` holds only asks seen PENDING, and dev's `pending-only` rides the mount. The auto-merge had dropped `agentAsks` (dev removed it; PR A still reads it) — restored. Emits: union (`reply-done` + `open-project`). - PortalSidebar.vue / Portal.vue: Inbox and Projects both — the emits, the sidebar events, the two stage branches (Inbox, then Projects + its unavailable state), both sets of route computeds. - Specs: portalThemeSwitch counts 5 header-end fills (+Projects); portalAgentPageUx keeps PR A's gate pin plus #3115's pending-only pin; portalAskSingleSource pins the one store read + `:ask-ids`; portalAskStripPendingOnly's wiring test mounts a chat with a pending and an ended ask and asserts pending-only + only the pending id (dropping `pending-only` turns it and the page-UX pin red); portalAsksTestidPrefix lists #3115's title ids. FE 4295/4295 + build + check:tokens; backend 1794 passed across 75 client_portal-adjacent files (seed 12345).
…e platform's note, never as its own words (ent#610 PR A, #3054 review item 1) The completion marker (`source=completion:done|failed`) takes the row out of `client_portal.db._TYPED`. The comment called that inert. It is not: the row stops being the resumed-turn cursor and is replayed into the next resumed turn. Decided (Andrii): keep the replay. The agent's live session never saw the background run, and this is how it learns the task finished and what it found. What changes: - `_context_lines` (the one renderer for both the cold replay and the resumed delta) tells a completion row as `[Background task report: …]`. Before, it read `You: **Finished** …`, under a header that said every line was spoken in a voice call. - `VOICE_DELTA_HEADER` now says only `(voice)` lines were spoken. - The "inert" comment in channel_completion_report.py and the workspace.md line now say what actually happens. Tests (test_ent610_inbox.py, real SQLite, driven by the real completion writer): - a completion row is never the resumed-turn cursor; a spoken row before it stays in the delta; - a resumed turn is told the task came back (done and failed); - the cold window carries the completion without spending a typed slot. Red first: the two rendering tests failed on the old `You:` line. The row inclusion already held. Mutations, each run once and restored from a scratch cp: - cursor subquery drops `_TYPED` → 3 red - `_TYPED` counts `completion:%` → 4 red - renderer branch removed → 3 red Neighbours: inbox, 2694 delta and window, ent457, ent358 and reply-to are 174 passed.
…(ent#610 PR A, #3054 review item 2) The server refuses a reply target it cannot prove, with a 422: "Remove the reply and send again." There was no way to do that: - `send()` clears the composer and emits `reply-done` before the send; - the failed message keeps its `replyTo`, and Retry re-sent the same id into the same 422; - the message's chip was `removable=false`. Now the sent message's chip is removable while that message is failed. Removing it drops `replyTo` (`dropReply`), and Retry sends the text as an ordinary turn. A delivered message's chip stays fixed. PortalReplyChip gains `placement` ('composer' | 'message'), so a removable chip on a message keeps the message look, not the composer tab. Red first: portalReplyRefused.mount.spec.js mounts the real PortalConversation with the real chip. The flow is 422, then remove the reply, then Retry, and the resend must succeed with `replyToMessageId: null`. It failed with "Cannot call trigger on an empty DOMWrapper" (no remove control). Mutations, each run once and restored from a scratch cp: - chip never removable → red - `dropReply` a no-op → red - chip always removable → red (the delivered case) portalReplyTo.spec.js is still green (7/7 across both files).
…y_hint nobody reads (ent#610 PR A, #3054 review mechanical a) `db.unread_arrivals_with_latest` selected `display_hint`, and `chat_previews._project` emitted `title` and `display_hint` on a deliverable's `latest`. No Inbox component reads either: - the list renders `latest.excerpt`, which for a deliverable already IS the sanitised title, plus a "Deliverable" badge; - the pane's `display_hint` comes from the deliverables read, not from `latest`. Plan §3 only listed them in the payload, and §3g names no use. So they are dropped: - `_project` stops emitting them; - `PortalChatArrival` loses both fields; - the SQL stops selecting `display_hint` (`r2.title` stays, since it is the excerpt's source). This also removes the second copy of the title that /cso round 3 had to redact. The excerpt is now the only copy and is still redacted, as the test pins. Red first: the two preview tests now assert the exact `latest` shape and that neither key is present. Both failed before the change. Mutation (re-emit `title`) → 2 red, restored from a scratch cp. test_ent610_inbox + test_models_centralized: 67 passed.
…er was its spec (ent#610 PR A, #3054 review mechanical b) Sign-off round 7 (7fe36ca) moved the availability chip under the agent's name. After that, the sidebar template no longer spent #2641's list-level reservation, so `portalUtils.js::reservesAvailabilitySlot` was called only by its own spec. Removed: - the helper; - its #2641 predicate block in portalSidebarDateFlushRight.spec.js. `availabilityChip`'s own states are covered in portalAvailabilityChip.spec.js. The spec's remaining source pins stay: where the chip renders, and #2580's date column. workspace-sidebar-ia.md now says the chip reserves nothing, and keeps the #2641 reasoning as history. portalSidebarDateFlushRight + sourceTextRatchet + portalAvailabilityChip: 33 passed. `grep reservesAvailabilitySlot src` finds no code reference.
…0 PR A, #3054 review mechanical c) In CI, every roster-gated test in workspace-inbox.spec.js self-skips, and that can't be fixed from inside this spec: - frontend-e2e.yml pins the baseline at zero user agents, which other @smoke specs and the contrast baseline depend on; - the CI admin has no email, so portal_auth 403s it and /my-agents is empty anyway (learnings.md, trinity#2559). Seeding a roster agent is a workflow change: an admin email plus an owned agent row that every other spec tolerates. The spec header now states this, and that a green CI run is not evidence for those arms. The PR body says the same. (The last CI run on 4b1499c reports 90 passed / 9 skipped / 1 flaky; the reporter does not name the skipped tests.) Comment-only change; no behaviour changes.
|
Thanks. Both items and the three mechanical ones are on the branch now (head 1. The completion marker changes the next resumed turn (
2. A refused reply has no way out (
Mechanical
Checks: backend targeted (ent610/2694/ent457/ent358/…) 323 passed on seeds 1/12345/99999 (py3.13); FE Also in the body: Round 5 (Work = the asks' home, the pinned chat ask strip) is superseded by the 09-30 ruling on trinity-enterprise#610; A2 #3101 carries that change. Ready for the next train. |
|
merge-train (#3143): on this train, merging first. /validate-pr came back READY with no criticals. Both items from the 09-30 train are settled: the completion-marker replay and the removable chip on a refused reply each fail a test when I revert them. Edited for you: the PR body's Non-blocking, for a follow-up:
|
Refs abilityai/trinity-enterprise#610
Base:
dev. #3049 (PR A0, asks honesty) has merged, so the diff is this PR's own changes only. The latestorigin/devmerge (ffee9d15b) resolved conflicts inclient_portal/asks/router.py,PortalAsks.vue,stores/clientPortal.js,workspaceAsks.spec.js,test_ent610_asks_unavailable.pyand three docs (architecture/api-endpoints.md,feature-flows/workspace-agent-page.md,requirements/security.md). This branch predated A0's final commits (the 503Retry-After, a 401 ending the session, a late read being dropped); the resolution keeps those and this PR'sasksAbsent,askIds/excludeIds/testidPrefixand the session-list sign-out reset.Scope of this PR: §3g L1–L5 (+A13, C10, F6 partial), plus Andrii's sign-off rounds 4–8 (below). With B1a #3065 underneath, an ask is answerable in the Inbox pane (
bdb2418e7). What's left of L6 (the rest of the asks UI, including the "Unconfirmed" chip), L7 (E1 context) and L8 (the E2/E3 brief) moves to a follow-up, PR A2, stacked on this one. L8 adds a DB column on both migration tracks plus a platform-prompt change, which is a separate review.Sign-off rounds 4–8 (commits
f890260a8..4db80224e, all red-first):PortalMarkdown; the pane header stops repeating the title; the list no longer stretches the page; a chat pins only its own asks.Round 5 (Work = the asks' home, the pinned chat ask strip) is superseded by the 09-30 ruling on abilityai/trinity-enterprise#610; A2 feat(workspace): Inbox asks UI + an ask's context (ent#610 PR A2: §3g L6 + L7) #3101 carries the change.
reply_to_message_idis resolved server-side byclient_portal/service.reply_context()(uniform 422) and quoted into the agent's prompt, on both resumed and cold prompts. Proven end to end by reading the agent's transcript inside the container.543f71d9b), and the ask card's "Open the conversation" line (4db80224e).What this is
The Workspace Inbox (PR A of four: A0 → A → B after #3028 → C for rooms). Bare
/workspacenow lands on/workspace/inbox, which has three tabs:There is also a reading pane and a pinned sidebar row with two counts. The design adds no new router, store, table or migration.
Backend
count_unread_by_sessiongains a deliverable arm: a report addressed to you and stamped to a chat you own. There is one_UNREAD_ARRIVALSfragment.GET /chat-state?previews=truegives each unread chat its latest arrival and its first unread message id. Both come from the same statement as the count (client_portal/chat_previews.py,db.unread_arrivals_with_latest).outcomecomes only from the platform-writtencompletion:*source marker.PortalChatArrivalhas no cost and no execution id (AC 7).client_portal/service.pygains only an optionalunread=parameter.added=0.Frontend
portalInbox.js, with a property test over 500 fleets: Σ Unread rows == pinned count == Σ agent-row unread ==totalUnread, over the samesidebarThreadsOfprojection.PortalInbox,PortalInboxList,PortalInboxPaneandPortalInboxRow.Portal.vuechanges:activeAgentName, so it mints no Main.PortalAsksgainsaskIdsandtestidPrefix, and the default ids are byte-identical.PortalConversationgetsdata-message-idand a one-shot?anchor=m:|d:, viauseConversationAnchoranduseStickToBottom.detach().AC scorecard
proposalproposalcomes in PR B, after #3028.costkey anywhere). The existing/agents/{name}/historyleak predates this PR and is a follow-up.A9: a platform owner is never the addressee of asks or deliverables (
include_owned=False), so their Action tab is empty by construction. Its empty state links to Operations.Evidence
v-html, and no selector injection through?anchor=.unread=passthrough,cost, latest order, body parsing, first-unread order, redaction, the agent-only gate, touch-before-insert.detach, the held snapshot,previewsFailed, focus-after-Back, deep-link hold, payload unwrap, total-line reserve, plus eight from the build.--skip-agent --skip-unit, run twice, the second at the final backend): build and import-smoke OK, boot and health OK, integration 70 passed / 13 skipped / 2 known-false-fail deselected.origin/dev4114d3d has the identical 40 failures (ent477, ent666, retention_floor — order pollution), so no new failures.test:unit3808/3808;build,check:tokensand both ratchets green. Measured per file against dev, no existing file grew. The new files carry gray classes only.getComputedStyle: ask count 5.18:1 and unread count 7.90:1 in both themes.--keep+ host Vite, seeded rows, no LLM agent):?agent=and?new=1are unchanged;chat-state?previews=truereturns nocostkey.agent-detail-request-dedupe, which fails identically on dev.Round 3 — §3g build reviewed and fixed (2026-09-29, head
d3202301f)§3g L1–L5 (+A13, C10, F6 partial) is built. L6, L7 and L8 moved to PR A2 (see Scope at the top). Their gates, B1a #3065 and #3028, have both merged.
Nine report-only reviewers ran in parallel on a reseeded live stack: 56 chats, 279 messages, and 24 pending asks, all filed through
ingest_asksochat_idis real. The reviewers were /review, /cso --diff, design review, plan-design-review (build vs §3g), desktop QA, 768/375 dark QA, benchmark, Codex GPT-6-Sol and the frontend gates. Triage: 31 findings; see the commits below.Round-1 items re-checked live, FIXED: A1–A13, B6, B7a/b, S3, S4, D-4. A12: poll CLS 0.19 → 0.000. A row read elsewhere stays in place as a ghost, and no other row moves.
Fixed in this round (red test first, mutation recorded in each commit):
ca002345dGET /executions) and plant a report card, now an unread Inbox arrival, in that client's chat.agent_publishnow gates both branches; a human publish places no card.9ca24c470439e2152cPortalRailPlaceholder) with the collapse control.036f21b1cinfovariant; phone subtitle.48d8e783fnextExpiryEntry); minutes floored; a vanished ask's ghost reads "No longer available"; exact count behind "99+"; the agent name keeps its width; stacked reading width; empty tab is one column; dark "Read" badge; OverflowTabs menu label; pane-Back / RO-disconnect / split-Back tests.2341f6c3elastSessions; tab title sumssidebarThreads(D13); dark primary button 600/700 (was 4.47:1 / ~2.9:1).d3202301ftitleis redacted like its excerpt.Verification this round
test:unit3974/3974,buildandcheck:tokensgreen. Both ratchets are exact per file vs dev;PortalSidebargray fell 43→35, and no file grew.test_ent610_inbox+test_ent365_report_audience86/86.--skip-agent;docker/base-imageuntouched):test_ent477_*,test_ent666_objective_joinandtest_retention_floor, files this PR doesn't touch. The same files pass 119/119 in isolation: order pollution, the same 40 as on pristine dev.--skip-unit: build +import mainOK, boot + health OK, integration 70 passed / 13 skipped / 2 known-false-fail deselected →pass.d3202301f: pending at the time of writing.workspace-inbox,contrast-ratchet), and/ui-sweep.workspace-inboxe2e in CI: the roster-gated tests self-skip there by construction.frontend-e2e.ymlpins zero user agents, and its admin has no email, so/my-agentsis empty (learnings.md, refactor(agent-detail): retire the chat-panel voice overlay — the Talk button opens voice in the Workspace #2559). Seeding one is a workflow change, so a green CI run is not evidence for those arms (spec header,ec5bdc00a).Open, needs a decision: a read row in All grows 43→86px when an arrival lands, pushing the rows below it 43px. Either reserve a line on read rows, or accept it (the arrival is the news). An insertion at the top at scrollTop 0 shifts 91px; accepted as the §3g.4 known risk, and scroll anchoring covers a scrolled list.
Deferred to PR A2: the "Unconfirmed" chip (L6 B5) and the rest of L6. Answer controls in the pane are live here (B1a +
bdb2418e7).Follow-ups
costleak), bug: Workspace unread count full-scans enterprise_portal_messages on every poll #3064 (message-arm SCAN index), abilityai/trinity-enterprise#722 (arrow keys, advance after Send), abilityai/trinity-enterprise#723 (default Workspace delivery).dangerin dark has the primary's old shape (white on red-500 ≈ 3.8:1).Open canvas in the pane: built (C10).Action badge colour: fixed (A3).🤖 Generated with Claude Code