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
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,9 @@ async def invoke(self, invocation: dict[str, Any]) -> dict[str, Any]:
if outcome.error is None:
self._completed_invocations += 1

telemetry_runtime, relay_artifacts, collect_error = self._telemetry_output()
telemetry_runtime, relay_artifacts, collect_error = self._telemetry_output(
inherited_quarantine=inherited_quarantine
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return normalize_output(
model_name=self._model_name,
base_url=self._base_url,
Expand Down Expand Up @@ -692,11 +694,18 @@ async def _invoke_agent(

def _telemetry_output(
self,
*,
inherited_quarantine: bool,
) -> tuple[dict[str, Any] | None, list[dict[str, str]] | None, str | None]:
"""Return the telemetry block, artifact references, and any collection fault.

Collecting references walks the filesystem, so it is returned as a fault rather
than raised: raising here would discard an already-completed turn.

``inherited_quarantine`` is the state from *before* this turn: the turn that
poisoned the runtime opened a scope and produced its own partial artifacts, so it
still publishes them; only turns that inherit the quarantine have none of their
own to publish.
"""

if self._observability is None:
Expand All @@ -708,7 +717,7 @@ def _telemetry_output(
}
if not self._observability.collect_artifacts:
return telemetry_runtime, None, None
if self._telemetry_quarantine is not None:
if inherited_quarantine:
return telemetry_runtime, None, None
try:
relay_artifacts = common_utils.collect_relay_artifacts(
Expand Down
11 changes: 10 additions & 1 deletion tests/adapters/test_deepagents.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,13 @@ def leaking_scope(name: str, scope_type: object, **kwargs: object):
)

monkeypatch.setattr(sys.modules["nemo_relay.scope"], "scope", leaking_scope)
# A recognisable artifact so the faulting turn's publication can be checked by value
# rather than by the key merely existing: real collection over an empty evidence dir
# returns [], which would pass a presence-only assertion either way.
artifacts = [{"kind": "atif", "path": str(tmp_path / "trajectory.atif.json")}]
monkeypatch.setattr(
adapter.common_utils, "collect_relay_artifacts", lambda _config: artifacts
)

first, second = await invoke_twice(relay_payload(tmp_path))

Expand All @@ -554,7 +561,9 @@ def leaking_scope(name: str, scope_type: object, **kwargs: object):
assert "unreliable" in second["telemetry"]["error"]
# Only turn 1 opened a request scope; turn 2 must not push onto the dirty stack.
assert entered == ["deepagents-request"]
# Artifacts on disk belong to turn 1, so turn 2 must not reference them as its own.
# Turn 1 opened a scope and produced its own (partial) artifacts, so it still
# publishes them; turn 2 has none of its own and must not claim turn 1's.
assert first["relay_artifacts"] == artifacts
assert "relay_artifacts" not in second

# Turn 1 owns the fault, so it reports it verbatim and needs no separate cause.
Expand Down
Loading