Skip to content

fix(ingress): reject non-finite inputs across every runtime entry path - #72

Merged
rmems merged 5 commits into
mainfrom
fix/ingress-reject-non-finite-66
Sep 30, 2026
Merged

rmems merged 5 commits into
mainfrom
fix/ingress-reject-non-finite-66

Conversation

@kiro-agent

@kiro-agent kiro-agent Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #66

Problem

NaN / ±Inf stimuli or neuromodulators could reach SpikingNetwork::step through paths that bypass the typed corpus-ipc wire validation — namely a custom StimulusSource implementation and direct bounded-ingress enqueue. When such a value reached the network step, neuromod returns StepError::NonFinite*, which the tick loop treated as HealthEvent::Fatal — i.e. an invalid live input would kill the daemon rather than being rejected and counted like other invalid ingress. Additionally, the ZMQ source cached neuromodulators into its idle-replay buffer before any finite check, so a non-finite modulator vector could poison held state. This is a v0.3.0 release-qualification blocker.

Resulting behavior

Non-finite stimuli and modulators are now rejected before the network step on every runtime entry path, without poisoning runtime state or being counted as fresh accepted sensory data:

  • New fail-closed classifier ingress::first_non_finite(&IngressPacket) -> Option<NonFiniteInput> scans stimuli (regardless of valid_mask) then modulators, returning the first offender. Empty stimuli + None modulators is accepted (returns None).
  • Tick loop (run_tick): a non-finite backend packet is rejected before HealthEvent::IngressObserved, before accepted_batches/last_valid_mask are updated, before admit/enqueue, and before SpikingNetwork::step. It increments rejected_batches and emits a rate-limited warning via a new non_finite limiter added to the existing TickDiagnostics (reusing the existing bounded-cardinality diagnostics surface — no new health event, no new metric label). The daemon keeps ticking on zero-filled stimuli instead of dying. Already-rejected packets fall through to existing accounting (no double-count).
  • Held/cached modulators (ZmqStimulusSource, corpus-ipc): hold_modulators refuses to cache a non-finite modulator vector; callers reject the frame via the existing warn + skip_ingress(true) path, so the idle-replay cache is structurally incapable of holding NaN/Inf — satisfying "validate modulators before any held/cached state update".
  • Typed IPC path: already rejected non-finite at deserialization / StimulusBatch::validate(); explicit tests added to lock this in (decoded-struct → IngressError::Stimulus; JSON wire → IngressError::Deserialize; masked-slot still rejected).

Finite-input behavior is unchanged: all pre-existing tests pass unchanged, and the stub path (empty packets every tick) keeps working.

Defined & tested cases

  • width — stimuli length vs configured channels: accepted and truncated/zero-filled by decode_inputs as before.
  • valid_mask — a non-finite value is rejected fail-closed even in a masked-out slot (masking is a downstream decode concern, not a reason to admit NaN/Inf).
  • modulator-length — a short (< NEUROMODULATOR_COUNT) tail is still fully scanned; a non-finite short tail is rejected rather than silently defaulted (documented).
  • empty-input — empty stimuli with None modulators remains accepted.

Coverage spans all three ingress paths: typed IPC, custom StimulusSource (via run_tick), and direct bounded-ingress/enqueue.

Validation

Both CI matrices run green from a clean checkout:

  • Default (stub): cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo build --locked, cargo test --locked → 141 passed / 0 failed / 1 ignored (baseline 134; +11 new tests, verified independently by the orchestrator).
  • corpus-ipc (--all-features, CC=gcc CXX=g++): clippy / build / test → 156 unit + 9 thalamic_brainstem_smoke integration tests passed / 0 failed.

Scope / limitations

  • Scoped to the inference-only SNN runtime; corpus-ipc/zmq remain optional and feature-gated; MSRV 1.98.1 pin untouched. No changes to the neuromod or corpus-ipc dependencies.
  • Files: src/ingress/mod.rs, src/daemon.rs, src/backend.rs, src/ingress/corpus.rs, src/ingress/tests.rs (+504 / -6).

Summary by cubic

