From 7af38cf4bfae760c0e4891f23f0fab0122172a65 Mon Sep 17 00:00:00 2001 From: amplifier-lane Date: Wed, 2 Sep 2026 16:46:34 -0700 Subject: [PATCH] fix: _capture_fork_prefix no longer forks onto an array that was never sent (#jnt) `_capture_fork_prefix()` preferred the caller's recorded wire array (`_sent_messages`) only while `_sent_serial == _view_serial`, and silently substituted `_last_request_view` whenever that equality failed. `_view_serial` counts VIEWS SERVED, not REQUESTS SENT, and the two are not 1:1: loop-streaming re-fetches the view (get_messages_for_request at :3215, :3329, :3453) up to three times per sent request, and the summary trigger is evaluated inside every one of them. On a re-fetch the serials differ, and the substituted `_last_request_view` holds the view the re-fetch just superseded -- built, discarded, never on the wire. Measured (model_performance-6da, 20 forked calls): 9 appended to a request the provider actually saw; 11 appended to an array never sent as any request. `mode_used` reported "fork" for all 20. Fix: the recorded wire array is the only source with positive evidence of having been sent, so it is never traded for one with none. Extra views are reported, not acted on. Staleness is caught exactly (span-presence) and refuses LOUDLY -- standalone + WARNING + counter -- instead of substituting. The module-view path (no `messages` supplied) is unchanged. Adds `prefix_source` and `prefix_views_since_send` to `last_summary_call_stats` so a measurement can separate the two prefix populations from the module's own report. Default `summary_call_mode` remains "standalone"; the default path is untouched. 286 passed (was 281), ruff clean. --- DONE-NOTE.md | 174 +++++++++++++++ amplifier_module_context_simple/__init__.py | 138 ++++++++++-- tests/test_summary_call_mode_fork.py | 229 +++++++++++++++++++- 3 files changed, 511 insertions(+), 30 deletions(-) diff --git a/DONE-NOTE.md b/DONE-NOTE.md index 0bd7671..c790066 100644 --- a/DONE-NOTE.md +++ b/DONE-NOTE.md @@ -1129,3 +1129,177 @@ would bury this change in unrelated noise. No PII, no team-internal data, no individual attribution. No merges to main. No files touched outside this module. No infrastructure created; nothing to tear down. + +--- + +# DONE-NOTE - model_performance-jnt + +**`_capture_fork_prefix()` appends to an array that was never sent (11 of 20 +forks measured).** + +**Verdict: CORRUPTING, not cosmetic.** It is not dead code and it produces a +record. It silently substitutes a message array that never went on the wire +for one that did, on a path whose entire purpose is byte-parity with the wire. +Fixed. **No 6da measured number is invalidated** — see §3. + +## 1. The array, named, at file:line + +Everything below is `amplifier_module_context_simple/__init__.py` at the +pre-fix commit `a877b36`. + +| | | +|---|---| +| **the array** | **`self._last_request_view`** | +| created | **:1015** (`__init__`), written **:1778** inside `_finalize_view()` | +| what it holds | this module's own last RETURNED view, pre-strip | +| why it is "never sent" | it is written on **every view served**, and a view served is not a request sent | +| consumed by | **:4187** `_capture_fork_prefix()`, as the fallback source | +| flows to | `_maybe_trigger_summary_compaction` **:4040** → `_run_summary_compaction_task` → `_build_fork_request` **:4422** → the provider | + +The selection at **:4187–4198** preferred the caller's recorded wire array +(`_sent_messages`, **:1017/:1210**) only while +`_sent_serial == _view_serial` (**:4189**), and substituted +`_last_request_view` whenever that equality failed (**:4187**, **:4192–4197** +— `logger.debug`, not warning). + +**Why the equality fails in production.** `_view_serial` counts **views +served**, not **requests sent**, and the two are not 1:1. The real +orchestrator serves the view more than once per sent request: +`amplifier-module-loop-streaming/__init__.py` calls +`context.get_messages_for_request()` at **:3215** and then re-fetches at +**:3329** and **:3453** after persisting an ephemeral injection — up to three +views for one `ChatRequest`. The summary trigger is evaluated inside *every* +one of them (**:1364**, which runs *before* that call's `_finalize_view` at +**:1480/:1482**). So: + +- trigger on the **first** view of a request → serials match → the wire array + is used → **correct fork** (wire offset 0 or 1); +- trigger on a **re-fetched** view → serials differ by 1–2 → the wire array is + discarded in favour of `_last_request_view`, which at that instant holds + **the view the re-fetch just superseded** — built, thrown away, never sent. + +That is the 9-vs-11 split 6da measured, and it reproduces exactly. + +## 2. Consequence — unambiguous + +**Corrupting to fork-mode behaviour, and self-concealing.** + +1. **Guaranteed cache miss.** A superseded view is not a prefix any provider + holds. Fork mode's only justification is appending onto a cached prefix; a + fork that misses pays full price for the whole conversation, which is + *strictly worse* than the standalone call it replaces. +2. **Silent.** `last_summary_call_stats["mode_used"]` reported `"fork"` for + all 20 calls. The substitution was `logger.debug`. There was no field + distinguishing the two sources — this is precisely why 6da had to + reconstruct the distinction from the provider's request log. +3. **Backwards under uncertainty.** The check traded the array with *positive + evidence* of having been sent (the caller said so) for one with *none* + (fate unknown to this module), and did so exactly when uncertainty was + highest. + +Not affected: history, `_seq` allocation, span selection, tool-pair +integrity, the served view, or any default-mode behaviour. The blast radius +is fork mode's cache economics and the honesty of its self-report. + +## 3. Does this invalidate any of 6da's measured numbers? **NO.** + +Stated plainly for the manager, because the item asked for it loudly: + +- **G-FORK-PREFIX (2/7/11 offset distribution, 45% aligned) — VALID, and is + the direct measurement of this bug.** Scored from the wire, not from the + module's self-report. +- **G-FORK-CACHED, G-FORK-NOBOUNDARY, the Anthropic guardrail, quality + parity, and the −0.8% run-cost delta — VALID.** All are wire/usage-derived + and none depend on `_capture_fork_prefix()` having chosen correctly. +- **The §7 correction (summarizer share ≈30%, not 2.4%/8.3–10.9%) — VALID + and untouched.** Independent of the fork path. + +**One caveat, in 6da's favour, not against it:** the −0.8% run-cost delta was +measured with only ~45% of forks byte-aligned. It is a **lower bound** on +what a correctly-aligned fork arm would deliver, not an upper bound. 6da's +"the mechanism works and the lever does not pay / DON'T-SHIP as-is" verdict +therefore **still stands as written**, but its cost figure is now known to +have been measured on a partially-broken treatment and should be **re-measured +before the DON'T-SHIP call is made final**. 6da itself flagged this +("one of them has a cheap fix worth a follow-up item"); this is that fix. + +## 4. The fix (minimal) + +`_capture_fork_prefix()` (**:4176**) no longer substitutes: + +- a recorded wire array, when one exists, is **used** — it is the only source + carrying positive evidence it was on the wire, so it is never traded for one + that carries none. Extra views served since the send are **reported, not + acted on**; +- the module's own view is used **only** when the caller has never supplied a + message array (the documented explicit-breakpoint/Anthropic path, unchanged); +- staleness in the wire record is still caught, but by an **exact** check + instead of a proxy: a record too old to contain the span fails + `_prefix_contains_span` and **refuses LOUDLY** — standalone call, `WARNING`, + `_summary_fork_fallbacks` incremented, named `reason`. "A fork that silently + missed" is no longer reachable on this path. + +Also added, because 6da needed it and could not get it: **`prefix_source`** +(`"wire_record"` / `"module_view"` / `None`) and **`prefix_views_since_send`** +on `last_summary_call_stats`. The next arm can separate the two populations +from the module's own report instead of reconstructing them from the wire. + +**Config surface: unchanged. Default `summary_call_mode` remains +`"standalone"`.** + +## 5. Tests + +`286 passed, 1 skipped` (was 281 passed at `a877b36`). `ruff check`: clean. + +New Group F in `tests/test_summary_call_mode_fork.py`: + +| Test | Pins | +|---|---| +| `test_a_re_fetched_view_does_not_displace_the_recorded_wire_array` | THE regression: a superseding re-fetch must not displace the wire array | +| `test_the_module_view_is_never_substituted_when_a_wire_record_exists` | same defect from the other side: never-sent content cannot reach the fork | +| `test_prefix_source_names_the_module_view_path_honestly` | the module-view path is allowed but reported as what it is | +| `test_prefix_source_is_none_when_the_call_did_not_fork` | a refused fork claims no alignment | +| `test_tool_pair_integrity_and_seq_stability_survive_the_re_fetch_path` | no `_seq` consumed, history byte-identical, same span absorbed, served view identical to an unforked control | + +**These three fail against the pre-fix selection logic and pass against the +fix** — verified by temporarily restoring the old branch and re-running; they +are load-bearing, not decoration. + +One existing test changed: `test_a_stale_caller_message_record_is_ignored_not_trusted` +→ `test_a_stale_caller_message_record_refuses_loudly_not_silently`. It +asserted the substitution *as correct behaviour*; it now asserts the loud +refusal. The rewritten docstring records why the original resolution was +wrong, so the reversal is not silent. + +Default-mode byte-identity re-verified by the pre-existing Group A/C tests, +strengthened with `assert context._fork_prefix_source is None` in +`test_default_mode_never_records_a_fork_prefix`. + +## 6. Residual, disclosed + +The **module-view path** (`note_request_sent(tools=...)` with no `messages`) +can still append to a superseded view — this module genuinely cannot know +whether its own view was sent. Not silently, now: `prefix_source == +"module_view"` says so on every call. **A caller that wants byte-parity must +pass `messages`.** Closing this properly needs a caller-side confirmation +signal, which is an orchestrator change and out of this lane's scope. + +Unchanged and still true: fork mode cannot fork the **first** summarization of +a CLI turn (each turn is a fresh `amplifier run --resume` process, and the +trigger is evaluated before any request is sent). 6da measured 12 of 24 +refusals from this; this fix does not address it. + +## 7. Deliverable ledger + +| Deliverable | Status | +|---|---| +| DRAFT PR on origin, branch `lane/jnt-fork-prefix-capture`, tests green, default inline byte-identical | **DONE** | +| The array named at file:line with why it was never sent + cosmetic-vs-corrupting verdict | **DONE** — §1, §2 (**corrupting**) | +| Explicit statement of whether any of 6da's measured fork numbers are invalidated | **DONE** — §3 (**none invalidated**; −0.8% is a lower bound and warrants re-measurement) | +| DONE-NOTE.md in the PR body | **DONE** — this section | + +**Spend: $0.00.** No API calls, no DTU, no containers, no infrastructure +created — the item was answerable from the code, the shipped tests, and 6da's +existing evidence files. Nothing to tear down; nothing registered in the infra +ledger. No PII or team-internal data. No merge to main. No files touched +outside this module. diff --git a/amplifier_module_context_simple/__init__.py b/amplifier_module_context_simple/__init__.py index 9fed112..47f8055 100644 --- a/amplifier_module_context_simple/__init__.py +++ b/amplifier_module_context_simple/__init__.py @@ -360,6 +360,24 @@ # "inline" is accepted as an alias for "standalone": the lane brief that # commissioned this work named the default mode "inline" while the work # item named it "standalone". Both mean "today's behavior, unchanged". +# Where a forked summarizer call's prefix came from, reported verbatim on +# `last_summary_call_stats["prefix_source"]`. This distinction is not +# cosmetic: only ONE of these two sources carries positive evidence that +# the array was ever on the wire. +# +# "wire_record" -- the caller's own note_request_sent(messages=...) +# record. The caller told us it sent exactly this. +# "module_view" -- this module's last RETURNED view. Whether it was +# actually sent is unknown to this module: an +# orchestrator may append a tail to it, or discard it +# entirely and re-fetch a fresh view before sending +# (amplifier's loop-streaming does exactly that, 1-3 +# times per sent request). A superseded view was never +# on the wire, so forking onto one is a guaranteed +# cache miss wearing a correct-looking API call. +FORK_PREFIX_SOURCE_WIRE = "wire_record" +FORK_PREFIX_SOURCE_VIEW = "module_view" + SUMMARY_CALL_MODE_STANDALONE = "standalone" SUMMARY_CALL_MODE_FORK = "fork" _VALID_SUMMARY_CALL_MODES = (SUMMARY_CALL_MODE_STANDALONE, SUMMARY_CALL_MODE_FORK) @@ -1019,6 +1037,11 @@ def __init__( self._sent_tools: Any = None self._sent_tools_supplied: bool = False self._sent_model: str | None = None + # Which of the two sources the last captured fork prefix came from + # (FORK_PREFIX_SOURCE_*). Reported on `last_summary_call_stats` so a + # measurement can tell a wire-parity fork from a module-view fork + # WITHOUT reconstructing it from the provider's request log. + self._fork_prefix_source: str | None = None # Observability: what the LAST summarizer call actually did, and how # many times a requested fork had to fall back. An eval arm reads # these to tell a real fork from a silently unforked one. @@ -1180,12 +1203,19 @@ def note_request_sent( enters `self.messages`, consumes a `_seq`, or moves any compaction state. Callers that do not know about it lose nothing. - Call it EVERY request, not once. A `messages` record is only used - while it still describes the most recent request this module served - -- a one-time wiring would otherwise have turn 40's fork append to - turn 1's request, which is a guaranteed cache miss dressed up as a - correct API call. A stale record is ignored (this module's own last - view is used instead), never trusted. + Call it EVERY request, not once. The most recent `messages` record + is the only array this module has positive evidence was ever on the + wire, so it is what a fork appends to -- a one-time wiring would + have turn 40's fork append to turn 1's request. That case is not + silently rerouted (rerouting is what produced the never-sent-array + bug); it is caught exactly, by the span-presence check, and refused + LOUDLY as a standalone call with a named reason. + + Calling it more than once per request is harmless, and so is + serving the view more than once per request: extra views do not + invalidate the record. `last_summary_call_stats` reports + `prefix_views_since_send` so that re-fetching is observable rather + than inferred. Args: messages: The exact message array sent, if known. Gives the fork @@ -1220,9 +1250,11 @@ def last_summary_call_stats(self) -> dict[str, Any] | None: None before the first one. Otherwise a dict with `mode_requested`, `mode_used`, `reason` (None when the requested mode was honored), - `prefix_messages`, and `fork_fallbacks` (session-cumulative). This - is how an eval arm distinguishes a real fork from a silently - unforked one WITHOUT patching the module. + `prefix_messages`, `prefix_source` (FORK_PREFIX_SOURCE_*, None when + not forked), `prefix_views_since_send`, and `fork_fallbacks` + (session-cumulative). This is how an eval arm distinguishes a real + fork from a silently unforked one -- and a wire-parity fork from a + module-view one -- WITHOUT patching the module. """ return dict(self._last_summary_call) if self._last_summary_call else None @@ -1630,6 +1662,7 @@ def _reset_summary_strategy_state(self) -> None: self._sent_tools = None self._sent_tools_supplied = False self._sent_model = None + self._fork_prefix_source = None async def should_compact(self) -> bool: """Check if context should be compacted. @@ -4176,26 +4209,68 @@ def _fork_armed(self) -> bool: def _capture_fork_prefix(self) -> list[dict[str, Any]] | None: """Snapshot the message array a forked call would append to. - Prefers the caller's own `note_request_sent(messages=...)` record - (byte-parity with the wire, including any tail the orchestrator - injected after this module returned). Falls back to this module's - last returned view, which still ends exactly where an + Uses the caller's own `note_request_sent(messages=...)` record when + there is one -- byte-parity with the wire, including any tail the + orchestrator injected after this module returned. Only when the + caller has never supplied a message array does this fall back to + the module's own last returned view, which is where an explicit-breakpoint provider places its cache breakpoint. + + WHY THERE IS NO SILENT DOWNGRADE BETWEEN THE TWO (the bug this + replaced): the previous implementation preferred the wire record + only while `_sent_serial == _view_serial`, and substituted + `_last_request_view` whenever that equality failed. That equality + asks "have any views been served since the caller last confirmed a + send?" -- which conflates two situations a view counter cannot tell + apart, and gets the important one backwards. + + A real orchestrator serves the view MORE THAN ONCE per sent request + (amplifier's loop-streaming re-fetches after persisting an ephemeral + injection -- `get_messages_for_request()` at three separate call + sites in one iteration). The summary trigger is evaluated inside + EVERY one of those calls. On the second and third, the serials no + longer match, and the old code swapped the caller's genuine wire + array for `_last_request_view` -- which at that instant holds a view + that was built, superseded by the re-fetch, and NEVER SENT. + + Measured on the wire (model_performance-6da, 20 forked calls): + 9 appended to a request the provider actually saw; the other 11 + appended to an array that was never sent as any request. The + substitution was invisible from outside -- `mode_used` reported + "fork" either way. + + The asymmetry that settles it: the wire record is the only source + carrying positive evidence that it was ever on the wire, so it is + never traded for one that carries none. Staleness in the wire record + is still caught, but by an EXACT check rather than a proxy: a record + too old to contain the span being absorbed fails + `_prefix_contains_span` and refuses LOUDLY (standalone + warning + + counter), which is the outcome a fork that cannot be byte-aligned is + supposed to have. """ if not self._fork_armed(): return None - source = self._last_request_view if self._sent_messages is not None: - if self._sent_serial == self._view_serial: - source = self._sent_messages - else: + self._fork_prefix_source = FORK_PREFIX_SOURCE_WIRE + if self._sent_serial != self._view_serial: + # Not an error, and deliberately not a substitution: the + # caller has served extra views since confirming this send + # (a re-fetch, or a view that was never sent at all). The + # last CONFIRMED array is still the best-evidenced prefix. logger.debug( - "context-simple: ignoring a stale note_request_sent() " - f"message record (recorded at view {self._sent_serial}, " - f"now at view {self._view_serial}); appending to this " - "module's own last returned view instead" + "context-simple: fork prefix is the caller's recorded " + f"wire array from view {self._sent_serial} (now at view " + f"{self._view_serial}); {self._view_serial - (self._sent_serial or 0)} " + "view(s) have been served since it was confirmed sent, " + "which is normal for an orchestrator that re-fetches the " + "view within a single request" ) - return list(source) if source is not None else None + return list(self._sent_messages) + if self._last_request_view is not None: + self._fork_prefix_source = FORK_PREFIX_SOURCE_VIEW + return list(self._last_request_view) + self._fork_prefix_source = None + return None @staticmethod def _message_identity(msg: dict[str, Any]) -> tuple[str, str, str]: @@ -4474,6 +4549,25 @@ async def _run_summary_compaction_task( if call_mode == SUMMARY_CALL_MODE_FORK else 0 ), + # WHICH array the fork appended to, from the module's own + # report rather than reconstructed from the wire. Only one + # of the two sources is evidenced as having been sent (see + # FORK_PREFIX_SOURCE_*), so a measurement that cannot see + # this field cannot tell a byte-aligned fork from a fork + # onto a view the provider never received. + "prefix_source": ( + self._fork_prefix_source + if call_mode == SUMMARY_CALL_MODE_FORK + else None + ), + # How many views were served since the caller last confirmed + # a send. >0 is normal (an orchestrator may re-fetch the view + # within one request); it is reported, not acted on. + "prefix_views_since_send": ( + self._view_serial - self._sent_serial + if self._sent_serial is not None + else None + ), "fork_fallbacks": self._summary_fork_fallbacks, } diff --git a/tests/test_summary_call_mode_fork.py b/tests/test_summary_call_mode_fork.py index e039f93..c75af84 100644 --- a/tests/test_summary_call_mode_fork.py +++ b/tests/test_summary_call_mode_fork.py @@ -242,6 +242,7 @@ async def test_default_mode_never_records_a_fork_prefix(): assert context._last_request_view is None assert context._sent_tools_supplied is False assert context._summary_fork_fallbacks == 0 + assert context._fork_prefix_source is None assert context.last_summary_call_stats is None @@ -607,11 +608,24 @@ async def test_fork_refuses_when_the_prefix_ends_on_unanswered_tool_calls(): @pytest.mark.asyncio -async def test_a_stale_caller_message_record_is_ignored_not_trusted(): +async def test_a_stale_caller_message_record_refuses_loudly_not_silently(caplog): """A caller that wires note_request_sent() ONCE (startup helper, first - turn only) would otherwise have turn N's fork append to turn 1's - request -- a guaranteed miss AND a wasted cache write, wearing a - correct-looking API call. The fresh module view must win.""" + turn only) would have turn N's fork append to turn 1's request. + + This test previously asserted the OPPOSITE resolution -- that the + module silently substituted its own last returned view. That + substitution is the defect this file now pins against + (model_performance-jnt): the module cannot know whether its own view + was ever sent, and under a real orchestrator that re-fetches the view + within a single request, the view it substitutes is one that was + superseded and never went on the wire. Trading an array KNOWN to have + been sent for one whose fate is unknown is backwards, and it was + invisible -- `mode_used` said "fork" either way. + + The correct resolution for a record too old to be usable is the one + every other misalignment already gets: refuse, run standalone, and SAY + SO. Caught exactly (the span is not in that prefix), not by a proxy. + """ context = _summary_manager(summary_call_mode="fork") await _fill(context, turns=10) # Turn 1: the caller records what it sent. @@ -625,13 +639,28 @@ async def test_a_stale_caller_message_record_is_ignored_not_trusted(): _cross_trigger(context) provider = _FakeProvider() - await context.get_messages_for_request(provider=provider) - await _await_pending_task(context) + with caplog.at_level(logging.WARNING): + await context.get_messages_for_request(provider=provider) + await _await_pending_task(context) + stats = context.last_summary_call_stats + assert stats["mode_used"] == "standalone", ( + "a prefix too stale to contain the span must refuse, not fork onto " + "a substituted view" + ) + assert "not present in the recorded prefix" in stats["reason"] + assert context._summary_fork_fallbacks == 1 + assert any("ran STANDALONE instead" in r.message for r in caplog.records) + + # The substitution specifically must not have happened: the standalone + # request is the two-message one, not an append onto the fresh view. request = provider.calls[0] - assert context.last_summary_call_stats["mode_used"] == "fork" - assert _digest(request.messages[:-1]) == _digest(fresh_view) + assert len(request.messages) == 2 + assert _digest(request.messages) != _digest(fresh_view) assert not any("long ago" in str(m.content) for m in request.messages) + # And the summary still happened -- refusing costs today's price, never + # the summary itself. + assert context._pending_summary is not None @pytest.mark.asyncio @@ -873,3 +902,187 @@ async def test_concurrent_forks_are_still_serialized_by_the_in_flight_guard(): assert len(provider.calls) == 1 assert context._pending_summary is not None assert context.last_summary_call_stats["mode_used"] == "fork" + + +# --------------------------------------------------------------------------- +# Group F -- the fork prefix is an array that was actually SENT +# +# model_performance-jnt. Measured on the wire (model_performance-6da, 20 +# forked calls): 9 appended to a request the provider actually saw, and 11 +# appended to an array that was never sent as any request. The module +# reported `mode_used == "fork"` for all 20. +# +# Cause: `_capture_fork_prefix()` preferred the caller's recorded wire array +# only while `_sent_serial == _view_serial`, and substituted +# `_last_request_view` otherwise. A real orchestrator serves the view more +# than once per sent request (amplifier's loop-streaming re-fetches after +# persisting an ephemeral injection), the trigger is evaluated inside every +# one of those calls, and on the second the substituted view is one that was +# superseded by the re-fetch and never went on the wire. +# +# These tests are written against that substitution, not against the happy +# path -- which the existing Group B tests already cover. +# --------------------------------------------------------------------------- + + +async def _serve_below_trigger(context: SimpleContextManager) -> list[dict]: + """Serve one more view without re-arming, i.e. a re-fetch within the + same request. `_arm_below_trigger` is the first view of a request; this + is the second one, which the orchestrator then supersedes.""" + view = await context.get_messages_for_request(provider=_FakeProvider()) + assert context._is_summarizing is False + return view + + +@pytest.mark.asyncio +async def test_a_re_fetched_view_does_not_displace_the_recorded_wire_array(): + """THE REGRESSION. An extra view served since the caller confirmed its + send is normal, not a reason to stop trusting the send. The wire array + is the only one with positive evidence of having been on the wire; the + intervening view has none.""" + context = _summary_manager(summary_call_mode="fork") + await _fill(context) + module_view = await _arm_below_trigger(context) + wire = [ + *module_view, + {"role": "user", "content": "injected"}, + ] + context.note_request_sent(wire, tools=_tools()) + sent_at = context._view_serial + + # The orchestrator re-fetches the view within the same request. This + # view is built, superseded, and never sent. + superseded = await _serve_below_trigger(context) + assert context._view_serial > sent_at, "the re-fetch must advance the view serial" + + _cross_trigger(context) + provider = _FakeProvider() + await context.get_messages_for_request(provider=provider) + await _await_pending_task(context) + + stats = context.last_summary_call_stats + assert stats["mode_used"] == "fork" + assert stats["prefix_source"] == "wire_record" + assert stats["prefix_views_since_send"] >= 1, ( + "the re-fetch must be visible in the stats, not silently acted on" + ) + + request = provider.calls[0] + assert _digest(request.messages[:-1]) == _digest(wire), ( + "the fork must append to the array the caller said it sent" + ) + assert _digest(request.messages[:-1]) != _digest(superseded) + assert "injected" in request.messages[-2].content + + +@pytest.mark.asyncio +async def test_the_module_view_is_never_substituted_when_a_wire_record_exists(): + """Belt and braces on the same defect, asserted from the other side: a + `_last_request_view` holding content that was demonstrably never sent + must not be able to reach the forked request at all.""" + context = _summary_manager(summary_call_mode="fork") + await _fill(context) + module_view = await _arm_below_trigger(context) + context.note_request_sent(module_view, tools=_tools()) + # A view that was built and discarded -- exactly what a re-fetch leaves + # behind, and what the old code would have forked onto. + context._last_request_view = [ + {"role": "user", "content": "a view that was never sent"} + ] + context._view_serial += 1 + + _cross_trigger(context) + provider = _FakeProvider() + await context.get_messages_for_request(provider=provider) + await _await_pending_task(context) + + request = provider.calls[0] + assert context.last_summary_call_stats["mode_used"] == "fork" + assert context.last_summary_call_stats["prefix_source"] == "wire_record" + assert not any("never sent" in str(m.content) for m in request.messages) + assert _digest(request.messages[:-1]) == _digest(module_view) + + +@pytest.mark.asyncio +async def test_prefix_source_names_the_module_view_path_honestly(): + """When the caller supplies tools but never a message array, the fork + appends to this module's own view -- whose send this module cannot + confirm. That is still allowed (it is what an explicit-breakpoint + provider wants), but it must be REPORTED as what it is, so a + measurement can separate the two populations without patching.""" + context = _summary_manager(summary_call_mode="fork") + await _fill(context) + context.note_request_sent(tools=_tools()) + parent_view = await _arm_below_trigger(context) + _cross_trigger(context) + + provider = _FakeProvider() + await context.get_messages_for_request(provider=provider) + await _await_pending_task(context) + + stats = context.last_summary_call_stats + assert stats["mode_used"] == "fork" + assert stats["prefix_source"] == "module_view" + assert stats["prefix_views_since_send"] is None, ( + "no message array was ever recorded, so there is no send to count from" + ) + assert _digest(provider.calls[0].messages[:-1]) == _digest(parent_view) + + +@pytest.mark.asyncio +async def test_prefix_source_is_none_when_the_call_did_not_fork(): + """A refused fork reports no prefix source. Reporting one would make a + standalone call look byte-aligned with something.""" + context = _summary_manager(summary_call_mode="fork") + await _fill(context) + _cross_trigger(context) + + provider = _FakeProvider() + await context.get_messages_for_request(provider=provider) + await _await_pending_task(context) + + stats = context.last_summary_call_stats + assert stats["mode_used"] == "standalone" + assert stats["prefix_source"] is None + + +@pytest.mark.asyncio +async def test_tool_pair_integrity_and_seq_stability_survive_the_re_fetch_path(): + """The re-fetch path must change WHICH array is appended to and nothing + else: the same span is selected, no `_seq` is consumed, history is + untouched, and the next served view matches an unforked control.""" + forked = _summary_manager(summary_call_mode="fork") + control = _summary_manager() + for ctx in (forked, control): + await _fill(ctx) + + view = await _arm_below_trigger(forked) + forked.note_request_sent([*view, {"role": "user", "content": "tail"}], tools=_tools()) + await _serve_below_trigger(forked) # the superseding re-fetch + await _arm_below_trigger(control) + for ctx in (forked, control): + _cross_trigger(ctx) + + seq_before = forked._next_seq + history_before = json.dumps(_strip_timestamps(forked.messages), default=str) + + for ctx in (forked, control): + await ctx.get_messages_for_request(provider=_FakeProvider("SAME SUMMARY")) + await _await_pending_task(ctx) + + assert forked.last_summary_call_stats["mode_used"] == "fork" + assert forked.last_summary_call_stats["prefix_source"] == "wire_record" + assert forked._next_seq == seq_before, "the fork must not consume a _seq" + assert ( + json.dumps(_strip_timestamps(forked.messages), default=str) == history_before + ), "the fork must not append to, reorder, or edit history" + assert set(forked._pending_summary["seqs"]) == set(control._pending_summary["seqs"]), ( + "the call mode must not change WHICH span is absorbed" + ) + + for ctx in (forked, control): + ctx.compact_threshold = 0.3 + forked_view = await forked.get_messages_for_request(provider=_FakeProvider()) + control_view = await control.get_messages_for_request(provider=_FakeProvider()) + assert _strip_timestamps(forked_view) == _strip_timestamps(control_view) + assert forked._removed_seqs == control._removed_seqs