wip: preserve partial decision receipts without pricing missing counters

Required usage counters are tracked separately from decoded provider evidence. Incomplete/zero counters produce explicit coverage gaps while bounded receipts retain actual raw cost and reported counters. Keep receipt-only and empty-owner batches on the existing projections, bound decision diagnostics to 64 entries, sanitize receipt labels, and move Auto decision client credential construction off Tokio workers. Repair the restart fixture to drop its existing Runtime store lock before reopening.

Previous focused packet: 30 passed, 1 failed because the fixture reopened a still-active Runtime. npm test && npm run check:web passed; 767 actual tests passed, 0 failed (wrapper68, SDK16, host54, web629), with one bounded wrapper-only receipt rerun because initial TAP summary output was truncated. Blocking-call budget and rustfmt/diff checks passed. Final focused rerun, clippy, negative controls and hosted CI remain pending.

Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale Bot committed 2026-09-30 03:17:28 -07:00
1 parent d89a4a1868
commit c3dd6d1d74
7 files changed
+189 -22

No files matched your search

+69 -7
View File
@@ -161,15 +161,36 @@ pub(crate) struct SystemOneAnswer {
/// Provider usage. OpenRouter adds `cost`; the token-count casing differs
/// between surfaces, so both spellings are accepted.
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone)]
pub(crate) struct SystemOneUsage {
/// Kept verbatim so the receipt never re-renders a float.
#[serde(default)]
pub(crate) cost: Option<Box<RawValue>>,
#[serde(default, alias = "inputTokens")]
pub(crate) input_tokens: u32,
#[serde(default, alias = "outputTokens")]
pub(crate) output_tokens: u32,
pub(crate) complete: bool,
}
impl<'de> Deserialize<'de> for SystemOneUsage {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
struct WireUsage {
#[serde(default)]
cost: Option<Box<RawValue>>,
#[serde(default, alias = "inputTokens")]
input_tokens: Option<u32>,
#[serde(default, alias = "outputTokens")]
output_tokens: Option<u32>,
}
let wire = WireUsage::deserialize(deserializer)?;
Ok(Self {
complete: wire.input_tokens.is_some() && wire.output_tokens.is_some(),
input_tokens: wire.input_tokens.unwrap_or(0),
output_tokens: wire.output_tokens.unwrap_or(0),
cost: wire.cost,
})
}
}
impl SystemOneUsage {
@@ -313,6 +334,8 @@ fn valid_decision_request(body: &Value) -> bool {
let Some(questions) = body.get("questions").and_then(Value::as_object) else {
return false;
};
let supported_value =
|value: &Value| matches!(value, Value::String(_) | Value::Object(_) | Value::Array(_));
let model = body.get("model").and_then(Value::as_str);
model.is_some_and(|m| !m.trim().is_empty() && m.len() <= 256)
&& body.get("state").is_some_and(|state| {
@@ -327,11 +350,21 @@ fn valid_decision_request(body: &Value) -> bool {
Some("choice") => q
.get("criteria")
.and_then(Value::as_object)
.is_some_and(|c| !c.is_empty() && c.len() <= 64),
.is_some_and(|c| {
!c.is_empty()
&& c.len() <= 64
&& c.iter().all(|(name, v)| {
!name.trim().is_empty()
&& name.len() <= 128
&& (v.is_null() || supported_value(v))
})
}),
Some("score") => q
.get("criteria")
.and_then(Value::as_array)
.is_some_and(|c| !c.is_empty() && c.len() <= 10),
.is_some_and(|c| {
!c.is_empty() && c.len() <= 10 && c.iter().all(supported_value)
}),
_ => false,
})
}
@@ -344,7 +377,7 @@ fn valid_decision_response(body: &Value, response: &SystemOneResponse) -> bool {
.model
.as_deref()
.is_some_and(|m| !m.trim().is_empty() && m.len() <= 256)
&& response.usage.is_some()
&& response.usage.as_ref().is_some_and(|usage| usage.complete)
&& response.answers.len() == questions.len()
&& questions.iter().all(|(name, question)| {
let Some(answer) = response.answers.get(name) else {
@@ -368,6 +401,9 @@ fn valid_decision_response(body: &Value, response: &SystemOneResponse) -> bool {
let Some(criteria) = question.get("criteria").and_then(Value::as_array) else {
return false;
};
if criteria.is_empty() || criteria.len() > 10 {
return false;
}
let Some(score) = answer.score else {
return false;
};
@@ -487,6 +523,7 @@ mod decisions_compatibility_tests {
("/answers/urgency/confidence", json!(1.1)),
("/model", Value::Null),
("/usage", Value::Null),
("/usage", json!({"input_tokens": 287, "cost":0.000012054})),
("/answers", json!({})),
];
for (pointer, value) in changes {
@@ -599,4 +636,29 @@ mod decisions_compatibility_tests {
"typesafe"
);
}
#[test]
fn decision_partial_usage_retains_raw_cost_but_cannot_authorize_policy() {
let mut body = response();
body["usage"] = json!({"inputTokens": 287, "cost": 0.000012054});
let decoded: SystemOneResponse = serde_json::from_value(body).expect("partial receipt");
assert!(!valid_decision_response(&request(), &decoded));
let usage = decoded.usage.expect("reported usage");
assert!(!usage.complete);
assert_eq!(usage.input_tokens, 287);
assert_eq!(usage.reported_cost().as_deref(), Some("0.000012054"));
let route = crate::cost_status::decision_receipt_fixture("partial").route;
let mut body = response();
body["usage"] = json!({"inputTokens": 287, "cost": 0.000012054});
let response: SystemOneResponse = serde_json::from_value(body).expect("partial response");
let batch = crate::model_routing::decision_usage_batch(&route, &response);
assert!(
batch.records.is_empty(),
"a partial token count cannot produce a complete priced subtotal"
);
assert_eq!(batch.dropped_records, 1);
assert_eq!(batch.drop_records.len(), 1);
let mut invalid = request();
invalid["questions"]["urgency"]["criteria"] = json!([null]);
assert!(!valid_decision_request(&invalid));
}
}
+28 -8
View File
@@ -667,7 +667,7 @@ pub fn child_usage_records_from_metadata(
if let Some(decisions) = metadata.get(CHILD_DECISION_RECEIPTS_KEY) {
if let Some(values) = decisions.as_array() {
for value in values.iter().take(MAX_CHILD_USAGE_RECORDS) {
let bounded = serde_json::to_vec(value).is_ok_and(|bytes| bytes.len() <= 16 * 1024);
let bounded = serde_json::to_vec(value).is_ok_and(|bytes| bytes.len() <= 8 * 1024);
let parsed = bounded
.then(|| serde_json::from_value::<RuntimeDecisionReceipt>(value.clone()).ok())
.flatten();
@@ -905,6 +905,8 @@ pub struct RuntimeDecisionReceipt {
pub source_id: String,
pub route: EffectiveRouteEnvelope,
pub usage: Option<Usage>,
#[serde(default)]
pub usage_complete: bool,
pub shadow: bool,
pub valid_answers: bool,
pub evidence: crate::model_routing::AutoRouteDecisionEvidence,
@@ -912,7 +914,8 @@ pub struct RuntimeDecisionReceipt {
impl RuntimeDecisionReceipt {
pub(crate) fn is_bounded(&self) -> bool {
self.source_id.len() <= 128
!self.source_id.trim().is_empty()
&& self.source_id.len() <= 128
&& self.evidence.choice.len() <= 128
&& self.evidence.probabilities_bp.len() <= 64
&& self
@@ -941,15 +944,31 @@ impl RuntimeDecisionReceipt {
&& v.parse::<f64>()
.is_ok_and(|cost| cost.is_finite() && cost >= 0.0)
})
&& serde_json::to_vec(self).is_ok_and(|bytes| bytes.len() <= 16 * 1024)
&& serde_json::to_vec(self).is_ok_and(|bytes| bytes.len() <= 8 * 1024)
}
pub(crate) fn sanitized(&self) -> Self {
Self {
source_id: usage_source_fingerprint(&self.source_id),
route: self.route.sanitized_for_persistence(),
..self.clone()
}
let mut receipt = self.clone();
receipt.source_id = usage_source_fingerprint(&receipt.source_id);
receipt.route = receipt.route.sanitized_for_persistence();
receipt.evidence.choice = sanitize_persisted_route_label(&receipt.evidence.choice);
receipt.evidence.thinking = receipt
.evidence
.thinking
.as_deref()
.map(sanitize_persisted_route_label);
receipt.evidence.response_model = receipt
.evidence
.response_model
.as_deref()
.map(sanitize_persisted_route_label);
receipt.evidence.probabilities_bp = receipt
.evidence
.probabilities_bp
.into_iter()
.map(|(key, value)| (sanitize_persisted_route_label(&key), value))
.collect();
receipt
}
pub(crate) fn diagnostic_receipt(&self) -> String {
@@ -994,6 +1013,7 @@ pub(crate) fn decision_receipt_fixture(source_id: &str) -> RuntimeDecisionReceip
output_tokens: 4,
..Default::default()
}),
usage_complete: true,
shadow: true,
valid_answers: false,
evidence: crate::model_routing::AutoRouteDecisionEvidence {
+18 -5
View File
@@ -1551,8 +1551,17 @@ async fn auto_route_decision_recommendation(
let route = inventory
.router_decision_route
.ok_or_else(|| anyhow::anyhow!("decision router has no route"))?;
let client =
CodewhaleClient::for_decision_route(config, route, inventory.router_base_url.as_deref())?;
// Resolve environment/keyring credentials off the async worker.
let decision_config = config.clone();
let base_url = inventory.router_base_url.clone();
#[cfg(test)]
let ticket = crate::test_support::env_scope_ticket();
let client = tokio::task::spawn_blocking(move || {
#[cfg(test)]
let _membership = crate::test_support::join_env_scope(ticket);
CodewhaleClient::for_decision_route(&decision_config, route, base_url.as_deref())
})
.await??;
let body = decision_request_body(
&client,
&inventory.router_model,
@@ -1584,8 +1593,10 @@ async fn auto_route_decision_recommendation(
_ => InventoryAutoRouteAttempt::failed(AutoRouterFailure::Timeout),
},
Ok(Err(failure)) => match request_route {
Some(route) => auto_route_attempt_with_dropped_response(route, failure),
None => InventoryAutoRouteAttempt::failed(failure),
Some(route) if dispatched.load(std::sync::atomic::Ordering::Acquire) => {
auto_route_attempt_with_dropped_response(route, failure)
}
_ => InventoryAutoRouteAttempt::failed(failure),
},
Ok(Ok(response)) => decision_attempt_from_response(
config.auto_cost_saving(),
@@ -1666,7 +1677,9 @@ pub(crate) fn decision_usage_batch(
let source_id =
auto_route_usage_source_id(request_route, response.id.as_deref().unwrap_or("systemone"));
let route = request_route.sanitized_for_persistence();
if auto_route_usage_has_reported_data(&usage) {
if response.usage.as_ref().is_some_and(|u| u.complete)
&& auto_route_usage_has_reported_data(&usage)
{
crate::cost_status::RuntimeUsageBatch {
decisions: Vec::new(),
records: vec![RuntimeUsageRecord {
+13 -1
View File
@@ -9473,7 +9473,19 @@ impl RuntimeThreadManager {
}
for receipt in &turn.decision_receipts {
if usage_timestamp_in_range(receipt.route.dispatched_at, since, until) {
totals.route_receipts.insert(receipt.diagnostic_receipt());
if totals
.route_receipts
.iter()
.filter(|r| r.starts_with("decision:"))
.count()
< 64
{
totals.route_receipts.insert(receipt.diagnostic_receipt());
} else {
totals
.route_receipts
.insert("decision:diagnostic_receipt_bound_reached".to_string());
}
}
}
for child in &turn.routed_usage {
+1
View File
@@ -22003,6 +22003,7 @@ async fn decision_receipt_lease_persists_terminal_turn_and_replays_exactly_once(
);
}
drop(lease);
drop(manager);
let restarted = test_manager(directory)?;
let reloaded = restarted.store.load_turn(&turn.id)?;
assert_eq!(reloaded.status, RuntimeTurnStatus::Completed);
+25 -1
View File
@@ -1809,7 +1809,7 @@ impl SessionManager {
turn_id,
&receipt.source_id,
&receipt.route,
receipt.usage.as_ref(),
receipt.usage.as_ref().filter(|_| receipt.usage_complete),
Some(receipt),
)
}
@@ -4280,6 +4280,30 @@ mod tests {
.expect("ledger")
.contains("raw-decision-response-id")
);
save_late_usage_test_session(&manager, "decision-partial-origin");
let mut partial = receipt.clone();
partial.source_id = "partial-provider-response-id".into();
partial.usage_complete = false;
partial.usage.as_mut().expect("partial usage").output_tokens = 0;
manager
.persist_late_decision_receipt("decision-partial-origin", "partial-turn", &partial)
.expect("partial diagnostic append");
let partial_session = manager
.load_session_snapshot("decision-partial-origin")
.expect("partial replay");
assert_eq!(
partial_session.metadata.total_tokens, 0,
"partial provider counters remain diagnostic rather than an authoritative subtotal"
);
assert_eq!(partial_session.metadata.cost.unpriced_turns, 1);
assert!(
partial_session
.metadata
.cost
.route_receipts
.iter()
.any(|r| r.contains("0.000012054"))
);
manager.delete_session("decision-origin").expect("delete");
assert!(
manager
+35
View File
@@ -85,6 +85,7 @@ struct ShadowUsageContext {
impl ShadowUsageContext {
fn capture(owner: Option<&str>) -> Self {
let owner = owner.map(str::trim).filter(|owner| !owner.is_empty());
Self {
scope: crate::cost_status::scope_token(),
runtime_owner: owner.map(str::to_owned),
@@ -409,6 +410,10 @@ async fn evaluate(
output_tokens: u.output_tokens,
..Default::default()
}),
usage_complete: response
.usage
.as_ref()
.is_some_and(|u| u.complete && (u.input_tokens > 0 || u.output_tokens > 0)),
shadow: true,
valid_answers: response.answers_validated != Some(false),
evidence: crate::model_routing::AutoRouteDecisionEvidence {
@@ -1124,4 +1129,34 @@ mod tests {
assert_eq!(drops.lock().expect("drop receipts").len(), 1);
crate::cost_status::finish_runtime_usage_owner(owner);
}
#[tokio::test]
async fn empty_origin_and_receipt_only_batch_use_the_existing_interactive_pool() {
let _scope = crate::cost_status::test_scope();
let context = ShadowUsageContext::capture(Some(" "));
assert!(context.runtime_owner.is_none());
context
.report(crate::cost_status::RuntimeUsageBatch {
decisions: vec![crate::cost_status::decision_receipt_fixture(
"ownerless-fixture",
)],
..Default::default()
})
.await;
let projected = crate::cost_status::drain();
assert!(
projected
.route_receipts
.iter()
.any(|r| r.contains("0.000012054"))
);
assert_eq!(
projected.unpriced_turns, 0,
"diagnostic receipts do not mint a new charge"
);
assert!(
crate::cost_status::take_runtime_usage(" ")
.decisions
.is_empty()
);
}
}