fix: _capture_fork_prefix no longer forks onto an array that was never sent (#jnt) - #28
Conversation
…r 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.
Merge-queue verification — PASSScratch clone: fresh checkout of Gate table
Scope / diff check
Concurrency checkNo conflict with DisclosureMerging with |
* Revert "fix: _capture_fork_prefix no longer forks onto an array that was never sent (#jnt) (#28)" This reverts commit 5cdbc62253ca80cb93f11a2fa6f7a5d3940394c78. Not in the owner's literal revert list (#20-27), but merged AFTER #27 and exists solely to fix a bug in the summary_call_mode="fork" code path that #27 introduced. Once #27 (the fork feature itself) is reverted next, this fix has no surviving target -- keeping it would leave dead code / an orphaned test file referencing a feature no longer on main. Reverting it first, before #27, keeps the revert of #27 itself clean. * Revert "feat: summary_call_mode -- cache-safe fork of the summarization call (#27)" This reverts commit a877b36. Merge policy: main carries wins only. summary_call_mode's fork mode was shipped with benefit explicitly labeled unmeasured (see the original merge note: "benefit correctly labeled unmeasured"). Unproven default-off feature -- belongs on a branch for evaluation, not on main. * Revert "feat: clear_at_least -- a worth-the-rebuild predicate in front of compaction (#2o9) (#26)" This reverts commit f851d12. Merge policy: main carries wins only. clear_at_least was an unproven, default-off predicate. Belongs on a branch for evaluation, not on main. * Revert "feat: replay_last_user_on_compaction -- opt-in last-user replay at the tail (#4i3) (#24)" This reverts commit 3972070. Merge policy: main carries wins only. replay_last_user_on_compaction was an unproven, opt-in feature. Belongs on a branch for evaluation, not on main. * Revert "feat: tool-result budget (token-denominated, head+tail, per-tool) + spill-to-disk -- all defaults no-op (#21)" This reverts commit 49e2799. Merge policy: main carries wins only. Tool-result budget + spill-to-disk was an unproven, all-defaults-no-op feature. Belongs on a branch for evaluation, not on main. * Revert "feat: token_meter \"hybrid\" -- provider-anchored counts with provenance (+ the never-run estimate-vs-hybrid-vs-actual measurement) (#22)" This reverts commit f47c894. Merge policy: main carries wins only. The hybrid token_meter mode's own measurement was never run. Unproven, opt-in feature -- belongs on a branch for evaluation, not on main. * Revert "feat: compaction_strategy \"summary\" -- LLM rolling-summary compaction (opt-in) (#20)" This reverts commit c6dfbba. Merge policy: main carries wins only. The summary compaction strategy's own gate measurement showed a +83% cost regression with summarizer share 26-35%% -- falsified, not a win. Belongs on a branch for evaluation, not on main.
DONE-NOTE - model_performance-jnt
_capture_fork_prefix()appends to an array that was never sent (11 of 20forks 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__.pyat thepre-fix commit
a877b36.self._last_request_view__init__), written :1778 inside_finalize_view()_capture_fork_prefix(), as the fallback source_maybe_trigger_summary_compaction:4040 →_run_summary_compaction_task→_build_fork_request:4422 → the providerThe 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_viewwhenever that equality failed (:4187, :4192–4197—
logger.debug, not warning).Why the equality fails in production.
_view_serialcounts viewsserved, 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__.pycallscontext.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 everyone of them (:1364, which runs before that call's
_finalize_viewat:1480/:1482). So:
is used → correct fork (wire offset 0 or 1);
discarded in favour of
_last_request_view, which at that instant holdsthe 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.
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.
last_summary_call_stats["mode_used"]reported"fork"forall 20 calls. The substitution was
logger.debug. There was no fielddistinguishing the two sources — this is precisely why 6da had to
reconstruct the distinction from the provider's request log.
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,
_seqallocation, span selection, tool-pairintegrity, 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:
the direct measurement of this bug. Scored from the wire, not from the
module's self-report.
parity, and the −0.8% run-cost delta — VALID. All are wire/usage-derived
and none depend on
_capture_fork_prefix()having chosen correctly.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: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;
message array (the documented explicit-breakpoint/Anthropic path, unchanged);
instead of a proxy: a record too old to contain the span fails
_prefix_contains_spanand refuses LOUDLY — standalone call,WARNING,_summary_fork_fallbacksincremented, namedreason. "A fork that silentlymissed" 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) andprefix_views_since_sendon
last_summary_call_stats. The next arm can separate the two populationsfrom the module's own report instead of reconstructing them from the wire.
Config surface: unchanged. Default
summary_call_moderemains"standalone".5. Tests
286 passed, 1 skipped(was 281 passed ata877b36).ruff check: clean.New Group F in
tests/test_summary_call_mode_fork.py:test_a_re_fetched_view_does_not_displace_the_recorded_wire_arraytest_the_module_view_is_never_substituted_when_a_wire_record_existstest_prefix_source_names_the_module_view_path_honestlytest_prefix_source_is_none_when_the_call_did_not_forktest_tool_pair_integrity_and_seq_stability_survive_the_re_fetch_path_seqconsumed, history byte-identical, same span absorbed, served view identical to an unforked controlThese 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. Itasserted 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 Noneintest_default_mode_never_records_a_fork_prefix.6. Residual, disclosed
The module-view path (
note_request_sent(tools=...)with nomessages)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 mustpass
messages. Closing this properly needs a caller-side confirmationsignal, 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 --resumeprocess, and thetrigger is evaluated before any request is sent). 6da measured 12 of 24
refusals from this; this fix does not address it.
7. Deliverable ledger
lane/jnt-fork-prefix-capture, tests green, default inline byte-identicalSpend: $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.