← Back to task

Commit 9af43bcf

commit 9af43bcfbc514eb404584a88466d7f8df151af3b
Author: Coder Agent <coder@agents.omni>
Date:   Thu Feb 19 08:36:44 2026

    Fix token accounting for semantic and knowledge context
    
    Task-Id: t-483

diff --git a/Omni/Agent/Context.hs b/Omni/Agent/Context.hs
index cc8cd894..e1033357 100755
--- a/Omni/Agent/Context.hs
+++ b/Omni/Agent/Context.hs
@@ -160,15 +160,22 @@ passiveSemanticSource scope =
       Nothing -> pure []
       Just chatId -> Memory.searchChatHistoryInChat chatId (spObservation params) (spMaxItems params)
 
-    let filtered =
+    let scoredMatches =
           [ (Memory.cheCreatedAt entry, Memory.cheRole entry, Memory.cheContent entry, score)
-            | (entry, score) <- results,
-              score >= spThreshold params
+            | (entry, score) <- results
           ]
-        totalTokens = sum [estimateTokensSimple (Memory.cheContent entry) | (entry, _) <- results]
 
-    pure
-      SemanticResult
+    pure (buildSemanticResult (spThreshold params) scoredMatches)
+
+-- | Build semantic context result from scored matches.
+--
+-- Token accounting is based only on matches that survive thresholding,
+-- so section token estimates reflect actual rendered semantic content.
+buildSemanticResult :: Float -> [(Time.UTCTime, Text, Text, Float)] -> SemanticResult
+buildSemanticResult threshold scoredMatches =
+  let filtered = filter (\(_, _, _, score) -> score >= threshold) scoredMatches
+      totalTokens = sum [estimateTokensSimple content | (_, _, content, _) <- filtered]
+   in SemanticResult
         { srMatches = filtered,
           srTotalTokens = totalTokens
         }
@@ -418,6 +425,15 @@ test =
           result <- runSource source params
           let contents = [content | (_, _, _, content) <- trMessages result]
           contents Test.@=? ["Group first", "Group second"],
+      Test.unit "buildSemanticResult counts only matches above threshold" <| do
+        let t = Time.UTCTime (Time.fromGregorian 2026 1 24) 0
+            scoredMatches =
+              [ (t, "user", "high relevance", 0.9),
+                (t, "assistant", "low relevance should be excluded", 0.4)
+              ]
+            result = buildSemanticResult 0.5 scoredMatches
+        length (srMatches result) Test.@=? 1
+        srTotalTokens result Test.@=? estimateTokensSimple "high relevance",
       Test.unit "passiveSemanticKnowledgeSource runs without error" <| do
         withTestDb <| \userId _chatId -> do
           let scope = OwnerOnly userId
diff --git a/Omni/Agent/Prompt/Hydrate.hs b/Omni/Agent/Prompt/Hydrate.hs
index 22c99f00..2c56da37 100644
--- a/Omni/Agent/Prompt/Hydrate.hs
+++ b/Omni/Agent/Prompt/Hydrate.hs
@@ -418,13 +418,14 @@ buildKnowledgeSection result _now =
           then ""
           else "\n(Note: " <> tshow (krSupersededCount result) <> " outdated memories were filtered out)"
       fullContent = formatted <> contradictionWarning <> supersededNote
+      contentTokens = estimateTokens fullContent
    in Section
         { secId = "passive_semantic_knowledge",
           secLabel = "## Passive semantic context (long-term memories)",
           secSource = SourceKnowledge,
           secContent = fullContent,
-          secTokens = krTotalTokens result,
-          secMinTokens = Just (krTotalTokens result `div` 2),
+          secTokens = contentTokens,
+          secMinTokens = Just (contentTokens `div` 2),
           secPriority = Medium,
           secRelevance = Just 0.7, -- Knowledge is generally relevant
           secRecency = Nothing, -- Knowledge is timeless
@@ -546,6 +547,22 @@ test =
             sec = buildSemanticSection result now
         secCompositionMode sec Test.@=? Additive
         secRelevance sec Test.@=? Just 0.85,
+      Test.unit "buildKnowledgeSection tokens match rendered content" <| do
+        now <- Time.getCurrentTime
+        let result =
+              KnowledgeResult
+                { krMemories = [("Remember project constraints", "project notes", ["planning"])],
+                  krContradictions =
+                    [ ( "Older plan says use approach A with many caveats",
+                        "Newer plan says use approach B with a different rollout"
+                      )
+                    ],
+                  krSupersededCount = 2,
+                  krTotalTokens = 1
+                }
+            sec = buildKnowledgeSection result now
+        secTokens sec Test.@=? estimateTokens (secContent sec)
+        (secTokens sec > krTotalTokens result) Test.@=? True,
       Test.unit "hydrateWithSources assembles sections correctly" <| do
         let cfg =
               defaultHydrationConfig