-
Notifications
You must be signed in to change notification settings - Fork 12
Fix wrong session token total after a tab reload #5845
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,7 +139,22 @@ export default class Room { | |
| event.content.data = JSON.parse(event.content.data); | ||
| } | ||
| } | ||
| eventId = eventId ?? stateKey; // room state may not necessary have an event ID | ||
| // Backfilled events carry their latest edit as a server-side aggregation | ||
| // bundle, and getAggregatedReplacement substitutes that bundle's content | ||
| // for the event's own — so its data needs the same decoding. Without | ||
| // this, every rebuilt message whose usage/context arrived on an edit | ||
| // reads `data` as a wire string and silently loses those fields (the | ||
| // session token total visibly shrank after a reload because pre-reload | ||
| // turns' usage vanished this way). | ||
| let bundledReplace = (event.unsigned as any)?.['m.relations']?.[ | ||
| 'm.replace' | ||
| ]; | ||
| if ( | ||
| bundledReplace?.content?.data && | ||
| typeof bundledReplace.content.data === 'string' | ||
| ) { | ||
| bundledReplace.content.data = JSON.parse(bundledReplace.content.data); | ||
| } | ||
|
Comment on lines
+149
to
+157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] The decode is correct, but it establishes an invariant — "the event Background — why the ordering matters. Two subsystems apply the same server-side aggregation, in opposite orders. Verified against the real path. The ask. Either match the server's ordering — export Second, smaller thing on the same lines: neither parse is tolerant. Scope: the first point is a missing guard on an otherwise-correct fix (regression class, non-blocking); the second is pre-existing and widened by this hunk (non-blocking). Generated by Claude Code |
||
| if (!eventId) { | ||
| throw new Error( | ||
|
Copilot marked this conversation as resolved.
|
||
| `bug: event ID is undefined for event ${JSON.stringify( | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] This test pins the decode but not the contract its own comment claims, and nothing else in the host suite covers aggregation bundles — so the regression it guards can come back green. Non-blocking, but the coverage ask is the one I'd most like to see land in this PR. What the suite covers today. What this test does and doesn't reach. It asserts that The ask. Add a case one layer up: feed an original plus its bundled edit through the room resource and assert the rebuilt message's Two smaller things. Scope: test coverage — the room-resource test is a follow-up at worst, the placement and typing are trivial. Generated by Claude Code |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { module, test } from 'qunit'; | ||
|
|
||
| import Room from '@cardstack/host/lib/matrix-classes/room'; | ||
|
|
||
| import type { EventStatus } from 'matrix-js-sdk'; | ||
|
|
||
| module('Unit | matrix | room', function () { | ||
| test('addEvent decodes the wire-encoded data of the event and of its aggregation bundle', function (assert) { | ||
| // `content.data` is a JSON string on the wire. Backfilled events also | ||
| // carry their latest edit as a server-side aggregation bundle under | ||
| // unsigned['m.relations']['m.replace'], whose content the room resource | ||
| // substitutes for the event's own (getAggregatedReplacement) — so a | ||
| // string left undecoded there silently drops every field read off | ||
| // `data` on rebuild: token usage, context.agentId, and with it the | ||
| // agent gate for tool auto-execution. | ||
| let room = new Room('!room1:localhost'); | ||
| let usage = { promptTokens: 10, completionTokens: 2, costUsd: 0.01 }; | ||
| let event = { | ||
| event_id: '$original', | ||
| type: 'm.room.message', | ||
| room_id: '!room1:localhost', | ||
| origin_server_ts: 1, | ||
| status: null as EventStatus | null, | ||
| content: { | ||
| msgtype: 'app.boxel.message', | ||
| body: 'streaming…', | ||
| data: JSON.stringify({ context: { agentId: 'agent-1' } }), | ||
| }, | ||
| unsigned: { | ||
| age: 0, | ||
| 'm.relations': { | ||
| 'm.replace': { | ||
| event_id: '$edit', | ||
| type: 'm.room.message', | ||
| origin_server_ts: 2, | ||
| content: { | ||
| msgtype: 'app.boxel.message', | ||
| body: 'the whole answer', | ||
| data: JSON.stringify({ context: { agentId: 'agent-1' }, usage }), | ||
| 'm.relates_to': { | ||
| rel_type: 'm.replace', | ||
| event_id: '$original', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| room.addEvent(event as any); | ||
|
|
||
| let added = room.events[0] as any; | ||
| assert.deepEqual( | ||
| added.content.data.context, | ||
| { agentId: 'agent-1' }, | ||
| 'the event’s own data is decoded', | ||
| ); | ||
| let bundled = added.unsigned['m.relations']['m.replace']; | ||
| assert.deepEqual( | ||
| bundled.content.data.usage, | ||
| usage, | ||
| 'the aggregation bundle’s data is decoded too, so a rebuilt message keeps its usage', | ||
| ); | ||
| assert.strictEqual( | ||
| bundled.content.data.context.agentId, | ||
| 'agent-1', | ||
| 'and keeps its agent id', | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code 🤖] Two rewording asks on this comment — non-blocking, but both cost a future reader real time.
The parenthetical narrates the bug in past tense. "the session token total visibly shrank after a reload because pre-reload turns' usage vanished this way" describes the behaviour before this commit. A reader two years out has no anchor for when "shrank" was true and will read it as a live symptom, then go hunting for a bug that isn't there.
evergreen-commentsasks for the contract stated timelessly; the mechanism in the first three lines already carries the value, so the parenthetical only needs to name what depends on the decode.getAggregatedReplacementnames two different functions in this repo —packages/host/app/resources/room.ts(the one this comment means) andpackages/runtime-common/ai/history.ts(which orders the decode the other way round, per the comment below). The reader has to open one of them to see why the ordering matters here, so it's worth saying which.Something like:
Scope: prose only, no behaviour change.
Generated by Claude Code