Skip to content

Commit 6e06581

Browse files
committed
[argus] telegram: scope orphan-artifact auto-present to viewable end-products
_orphan_artifacts was too greedy: it linked ANY new file in outputs/, including throwaway scripts. Asking for the weather (Qwen writes a fetch .py into outputs/) would yield a spurious /f/fetch.py link. Now gated by _ORPHAN_PRESENT_EXTS (.html/.svg/.pdf/.png/.jpg/.gif/.webp) — viewable deliverables only; code/data/scratch (.py/.json/.csv/.txt/.log) is ignored. Explicit present_files still presents anything. 3 new tests.
1 parent 916f3e0 commit 6e06581

2 files changed

Lines changed: 61 additions & 6 deletions

File tree

backend/app/channels/manager.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -388,13 +388,27 @@ def _resolve_attachments(thread_id: str, artifacts: list[str]) -> list[ResolvedA
388388
return attachments
389389

390390

391+
# [argus patch #10] Only these extensions are auto-presented when the agent
392+
# writes a file to outputs/ but forgets to present_files it. The rule:
393+
# auto-present VIEWABLE END-PRODUCTS only (a report, a diagram, an image), and
394+
# NEVER the means used to produce them (a .py fetch script, a .json blob, a
395+
# scratch .csv/.txt/.log). E.g. asking for the weather, where Qwen writes a
396+
# throwaway fetch script into outputs/, must NOT yield a "/f/fetch.py" link.
397+
# The agent can still explicitly present ANYTHING via present_files — this
398+
# allowlist only gates the *automatic* rescue path.
399+
_ORPHAN_PRESENT_EXTS = {".html", ".htm", ".svg", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp"}
400+
401+
391402
def _orphan_artifacts(thread_id: str, since: float, already: list[str]) -> list[str]:
392-
"""[argus patch #10] Files the agent wrote to the outputs dir during this
393-
run but did NOT present via present_files. Models sometimes write a file
394-
and then paste its contents into chat instead of calling present_files
395-
(observed with SVG/HTML). We detect those by mtime so they still get a
396-
/f/ link rather than a wall of source. Excludes already-presented paths
397-
and the render-and-verify skill's *.screenshot.png sidecars (noise)."""
403+
"""[argus patch #10] Viewable deliverables the agent wrote to outputs/
404+
during this run but did NOT present via present_files. Models sometimes
405+
write a file and paste its contents into chat instead (observed with SVG);
406+
we rescue those so they get a /f/ link rather than a wall of source.
407+
408+
Deliberately conservative — only the viewable-end-product extensions in
409+
_ORPHAN_PRESENT_EXTS qualify. Code/data/scratch files (.py, .json, .csv,
410+
.txt, .log, …) are ignored: they're the means, not the answer. Also skips
411+
already-presented files and render-and-verify *.screenshot.png sidecars."""
398412
try:
399413
from deerflow.config.paths import get_paths
400414

@@ -413,6 +427,8 @@ def _orphan_artifacts(thread_id: str, since: float, already: list[str]) -> list[
413427
continue
414428
if f.name in already_names or f.name.endswith(".screenshot.png"):
415429
continue
430+
if f.suffix.lower() not in _ORPHAN_PRESENT_EXTS:
431+
continue # not a viewable end-product — skip (e.g. a fetch .py)
416432
found.append(_OUTPUTS_VIRTUAL_PREFIX + f.name)
417433
return found
418434

backend/tests/test_channel_file_attachments.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,42 @@ def test_format_artifact_text_for_unresolved(self):
562562
result = _format_artifact_text(["/mnt/user-data/outputs/a.txt", "/mnt/user-data/outputs/b.txt"])
563563
assert "a.txt" in result
564564
assert "b.txt" in result
565+
566+
567+
class TestOrphanArtifacts:
568+
"""[argus patch #10] _orphan_artifacts must auto-present only viewable
569+
end-products (HTML/SVG/PDF/images), never the means (a .py fetch script,
570+
scratch .json/.csv/.txt). This is the weather-query guard: asking for the
571+
weather, where the model writes a throwaway fetch.py into outputs/, must
572+
not produce a /f/fetch.py link."""
573+
574+
def _orphans(self, tmp_path, files, since=0.0, already=None):
575+
from unittest.mock import MagicMock, patch
576+
from app.channels.manager import _orphan_artifacts
577+
578+
thread_id = "t-orphan"
579+
outputs_dir = tmp_path / "threads" / thread_id / "user-data" / "outputs"
580+
outputs_dir.mkdir(parents=True)
581+
for name in files:
582+
(outputs_dir / name).write_text("x")
583+
mock_paths = MagicMock()
584+
mock_paths.sandbox_outputs_dir.return_value = outputs_dir
585+
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
586+
return _orphan_artifacts(thread_id, since, already or [])
587+
588+
def test_ignores_code_and_data_files(self, tmp_path):
589+
got = self._orphans(tmp_path, ["fetch.py", "data.json", "notes.txt", "out.csv", "run.log"])
590+
assert got == [] # none are viewable end-products
591+
592+
def test_presents_viewable_products(self, tmp_path):
593+
got = self._orphans(tmp_path, ["report.html", "diagram.svg", "chart.png", "scratch.py"])
594+
names = {p.rsplit("/", 1)[-1] for p in got}
595+
assert names == {"report.html", "diagram.svg", "chart.png"} # .py excluded
596+
597+
def test_skips_already_presented_and_screenshots(self, tmp_path):
598+
got = self._orphans(
599+
tmp_path,
600+
["report.html", "report.screenshot.png"],
601+
already=["/mnt/user-data/outputs/report.html"],
602+
)
603+
assert got == [] # report.html already presented; screenshot is a sidecar

0 commit comments

Comments
 (0)