Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 39 additions & 9 deletions .github/workflows/brain-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ env:
BRAIN_HEALTH_THRESHOLD: "80" # Minimum health score for merge
STRESS_TEST_THRESHOLD: "270" # Minimum stress test score (270/300 = 90%)

# The repository's default GITHUB_TOKEN is read-only, so every github-script
# step here that comments on the PR (health, stress, critical state) failed
# with 403 "Resource not accessible by integration". Nobody saw it while the
# health check failed first and those steps were skipped. pull-requests: write
# covers listing and posting comments on a PR; contents: read keeps checkout
# working. A PR from a fork still gets a read-only token.
permissions:
contents: read
pull-requests: write

jobs:
# =========================================================================
# Phase 0: Quick Health Check (fast feedback, runs on all commits)
Expand Down Expand Up @@ -49,17 +59,30 @@ jobs:
# Run health check and capture output
./zig-out/bin/tri stress --health 2>&1 | tee /tmp/health-output.txt || true

# What the score is: `tri stress --health` (src/tri/brain_health.zig)
# runs a fixed list of probes against the three brain regions linked
# into tri -- basal_ganglia, reticular_formation, locus_coeruleus --
# and prints exactly one plain-ASCII line
# Score: <n.n>/100 (<passed> of <total> probes passed)
# where n = 100 * passed / total, followed by "Status: HEALTHY" (all
# probes passed) or "Status: UNHEALTHY". It exits 1 on any failed
# probe. This step ignores that exit code (`|| true`) and gates only
# on the threshold, so a run with a few failed probes (3 of 17 is
# 82.4) still passes here; it gets a warning annotation below.
# The CLI Smoke Test job runs the same command without `|| true` and
# fails on any failed probe, but it only runs after brain-unit,
# brain-integration and brain-stress have succeeded.
#
# An absent measurement is not a measurement of zero.
#
# `grep "Score:" ... || echo "0"` used to turn "the command printed no
# Score: line at all" into the NUMBER 0, which the threshold check
# below then reported as "Brain health 0 below threshold 80". That
# reads as a measured regression. It is not one: `tri stress --health`
# currently prints "stress-test: TODO - not implemented yet", so there
# has never been a score to compare. The gate has failed on every run
# in its history, including on main, for that reason alone -- and a
# check that is always red carries as little information as one that
# is always green.
# reads as a measured regression. It was not one: until the probes
# above existed, `tri stress --health` printed "stress-test: TODO -
# not implemented yet", so there was never a score to compare, and the
# gate failed on every run for that reason alone. If the Score: line
# ever disappears again, this step says "not measured", not "0".
#
# Same class as A36/A37, where an unmeasured field rendered as an FPGA
# measurement on a public route. Report what happened, not a number
Expand All @@ -77,7 +100,12 @@ jobs:
# "Score: 92" would have parsed as empty and reached bc as an empty
# string. A Score: line we cannot parse is also "not measured" -- it
# is not zero either.
HEALTH_SCORE=$(grep "Score:" /tmp/health-output.txt | grep -oE "[0-9]+(\.[0-9]+)?" | head -1 || true)
#
# Only the number directly after "Score:" on the first Score: line is
# read. Taking the first number anywhere on the line would read "32"
# out of a colour escape such as ESC[32m placed before "Score:".
SCORE_LINE=$(grep -m1 "Score:" /tmp/health-output.txt || true)
HEALTH_SCORE=$(printf '%s\n' "$SCORE_LINE" | sed -nE 's/.*Score:[[:space:]]*([0-9]+(\.[0-9]+)?).*/\1/p')
if [ -z "$HEALTH_SCORE" ]; then
echo "HEALTH_SCORE=" >> $GITHUB_OUTPUT
echo "HEALTHY=unknown" >> $GITHUB_OUTPUT
Expand All @@ -87,11 +115,13 @@ jobs:
fi
echo "HEALTH_SCORE=$HEALTH_SCORE" >> $GITHUB_OUTPUT

# Check if healthy
if grep -q "HEALTHY" /tmp/health-output.txt; then
# Check if healthy. Match the whole status line: a bare
# `grep -q "HEALTHY"` also matches "UNHEALTHY".
if grep -qx "Status: HEALTHY" /tmp/health-output.txt; then
echo "HEALTHY=true" >> $GITHUB_OUTPUT
else
echo "HEALTHY=false" >> $GITHUB_OUTPUT
echo "::warning::brain health $HEALTH_SCORE/100: at least one probe failed -- see the FAIL rows in the output above"
fi

# Fail if below threshold
Expand Down
38 changes: 27 additions & 11 deletions src/brain/reticular_formation.zig
Original file line number Diff line number Diff line change
Expand Up @@ -449,27 +449,24 @@ pub const EventBus = struct {
pub fn poll(self: *EventBus, since: i64, allocator: std.mem.Allocator, max_events: usize) ![]AgentEventRecord {
self.mutex.lock();

// First pass: count matching events for exact capacity
const limit = if (max_events == 0) self.count else @min(self.count, max_events);
// First pass: count matching events for exact capacity.
// `max_events` caps the events that pass the `since` filter, so the
// scan covers the whole buffer and stops only once the cap is reached.
const cap = if (max_events == 0) self.count else max_events;
var match_count: usize = 0;
var i: usize = 0;
while (i < limit) : (i += 1) {
while (i < self.count and match_count < cap) : (i += 1) {
const idx = (self.head_idx + i) % MAX_EVENTS;
if (self.buffer[idx].timestamp > since) {
match_count += 1;
if (max_events > 0 and match_count >= max_events) break;
}
if (self.buffer[idx].timestamp > since) match_count += 1;
}

// Allocate with exact capacity
var results = try std.ArrayList(AgentEventRecord).initCapacity(allocator, match_count);
errdefer results.deinit(allocator);

// Second pass: collect events
// Second pass: collect the same `match_count` events, oldest first
i = 0;
while (i < limit) : (i += 1) {
if (max_events > 0 and results.items.len >= max_events) break;

while (i < self.count and results.items.len < match_count) : (i += 1) {
const idx = (self.head_idx + i) % MAX_EVENTS;
const stored = &self.buffer[idx];

Expand Down Expand Up @@ -812,6 +809,25 @@ test "EventBus poll with max_events limit" {
try std.testing.expectEqual(@as(usize, 2), events.len);
}

test "EventBus poll applies max_events after the since filter" {
const allocator = std.testing.allocator;
var bus = EventBus.init(allocator);
defer bus.deinit();

// Three events at or before `boundary`, then two strictly after it.
for (0..3) |_| try bus.publish(.agent_spawned, .{ .agent_spawned = .{ .agent_id = "old" } });
const boundary = std.time.milliTimestamp();
while (std.time.milliTimestamp() <= boundary) std.Thread.sleep(std.time.ns_per_ms / 4);
for (0..2) |_| try bus.publish(.agent_spawned, .{ .agent_spawned = .{ .agent_id = "new" } });

// Two events match; the cap of 1 must return the older of those two,
// not stop after scanning the first (non-matching) buffered event.
const events = try bus.poll(boundary, allocator, 1);
defer allocator.free(events);
try std.testing.expectEqual(@as(usize, 1), events.len);
try std.testing.expectEqualStrings("new", events[0].data.agent_spawned.agent_id);
}

test "EventBus poll returns empty when no events" {
const allocator = std.testing.allocator;
var bus = EventBus.init(allocator);
Expand Down
Loading
Loading