Skip to content

Commit 5f04ebd

Browse files
rust mode: keep the pristine module array as the delta basis; hold the demoted signed assistant's native vector until a priced pass (ASTRO 05:32Z busts)
Co-authored-by: Alfonso <alfonso-magic-context@users.noreply.github.com>
2 parents c536019 + 792347b commit 5f04ebd

6 files changed

Lines changed: 265 additions & 11 deletions

File tree

‎crates/mc-module/src/lib.rs‎

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12202,7 +12202,7 @@ fn attach_native_messages_with_tags(
1220212202
return;
1220312203
}
1220412204
let native_messages = encode_full_native_messages(
12205-
response.messages(),
12205+
response,
1220612206
request,
1220712207
reasoning_watermark,
1220812208
tag_numbers,
@@ -12388,14 +12388,16 @@ fn native_reasoning_should_clear(
1238812388
}
1238912389

1239012390
fn encode_full_native_messages(
12391-
served: &[transform::ServedMessage],
12391+
response: &transform::TransformResponse,
1239212392
request: &TransformRequest,
1239312393
reasoning_watermark: u64,
1239412394
tag_numbers: &BTreeMap<String, u64>,
1239512395
mutation_exempt_mid: Option<&str>,
1239612396
lineage_anchor_mid: Option<&str>,
1239712397
transition_consumed: bool,
1239812398
) -> Vec<Value> {
12399+
let served = response.messages();
12400+
let native_reasoning_keep_mids = &response.native_reasoning_keep_mids;
1239912401
let sidecar = request
1240012402
.native_messages
1240112403
.as_deref()
@@ -12433,9 +12435,30 @@ fn encode_full_native_messages(
1243312435
tag_numbers,
1243412436
);
1243512437
}
12438+
replay_native_reasoning_keeps(&mut native_messages, &sidecar, native_reasoning_keep_mids);
1243612439
native_messages
1243712440
}
1243812441

