diff --git a/docs/INTERNAL-SUGGESTION-JOBS.md b/docs/INTERNAL-SUGGESTION-JOBS.md new file mode 100644 index 0000000..d8da341 --- /dev/null +++ b/docs/INTERNAL-SUGGESTION-JOBS.md @@ -0,0 +1,20 @@ +# Suggestion job provenance + +The default suggestion judge launches `amplifier run --output-format json` with +these values in a private copy of its child environment: + +- `AMPLIFIER_SESSION_VISIBILITY=internal` +- `AMPLIFIER_SESSION_PURPOSE=memory.suggestion` +- `AMPLIFIER_SESSION_ORIGIN=agent` + +Only this implementation job gets the declaration. The caller's environment, +provider routing, credentials, CLI arguments, timer scheduling, and retry +behavior are unchanged. No raw environment values are logged. + +The compatible CLI persists visibility and purpose as creation metadata. Unified +can then keep this history available for diagnostics while excluding it from +ordinary chat lists and default searches. A generic agent origin does not make +other standalone CLI conversations internal. End-to-end presentation requires +both the CLI persistence support and the Unified consumer support; an older CLI +ignores the visibility declaration. Historical unmarked sessions are not +rewritten or classified by prompt wording. diff --git a/src/amplifier_memory/suggest.py b/src/amplifier_memory/suggest.py index bcbbf54..bf87ca5 100644 --- a/src/amplifier_memory/suggest.py +++ b/src/amplifier_memory/suggest.py @@ -1060,12 +1060,22 @@ def default_model_call(prompt: str, *, timeout: float = 300.0, call: Judge | Non "`run_suggest` (and point AMPLIFIER_CONTEXT_INTELLIGENCE_BASE_PATH at a fixture)" ) argv = build_argv(prompt, call) + # Only this background judge is internal. Preserve routing/credentials in + # the child environment without relabelling the caller or other CLI jobs. + child_env = dict(os.environ) + child_env.update( + AMPLIFIER_SESSION_VISIBILITY="internal", + AMPLIFIER_SESSION_PURPOSE="memory.suggestion", + AMPLIFIER_SESSION_ORIGIN="agent", + ) # The command as run, minus the request itself: a failure names the flags that # produced it (a provider id that does not exist on this device is the likely one), # never the whole transcript. shown = " ".join(argv[:-1]) try: - proc = subprocess.run(argv, capture_output=True, text=True, check=False, timeout=timeout) + proc = subprocess.run( + argv, capture_output=True, text=True, check=False, timeout=timeout, env=child_env + ) except (OSError, ValueError, subprocess.TimeoutExpired) as exc: raise RuntimeError(f"{shown} failed: {type(exc).__name__}: {exc}") from exc if proc.returncode != 0: diff --git a/tests/test_suggest.py b/tests/test_suggest.py index 7d25311..6c585db 100644 --- a/tests/test_suggest.py +++ b/tests/test_suggest.py @@ -1287,3 +1287,34 @@ def test_a_run_over_a_worker_only_night_still_reports(tmp_path: Path, store: Pat assert report.status == "ok", "a night with no human sessions is not a degraded night" assert "sessions=0 origin_excluded=3" in report.log_line assert call.prompts == [], "no session read means no model call" + + +@pytest.mark.parametrize("returncode", [0, 1]) +def test_only_suggestion_child_is_marked_internal(monkeypatch, returncode): + """Explicit provenance reaches CLI without mutating this process or retrying.""" + import os + + seen = [] + monkeypatch.setenv("AMPLIFIER_SESSION_VISIBILITY", "chat") + monkeypatch.setenv("AMPLIFIER_SESSION_PURPOSE", "caller.task") + monkeypatch.setenv("AMPLIFIER_SESSION_ORIGIN", "human") + monkeypatch.setenv("SYNTHETIC_ROUTING_SETTING", "keep") + monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False) + before = dict(os.environ) + + def fake_run(argv, **kwargs): + seen.append((argv, kwargs)) + return subprocess.CompletedProcess(argv, returncode, '{"response":"[]"}', "synthetic failure") + + monkeypatch.setattr(suggest.subprocess, "run", fake_run) + if returncode: + with pytest.raises(RuntimeError, match="exited 1"): + suggest.default_model_call("synthetic") + else: + assert suggest.default_model_call("synthetic") == "[]" + assert len(seen) == 1 + argv, kwargs = seen[0] + assert argv == ["amplifier", "run", "--output-format", "json", "synthetic"] + assert kwargs["env"] == {**before, "AMPLIFIER_SESSION_VISIBILITY": "internal", + "AMPLIFIER_SESSION_PURPOSE": "memory.suggestion", "AMPLIFIER_SESSION_ORIGIN": "agent"} + assert dict(os.environ) == before