Skip to content

fix: bound historical context snapshots - #58

Merged
IceCodeNew merged 1 commit into
masterfrom
codex/bound-llm-context
Jul 17, 2026
Merged

fix: bound historical context snapshots#58
IceCodeNew merged 1 commit into
masterfrom
codex/bound-llm-context

Conversation

@IceCodeNew

Copy link
Copy Markdown
Owner

Summary

  • collapse consecutive duplicate API snapshots per source
  • retain each source latest value, retention baseline, and recent changes with explicit roles
  • cap historical snapshots at 8 documents and 16,000 serialized characters by default
  • log only safe snapshot and full-payload size metadata
  • document that articles and published briefings remain governed by HISTORY_HOURS

Root cause

Hourly API snapshots were all copied into every LLM request for the full 48-hour retention window. The incident already carried 16 historical snapshots; without a bound this category continued growing.

Validation

  • prek run --all-files
  • uv run --with pytest --with pytest-cov -- pytest --cov --cov-branch --cov-report=xml (612 passed)

@IceCodeNew

Copy link
Copy Markdown
Owner Author

/agentic_review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@IceCodeNew, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1471c05a-39a3-46fb-9b8d-cafc20cefc89

📥 Commits

Reviewing files that changed from the base of the PR and between 7730ec5 and 56297ac.

📒 Files selected for processing (9)
  • docs/design.md
  • docs/notes.md
  • env.example
  • tests/test_cli.py
  • tests/test_config.py
  • tests/test_service.py
  • weather_briefing/config.py
  • weather_briefing/prompts.py
  • weather_briefing/service.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/bound-llm-context

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.72%. Comparing base (9e395a8) to head (56297ac).
⚠️ Report is 3 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff            @@
##           master      #58    +/-   ##
========================================
  Coverage   99.71%   99.72%            
========================================
  Files          38       38            
  Lines        6418     6546   +128     
  Branches      363      376    +13     
========================================
+ Hits         6400     6528   +128     
  Misses         13       13            
  Partials        5        5            

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@qodo-code-review

qodo-code-review Bot commented Jul 17, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 31 rules

Grey Divider


Action required

1. Unbounded citation ID set 🐞 Bug ≡ Correctness ⭐ New
Description
BriefingService._run still builds valid_source_ids from the full unbounded historical_context via
reference_context, even though only historical_context_payload is sent to the LLM. parse_result
therefore can accept model citations to source IDs that were not present in the LLM input, weakening
citation integrity when history is bounded.
Code

weather_briefing/service.py[R277-291]

        historical_context = self._state.recent_context_documents(now, self._settings.history_hours)
+        historical_context_payload, historical_context_characters = _bounded_context_history(
+            historical_context,
+            max_documents=self._settings.llm_history_max_documents,
+            max_characters=self._settings.llm_history_max_characters,
+        )
+        _LOGGER.debug(
+            "Historical context bounded: input_documents=%d selected_documents=%d serialized_characters=%d",
+            len(historical_context),
+            len(historical_context_payload),
+            historical_context_characters,
+        )
        reference_context = _unique_documents((*historical_context, *context))
        active_warnings = self._state.active_warnings(now, self._settings.warning_retention_hours)
        if not unpublished_articles and not context and not active_warnings:
Relevance

⭐⭐⭐ High

