Cap incremental EXISTING TOPICS to the most recently updated aggregates #80
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
3 participants
Notifications
Due date
No due date set.
Reference
marvin8/yunjin!80
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/issue-74-cap-existing-topics"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Incremental aggregation runs sent the user's entire aggregate set to the LLM as EXISTING TOPICS — ~900 entries and growing, the dominant token cost, and a suspected cause of poor match rates.
Closes #74
What
YUNJIN_MAX_PROMPT_TOPICSenv var (default 100; unset/invalid/below-1 fall back to the default, same convention as the other aggregator vars).updated_at DESC, so the cap is a slice).Why this is safe
Impact
At the default cap the incremental prompt drops from ~30–47k to ~5–8k tokens per run (5–8× vs post-TTL steady state) and the matching haystack shrinks accordingly.
Docs
docs/config.mdgains theYUNJIN_MAX_PROMPT_TOPICSrow in the LLM aggregation table.WuMing
Found 3 issue(s). See inline comments below.
@ -591,0 +601,4 @@# already ordered most-recently-updated first, so slicing keeps the# freshest candidates. Consolidation passes are bounded separately# by their input articles and are not affected.prompt_aggregates = user_aggregates[: _max_prompt_topics()]code [MEDIUM]
The truncation is completely silent: there is no log line or metric when
len(user_aggregates) > _max_prompt_topics(), so nothing in the logs ever indicates that the matching haystack was cut. Because capped-out aggregates still exist in the DB, articles about those stories will be turned into brand-new (duplicate) topics by the incremental pass, and an operator loweringYUNJIN_MAX_PROMPT_TOPICS(e.g. to 10) to fix token cost would see match-rate regressions with no diagnostic. Add an explicit signal, e.g.if len(prompt_aggregates) < len(user_aggregates): logger.debug(f"User {user.id}: capped EXISTING TOPICS {len(user_aggregates)} -> {len(prompt_aggregates)}")(or a counter metric), so the effect of the cap is observable.@ -1470,6 +1471,81 @@ class TestEligibility:assert _eligibility_days() == 30class TestExistingTopicsCap:code [LOW]
Coverage gaps for the PR's central claims: (1) nothing guards the statement that consolidation passes are untouched — the only prompt-affecting call site that changed is the incremental one, but since the same
user_aggregateslist is fetched in this method, a future refactor could easily cap the consolidation prompt too. Add a test asserting a consolidation run still sees all (uncapped) aggregates, e.g. by forcinglast_consolidation_atpast the interval and asserting an aggregate outside the cap is present in the prompt. (2) There is no boundary test: exactlyNaggregates (no truncation) and the default cap of 100 with >100 aggregates are untested.@ -1473,0 +1508,4 @@def test_incremental_run_caps_existing_topics_to_most_recent(self, db_conn, monkeypatch):"""Test an over-cap prompt keeps only the most recently updated aggregates."""for index in range(5):self._make_aggregate(db_conn, f"Oldest {index}", updated_minutes_ago=100 - index)code [LOW]
This test does not actually prove the requirement 'most recently updated' survives the cap. The aggregates are both created and back-dated in the same order (
Oldest 4is created last and has the newestupdated_at), so the assertions pass for any ordering that is effectively id/created_at DESC as well as updated_at DESC — i.e. the test would not catch a regression whereget_all_aggregatesstops ordering byupdated_at. Make the creation order conflict with the update order (e.g. create the aggregate withupdated_minutes_ago=96first and the one withupdated_minutes_ago=100last, or use a shuffled ordering) so only a genuineupdated_at DESCslice yields the asserted names.src/yunjin/services/llm_aggregator.py✅ Fixed in commit
f416c0c— the incremental path now logsUser {id}: capped EXISTING TOPICS {n} -> {m}at DEBUG when truncation occurs, pinned by a caplog test asserting the5 -> 3record.tests/test_aggregator.py✅ Fixed in commit
f416c0c— creation order now conflicts with update recency (ids ascend as Second, Fourth, Newest, Oldest, Third with cap 3), so an id- or created_at-ordered slice fails the assertions. RED-verified against a simulated wrong-end slice; the correct implementation passes.tests/test_aggregator.py✅ Both added in commit
f416c0c. (1) A guard test proves a due consolidation pass lists an aggregate the cap would exclude (an old aggregate holding the window article appears as a candidate with cap=3 and 5 newer aggregates present). One precision on the suggested shape: consolidation never sent all aggregates — its EXISTING TOPICS are the candidates holding input articles — so the test pins the real invariant (candidate selection bypasses the cap). (2) Boundary coverage: exactly-at-cap (3/3, nothing cut) and the default 100 with 101 aggregates (newest 100 kept, oldest cut) are both tested end-to-end.