Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions graph/research_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2687,6 +2687,19 @@ def archive_writer(state: ResearchState) -> dict:
except Exception as e:
logger.error("Failed to write signals.json: %s", e)

# Persist the consolidated morning brief alongside signals.json so
# the dashboard's Research Briefing Archive page can read it. The
# brief is the same body that goes out in the morning email
# (`email_sender` node downstream) — emailing it without persisting
# it leaves no audit trail and the archive page stales out, which
# is what happened from 2026-03-16 through 2026-05-20.
consolidated = state.get("consolidated_report", "") or ""
if consolidated:
try:
am.save_consolidated_report(run_date, consolidated)
except Exception as e:
logger.error("Failed to save consolidated_report: %s", e)

# Extract semantic memories from this run (Phase 3)
try:
from memory.semantic import extract_semantic_memories
Expand Down
48 changes: 48 additions & 0 deletions tests/test_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,51 @@ def test_dated_and_latest_have_identical_macro(self, archive_in_memory):
if call.kwargs.get("Key", "").startswith("population/")
}
assert bodies["population/latest.json"] == bodies["population/2026-05-11.json"]


# ── Consolidated morning brief persistence ──────────────────────────────
#
# Regression coverage for the 2026-05-20 finding: archive_writer was
# building the consolidated_report state field, emailing it via
# email_sender, and then dropping it on the floor — save_consolidated_report
# existed but had no caller for ~2 months (last morning.md write
# 2026-03-16). The dashboard's Research Briefing Archive page was
# correctly reading what was in S3, which was nothing.


class TestConsolidatedReportPersistence:
def test_save_consolidated_report_writes_morning_md_to_s3(
self, archive_in_memory
):
archive_in_memory.save_consolidated_report(
"2026-05-20", "# Weekly research brief\n\nTop picks: ..."
)
calls = archive_in_memory.s3.put_object.call_args_list
morning_calls = [
c for c in calls
if c.kwargs.get("Key", "").endswith("/morning.md")
]
assert len(morning_calls) == 1
c = morning_calls[0]
assert c.kwargs["Key"] == "consolidated/2026-05-20/morning.md"
body = c.kwargs["Body"]
if isinstance(body, bytes):
body = body.decode("utf-8")
assert "Weekly research brief" in body

def test_archive_writer_wires_save_consolidated_report(self):
# Structural regression: pin that archive_writer's source calls
# save_consolidated_report. If the call is removed again, this
# test fails at CI time instead of staling the archive page
# silently for two months.
import inspect
rg = pytest.importorskip(
"graph.research_graph",
reason="graph.research_graph requires gitignored config",
)
src = inspect.getsource(rg.archive_writer)
assert "save_consolidated_report" in src, (
"archive_writer must persist consolidated_report — without "
"this call the dashboard's Research Briefing Archive stales "
"out (regression of 2026-03-16 silent drop, fixed 2026-05-20)"
)
Loading