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