Skip to content

feat: runtime events hardening — trace_id, circuit breaker, metrics, prune CLI - #1053

Merged
namastex888 merged 4 commits into
devfrom
events-fix
Apr 5, 2026
Merged

namastex888 merged 4 commits into
devfrom
events-fix

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

Hardens the runtime events system with four improvements from the runtime-events-hardening wish.

Closes

Test plan

  • bun run build passes
  • bun test — 2045 tests pass
  • Migration applies cleanly on fresh DB
  • genie db prune-events --dry-run shows count
  • genie metrics now shows event metrics
  • Circuit breaker opens/closes under simulated failures

Test added 4 commits April 4, 2026 19:19
Add first-class trace_id (UUID) and parent_event_id (BIGINT FK) columns
to genie_runtime_events for distributed tracing. Includes partial index
on trace_id, query support via traceId filter, and promotes traceId from
event-router data JSONB to the dedicated column.
Add prune-events subcommand with --older-than and --dry-run options
for on-demand cleanup of old runtime events. Document all retention
policies in docs/RETENTION.md. Includes integration tests.
Wire getEventMetrics() into the metrics CLI to expose events_emitted,
events_failed, last_emit_duration_ms, and circuit_state in both text
and JSON output. Also log warnings in runtime-emit handler instead of
silently swallowing errors.
resolveTargetTeams() was broadcasting executor events to ALL active teams
regardless of whether the erroring agent belonged to them. Test fixtures
creating executors in shared PG (e.g. err-agent in protocol-router.test.ts)
would flood every live agent session with spurious [executor.error] messages.

Now checks event.agentId against team.members and team.leader before routing.
Events without an agentId still broadcast to all active teams (existing behavior).

Closes #1048
@coderabbitai

coderabbitai Bot commented Apr 4, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0f20a19c-25b5-42da-a62e-a3ceeee8e668

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch events-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements data retention policies, including a new db prune-events command and comprehensive documentation. It also introduces distributed tracing support by adding trace_id and parent_event_id columns to the genie_runtime_events table. To enhance system resilience and observability, a circuit breaker mechanism and throughput metrics were added to the event publishing logic. Review feedback identifies a critical issue where the lack of an ON DELETE action on the new self-referencing foreign key will cause pruning to fail. Additionally, improvements are suggested for the circuit breaker's state management logic and the database connection cleanup in the pruning command.

