Location
plugins/tps-meter.tsx:112,143,233,243
Problem
The in-flight message (currently streaming message) is tracked via two independent mechanisms that can diverge:
Mechanism 1 — Mutable variable (currentMsgId, line 112):
let currentMsgId = null;
// In onPart handler (line 112):
currentMsgId = part.messageID;
// Cleared in onMessage handler when message completes (line 143):
currentMsgId = null;
Mechanism 2 — Derived from message list (inflightId, line 233):
// In createMemo (line 228-234):
let inflightId = null;
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (!isAssistant(m)) continue;
if (!m.time?.completed) {
inflightId = m.id;
break;
}
}
The fallback on line 243:
const inflight = (inflightId && timers.get(inflightId)) || (currentMsgId && timers.get(currentMsgId)) || null;
Divergence Window
There is a transient window where the two sources disagree:
currentMsgId is set synchronously in onPart (immediately when a stream chunk arrives)
inflightId is derived from props.api.state.session.messages() — a reactive call that may reflect state slightly before or after the event that triggered bump()
If the reactive message list lags behind the event stream (e.g., a message.part.updated event fires but the message list still shows the previous message as in-flight), inflightId points to the wrong message. The fallback || picks whichever exists, masking the divergence silently.
Concrete Scenario
- Message A completes →
onMessage fires → currentMsgId cleared to null
- Message B starts streaming — first
message.part.updated fires → currentMsgId = B
bump() triggers createMemo re-evaluation
messages list still shows Message A (with time.completed) as the last assistant message — Message B hasn't appeared in the list yet
inflightId scanning loop finds all messages completed → inflightId = null
- Fallback kicks in:
currentMsgId = B → picks the right timer
This happens to work because the fallback exists, but the dual tracking means the code has two paths to the same answer, with no validation that they agree.
Why This Matters
If the fallback were ever removed or reordered, or if both mechanisms returned different but valid-looking results (e.g., currentMsgId points to a message that completed but wasn't cleared because the message.updated event fired before message.part.updated — see issue #27), the view would show wrong live TPS data.
Expected Behavior
Choose one source of truth. Since currentMsgId is derived from events and inflightId is derived from the reactive message list, either:
- Event-driven: Use only
currentMsgId, remove the message-list scanning
- Reactive-driven: Use only
inflightId, remove currentMsgId
The event-driven approach is more responsive. Remove the message-list scan for in-flight detection and trust the event-derived state.
Severity
Low — the fallback prevents visible bugs, but the dual-source architecture is fragile and masks future regressions.
Location
plugins/tps-meter.tsx:112,143,233,243Problem
The in-flight message (currently streaming message) is tracked via two independent mechanisms that can diverge:
Mechanism 1 — Mutable variable (
currentMsgId, line 112):Mechanism 2 — Derived from message list (
inflightId, line 233):The fallback on line 243:
Divergence Window
There is a transient window where the two sources disagree:
currentMsgIdis set synchronously inonPart(immediately when a stream chunk arrives)inflightIdis derived fromprops.api.state.session.messages()— a reactive call that may reflect state slightly before or after the event that triggeredbump()If the reactive message list lags behind the event stream (e.g., a
message.part.updatedevent fires but the message list still shows the previous message as in-flight),inflightIdpoints to the wrong message. The fallback||picks whichever exists, masking the divergence silently.Concrete Scenario
onMessagefires →currentMsgIdcleared tonullmessage.part.updatedfires →currentMsgId = Bbump()triggerscreateMemore-evaluationmessageslist still shows Message A (withtime.completed) as the last assistant message — Message B hasn't appeared in the list yetinflightIdscanning loop finds all messages completed →inflightId = nullcurrentMsgId = B→ picks the right timerThis happens to work because the fallback exists, but the dual tracking means the code has two paths to the same answer, with no validation that they agree.
Why This Matters
If the fallback were ever removed or reordered, or if both mechanisms returned different but valid-looking results (e.g.,
currentMsgIdpoints to a message that completed but wasn't cleared because themessage.updatedevent fired beforemessage.part.updated— see issue #27), the view would show wrong live TPS data.Expected Behavior
Choose one source of truth. Since
currentMsgIdis derived from events andinflightIdis derived from the reactive message list, either:currentMsgId, remove the message-list scanninginflightId, removecurrentMsgIdThe event-driven approach is more responsive. Remove the message-list scan for in-flight detection and trust the event-derived state.
Severity
Low — the fallback prevents visible bugs, but the dual-source architecture is fragile and masks future regressions.