Repository navigation
fix(workspace): history never carries a turn's cost (#3063) - #3075
Conversation
GET /client-portal/agents/{name}/history serialised `cost` on every message to
every principal, a portal token (an external Workspace client) included. The
Workspace contract excludes costs for external clients, and every other
projection already honours it. The history door was the one that leaked.
- PortalHistoryMessage no longer declares `cost`, so the route's response_model
strips it. The row still stores it.
- Dropped for EVERY principal, not split by principal: nothing reads a history
message's cost. replyFromHistory copied it into a reply object whose `cost`
no caller used, so that dead copy goes too.
- Execution ids are kept on purpose. in_flight_execution_id and
last_turn_outcome.execution_id are the client's OWN turn handles: the 202
dispatch response issues the same id, and the client streams, cancels and
reattaches by it. Removing them would break reattach-after-reload and the
failed-turn verdict (#2320).
Test: test_3063 runs the real route against a real sqlite row written with a
cost of 0.4242, for a portal token and a platform principal, plus the reply
poll's narrow read. It was red on dev ("cost reached the wire … 0.4242") and is
green now. It has a positive control that the costed row is in the response.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/review — #3075The history projection is right, but two other routes still send a turn's cost to a portal token. No critical findings; those two are informational. Scope: clean. Execution coverage
Fix mutation: on the merge-base, the new route test is red ( Checked and clean:
Informational (the same class, on sibling paths)[I1] Sync chat still returns cost to portal tokens (confidence 9/10)
[I2] The live stream relays the agent's raw result event, including
Both are the principal's own turns, so the severity is the same as the issue's (a contract breach, not a cross-tenant leak). Either widen this PR (I1 is trivial) or file a follow-up for I1 and I2 before |
|
@dolho validated: the history fix is correct. Reverting the model change turns all 4 new tests red, and Tier 1 CI is green. One change needed before merge:
Non-blocking: keeping |
#3063) POST /client-portal/agents/{name}/chat, the streaming fallback, still declared PortalChatResponse.cost, so a portal token received the turn's cost. Drop the field; the service keeps returning it for internal accounting and response_model strips it. Route test for both principals (positive control on response/message_id) plus a model-level pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@obasilakis, the sibling leak is fixed in fea7e36.
Still open: the live SSE stream relays Claude Code's final 🤖 Generated with Claude Code |
Summary
GET /api/enterprise/client-portal/agents/{name}/historysentcoston every message to every principal, including a portal token (an external client), against the Workspace contract. It is now projected out:PortalHistoryMessageno longer declares it, so the route'sresponse_modelstrips it. The DB row is unchanged.replyFromHistorycopied it into a reply object whosecostno caller used, so that dead copy is removed as well. (AC 2: "otherwise drop them for everyone".)Decision: execution ids stay (deviation from the AC's "no
execution_idanywhere")in_flight_execution_idandlast_turn_outcome.execution_idare the client's own turn handles, not internal data about anyone else:PortalTurnStarted.execution_id) already issues the same id, and the client streams and cancels by it;in_flight_execution_idis how a reload reattaches to a running turn (ent#286);last_turn_outcome.execution_idis how the client matches a failure to the turn it is waiting on (bug(workspace): failed turns are invisible to the client — fast failures misreported as "lost track", backend error discarded, Retry suppressed where it is safe #2320).Removing them would break reattach and the failed-turn verdict. If the contract should forbid raw ids on the portal surface entirely, the fix is an opaque per-turn handle across dispatch, stream, cancel and history. That is a separate, larger change and can be a follow-up.
Changes
src/backend/client_portal/models.py:costremoved fromPortalHistoryMessage.src/frontend/src/components/portal/portalUtils.js: the deadcostcopy is removed fromreplyFromHistory, andportalReplyRateable.spec.jsis updated to match.tests/unit/test_3063_portal_history_no_cost.py(new).Test Plan
cost=0.4242, for a portal token and a platform principal, plus the reply poll's?limit=read. Ondevit fails withcost reached the wire … "cost": 0.4242; with the fix it passes. A positive control asserts the costed row is in the response.2694history window,ent534voice,2320failed-turn visibility,163delegate scope, andmodels_centralized.npm run test:unit: 180 files, 3723 tests.npm run buildOK.Fixes #3063
🤖 Generated with Claude Code