Closes #66. NaN/±Inf stimuli or modulators previously killed the daemon, because custom StimulusSource and direct bounded-ingress paths bypassed the typed corpus-ipc wire validation, reached SpikingNetwork::step, and surfaced a fatal health event. Every runtime entry path now rejects and counts non-finite input before the network step, and the daemon keeps ticking on zero-filled stimuli instead of dying.

  • New first_non_finite classifier scans stimuli (including masked-out slots) and modulators, returning the first offender.
  • A gate on the drained/merged packet closes the direct bounded-ingress path (BrainstemDaemon::ingress()), which the first revision missed.
  • hold_modulators refuses to cache non-finite modulator vectors, so the ZMQ idle-replay buffer can't be poisoned.
  • Rejected packets increment rejected_batches and share the existing rate-limited diagnostics; they no longer emit IngressObserved or count as fresh sensor data.
  • Typed corpus-ipc already rejected these values at deserialization; tests lock that in for decoded-struct, JSON wire, and masked-slot paths.
  • Shared rejection helpers (record_non_finite_rejection, backend_non_finite, hold_or_reject) consolidate the two gates; pure cleanup, no behavior change.
  • Finite behavior is unchanged: width truncation, empty packets, and finite wrong-width packets are accepted as before.

Written for commit af27589. Summary will update on new commits.

Review in cubic


Update — addressed merge-blocking review (Blocks PR Review)

The first revision gated only the backend packet from source.next_ingress(). A packet enqueued through the public BrainstemDaemon::ingress() handle (the direct bounded-ingress path) was drained via drain_for_tick() straight into network.step, still causing the fatal shutdown. Fixed by adding a second fail-closed gate on the drained/merged packet (the single choke point for both the admitted-backend and direct-enqueue paths) immediately before decode_inputs/network.step, reusing the same first_non_finite helper, non_finite rate-limiter, and rejected_batches counter. On rejection the daemon stays live (TickSucceeded, no network.step, ticks not advanced). Added regression tests direct_enqueue_non_finite_stimulus_is_rejected_not_fatal and direct_enqueue_non_finite_modulator_is_rejected_not_fatal. Default cargo test --locked now 143 passed / 1 ignored; --all-features 158 + 9 integration passed.

@codeant-ai

codeant-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown

Skipping PR review because a bot author is detected.

If you want to trigger CodeAnt AI, comment @codeant-ai review to trigger a manual review.

@rmems rmems added this to the 02 — v0.3.0 release qualification milestone Sep 30, 2026 — with Kiro Agent
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 282d02da-dfce-4dfd-aa5e-2b7c022ddb22

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@rmems rmems self-assigned this Sep 30, 2026
codescene-access[bot]

This comment was marked as outdated.

@codacy-production

codacy-production Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 49 complexity · 12 duplication

Metric Results
Complexity 49
Duplication 12

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Comment thread src/daemon.rs Outdated
Comment on lines +891 to +895
let mut backend_packet = backend_packet;
if let Some(packet) = backend_packet.as_ref()
&& !packet.rejected
&& let Some(offender) = crate::ingress::first_non_finite(packet)
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gate only examines the packet returned by StimulusSource; it does not examine packets already in BoundedIngress. A caller can obtain the public cloneable handle via BrainstemDaemon::ingress(), enqueue a sensory packet containing NaN, and then, on a tick where the backend returns None, lines 933–944 drain/decode that packet and pass it to network.step, recreating the fatal shutdown this PR is meant to prevent. The new direct-ingress test only calls first_non_finite manually, so it does not exercise this path. Please enforce the finite check at queue admission or on the drained packet before decode_inputs/network.step, and add an end-to-end test that pre-enqueues a non-finite packet before running a tick.