Team tightens LLM trust boundaries (PR #33 rejected unknown IDs); likely will align valid_source_ids
to bounded payload.

PR-#33
PR-#58

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The payload uses historical_context_payload (bounded) while valid_source_ids is derived from
reference_context built from the unbounded historical_context. Since parse_result() only
rejects citations not in valid_source_ids, extra unprompted IDs become acceptable.

weather_briefing/service.py[277-329]
weather_briefing/llm.py[197-210]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`BriefingService._run()` sends a bounded `recent_context_documents` payload, but still constructs `reference_context`/`valid_source_ids` from the *unbounded* `historical_context`. Output validation in `parse_result()` only checks that citations are within `valid_source_ids`, so citations to sources omitted from the prompt can pass.

## Issue Context
The PR intentionally drops historical snapshots from the payload when exceeding document/character budgets; validation should reflect what the model actually saw.

## Fix Focus Areas
- weather_briefing/service.py[277-329]

### Implementation sketch
- Have `_bounded_context_history(...)` also return the selected `SourceDocument` objects (or at least the set of selected `source_id`s).
- Build `reference_context` (and thus `valid_source_ids`) from `context` plus the *selected* historical documents/IDs, not from all of `historical_context`.
- Keep renderer context consistent with the same bounded set so that citations map to sources actually provided.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Dedupe includes timestamps 🐞 Bug ≡ Correctness ⭐ New
Description
_context_history_candidates treats snapshots as different when (name, url, content) differs, but
weather/air-quality documents embed observation/update timestamps in content. This causes
otherwise identical hourly snapshots to be treated as changes whenever the timestamp changes,
undermining the intended “collapse consecutive identical values” behavior.
Code

weather_briefing/service.py[R585-626]

+    changes_by_source: dict[str, list[tuple[int, SourceDocument]]] = {}
+    for index, document in enumerate(documents):
+        source_changes = changes_by_source.setdefault(document.id, [])
+        previous_document = source_changes[-1][1] if source_changes else None
+        if previous_document is None or _context_document_value(previous_document) != _context_document_value(document):
+            source_changes.append((index, document))
+
+    source_changes = sorted(changes_by_source.values(), key=lambda changes: changes[-1][0], reverse=True)
+    prioritized: list[tuple[int, _HistoricalContextCandidate]] = []
+    selected_indexes: set[int] = set()
+
+    def add(
+        snapshot: tuple[int, SourceDocument],
+        role: Literal["latest", "retention_baseline", "recent_change"],
+    ) -> None:
+        if snapshot[0] not in selected_indexes:
+            prioritized.append((snapshot[0], _HistoricalContextCandidate(snapshot[1], role)))
+            selected_indexes.add(snapshot[0])
+
+    for changes in source_changes:
+        add(changes[-1], "latest")
+    for changes in source_changes:
+        add(changes[0], "retention_baseline")
+
+    depth = 2
+    while len(prioritized) < max_documents:
+        added_change = False
+        for changes in source_changes:
+            if len(changes) > depth:
+                add(changes[-depth], "recent_change")
+                added_change = True
+                if len(prioritized) == max_documents:
+                    break
+        if not added_change:
+            break
+        depth += 1
+
+    return tuple(candidate for _, candidate in prioritized[:max_documents])
+
+
+def _context_document_value(document: SourceDocument) -> tuple[str, str, str]:
+    return document.name, document.url, document.content
Relevance

⭐⭐ Medium

No historical evidence on timestamp-insensitive dedupe; PR #58 introduced collapsing but no prior
feedback.

PR-#58

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new dedupe key includes document.content. Weather and air-quality SourceDocuments include
timestamp lines in their content, so content changes when the observation/update time changes even
if the underlying values do not, preventing collapse of consecutive duplicates as documented.

weather_briefing/service.py[581-626]
weather_briefing/weather_context.py[729-744]
weather_briefing/air_quality.py[93-112]
docs/design.md[79-82]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The dedupe logic compares `document.content` verbatim, but several generated documents include volatile timestamp lines. As a result, dedupe will fail to collapse semantically identical snapshots when only the observation/update time changes.

## Issue Context
The design doc states snapshots should be folded by source on consecutive identical values before selecting latest/baseline/recent changes.

## Fix Focus Areas
- weather_briefing/service.py[585-626]
- weather_briefing/weather_context.py[729-744]
- weather_briefing/air_quality.py[93-112]

### Implementation sketch
- Change `_context_document_value()` to compute a *stable* comparison key (e.g., strip a leading `更新时间:...` / `观测时间:...` line, or otherwise remove known volatile prefixes).
- Prefer comparing semantic fields (or a normalized content body) rather than the fully rendered content string.
- Add/extend tests to cover two snapshots with identical measurements but different timestamps being collapsed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Budget JSON mismatch 🐞 Bug ☼ Reliability ⭐ New
Description
_bounded_context_history enforces LLM_HISTORY_MAX_CHARACTERS using a compact JSON serialization
(no spaces via separators=(',', ':')), but the actual LLM request serializes the payload with
default separators that add spaces. This can make the real recent_context_documents JSON exceed
the configured character budget.
Code

weather_briefing/service.py[R562-575]

+def _bounded_context_history(
+    documents: tuple[SourceDocument, ...],
+    *,
+    max_documents: int,
+    max_characters: int,
+) -> tuple[list[dict[str, object]], int]:
+    candidates = _context_history_candidates(documents, max_documents)
+    selected: list[dict[str, object]] = []
+    serialized_characters = len("[]")
+    for candidate in candidates:
+        entry = _serialize_context_document(candidate.document, history_role=candidate.role)
+        candidate_payload = [*selected, entry]
+        candidate_characters = len(json.dumps(candidate_payload, ensure_ascii=False, separators=(",", ":")))
+        if candidate_characters <= max_characters:
Relevance

⭐⭐ Medium

No precedent on JSON separators matching budgets; only PR #58 uses compact dumps for diagnostics.

PR-#58

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The budget is computed with compact separators in service.py, but the request body is produced
with default json.dumps in llm.py, which includes extra whitespace. Therefore the measured
character count can be lower than what is actually sent.

weather_briefing/service.py[562-578]
weather_briefing/llm.py[152-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Character budgeting uses a different JSON serialization format than the actual LLM request, so the enforced budget is not the budget actually sent over the wire.

## Issue Context
Budgeting aims to prevent LLM context-length/timeouts; even small systematic undercounting can matter when near the limit.

## Fix Focus Areas
- weather_briefing/service.py[562-578]
- weather_briefing/llm.py[152-165]

### Implementation sketch
- Use the *same* `json.dumps(..., ensure_ascii=False, separators=(",", ":"))` (or a shared helper) both for:
 - computing `candidate_characters` in `_bounded_context_history`, and
 - serializing `payload` in `AnyLLMStructuredProvider.summarize()`.
- Optionally adjust the DEBUG size log to use the same serialization to keep metrics consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Eager payload serialization ✓ Resolved 🐞 Bug ➹ Performance
Description
BriefingService._run always JSON-serializes the entire LLM payload just to compute a DEBUG metric,
because json.dumps() is executed as a function argument before the logger checks the log level. This
adds avoidable CPU/memory cost on every run (even at INFO/WARN levels) and becomes a latent failure
point if any future payload field is not JSON-serializable.
Code

weather_briefing/service.py[R322-325]

+        _LOGGER.debug(
+            "LLM payload prepared: serialized_characters=%d",
+            len(json.dumps(payload, ensure_ascii=False, separators=(",", ":"))),
+        )
Relevance

⭐⭐ Medium

No prior evidence for guarding expensive debug args; perf-focused linting accepted (Ruff PERF in PR
#39) suggests possible acceptance.

PR-#39
PR-#11

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The debug log computes len(json.dumps(payload, ...)) directly in the call site; Python evaluates
function arguments before calling _LOGGER.debug, so the serialization executes regardless of
configured log level.

weather_briefing/service.py[307-326]
weather_briefing/service.py[487-540]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`json.dumps(payload, ...)` is evaluated eagerly as an argument to `_LOGGER.debug(...)`, so full-payload serialization happens even when DEBUG logging is disabled.

## Issue Context
This was introduced with the new “LLM payload prepared: serialized_characters=...” debug line. The payload can be large (articles + briefings + context), so serializing it unconditionally adds overhead.

## Fix Focus Areas
- weather_briefing/service.py[322-325]

## Suggested fix
Wrap the serialization in a log-level guard so the expensive computation only runs when DEBUG is enabled:

```py
if _LOGGER.isEnabledFor(logging.DEBUG):
   _LOGGER.debug(
       "LLM payload prepared: serialized_characters=%d",
       len(json.dumps(payload, ensure_ascii=False, separators=(",", ":"))),
   )
```

This preserves the metric for tests that enable DEBUG (caplog) while removing the always-on cost.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 56297ac

Results up to commit aa686c8


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Eager payload serialization ✓ Resolved 🐞 Bug ➹ Performance
Description
BriefingService._run always JSON-serializes the entire LLM payload just to compute a DEBUG metric,
because json.dumps() is executed as a function argument before the logger checks the log level. This
adds avoidable CPU/memory cost on every run (even at INFO/WARN levels) and becomes a latent failure
point if any future payload field is not JSON-serializable.
Code

weather_briefing/service.py[R322-325]

+        _LOGGER.debug(
+            "LLM payload prepared: serialized_characters=%d",
+            len(json.dumps(payload, ensure_ascii=False, separators=(",", ":"))),
+        )
Relevance

⭐⭐ Medium

No prior evidence for guarding expensive debug args; perf-focused linting accepted (Ruff PERF in PR
#39) suggests possible acceptance.

PR-#39
PR-#11

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The debug log computes len(json.dumps(payload, ...)) directly in the call site; Python evaluates
function arguments before calling _LOGGER.debug, so the serialization executes regardless of
configured log level.

weather_briefing/service.py[307-326]
weather_briefing/service.py[487-540]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`json.dumps(payload, ...)` is evaluated eagerly as an argument to `_LOGGER.debug(...)`, so full-payload serialization happens even when DEBUG logging is disabled.

## Issue Context
This was introduced with the new “LLM payload prepared: serialized_characters=...” debug line. The payload can be large (articles + briefings + context), so serializing it unconditionally adds overhead.

## Fix Focus Areas
- weather_briefing/service.py[322-325]

## Suggested fix
Wrap the serialization in a log-level guard so the expensive computation only runs when DEBUG is enabled:

```py
if _LOGGER.isEnabledFor(logging.DEBUG):
   _LOGGER.debug(
       "LLM payload prepared: serialized_characters=%d",
       len(json.dumps(payload, ensure_ascii=False, separators=(",", ":"))),
   )
```

This preserves the metric for tests that enable DEBUG (caplog) while removing the always-on cost.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread weather_briefing/service.py Outdated
@IceCodeNew
IceCodeNew force-pushed the codex/bound-llm-context branch from aa686c8 to 56297ac Compare July 17, 2026 03:15
@IceCodeNew

Copy link
Copy Markdown
Owner Author

/agentic_review

@IceCodeNew
IceCodeNew marked this pull request as ready for review July 17, 2026 03:24
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Bound LLM historical context snapshots by source and size

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Bound hourly API snapshot history sent to the LLM to avoid unbounded payload growth.
• Collapse duplicate snapshots per source and mark retained items with explicit history roles.
• Add env-configurable document and character budgets with safe DEBUG-only size logging.
Diagram

graph TD
  A["env vars"] --> B["Settings"] --> C["BriefingService"] --> D["Bound history"] --> E["LLM payload"] --> F["LLM provider"]
  G[("SQLiteStateStore")] --> C

  subgraph Legend
    direction LR
    _db[("Database")] ~~~ _mod["Module"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Token-based budgeting (model-aware)
  • ➕ More accurate than character counting for actual context window usage
  • ➕ Less variance across languages/emoji/UTF-8 vs ASCII
  • ➖ Requires tokenizer dependency and model-specific assumptions
  • ➖ Harder to test deterministically across model/provider changes
2. Truncate oversized documents instead of skipping them
  • ➕ Keeps representation from every source even under tight budgets
  • ➕ Potentially higher recall of key facts when sources are long
  • ➖ Risk of cutting mid-sentence and degrading instruction quality
  • ➖ May increase prompt-injection surface by including partial hostile content
  • ➖ Harder to reason about what was retained vs lost
3. Persist per-source rollups (baseline/latest/deltas) in storage
  • ➕ Moves bounding upstream; reduces runtime payload work
  • ➕ Controls growth at the data model level
  • ➖ More invasive schema/logic changes
  • ➖ May reduce diagnostic fidelity versus keeping raw snapshots

Recommendation: The PR’s approach (per-request bounding with per-source dedupe + role-tagging + skip-on-oversize) is a good balance of safety and simplicity: it stops unbounded growth without changing persistence, and avoids logging sensitive content. If payload pressure reappears, consider token-based budgeting as the next step; truncation and storage rollups are higher-risk/more invasive.

Files changed (9) +242 / -14

Bug fix (2) +128 / -12
config.pyIntroduce validated Settings fields for LLM history document/character caps +9/-0

Introduce validated Settings fields for LLM history document/character caps

• Adds llm_history_max_documents and llm_history_max_characters to Settings and loads them from env with bounded validation (docs: 8 default, 8..256 range; chars: 16,000 default with safety bounds).

weather_briefing/config.py

service.pyBound and role-tag historical context snapshots in LLM payload +119/-12

Bound and role-tag historical context snapshots in LLM payload

• Adds candidate selection that collapses consecutive identical snapshots per source, retains latest + retention baseline + recent changes up to a configurable max document count, and enforces a serialized character budget by skipping oversize entries. Replaces raw historical_context payload with role-tagged dictionaries and adds DEBUG-only logging of size metadata (including full payload serialized size) without logging sensitive content.

weather_briefing/service.py

Tests (3) +103 / -2
test_cli.pyExtend CLI test settings with LLM history limits +2/-0

Extend CLI test settings with LLM history limits

• Updates the test helper settings used by CLI tests to include llm_history_max_documents and llm_history_max_characters so new Settings/Protocol fields are satisfied.

tests/test_cli.py

test_config.pyAdd Settings coverage for LLM history defaults, overrides, and validation +31/-0

Add Settings coverage for LLM history defaults, overrides, and validation

• Asserts default values (8 docs, 16,000 chars), verifies environment overrides, and adds rejection tests for unsafe limits to ensure bounded parsing rules are enforced.

tests/test_config.py

test_service.pyAdd unit tests for history candidate selection, budgets, and safe DEBUG logging +70/-2

Add unit tests for history candidate selection, budgets, and safe DEBUG logging

• Adds targeted tests for per-source dedupe + role assignment ordering (latest/baseline/recent_change) and enforcement of the serialized character budget. Extends an integration-style service test to assert DEBUG logs include only size metadata and do not leak historical content.

tests/test_service.py

Documentation (3) +9 / -0
design.mdDocument bounded historical context snapshot policy and safe logging +2/-0

Document bounded historical context snapshot policy and safe logging

• Adds a design note describing per-source dedupe, retention roles (latest/baseline/recent changes), and the new document/character caps. Clarifies that oversize documents are skipped (not truncated) and that DEBUG logs only size metadata.

docs/design.md

notes.mdExplain current scope of LLM history budgeting and future escalation criteria +6/-0

Explain current scope of LLM history budgeting and future escalation criteria

• Introduces guidance that the new budgets apply only to hourly API snapshots, while articles/briefings remain governed by HISTORY_HOURS. Documents when to migrate toward a unified cross-category budget if growth continues.

docs/notes.md

prompts.pyDocument new history_role semantics for recent_context_documents +1/-0

Document new history_role semantics for recent_context_documents

• Updates prompt guidance so the model understands history_role values and can interpret latest/baseline/recent change snapshots correctly.

weather_briefing/prompts.py

Other (1) +2 / -0
env.exampleAdd default LLM history bounding environment variables +2/-0

Add default LLM history bounding environment variables

• Adds LLM_HISTORY_MAX_DOCUMENTS=8 and LLM_HISTORY_MAX_CHARACTERS=16000 to the example environment file to reflect the new defaults.

env.example

@IceCodeNew
IceCodeNew merged commit 6bb286f into master Jul 17, 2026
20 checks passed
@IceCodeNew
IceCodeNew deleted the codex/bound-llm-context branch July 17, 2026 03:27
Comment on lines 277 to 291
historical_context = self._state.recent_context_documents(now, self._settings.history_hours)
historical_context_payload, historical_context_characters = _bounded_context_history(
historical_context,
max_documents=self._settings.llm_history_max_documents,
max_characters=self._settings.llm_history_max_characters,
)
_LOGGER.debug(
"Historical context bounded: input_documents=%d selected_documents=%d serialized_characters=%d",
len(historical_context),
len(historical_context_payload),
historical_context_characters,
)
reference_context = _unique_documents((*historical_context, *context))
active_warnings = self._state.active_warnings(now, self._settings.warning_retention_hours)
if not unpublished_articles and not context and not active_warnings:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Unbounded citation id set 🐞 Bug ≡ Correctness

BriefingService._run still builds valid_source_ids from the full unbounded historical_context via
reference_context, even though only historical_context_payload is sent to the LLM. parse_result
therefore can accept model citations to source IDs that were not present in the LLM input, weakening
citation integrity when history is bounded.
Agent Prompt
## Issue description
`BriefingService._run()` sends a bounded `recent_context_documents` payload, but still constructs `reference_context`/`valid_source_ids` from the *unbounded* `historical_context`. Output validation in `parse_result()` only checks that citations are within `valid_source_ids`, so citations to sources omitted from the prompt can pass.

## Issue Context
The PR intentionally drops historical snapshots from the payload when exceeding document/character budgets; validation should reflect what the model actually saw.

## Fix Focus Areas
- weather_briefing/service.py[277-329]

### Implementation sketch
- Have `_bounded_context_history(...)` also return the selected `SourceDocument` objects (or at least the set of selected `source_id`s).
- Build `reference_context` (and thus `valid_source_ids`) from `context` plus the *selected* historical documents/IDs, not from all of `historical_context`.
- Keep renderer context consistent with the same bounded set so that citations map to sources actually provided.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +585 to +626
changes_by_source: dict[str, list[tuple[int, SourceDocument]]] = {}
for index, document in enumerate(documents):
source_changes = changes_by_source.setdefault(document.id, [])
previous_document = source_changes[-1][1] if source_changes else None
if previous_document is None or _context_document_value(previous_document) != _context_document_value(document):
source_changes.append((index, document))

source_changes = sorted(changes_by_source.values(), key=lambda changes: changes[-1][0], reverse=True)
prioritized: list[tuple[int, _HistoricalContextCandidate]] = []
selected_indexes: set[int] = set()

def add(
snapshot: tuple[int, SourceDocument],
role: Literal["latest", "retention_baseline", "recent_change"],
) -> None:
if snapshot[0] not in selected_indexes:
prioritized.append((snapshot[0], _HistoricalContextCandidate(snapshot[1], role)))
selected_indexes.add(snapshot[0])

for changes in source_changes:
add(changes[-1], "latest")
for changes in source_changes:
add(changes[0], "retention_baseline")

depth = 2
while len(prioritized) < max_documents:
added_change = False
for changes in source_changes:
if len(changes) > depth:
add(changes[-depth], "recent_change")
added_change = True
if len(prioritized) == max_documents:
break
if not added_change:
break
depth += 1

return tuple(candidate for _, candidate in prioritized[:max_documents])


def _context_document_value(document: SourceDocument) -> tuple[str, str, str]:
return document.name, document.url, document.content

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Dedupe includes timestamps 🐞 Bug ≡ Correctness

_context_history_candidates treats snapshots as different when (name, url, content) differs, but
weather/air-quality documents embed observation/update timestamps in content. This causes
otherwise identical hourly snapshots to be treated as changes whenever the timestamp changes,
undermining the intended “collapse consecutive identical values” behavior.
Agent Prompt
## Issue description
The dedupe logic compares `document.content` verbatim, but several generated documents include volatile timestamp lines. As a result, dedupe will fail to collapse semantically identical snapshots when only the observation/update time changes.

## Issue Context
The design doc states snapshots should be folded by source on consecutive identical values before selecting latest/baseline/recent changes.

## Fix Focus Areas
- weather_briefing/service.py[585-626]
- weather_briefing/weather_context.py[729-744]
- weather_briefing/air_quality.py[93-112]

### Implementation sketch
- Change `_context_document_value()` to compute a *stable* comparison key (e.g., strip a leading `更新时间:...` / `观测时间:...` line, or otherwise remove known volatile prefixes).
- Prefer comparing semantic fields (or a normalized content body) rather than the fully rendered content string.
- Add/extend tests to cover two snapshots with identical measurements but different timestamps being collapsed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +562 to +575
def _bounded_context_history(
documents: tuple[SourceDocument, ...],
*,
max_documents: int,
max_characters: int,
) -> tuple[list[dict[str, object]], int]:
candidates = _context_history_candidates(documents, max_documents)
selected: list[dict[str, object]] = []
serialized_characters = len("[]")
for candidate in candidates:
entry = _serialize_context_document(candidate.document, history_role=candidate.role)
candidate_payload = [*selected, entry]
candidate_characters = len(json.dumps(candidate_payload, ensure_ascii=False, separators=(",", ":")))
if candidate_characters <= max_characters:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Budget json mismatch 🐞 Bug ☼ Reliability

_bounded_context_history enforces LLM_HISTORY_MAX_CHARACTERS using a compact JSON serialization
(no spaces via separators=(',', ':')), but the actual LLM request serializes the payload with
default separators that add spaces. This can make the real recent_context_documents JSON exceed
the configured character budget.
Agent Prompt
## Issue description
Character budgeting uses a different JSON serialization format than the actual LLM request, so the enforced budget is not the budget actually sent over the wire.

## Issue Context
Budgeting aims to prevent LLM context-length/timeouts; even small systematic undercounting can matter when near the limit.

## Fix Focus Areas
- weather_briefing/service.py[562-578]
- weather_briefing/llm.py[152-165]

### Implementation sketch
- Use the *same* `json.dumps(..., ensure_ascii=False, separators=(",", ":"))` (or a shared helper) both for:
  - computing `candidate_characters` in `_bounded_context_history`, and
  - serializing `payload` in `AnyLLMStructuredProvider.summarize()`.
- Optionally adjust the DEBUG size log to use the same serialization to keep metrics consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 56297ac

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant