PINK DITAv2: kernel-level finiteness guard (no more null-string crash on inf/NaN)
The aborted hard cutover crash-looped with "Rust kernel returned null string" from process_intent on the first live trading step. Root cause (reproduced): a non-finite (inf/NaN) numeric field reaching the kernel — Python json.dumps emits the Infinity/NaN token, serde_json rejects it at parse, and the FFI returned null. Magnitude is fine; only finiteness was the problem. Defense in depth, kernel catches it: - Rust FFI (lib.rs): dita_kernel_process_intent_json / _on_venue_event_json now return a clean INVALID_INTENT KernelResult on parse failure (incl. Infinity/NaN tokens) AND on serialize failure (a non-finite produced internally) — never a null string. - Python bridge (rust_backend.py): ExecutionKernel.process_intent validates intent finiteness/bounds (target_size, reference_price, limit_price, leverage, exit_leg_ratios; size>=0) BEFORE the FFI and rejects INVALID_INTENT, naming the offending field+value. - contracts.py: add KernelDiagnosticCode.INVALID_INTENT. - pink_direct.py: on INVALID_INTENT, log full upstream provenance (snapshot.price, capital, leverage, sizes) so the numerical SOURCE can be located on the next live run. - on_venue_event bridge tolerates the fallback's null slot (uses the live slot). Verified: kernel recompiled; offline 65 + 7 new guard tests green (no regression); direct-FFI inf payload -> INVALID_INTENT (no null crash). NOTE: this turns the cutover crash into a clean rejection — the upstream source of the non-finite (the live run's inf) still needs locating, now aided by the provenance log. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -427,6 +427,27 @@ class PinkDirectRuntime:
|
||||
kernel_intent = _decision_to_kernel_intent(decision, intent, slot_id=0)
|
||||
outcome = self.kernel.process_intent(kernel_intent)
|
||||
|
||||
# Locate the source of any non-finite intent the kernel rejected:
|
||||
# log the full upstream provenance (snapshot price, account capital,
|
||||
# leverage, sizing) so a numerical error can be traced to its origin
|
||||
# rather than silently rejected.
|
||||
if outcome.diagnostic_code == KernelDiagnosticCode.INVALID_INTENT:
|
||||
self.logger.error(
|
||||
"INVALID_INTENT rejected by kernel: %s | provenance: "
|
||||
"snapshot.price=%r capital=%r open_positions=%r leverage=%r "
|
||||
"target_size=%r reference_price=%r limit_price=%r action=%s asset=%s",
|
||||
dict(outcome.details or {}),
|
||||
getattr(snapshot, "price", None),
|
||||
context.capital,
|
||||
context.open_positions,
|
||||
getattr(kernel_intent, "leverage", None),
|
||||
getattr(kernel_intent, "target_size", None),
|
||||
getattr(kernel_intent, "reference_price", None),
|
||||
getattr(kernel_intent, "limit_price", None),
|
||||
decision.action.value,
|
||||
intent.asset,
|
||||
)
|
||||
|
||||
# Read authoritative final state from kernel.
|
||||
final_slot = self.kernel.slot(0)
|
||||
slot_dict = final_slot.to_dict()
|
||||
|
||||
Reference in New Issue
Block a user