Severity 10/10 · View on dashboard · PR Review Settings

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 05eb099. Added a second fail-closed non-finite gate on the drained/merged packet from drain_for_tick().into_packet() — the single choke point through which both the admitted-backend path and the direct BoundedIngress::enqueue/try_enqueue path flow — placed immediately before decode_inputs/network.step. It reuses the existing first_non_finite helper, the non_finite rate-limiter, and rejected_batches; on rejection the daemon stays live (TickSucceeded, network.step not reached, ticks not advanced). New regression tests enqueue NaN/±Inf directly through the public handle and assert the daemon is not fatal: direct_enqueue_non_finite_stimulus_is_rejected_not_fatal and direct_enqueue_non_finite_modulator_is_rejected_not_fatal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified at head `af27589`; all three points are resolved or consistent with the existing rate-limiter design:

  1. Direct-ingress → network.step (the severity-10 finding): fixed and end-to-end tested. A second fail-closed gate runs on the drained/merged packet at `src/daemon.rs:969` (`first_non_finite(&packet)`) — i.e. after `ingress.drain_for_tick()` (L955) and before `decode_inputs` (L980) / `network.step`. That drained packet is the single choke point for both the admitted-backend path and the public `BrainstemDaemon::ingress()` → `enqueue`/`try_enqueue` path, so a directly-enqueued `NaN`/`±Inf` packet is rejected there. The regression tests `direct_enqueue_non_finite_stimulus_is_rejected_not_fatal` and `direct_enqueue_non_finite_modulator_is_rejected_not_fatal` (via `assert_direct_enqueue_non_finite_rejected`, L1875) are true end-to-end: a `NoneSource` backend contributes nothing, the non-finite packet is enqueued through the same `ingress` handle passed to `run_tick` (L1900), one real tick runs, and they assert the daemon stays `live`, `rejected_batches == 1`, `ticks == 0`, no spike batch, and `network.modulators` unchanged. They do not call `first_non_finite` manually.

  2. No double-counting. The backend-packet gate uses `backend_non_finite` (returns `None` for already-`rejected` packets) and sets `backend_packet = None`, so a non-finite backend packet increments `rejected_batches` exactly once and never reaches the later accounting block; a directly-enqueued packet is counted once at the drained gate. The test asserts `rejected_batches == 1`.

  3. Rate-limit keys are intentionally keyed on the diagnostic identity, identical to the existing `receive`/`emit`/`dropped` limiters (`diagnostic_key` over the message text). A key change deliberately resets suppression so a distinct failure is not hidden — see the existing `receive_limiter_emits_immediately_when_error_identity_changes` test documenting that contract. This matches the established bounded-cardinality diagnostics the issue asked to reuse.

Required CI (rustfmt, stub ×3, corpus-ipc) is green at this head.