12442+
fn replay_native_reasoning_keeps(
12443+
messages: &mut [Value],
12444+
sidecar: &codec::DecodeSidecar,
12445+
keep_mids: &[String],
12446+
) {
12447+
for message in messages {
12448+
let Some(mid) = message["info"]["id"].as_str() else {
12449+
continue;
12450+
};
12451+
if !keep_mids.iter().any(|keep| keep == mid) {
12452+
continue;
12453+
}
12454+
if let Some(meta) = sidecar.messages.get(mid) {
12455+
if let Some(parts) = meta.raw.get("parts") {
12456+
message["parts"] = parts.clone();
12457+
}
12458+
}
12459+
}
12460+
}
12461+
1243912462
fn native_ingress_chunks(
1244012463
request: &TransformRequest,
1244112464
encoded_chunks: &[NativeEncodedChunk],
@@ -12599,7 +12622,13 @@ fn attach_native_messages_incremental(
1259912622
}
1260012623
Some(hash)
1260112624
});
12602-
let mutation_exempt = slot.is_some_and(|mid| mutation_exempt_mids.contains(&mid));
12625+
let mutation_exempt = slot.is_some_and(|mid| {
12626+
mutation_exempt_mids.contains(&mid)
12627+
|| response
12628+
.native_reasoning_keep_mids
12629+
.iter()
12630+
.any(|keep| keep == mid)
12631+
});
1260312632
let reasoning_exempt = slot.is_some_and(|mid| Some(mid) == newest_assistant_mid);
1260412633
let (tag_number, reasoning_should_clear) = native_reasoning_should_clear(
1260512634
served,
@@ -12713,6 +12742,11 @@ fn attach_native_messages_incremental(
1271312742
tag_numbers,
1271412743
);
1271512744
}
12745+
replay_native_reasoning_keeps(
12746+
&mut suffix_values,
12747+
&sidecar,
12748+
&response.native_reasoning_keep_mids,
12749+
);
1271612750
chunks.extend(
1271712751
encoded_suffix
1271812752
.into_iter()
@@ -12734,7 +12768,7 @@ fn attach_native_messages_incremental(
1273412768

1273512769
if native_attachment_differential_enabled() {
1273612770
let full = encode_full_native_messages(
12737-
response.messages(),
12771+
response,
1273812772
request,
1273912773
reasoning_watermark,
1274012774
tag_numbers,
@@ -12845,7 +12879,7 @@ fn finalize_native_messages_response(
1284512879
fallback_reason.get_or_insert(NativeDeltaFallbackReason::MissingNativeContent);
1284612880
response.native_messages = Some(
1284712881
encode_full_native_messages(
12848-
response.messages(),
12882+
response,
1284912883
request,
1285012884
reasoning_watermark,
1285112885
tag_numbers,
@@ -21708,6 +21742,103 @@ mod tests {
2170821742
.all(|block| block.synthetic));
2170921743
}
2171021744

21745+
#[tokio::test(flavor = "current_thread")]
21746+
async fn post_hard_defer_preserves_demoted_native_thinking_until_priced_pass() {
21747+
let producer = Arc::new(ProducerState::default());
21748+
let (handler, store, _dir, _project) = handler_with_store(producer, default_test_config());
21749+
let mut native = vec![
21750+
json!({"info":{"id":"user","role":"user"},"parts":[{"type":"text","text":"start"}]}),
21751+
json!({"info":{"id":"previous","role":"assistant"},"parts":[{"type":"text","text":"earlier step"}]}),
21752+
];
21753+
let make_request = |native: &Vec<Value>, model: &str| {
21754+
let decoded = codec::decode_opencode(native);
21755+
let mut req = request(decoded.messages);
21756+
req["serializer_profile"] = json!("opencode-aisdk");
21757+
req["serve_native"] = json!(true);
21758+
req["tool_present"] = json!(true);
21759+
req["provider_id"] = json!("anthropic");
21760+
req["model_key"] = json!(model);
21761+
req["mid_turn"] = json!(false);
21762+
req["native_messages"] = json!(native);
21763+
req
21764+
};
21765+
let warm = call_transform_request(&handler, make_request(&native, "claude-opus-5")).await;
21766+
assert_eq!(warm["action"], "HARD");
21767+
let thinking = json!({"type":"reasoning","text":"signed thinking", "metadata":{"anthropic":{"signature":"fixture-signature"}}});
21768+
native.push(json!({"info":{"id":"thinking","role":"assistant"},"parts":[
21769+
thinking.clone(), {"type":"text","text":"dispatching"},
21770+
{"type":"tool","tool":"work","callID":"call-thinking","state":{"status":"completed","input":{"action":"run"},"output":"dispatched"}}
21771+
]}));
21772+
native.push(json!({"info":{"id":"continue","role":"user"},"parts":[{"type":"text","text":"continue"}]}));
21773+
let hard =
21774+
call_transform_request(&handler, make_request(&native, "claude-fable-5-1")).await;
21775+
assert_eq!(hard["action"], "HARD");
21776+
let mut previous = hard["native_messages"].as_array().unwrap().clone();
21777+
let hash = |messages: &[Value]| -> String {
21778+
format!(
21779+
"{:x}",
21780+
Sha256::digest(serde_json::to_vec(messages).unwrap())
21781+
)
21782+
};
21783+
for step in 0..2 {
21784+
native.push(json!({"info":{"id":format!("step-{step}"),"role":"assistant"},"parts":[
21785+
{"type":"reasoning","text":format!("adaptive step {step}"),"metadata":{"anthropic":{"signature":format!("signature-{step}")}}},
21786+
{"type":"tool","tool":"work","callID":format!("call-{step}"),"state":{"status":"completed","input":{"action":"show"},"output":"done"}}
21787+
]}));
21788+
let deferred =
21789+
call_transform_request(&handler, make_request(&native, "claude-fable-5-1")).await;
21790+
assert_eq!(deferred["action"], "SOFT+");
21791+
let served = deferred["native_messages"].as_array().unwrap();
21792+
assert!(served.len() > previous.len());
21793+
assert_eq!(
21794+
hash(&served[..previous.len()]),
21795+
hash(&previous),
21796+
"demotion must not rewrite the served prefix: {:?}",
21797+
served
21798+
.iter()
21799+
.zip(&previous)
21800+
.enumerate()
21801+
.filter(|(_, (new, old))| new != old)
21802+
.collect::<Vec<_>>()
21803+
);
21804+
let target = served
21805+
.iter()
21806+
.find(|message| message["info"]["id"] == "thinking")
21807+
.unwrap();
21808+
assert_eq!(target["parts"][0], thinking);
21809+
assert!(!store
21810+
.load("ses")
21811+
.unwrap()
21812+
.core
21813+
.frozen_units
21814+
.iter()
21815+
.any(|unit| unit.key == "strip:merged_reasoning:thinking"));
21816+
previous = served.clone();
21817+
}
21818+
let mut priced_request = make_request(&native, "claude-fable-5-1");
21819+
priced_request["render_config"] = json!("independent-priced-config-change");
21820+
let priced = call_transform_request(&handler, priced_request).await;
21821+
assert_eq!(priced["action"], "HARD");
21822+
let target = priced["native_messages"]
21823+
.as_array()
21824+
.unwrap()
21825+
.iter()
21826+
.find(|message| message["info"]["id"] == "thinking")
21827+
.unwrap();
21828+
assert!(target["parts"]
21829+
.as_array()
21830+
.unwrap()
21831+
.iter()
21832+
.all(|part| part["type"] != "reasoning"));
21833+
assert!(store
21834+
.load("ses")
21835+
.unwrap()
21836+
.core
21837+
.frozen_units
21838+
.iter()
21839+
.any(|unit| unit.key == "strip:merged_reasoning:thinking"));
21840+
}
21841+
2171121842
#[tokio::test(flavor = "current_thread")]
2171221843
async fn native_attachment_reuses_transform_tag_baseline_and_preserves_bytes() {
2171321844
let producer = Arc::new(ProducerState::default());

‎crates/mc-module/src/transform.rs‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,6 +1473,9 @@ pub struct NativeMessagesDelta {
14731473

14741474
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14751475
pub struct TransformResponse {
1476+
/// Native vectors protected while newest remain unchanged until the next priced pass.
1477+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
1478+
pub native_reasoning_keep_mids: Vec<String>,
14761479
pub status: TransformStatus,
14771480
pub served_from: ServedFrom,
14781481
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1569,6 +1572,7 @@ impl TransformResponse {
15691572

15701573
pub fn need_full_sync(full_array_fingerprint: Option<String>) -> Self {
15711574
Self {
1575+
native_reasoning_keep_mids: Vec::new(),
15721576
status: TransformStatus::NeedFullSync,
15731577
served_from: ServedFrom::Transform,
15741578
full_array_fingerprint,
@@ -1606,6 +1610,7 @@ impl TransformResponse {
16061610
full_array_fingerprint: Option<String>,
16071611
) -> Self {
16081612
Self {
1613+
native_reasoning_keep_mids: Vec::new(),
16091614
status: TransformStatus::Ok,
16101615
served_from: ServedFrom::Transform,
16111616
full_array_fingerprint,
@@ -3094,6 +3099,15 @@ fn apply_additive_only(
30943099
mutation_exempt_mid: None,
30953100
lineage_anchor_mid: None,
30963101
response: TransformResponse {
3102+
native_reasoning_keep_mids: core
3103+
.frozen_units
3104+
.iter()
3105+
.filter_map(|unit| {
3106+
unit.key
3107+
.strip_prefix("strip:native_reasoning_keep:")
3108+
.map(str::to_owned)
3109+
})
3110+
.collect(),
30973111
status: TransformStatus::Ok,
30983112
served_from: ServedFrom::Transform,
30993113
full_array_fingerprint: req.full_array_fingerprint.clone(),
@@ -5347,6 +5361,26 @@ fn apply_once(
53475361
no_trim_meta = Some(output_meta);
53485362
}
53495363
let output_meta = no_trim_meta.as_ref().unwrap_or(&meta);
5364+
// OpenCode must replay the newest signed assistant's complete native vector. Its
5365+
// demotion is not permission to apply previously withheld overlays on a defer.
5366+
// Release these keeps only when the prefix is already being repriced.
5367+
if is_provider_prefix_mutation_pass {
5368+
core.frozen_units
5369+
.retain(|unit| !unit.key.starts_with("strip:native_reasoning_keep:"));
5370+
}
5371+
if serializer_profile == Some(SerializerProfile::OpencodeAiSdk) && req.serve_native {
5372+
if let Some(mid) = latest_assistant_reasoning_mutation_exempt_mid(&req.messages) {
5373+
if req.messages.iter().any(|message| {
5374+
message.mid == mid && message.ck.content.iter().any(is_reasoning_block)
5375+
}) {
5376+
let key = format!("strip:native_reasoning_keep:{mid}");
5377+
if !core.frozen_units.iter().any(|unit| unit.key == key) {
5378+
core.frozen_units
5379+
.push(strip_unit("native_reasoning_keep", mid, ""));
5380+
}
5381+
}
5382+
}
5383+
}
53505384
let output_cache_snapshot = output_cache.map(|cache| {
53515385
cache
53525386
.lock()
@@ -5676,6 +5710,15 @@ fn apply_once(
56765710
mutation_exempt_mid: mutation_exempt_mid.map(str::to_string),
56775711
lineage_anchor_mid: lineage_anchor_mid.map(str::to_string),
56785712
response: TransformResponse {
5713+
native_reasoning_keep_mids: core
5714+
.frozen_units
5715+
.iter()
5716+
.filter_map(|unit| {
5717+
unit.key
5718+
.strip_prefix("strip:native_reasoning_keep:")
5719+
.map(str::to_owned)
5720+
})
5721+
.collect(),
56795722
status: TransformStatus::Ok,
56805723
served_from: ServedFrom::Transform,
56815724
full_array_fingerprint: req.full_array_fingerprint.clone(),

‎packages/plugin/scripts/analyze-cache-busts.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,27 @@ function snapshotsFor(dir: string, session: string) {
102102
}
103103

104104
describe("analyze-cache-bust dump discovery", () => {
105+
test("prints complete UTF-8 body bytes separately from reusable normalized prefix bytes", () => {
106+
const dir = mkdtempSync(join(tmpdir(), "cache-bust-body-bytes-"));
107+
tempDirs.push(dir);
108+
const session = "ses_bodyBytes";
109+
const previous = bodyWithTailCheckpoint("old § tail");
110+
const current = bodyWithTailCheckpoint("new § tail with more bytes");
111+
const previousStem = `2026-09-02T08-41-00-000Z-${session}`;
112+
const currentStem = `2026-09-02T08-42-00-000Z-${session}`;
113+
writeDump(dir, previousStem, "2026-09-02T08:41:00Z", session, previous);
114+
writeDump(dir, currentStem, "2026-09-02T08:42:00Z", session, current);
115+
const prettyBody = `${JSON.stringify(current, null, 2)}\n`;
116+
writeFileSync(join(dir, `${currentStem}.body.json`), prettyBody);
117+
const expected = [Buffer.byteLength(JSON.stringify(previous)), Buffer.byteLength(prettyBody)];
118+
expect(snapshotsFor(dir, session).map((snapshot) => snapshot.bodyBytes)).toEqual(expected);
119+
const run = Bun.spawnSync([process.execPath, join(import.meta.dir, "analyze-cache-busts.ts"), "--session", session, "--dir", dir, "--all-rows"]);
120+
expect(run.exitCode).toBe(0);
121+
const output = run.stdout.toString();
122+
expect(output).toContain("prevBodyBytes → curBodyBytes");
123+
expect(output).toContain("reusableNormalizedPrefix@breakpoint");
124+
expect(output).toContain(`${expected[0]!.toLocaleString()}B → ${expected[1]!.toLocaleString()}B`);
125+
});
105126
test("loads and orders legacy and sequence+routing filename layouts by timestamp and sequence", () => {
106127
const dir = mkdtempSync(join(tmpdir(), "cache-bust-fixture-"));
107128
tempDirs.push(dir);

‎packages/plugin/scripts/analyze-cache-busts.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ interface MeterUsage {
5353
interface Snapshot {
5454
file: string;
5555
bodyPath: string;
56+
bodyBytes: number;
5657
createdAt: string;
5758
session: string;
5859
messagesCount: number;
@@ -350,8 +351,11 @@ function loadSnapshots(opts: ReturnType<typeof parseArgs>): Snapshot[] {
350351
if (!bodyPath) continue;
351352

352353
let body: Json;
354+
let bodyBytes: number;
353355
try {
354-
body = JSON.parse(readFileSync(bodyPath, "utf8")) as Json;
356+
const rawBody = readFileSync(bodyPath);
357+
bodyBytes = rawBody.byteLength;
358+
body = JSON.parse(rawBody.toString("utf8")) as Json;
355359
} catch {
356360
continue;
357361
}
@@ -367,6 +371,7 @@ function loadSnapshots(opts: ReturnType<typeof parseArgs>): Snapshot[] {
367371
snaps.push({
368372
file: metaFile,
369373
bodyPath,
374+
bodyBytes,
370375
createdAt,
371376
session,
372377
messagesCount: typeof bodyMeta?.messagesCount === "number" ? bodyMeta.messagesCount : -1,
@@ -554,7 +559,7 @@ function main(): void {
554559
console.log("Dashboard times are local (UTC+2); table times are UTC.");
555560
console.log("Meter rule: shortRead when cacheRead + current input < prevTotal - ε, where ε=max(64, previous input). A short read is BUST only when bytes diverge before the current tail breakpoint; otherwise it is LATENCY.");
556561
console.log(
557-
"time(UTC) | segs | verdict | meter | meterVsBytes | first-divergence | prevBytes → curBytes | cachedPrefix@breakpoint",
562+
"time(UTC) | segs | verdict | meter | meterVsBytes | first-divergence | prevBodyBytes → curBodyBytes | reusableNormalizedPrefix@breakpoint",
558563
);
559564
console.log(
560565
"-------------------|------|------------------|--------------------------------------------------------|--------------|---------------------------------|-----------------------------|------------------------",
@@ -586,9 +591,8 @@ function main(): void {
586591
const attribution = segment
587592
? `${segment.id} (bytes ${row.byteVerdict})`
588593
: `(identical; bytes ${row.byteVerdict})`;
589-
const previousPrefix = cachedPrefixBytes(previous.segments, previous.segments.length);
590594
const currentPrefix = cachedPrefixBytes(row.current.segments, index);
591-
const byteDelta = `${previousPrefix.bytes.toLocaleString()}B → ${currentPrefix.bytes.toLocaleString()}B`;
595+
const byteDelta = `${previous.bodyBytes.toLocaleString()}B → ${row.current.bodyBytes.toLocaleString()}B`;
592596
const verdictLabel =
593597
row.verdict === "UNMETERED"
594598
? `UNMETERED (bytes ${row.byteVerdict})`

0 commit comments

Comments
 (0)