Loading results from the API… If the hosted API was asleep, this can take up to a minute.
Loading results from the API… If the hosted API was asleep, this can take up to a minute.
R1: request id and timestamp prepended to the system prompt 'for debugging'
Live runs are turned off on this public site. The results below are recorded; live runs happen on the owner's runner.
Recorded in RQ3 (2026-10-06). Its perf-gate figures predate the gate's warmup fix; see the corrections; the re-measured numbers are in the repair timeline below.
| check | selected by Guard | result | s | detail |
|---|---|---|---|---|
| unit:tests/test_retriever.py | no: exercises nothing this change touches | pass | 1.7 | 2 passed in 0.59s |
| unit:tests/test_prompts.py | yes | pass | 1.7 | 2 passed in 0.58s |
| check:citation_required | yes | pass | 12.6 | citation contract met in 0/34 answers on the PR vs 0/34 on base (-0.0 pts; budget -5 pts; p=1.00); via full answer path (incl. post-processing) |
| judge:ungrounded_answer | yes | skip | 0.0 | no judge model key configured (BYOK); judge not run |
| check:prompt_prefix_stable | yes | fail | 3.8 | requests share 15 leading characters (base 3954, need 3558) |
| check:context_budget | yes | pass | 7.9 | largest prompt 1025 tokens (base 980) against a budget of 3840; median 1008 vs 964 |
| security:approval_gate | yes | pass | 1.6 | 1 approval test(s) (generated from the base revision): 1 passed in 0.39s |
| bench:chat_perf_gate | yes | fail | 55.8 | median p95 TTFT 252.3 -> 1031.5 ms (+308.9%, block; 3 runs each, runs separated); prefix-cache hit 98% -> 0% |
# 2 regressions found: prompt_prefix_stable, chat_perf_gate conclusion: failure **Ran 7 of 8 suite items** selected from the behavior-to-code graph. Change categories: code, imports, prompt. ### Quality | check | result | detail | |---|---|---| | `check:citation_required` | **PASS** | citation contract met in 0/34 answers on the PR vs 0/34 on base (-0.0 pts; budget -5 pts; p=1.00); via full answer path (incl. post-processing) | | `judge:ungrounded_answer` | **SKIP** | no judge model key configured (BYOK); judge not run | ### Performance | check | result | detail | |---|---|---| | `check:prompt_prefix_stable` | **FAIL** | requests share 15 leading characters (base 3954, need 3558) | | `check:context_budget` | **PASS** | largest prompt 1025 tokens (base 980) against a budget of 3840; median 1008 vs 964 | | `bench:chat_perf_gate` | **FAIL** | median p95 TTFT 252.3 -> 1031.5 ms (+308.9%, block; 3 runs each, runs separated); prefix-cache hit 98% -> 0% | ### Security | check | result | detail | |---|---|---| | `security:approval_gate` | **PASS** | 1 approval test(s) (generated from the base revision): 1 passed in 0.39s | ### Tests | check | result | detail | |---|---|---| | `unit:tests/test_prompts.py` | **PASS** | 2 passed in 0.58s | <details><summary>Not run for this change</summary> - `unit:tests/test_retriever.py`: exercises nothing this change touches </details> ### What changed in the graph - `prompt:app/prompts.py::build_messages#system`: `static_chars` 3927 -> 3940; `static_prefix_chars` 3927 -> 8; `dynamic_head` False -> True
3/3 attempts verified. Furnace opens a draft pull request; a human merges.
generated furnace_harness.py from the route handler
wrote tests/furnace_regression/test_prefix_stability_a4eaf424.py: base shares 3954 chars, PR head shares 15
regression test fails on the PR head and passes on the base revision
2 hunk(s) in app/prompts.py::build_messages
rule:prefix_stability.move_dynamic_to_suffix: Moved the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the start of the system message to the end of the last user message. They are still sent to the model, but every request now begins with the same static system prompt and nothing per-request precedes the retrieved context, so the prefix cache can reuse both.
regression test pass, existing tests pass
geometric-mean TTFT base 91.0 | PR 91.0 -> 498.4 ms (+448.0%, 90% interval +270.0% to +711.7%; BLOCK at tolerance 10%) | repair 91.0 -> 99.6 ms (+9.5%, 90% interval -26.0% to +62.1%; INCONCLUSIVE at tolerance 10%)
rule:prefix_stability.move_dynamic_to_suffix rejected; trying the next strategy
rule:prefix_stability.move_dynamic_to_log: Removed the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the system message and log them with the standard logging module just before the messages are built. The debugging information is kept in the application log, and every request again starts with exactly the base revision's prompt, so the prefix cache reuses all of it. Trade-off: the model no longer sees these values; if it needs them, the suffix variant keeps them in the prompt at the measured latency cost.
regression test pass, existing tests pass
geometric-mean TTFT base 68.6 | PR 68.6 -> 494.2 ms (+620.7%, 90% interval +590.3% to +652.5%; BLOCK at tolerance 10%) | repair 68.6 -> 71.5 ms (+4.2%, 90% interval -0.8% to +9.5%; PASS at tolerance 10%)
Failing test first: fails on the PR head true, passes on the base revision true.
| revision | base geometric-mean TTFT, ms | geometric-mean TTFT, ms | change | interval | prefix-cache hit | runs | gate |
|---|---|---|---|---|---|---|---|
| PR head | 68.6 | 494.2 | +620.7% | +590.3% to +652.5% | 98% → 0% | separated | block |
| repair | 68.6 | 71.5 | +4.2% | -0.8% to +9.5% | 98% → 98% | overlap | pass |
geometric-mean TTFT base 91.0 | PR 91.0 -> 498.4 ms (+448.0%, 90% interval +270.0% to +711.7%; BLOCK at tolerance 10%) | repair 91.0 -> 99.6 ms (+9.5%, 90% interval -26.0% to +62.1%; INCONCLUSIVE at tolerance 10%)
Moved the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the start of the system message to the end of the last user message. They are still sent to the model, but every request now begins with the same static system prompt and nothing per-request precedes the retrieved context, so the prefix cache can reuse both.
geometric-mean TTFT base 68.6 | PR 68.6 -> 494.2 ms (+620.7%, 90% interval +590.3% to +652.5%; BLOCK at tolerance 10%) | repair 68.6 -> 71.5 ms (+4.2%, 90% interval -0.8% to +9.5%; PASS at tolerance 10%)
Removed the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the system message and log them with the standard logging module just before the messages are built. The debugging information is kept in the application log, and every request again starts with exactly the base revision's prompt, so the prefix cache reuses all of it. Trade-off: the model no longer sees these values; if it needs them, the suffix variant keeps them in the prompt at the measured latency cost.
Draft PR diff (fix + regression test)
--- a/app/prompts.py+++ b/app/prompts.py@@ -1,3 +1,4 @@+import loggingimport uuidfrom datetime import UTC, datetime@@ -59,10 +60,13 @@def build_messages(question: str, chunks: list[Chunk], max_chars: int) -> list[dict[str, str]]:context = build_context(chunks, max_chars)+ logging.getLogger(__name__).info(+ "per-request values (logged, not sent in the prompt): %s", f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n"+ )return [{"role": "system",- "content": f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" + SYSTEM_PROMPT,+ "content": SYSTEM_PROMPT,},{"role": "user", "content": f"CONTEXT:\n{context}\n\nQUESTION: {question}"},]--- a/furnace_harness.py+++ b/furnace_harness.py@@ -0,0 +1,34 @@+"""Generated by Furnace Forge from app/main.py::chat.++render(question): the exact chat messages the handler sends to the model, without+starting the server or calling the model.+answer(question): the handler's full answer path (model call and any output+post-processing), as text. Needs the app's model endpoint.+Regenerate this file if that handler changes."""++from types import SimpleNamespace++from app.main import build_messages, rag_config, retrieve, stream_answer+++def render(question: str) -> list[dict]:+ req = SimpleNamespace(question=question)+ cfg = rag_config()+ chunks = retrieve(req.question, top_k=cfg["retrieval"]["top_k"])+ messages = build_messages(req.question, chunks, cfg["retrieval"]["max_context_chars"])+ return messages+++def answer(question: str) -> str:+ req = SimpleNamespace(question=question)+ cfg = rag_config()+ chunks = retrieve(req.question, top_k=cfg["retrieval"]["top_k"])+ messages = build_messages(req.question, chunks, cfg["retrieval"]["max_context_chars"])+ gen = cfg["generation"]+ out = stream_answer(+ messages,+ max_tokens=gen["max_tokens"],+ temperature=gen["temperature"],+ route="/chat",+ )+ return out if isinstance(out, str) else "".join(out)--- a/tests/furnace_regression/test_prefix_stability_a4eaf424.py+++ b/tests/furnace_regression/test_prefix_stability_a4eaf424.py@@ -0,0 +1,37 @@+"""Generated by Furnace Guard: regression test written before any fix was attempted.++Failure mode: prefix instability (dynamic value at the head of the system prompt)+Change under test: r1_dynamic_head++Every request must start with the same prompt text so the serving engine can reuse+the cached prefill of that shared prefix (prefix/KV caching). On the base revision,+two different requests shared 3954 leading characters; this test requires at+least 3558 (90% of that).+"""++from furnace_harness import render++MIN_SHARED_CHARS = 3558+QUESTIONS = ['How much does the Studio plan cost?', 'How many kilns does the Workshop plan support?', 'Is there a free trial?', 'When are invoices issued?', 'Which payment methods are accepted?', 'How do I get a refund?']+++def _text(question: str) -> str:+ return "".join(f"{m['role']}\n{m['content']}\n" for m in render(question))+++def _shared(a: str, b: str) -> int:+ n = min(len(a), len(b))+ i = 0+ while i < n and a[i] == b[i]:+ i += 1+ return i+++def test_prompt_prefix_is_shared_across_requests():+ first = _text(QUESTIONS[0])+ for q in QUESTIONS[1:]:+ shared = _shared(first, _text(q))+ assert shared >= MIN_SHARED_CHARS, (+ f"requests diverge after {shared} characters (need {MIN_SHARED_CHARS}); "+ f"first difference: {first[shared:shared + 60]!r}"+ )
generated furnace_harness.py from the route handler
wrote tests/furnace_regression/test_prefix_stability_a4eaf424.py: base shares 3954 chars, PR head shares 15
regression test fails on the PR head and passes on the base revision
2 hunk(s) in app/prompts.py::build_messages
rule:prefix_stability.move_dynamic_to_suffix: Moved the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the start of the system message to the end of the last user message. They are still sent to the model, but every request now begins with the same static system prompt and nothing per-request precedes the retrieved context, so the prefix cache can reuse both.
regression test pass, existing tests pass
geometric-mean TTFT base 71.3 | PR 71.3 -> 501.7 ms (+603.5%, 90% interval +556.3% to +654.0%; BLOCK at tolerance 10%) | repair 71.3 -> 98.3 ms (+37.8%, 90% interval +28.6% to +47.6%; BLOCK at tolerance 10%)
rule:prefix_stability.move_dynamic_to_suffix rejected; trying the next strategy
rule:prefix_stability.move_dynamic_to_log: Removed the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the system message and log them with the standard logging module just before the messages are built. The debugging information is kept in the application log, and every request again starts with exactly the base revision's prompt, so the prefix cache reuses all of it. Trade-off: the model no longer sees these values; if it needs them, the suffix variant keeps them in the prompt at the measured latency cost.
regression test pass, existing tests pass
geometric-mean TTFT base 70.5 | PR 70.5 -> 494.7 ms (+601.8%, 90% interval +577.9% to +626.5%; BLOCK at tolerance 10%) | repair 70.5 -> 71.1 ms (+0.9%, 90% interval -2.2% to +4.1%; PASS at tolerance 10%)
Failing test first: fails on the PR head true, passes on the base revision true.
| revision | base geometric-mean TTFT, ms | geometric-mean TTFT, ms | change | interval | prefix-cache hit | runs | gate |
|---|---|---|---|---|---|---|---|
| PR head | 70.5 | 494.7 | +601.8% | +577.9% to +626.5% | 98% → 0% | separated | block |
| repair | 70.5 | 71.1 | +0.9% | -2.2% to +4.1% | 98% → 98% | overlap | pass |
geometric-mean TTFT base 71.3 | PR 71.3 -> 501.7 ms (+603.5%, 90% interval +556.3% to +654.0%; BLOCK at tolerance 10%) | repair 71.3 -> 98.3 ms (+37.8%, 90% interval +28.6% to +47.6%; BLOCK at tolerance 10%)
Moved the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the start of the system message to the end of the last user message. They are still sent to the model, but every request now begins with the same static system prompt and nothing per-request precedes the retrieved context, so the prefix cache can reuse both.
geometric-mean TTFT base 70.5 | PR 70.5 -> 494.7 ms (+601.8%, 90% interval +577.9% to +626.5%; BLOCK at tolerance 10%) | repair 70.5 -> 71.1 ms (+0.9%, 90% interval -2.2% to +4.1%; PASS at tolerance 10%)
Removed the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the system message and log them with the standard logging module just before the messages are built. The debugging information is kept in the application log, and every request again starts with exactly the base revision's prompt, so the prefix cache reuses all of it. Trade-off: the model no longer sees these values; if it needs them, the suffix variant keeps them in the prompt at the measured latency cost.
Draft PR diff (fix + regression test)
--- a/app/prompts.py+++ b/app/prompts.py@@ -1,3 +1,4 @@+import loggingimport uuidfrom datetime import UTC, datetime@@ -59,10 +60,13 @@def build_messages(question: str, chunks: list[Chunk], max_chars: int) -> list[dict[str, str]]:context = build_context(chunks, max_chars)+ logging.getLogger(__name__).info(+ "per-request values (logged, not sent in the prompt): %s", f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n"+ )return [{"role": "system",- "content": f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" + SYSTEM_PROMPT,+ "content": SYSTEM_PROMPT,},{"role": "user", "content": f"CONTEXT:\n{context}\n\nQUESTION: {question}"},]--- a/furnace_harness.py+++ b/furnace_harness.py@@ -0,0 +1,34 @@+"""Generated by Furnace Forge from app/main.py::chat.++render(question): the exact chat messages the handler sends to the model, without+starting the server or calling the model.+answer(question): the handler's full answer path (model call and any output+post-processing), as text. Needs the app's model endpoint.+Regenerate this file if that handler changes."""++from types import SimpleNamespace++from app.main import build_messages, rag_config, retrieve, stream_answer+++def render(question: str) -> list[dict]:+ req = SimpleNamespace(question=question)+ cfg = rag_config()+ chunks = retrieve(req.question, top_k=cfg["retrieval"]["top_k"])+ messages = build_messages(req.question, chunks, cfg["retrieval"]["max_context_chars"])+ return messages+++def answer(question: str) -> str:+ req = SimpleNamespace(question=question)+ cfg = rag_config()+ chunks = retrieve(req.question, top_k=cfg["retrieval"]["top_k"])+ messages = build_messages(req.question, chunks, cfg["retrieval"]["max_context_chars"])+ gen = cfg["generation"]+ out = stream_answer(+ messages,+ max_tokens=gen["max_tokens"],+ temperature=gen["temperature"],+ route="/chat",+ )+ return out if isinstance(out, str) else "".join(out)--- a/tests/furnace_regression/test_prefix_stability_a4eaf424.py+++ b/tests/furnace_regression/test_prefix_stability_a4eaf424.py@@ -0,0 +1,37 @@+"""Generated by Furnace Guard: regression test written before any fix was attempted.++Failure mode: prefix instability (dynamic value at the head of the system prompt)+Change under test: r1_dynamic_head++Every request must start with the same prompt text so the serving engine can reuse+the cached prefill of that shared prefix (prefix/KV caching). On the base revision,+two different requests shared 3954 leading characters; this test requires at+least 3558 (90% of that).+"""++from furnace_harness import render++MIN_SHARED_CHARS = 3558+QUESTIONS = ['How much does the Studio plan cost?', 'How many kilns does the Workshop plan support?', 'Is there a free trial?', 'When are invoices issued?', 'Which payment methods are accepted?', 'How do I get a refund?']+++def _text(question: str) -> str:+ return "".join(f"{m['role']}\n{m['content']}\n" for m in render(question))+++def _shared(a: str, b: str) -> int:+ n = min(len(a), len(b))+ i = 0+ while i < n and a[i] == b[i]:+ i += 1+ return i+++def test_prompt_prefix_is_shared_across_requests():+ first = _text(QUESTIONS[0])+ for q in QUESTIONS[1:]:+ shared = _shared(first, _text(q))+ assert shared >= MIN_SHARED_CHARS, (+ f"requests diverge after {shared} characters (need {MIN_SHARED_CHARS}); "+ f"first difference: {first[shared:shared + 60]!r}"+ )
generated furnace_harness.py from the route handler
wrote tests/furnace_regression/test_prefix_stability_a4eaf424.py: base shares 3954 chars, PR head shares 15
regression test fails on the PR head and passes on the base revision
2 hunk(s) in app/prompts.py::build_messages
rule:prefix_stability.move_dynamic_to_suffix: Moved the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the start of the system message to the end of the last user message. They are still sent to the model, but every request now begins with the same static system prompt and nothing per-request precedes the retrieved context, so the prefix cache can reuse both.
regression test pass, existing tests pass
geometric-mean TTFT base 70.7 | PR 70.7 -> 502.7 ms (+610.8%, 90% interval +597.8% to +624.1%; BLOCK at tolerance 10%) | repair 70.7 -> 98.7 ms (+39.6%, 90% interval +33.7% to +45.8%; BLOCK at tolerance 10%)
rule:prefix_stability.move_dynamic_to_suffix rejected; trying the next strategy
rule:prefix_stability.move_dynamic_to_log: Removed the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the system message and log them with the standard logging module just before the messages are built. The debugging information is kept in the application log, and every request again starts with exactly the base revision's prompt, so the prefix cache reuses all of it. Trade-off: the model no longer sees these values; if it needs them, the suffix variant keeps them in the prompt at the measured latency cost.
regression test pass, existing tests pass
geometric-mean TTFT base 69.1 | PR 69.1 -> 501.6 ms (+626.1%, 90% interval +608.8% to +643.8%; BLOCK at tolerance 10%) | repair 69.1 -> 70.2 ms (+1.7%, 90% interval -3.6% to +7.2%; PASS at tolerance 10%)
Failing test first: fails on the PR head true, passes on the base revision true.
| revision | base geometric-mean TTFT, ms | geometric-mean TTFT, ms | change | interval | prefix-cache hit | runs | gate |
|---|---|---|---|---|---|---|---|
| PR head | 69.1 | 501.6 | +626.1% | +608.8% to +643.8% | 98% → 0% | separated | block |
| repair | 69.1 | 70.2 | +1.7% | -3.6% to +7.2% | 98% → 98% | overlap | pass |
geometric-mean TTFT base 70.7 | PR 70.7 -> 502.7 ms (+610.8%, 90% interval +597.8% to +624.1%; BLOCK at tolerance 10%) | repair 70.7 -> 98.7 ms (+39.6%, 90% interval +33.7% to +45.8%; BLOCK at tolerance 10%)
Moved the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the start of the system message to the end of the last user message. They are still sent to the model, but every request now begins with the same static system prompt and nothing per-request precedes the retrieved context, so the prefix cache can reuse both.
geometric-mean TTFT base 69.1 | PR 69.1 -> 501.6 ms (+626.1%, 90% interval +608.8% to +643.8%; BLOCK at tolerance 10%) | repair 69.1 -> 70.2 ms (+1.7%, 90% interval -3.6% to +7.2%; PASS at tolerance 10%)
Removed the per-request value(s) f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" from the system message and log them with the standard logging module just before the messages are built. The debugging information is kept in the application log, and every request again starts with exactly the base revision's prompt, so the prefix cache reuses all of it. Trade-off: the model no longer sees these values; if it needs them, the suffix variant keeps them in the prompt at the measured latency cost.
Draft PR diff (fix + regression test)
--- a/app/prompts.py+++ b/app/prompts.py@@ -1,3 +1,4 @@+import loggingimport uuidfrom datetime import UTC, datetime@@ -59,10 +60,13 @@def build_messages(question: str, chunks: list[Chunk], max_chars: int) -> list[dict[str, str]]:context = build_context(chunks, max_chars)+ logging.getLogger(__name__).info(+ "per-request values (logged, not sent in the prompt): %s", f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n"+ )return [{"role": "system",- "content": f"Request {uuid.uuid4().hex[:8]} at {datetime.now(UTC).isoformat()}\n" + SYSTEM_PROMPT,+ "content": SYSTEM_PROMPT,},{"role": "user", "content": f"CONTEXT:\n{context}\n\nQUESTION: {question}"},]--- a/furnace_harness.py+++ b/furnace_harness.py@@ -0,0 +1,34 @@+"""Generated by Furnace Forge from app/main.py::chat.++render(question): the exact chat messages the handler sends to the model, without+starting the server or calling the model.+answer(question): the handler's full answer path (model call and any output+post-processing), as text. Needs the app's model endpoint.+Regenerate this file if that handler changes."""++from types import SimpleNamespace++from app.main import build_messages, rag_config, retrieve, stream_answer+++def render(question: str) -> list[dict]:+ req = SimpleNamespace(question=question)+ cfg = rag_config()+ chunks = retrieve(req.question, top_k=cfg["retrieval"]["top_k"])+ messages = build_messages(req.question, chunks, cfg["retrieval"]["max_context_chars"])+ return messages+++def answer(question: str) -> str:+ req = SimpleNamespace(question=question)+ cfg = rag_config()+ chunks = retrieve(req.question, top_k=cfg["retrieval"]["top_k"])+ messages = build_messages(req.question, chunks, cfg["retrieval"]["max_context_chars"])+ gen = cfg["generation"]+ out = stream_answer(+ messages,+ max_tokens=gen["max_tokens"],+ temperature=gen["temperature"],+ route="/chat",+ )+ return out if isinstance(out, str) else "".join(out)--- a/tests/furnace_regression/test_prefix_stability_a4eaf424.py+++ b/tests/furnace_regression/test_prefix_stability_a4eaf424.py@@ -0,0 +1,37 @@+"""Generated by Furnace Guard: regression test written before any fix was attempted.++Failure mode: prefix instability (dynamic value at the head of the system prompt)+Change under test: r1_dynamic_head++Every request must start with the same prompt text so the serving engine can reuse+the cached prefill of that shared prefix (prefix/KV caching). On the base revision,+two different requests shared 3954 leading characters; this test requires at+least 3558 (90% of that).+"""++from furnace_harness import render++MIN_SHARED_CHARS = 3558+QUESTIONS = ['How much does the Studio plan cost?', 'How many kilns does the Workshop plan support?', 'Is there a free trial?', 'When are invoices issued?', 'Which payment methods are accepted?', 'How do I get a refund?']+++def _text(question: str) -> str:+ return "".join(f"{m['role']}\n{m['content']}\n" for m in render(question))+++def _shared(a: str, b: str) -> int:+ n = min(len(a), len(b))+ i = 0+ while i < n and a[i] == b[i]:+ i += 1+ return i+++def test_prompt_prefix_is_shared_across_requests():+ first = _text(QUESTIONS[0])+ for q in QUESTIONS[1:]:+ shared = _shared(first, _text(q))+ assert shared >= MIN_SHARED_CHARS, (+ f"requests diverge after {shared} characters (need {MIN_SHARED_CHARS}); "+ f"first difference: {first[shared:shared + 60]!r}"+ )