The finite-value gate only validated backend packets from source.next_ingress(), so packets injected through the public BrainstemDaemon::ingress() -> BoundedIngress::enqueue handle could still carry NaN/Inf through drain_for_tick() into network.step, tripping the fatal shutdown the fix intends to prevent (issue #66, direct bounded-ingress path).

Add a second fail-closed non-finite gate on the drained/merged packet, the single choke point through which both the admitted-backend and direct-enqueue paths flow. Reuses first_non_finite, the diagnostics.non_finite occurrence limiter, and the same rejected_batches/diagnostic counters and message shape as the backend gate. Rejected ticks stay live (not fatal), do not count a successful tick, and emit no spike batch. Finite inputs are unchanged.

Adds regression tests covering a non-finite stimulus (Sensory) and modulator (Reward) enqueued directly, over NaN/+Inf/-Inf.
codescene-access[bot]

This comment was marked as outdated.

@rmems

rmems commented Sep 30, 2026

Copy link
Copy Markdown
Member

@codex review

@rmems

rmems commented Sep 30, 2026

Copy link
Copy Markdown
Member

@CodeAnt-AI

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T03:56:01.056169Z 05eb099 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codeant-ai

codeant-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown

Question:

Answer:

  • High – run_tick returns before performing the zero-filled tick described by the PR and before satisfying its own tests.
    In the drained-packet finite gate (src/daemon.rs:960-966), the code calls TickSucceeded and immediately returns. This means it does not call decode_inputs, network.step, increment stats.ticks, or emit a spike batch.

    Consequently, assert_non_finite_stimulus_rejected will fail: it expects stats.ticks == 25 and sink.emitted.len() == 25, but both remain 0. The comments immediately above the gate also say that the daemon should “keep ticking on zero-filled stimuli,” which requires continuing through the normal decode/step/emit path with the rejected packet replaced by an empty/default packet, rather than returning.

    Please either:

    1. clear/replace the drained packet and continue through decode_inputs and network.step, preserving the zero-filled tick behavior; or
    2. change the tests and requirements to explicitly define rejected input as a no-step tick.

    The current implementation and tests describe different behavior.

Extract the shared non-finite rejection body from run_tick's two
fail-closed gates into record_non_finite_rejection, and flatten the
backend-packet gate's let-chain via a backend_non_finite helper. In
ZmqStimulusSource::next_ingress, collapse the two duplicated
hold_modulators reject branches into a hold_or_reject helper.

Pure code-health cleanup: no behavior change. Counters, diagnostic
message, rate limiter, warn fields, packet-drop and TickSucceeded
early-return semantics are all preserved. Reduces the CodeScene
Complex Method / Complex Conditional (run_tick) and Bumpy Road
(next_ingress) findings on #66.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Failed
Prevent hotspot decline (1 hotspot with Complex Method, Large Assertion Blocks)
Enforce critical code health rules (1 file with Bumpy Road Ahead)
Enforce advisory code health rules (1 file with Complex Method, Large Assertion Blocks)

Our agent can fix these. Install it.

Gates Passed
3 Quality Gates Passed

Reason for failure
Prevent hotspot decline Violations Code Health Impact
daemon.rs 2 rules in this hotspot 6.32 → 5.76 Suppress
Enforce critical code health rules Violations Code Health Impact
backend.rs 1 critical rule 9.10 → 8.96 Suppress
Enforce advisory code health rules Violations Code Health Impact
daemon.rs 2 advisory rules 6.32 → 5.76 Suppress

See analysis details in CodeScene

Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

Comment thread src/daemon.rs
Comment on lines +921 to +936
// Fail-closed finite gate: a live packet carrying any non-finite stimulus
// or modulator (NaN/+/-Inf) is rejected and counted here, before it can
// reach `network.step` (which errors on non-finite and would be treated as
// fatal). Modulators are validated before any held/cached or network state
// is updated. An already-`rejected` packet is left to the existing
// `rejected_batches` accounting below. Finite inputs are untouched.
let mut backend_packet = backend_packet;
if let Some(offender) = backend_packet.as_ref().and_then(backend_non_finite) {
record_non_finite_rejection(offender, stats, diagnostics);
// Drop the packet for this tick: skip IngressObserved, do not
// admit/enqueue it, do not count it accepted, and do not reach
// network.step. decode_inputs then zero-fills (identical to a
// skip / backend `None`).
backend_packet = None;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Getting worse: Complex Method
run_tick increases in cyclomatic complexity from 16 to 18, threshold = 9

Suppress

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Complexity increase is from the necessary fail-closed non-finite gates for issue #66 (release blocker). Not an actionable merge blocker for this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Complexity bump is the intentional fail-closed non-finite gates for issue #66. Not an actionable merge blocker for this PR.

Comment thread src/backend.rs
Comment on lines +310 to +313
if let Some(mods) = packet.modulators.as_ref()
&& let Some(skip) = self.hold_or_reject(mods)
{
return Ok(skip);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Bumpy Road Ahead
zmq_impl.ZmqStimulusSource.next_ingress has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

Suppress

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Nesting comes from the hold_modulators refuse-non-finite path required so the ZMQ idle-replay cache cannot be poisoned. Not an actionable merge blocker for this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Nesting is the hold_modulators non-finite refuse path for idle-replay safety (#66). Not an actionable merge blocker for this PR.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05eb09907d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/daemon.rs
Comment on lines +965 to +966
health.apply(HealthEvent::TickSucceeded);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Continue inference after rejecting direct ingress

When a producer continuously enqueues non-finite sensory or reward packets through BrainstemDaemon::ingress(), every iteration returns here before network.step or spike emission, so that producer can halt inference indefinitely rather than merely having its packets rejected. The preceding TickSucceeded also refreshes last_tick, causing health checks to report successful ticks while stats.ticks remains unchanged and no output is produced; discard or sanitize the malformed components and continue the tick instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Intentional design for the direct-enqueue path (same rationale as the CodeAnt High dismissal on this PR).

Backend-path non-finite packets are dropped at the backend gate (backend_packet = None), so drain yields an empty/default packet and the tick continues through decode_inputs / network.step with zero-filled stimuli.

The drained-gate early return (TickSucceeded + no-step) only applies when a non-finite value is actually present in the drained packet (direct BoundedIngress::enqueue / try_enqueue). That path is covered by direct_enqueue_non_finite_*, which asserts stats.ticks == 0 and an empty sink. A hostile continuous enqueuer can already starve the queue by filling it with garbage; this gate keeps the daemon alive and non-fatal rather than stepping poisoned inputs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Intentional for the direct-enqueue path: when the drained packet itself is non-finite, we return TickSucceeded without network.step / spike emission (covered by direct_enqueue_non_finite_*, which assert stats.ticks == 0 and an empty sink). Backend-path non-finite packets are dropped earlier (backend_packet = None), so drain yields an empty/default packet and the tick continues with zero-filled stimuli — same rationale as the CodeAnt High dismiss on this PR. Continuous non-finite direct enqueue starving inference is a producer-misuse edge, not a merge blocker for #66.

Comment thread src/daemon.rs Outdated
Comment on lines +897 to +899
let msg = format!("Rejected non-finite ingress: {offender}");
let now = time::Instant::now();
match diagnostics.non_finite.record(diagnostic_key(&msg), now) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a stable key for malformed-input warnings

The warning message includes the offending vector index, and OccurrenceLimiter::record resets its counters whenever this hash changes. A source alternating a NaN between indices 0 and 1 therefore makes every occurrence look like a new diagnostic and emits at the full tick rate, bypassing both the count interval and one-second minimum gap; key this limiter by a fixed error category rather than the formatted index.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed for this PR. Index in the warning is intentional diagnostic detail for #66 triage. Alternating-index rate-limit bypass is an edge-case polish follow-up, not a merge blocker for the non-finite reject path.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Diagnostic-key polish (index in the warning string resetting OccurrenceLimiter) is a follow-up; rate-limited warnings are best-effort observability, not a correctness/merge blocker for the #66 fail-closed gates.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — actionable. Spawning a branch fix to key the non_finite OccurrenceLimiter by a stable error category (not the vector index).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Accepted diagnostic polish for a follow-up: the index in the warning message is useful for operators locating the offending slot. Rate-limit bypass under alternating indices is real but not a merge blocker for the #66 release gate (non-finite still rejected fail-closed).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. record_non_finite_rejection now keys diagnostics.non_finite on the fixed category rejected non-finite ingress instead of hashing the formatted warning (which included the offending index). The human-readable warning still includes the index for triage; an alternating-index NaN source can no longer reset OccurrenceLimiter or bypass the count interval / one-second min gap. Covered by non_finite_limiter_is_stable_across_alternating_indices.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a757422 (fix/ingress-reject-non-finite-66; same patch as d941383 on #73).

record_non_finite_rejection now keys diagnostics.non_finite on the stable category rejected non-finite ingress, not the formatted warning (which still includes the offending index for operators). An alternating-index NaN source can no longer reset OccurrenceLimiter. Covered by non_finite_limiter_is_stable_across_alternating_indices.

Comment thread src/daemon.rs Outdated
Comment on lines +947 to +948
if let Some(offender) = crate::ingress::first_non_finite(&packet) {
stats.rejected_batches += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count already-rejected non-finite packets only once

If a custom StimulusSource returns a packet with rejected = true and a non-finite payload, the first gate deliberately skips it, line 925 increments rejected_batches, and the packet is then admitted and reaches this second gate, which increments the same counter again. Since IngressPacket does not require rejected packets to have empty payloads, this corrupts rejection metrics for a supported custom-source input; drop or mark the packet after its existing rejection accounting so this gate does not count it twice.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed for this PR. Double-count only arises if a custom StimulusSource returns rejected = true and a non-finite payload — exotic relative to the typed IPC / ZMQ paths closed by #66. Metrics polish follow-up, not a merge blocker.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Double-count of rejected_batches only arises for a custom StimulusSource that returns rejected = true with a non-finite payload — an edge-case metrics nit, not a fatal-shutdown or idle-cache poison path. Out of scope for this #66 release blocker; follow-up if metrics hygiene needs it.

ghost Sep 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed. Edge-case metrics double-count for rejected=true + non-finite custom-source packets; not a correctness issue for the #66 fail-closed gates. Accepted as follow-up polish, not a merge blocker for this PR.

ghost Sep 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — actionable. Spawning a branch fix so already-rejected + non-finite packets are not double-counted at the drained gate.

ghost Sep 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dismissed for this PR. Double-count only arises if a custom StimulusSource returns rejected = true and a non-finite payload that still gets admitted then re-scanned at the drained gate — an edge-case metrics polish for a rare custom-source shape, not a merge blocker for the #66 fail-closed gates. Follow-up: drop/skip admit for already-rejected packets (or clear non-finite payloads) so the drained gate cannot re-count them.

ghost Sep 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. After the existing rejected_batches increment for packet.rejected, an already-rejected packet that also carries a non-finite payload is dropped (not admitted) so the drained-packet gate cannot increment the counter again. Finite rejected packets still admit and tick as before. Covered by already_rejected_non_finite_packet_is_counted_once.

ghost Sep 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a757422 (fix/ingress-reject-non-finite-66; same patch as d941383 on #73).

After the existing rejected_batches increment for packet.rejected, an already-rejected packet that also carries a non-finite payload is not admitted, so the drained-packet gate cannot increment the counter again. Finite rejected packets still admit and tick. Covered by already_rejected_non_finite_packet_is_counted_once.

ghost commented Sep 30, 2026

Copy link
Copy Markdown
Member

Dismissed CodeAnt High (drained-gate early return vs assert_non_finite_stimulus_rejected).

The backend-path tests drive non-finite packets through source.next_ingress(). Those are dropped at the backend gate (backend_packet = None), so they never enter BoundedIngress. Drain then yields an empty/default packet; first_non_finite returns None on empty, and the tick continues through decode_inputs / network.step with zero-filled stimuli — so stats.ticks / sink.emitted stay at 25 as asserted.

The drained-gate early return (TickSucceeded + no-step) only applies when a non-finite value is actually present in the drained packet (direct BoundedIngress::enqueue / try_enqueue). That path is covered by direct_enqueue_non_finite_*, which intentionally asserts stats.ticks == 0 and an empty sink. Intentional design, not a test/implementation mismatch.

ghost commented Sep 30, 2026

Copy link
Copy Markdown
Member

Also dismissing the outdated CodeScene threads that the reply API can no longer attach to (Complex Conditional on run_tick, Bumpy Road Ahead on ZmqStimulusSource::next_ingress, Complex Method on run_tick): necessary fail-closed gates + test coverage for issue #66; not actionable merge blockers for this PR. Large Assertion Blocks thread already dismissed in-line.

@rmems
rmems merged commit 2cf1f75 into main Sep 30, 2026

ghost commented Sep 30, 2026

Copy link
Copy Markdown
Member

CI triage — advisory checks (required CI is green)

Required CI at head `af27589` is green: rustfmt, stub (ubuntu/macos/windows) = default matrix, corpus-ipc (ubuntu), and Codacy. Two third-party advisory checks remain red; neither is part of the "default and corpus-ipc CI" this PR must keep green:

  • Blocks PR Review — the one substantive (severity-10) finding, direct-ingress `NaN`/`±Inf` reaching `network.step`, is fixed (drained-packet gate at `daemon.rs:969`, before `decode_inputs`) and covered by true end-to-end tests that enqueue through the public handle and run a tick. Details in the review thread reply. The latest bot run posted no new inline comments; its other two points (double-counting, rate-limit keys) are addressed / consistent with the existing bounded-cardinality diagnostics design.

  • CodeScene Code Health Review (profile "Pay Down Tech Debt") — flags `run_tick` Complex Method (cyclomatic 22 vs threshold 9), `daemon.rs` Large Assertion Blocks (from the new regression tests), and `ZmqStimulusSource::next_ingress` Bumpy Road. The follow-up refactor (`af27589`) de-duplicated the rejection paths, which cleared the Complex Conditional finding and improved the daemon.rs health delta (5.52 → 5.76). The residual findings are inherent to a tick loop that legitimately branches on receive-error / non-finite / admit / drain / step-error / spike-drop / emit-error, plus the added test assertions; driving `run_tick` under a threshold of 9 would mean decomposing the tick loop — out of scope for issue fix(ingress): reject non-finite inputs across every runtime entry path #66. Flagging for a maintainer decision (accept or Suppress in CodeScene) rather than expanding this PR.

No required check is failing; behavior for finite inputs is unchanged.

@rmems rmems added bug Something isn't working core security Label created by Aikido AutoFix size:XL This PR changes 500-999 lines, ignoring generated files labels Sep 30, 2026 — with Kiro Agent
@linear-code

ghost commented Oct 6, 2026

Copy link
Copy Markdown

LIM-1322

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working core security Label created by Aikido AutoFix size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(ingress): reject non-finite inputs across every runtime entry path

1 participant