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