-- 026_events_trace_id.sql — Add trace_id and parent_event_id for distributed tracing (#859)

ALTER TABLE genie_runtime_events ADD COLUMN IF NOT EXISTS trace_id UUID;
ALTER TABLE genie_runtime_events ADD COLUMN IF NOT EXISTS parent_event_id BIGINT REFERENCES genie_runtime_events(id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The self-referencing foreign key on parent_event_id lacks an ON DELETE action. By default, this uses NO ACTION, which will cause the genie db prune-events command (and automatic retention) to fail with a foreign key violation if it attempts to delete a parent event while a newer child event still exists in the table. Given that this table is a high-volume event log with a 14-day retention policy, you should use ON DELETE SET NULL to ensure pruning can proceed without breaking the referential integrity of newer events.

ALTER TABLE genie_runtime_events ADD COLUMN IF NOT EXISTS parent_event_id BIGINT REFERENCES genie_runtime_events(id) ON DELETE SET NULL;

Comment thread src/lib/runtime-events.ts
Comment on lines +13 to +20
isOpen(): boolean {
if (this.failures < this.threshold) return false;
if (Date.now() - this.lastFailure > this.cooldown) {
this.failures = 0;
return false;
}
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The isOpen() method contains a side effect that resets this.failures to 0 when the cooldown period expires. This is problematic because:

  1. It causes the state getter to return closed instead of half-open as soon as isOpen() is called, making the state machine inconsistent.
  2. In high-concurrency scenarios, it allows multiple concurrent requests to proceed immediately after the cooldown (a 'thundering herd'), rather than allowing a single trial request in a true 'half-open' state.
  3. It prevents the recordSuccess() method from detecting that it was in a failed state, so the 'circuit breaker closed' warning will never be logged.

You should remove the side effect and let recordSuccess() handle the transition back to the closed state.

  isOpen(): boolean {
    if (this.failures < this.threshold) return false;
    return Date.now() - this.lastFailure <= this.cooldown;
  }

Comment thread src/term-commands/db.ts
Comment on lines +258 to +283
try {
const sql = await getConnection();

if (options.dryRun) {
const rows = await sql`
SELECT count(*) AS cnt
FROM genie_runtime_events
WHERE created_at < now() - make_interval(secs => ${intervalSec})
`;
const count = Number(rows[0].cnt);
console.log(`Would delete ${count} event${count === 1 ? '' : 's'} older than ${options.olderThan}.`);
} else {
const result = await sql`
DELETE FROM genie_runtime_events
WHERE created_at < now() - make_interval(secs => ${intervalSec})
`;
const count = Number(result.count);
console.log(`Deleted ${count} event${count === 1 ? '' : 's'} older than ${options.olderThan}.`);
}

await shutdown();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Prune failed: ${message}`);
process.exit(1);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The shutdown() call should be moved to a finally block to ensure the database connection pool is always closed, even if a query fails. Note that since process.exit(1) terminates the process immediately, you should replace it with process.exitCode = 1 to allow the finally block to execute before the process exits.

  try {
    const sql = await getConnection();

    if (options.dryRun) {
      const rows = await sql`
        SELECT count(*) AS cnt
        FROM genie_runtime_events
        WHERE created_at < now() - make_interval(secs => ${intervalSec})
      `;
      const count = Number(rows[0].cnt);
      console.log(`Would delete ${count} event${count === 1 ? '' : 's'} older than ${options.olderThan}.`);
    } else {
      const result = await sql`
        DELETE FROM genie_runtime_events
        WHERE created_at < now() - make_interval(secs => ${intervalSec})
      `;
      const count = Number(result.count);
      console.log(`Deleted ${count} event${count === 1 ? '' : 's'} older than ${options.olderThan}.`);
    }
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    console.error(`Prune failed: ${message}`);
    process.exitCode = 1;
  } finally {
    await shutdown();
  }

@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: dc211ac19d

ℹ️ 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".

-- 026_events_trace_id.sql — Add trace_id and parent_event_id for distributed tracing (#859)

ALTER TABLE genie_runtime_events ADD COLUMN IF NOT EXISTS trace_id UUID;
ALTER TABLE genie_runtime_events ADD COLUMN IF NOT EXISTS parent_event_id BIGINT REFERENCES genie_runtime_events(id);

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 Use ON DELETE handling for parent_event_id foreign key

The new parent_event_id reference is a strict self-FK without an ON DELETE action, so age-based cleanup can fail as soon as a newer event references an older parent: DELETE ... WHERE created_at < ... will hit a FK violation on the parent row and abort pruning. That directly affects both automatic retention and genie db prune-events, causing runtime-event cleanup to stop and table growth to continue once parentEventId is used.

Useful? React with 👍 / 👎.

Comment thread src/lib/runtime-events.ts
Comment on lines +53 to +55
let eventsEmitted = 0;
let eventsFailed = 0;
let lastEmitDuration = 0;

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 Persist event throughput metrics outside process memory

getEventMetrics() is backed only by module-level counters (eventsEmitted, eventsFailed, lastEmitDuration), so values reset whenever a new CLI process starts. In practice, genie metrics now runs in a fresh process and therefore reports near-zero/default throughput and circuit state instead of the real system-wide runtime-event activity, making the new metrics misleading for operators.

Useful? React with 👍 / 👎.

@namastex888
namastex888 merged commit 0a637e9 into dev Apr 5, 2026
6 checks passed
@namastex888
namastex888 deleted the events-fix branch April 10, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant