diff --git a/scripts/src/hwlab-node-web-observe-analyzer-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-source.ts index 7199a8c0..04eb8ce7 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-source.ts @@ -1369,10 +1369,18 @@ function buildSessionInvariantFindings(control, manifest = {}) { const traceIds = arrayStrings(messageOrder.traceIds).slice(0, 12); const findingId = stringOrNull(detail.findingId) ?? "workbench-message-order-user-clustered-after-navigation"; const severity = stringOrNull(detail.severity) ?? "amber"; + const rootCause = "workbench-message-order-projection-drops-terminal-cards-after-navigation"; + const rootCauseStatus = "confirmed-from-controlled-refresh-dom;otel-read-model-comparison-required"; + const rootCauseConfidence = "medium"; + const nextAction = "Use OTel session_messages_read/session detail for the same canarySessionId and traceIds. If the read model is interleaved but DOM is clustered, fix Workbench renderer/hydration ordering; if the read model is already clustered, fix session message projection/read-model ordering."; findings.push({ id: findingId, severity, - summary: "controlled refresh/switch-back afterRound=" + (afterRound ?? "-") + " left consecutive user message cards without interleaved assistant/code-agent terminal cards" + (sentinelRange ? " (" + sentinelRange + ")" : ""), + summary: "message-order root cause visible: controlled refresh/switch-back afterRound=" + (afterRound ?? "-") + " left consecutive user message cards without interleaved assistant/code-agent terminal cards" + (sentinelRange ? " (" + sentinelRange + ")" : ""), + rootCause, + rootCauseStatus, + rootCauseConfidence, + nextAction, count: Math.max(1, consecutiveUserMessageCount), blocking: detail.blocking === true ? true : false, afterRound, @@ -1389,6 +1397,17 @@ function buildSessionInvariantFindings(control, manifest = {}) { stateDir: stringOrNull(manifest.stateDir), commandId: stringOrNull(row.commandId), commandTs: stringOrNull(row.ts), + evidence: { + afterRound, + consecutiveUserMessageCount, + sentinelRange, + sampleSeq: numberOrNull(detail.sampleSeq), + traceIds, + canarySessionId: stringOrNull(detail.canarySessionId), + routeSessionId: stringOrNull(detail.routeSessionId), + activeSessionId: stringOrNull(detail.activeSessionId), + valuesRedacted: true, + }, messageOrder: { sequence: Array.isArray(messageOrder.sequence) ? messageOrder.sequence.slice(-20) : [], clusters: Array.isArray(messageOrder.clusters) ? messageOrder.clusters.slice(0, 8) : [],