Skip to content

feat(core): wire session:* onto the shared RunEventBus (1.W)#28

Merged
cemililik merged 4 commits into
mainfrom
development
Jun 17, 2026
Merged

feat(core): wire session:* onto the shared RunEventBus (1.W)#28
cemililik merged 4 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What & why

Wires AgentSession's injected SessionEventSink onto the one shared RunEventBus and adds the session counterpart of RunHandle, completing Phase-1 workstream 1.W — the session:* event namespace (ADR-0036 "one bus, two namespaces"). Additive Lane-C work; it does not gate M2 (already reached). A session now produces a gap-free, per-sessionId event stream on the same bus a run uses — the substrate relavium chat and the desktop chat tab consume.

Key design decisions

  • next/emit overloads, not a widened shared draft. The engine stays RunEvent-precise (run draft → RunEvent, session draft → SessionEvent, either → RunOrSessionEvent) — no precision lost in the run loop, no unsafe casts.
  • agent:file_patch_proposed is run-only (...runBase, emitted by the AgentRunner adapter, not the shared turn core), so it's dropped defensively at the sink seam — the session stream stays to its contract.
  • BoundedEventStream<E> extracted from RunHandle into event-stream.ts (shared, no-drop push→pull queue) — RunHandle now reuses it (~90 lines de-duplicated).
  • No new ADR — ADR-0036 already decided the one-bus / two-namespaces substrate.

Changes

  • @relavium/shared: RunOrSessionEventSchema + RunOrSessionEvent — the combined validation gate.
  • RunEventBus: next/emit overloads + BusEventListener + SessionEventDraft / BusEventDraft.
  • session-handle.ts (new): SessionHandle (long-lived; terminal only on session:cancelled) + createSessionEventSink.
  • event-stream.ts (new): BoundedEventStream<E>; run-handle.ts refactored to reuse it.
  • docs: sse-event-schema.md session-stream note refined.

Also carries one earlier docs-only commit (632cab1) marking 1.U Done + M2 reached after the PR #27 merge — roadmap update only, no code.

Conformance checklist

  • Strict TS — no any, no unsafe as, no @ts-ignore (reviewer grep-verified)
  • @relavium/llm seam holds — no vendor SDK type in core/shared
  • Engine purity — zero platform imports (tsconfig.purity.json + ESLint fence green)
  • No new runtime dependency
  • Secrets — transport-only; no secret reaches an event payload (masking stays upstream of the bus, ADR-0036)
  • Canonical event names — colon-namespaced + sequenceNumber
  • Docs updated in the one canonical home (sse-event-schema.md)
  • pnpm turbo run lint typecheck test build green (700 core / 249 shared tests)
  • Leakwatch: 0 findings; coverage 100% lines on session-handle.ts
  • relavium-reviewer: ✅ Approve (1 LOW JSDoc fix applied; 3 notes confirmed out-of-scope)

Tests

  • Session-lifecycle bus keying (combined gate, per-session sequence, run/session disjoint)
  • SessionHandle — filter by sessionId, stays open across turn_completed, closes only on session:cancelled, no cross-session/run leak
  • createSessionEventSinksessionId injection + file_patch drop (no sequence consumed)
  • AgentSession → sink → bus → SessionHandle end-to-end

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added session-scoped event streaming (bounded async queue) that stays open across turns and closes on cancellation, with clearer per-turn boundaries and backpressure behavior.
    • Extended the shared event bus to support both run and session namespaces together, with stronger validation and consistent sequence stamping.
    • Exposed run-loop invariant error details for improved diagnostics.
  • Documentation

    • Updated Phase 1/M2 completion messaging (PR #27, 2026-06-16) and confirmed Phase 2 (CLI) is unblocked.
    • Added/updated SSE session event stream semantics documentation.

cemililik and others added 2 commits June 16, 2026 14:47
…#27 merge

PR #27 merged, so per roadmap-done-after-merge the M2 milestone is reached: the 1.U Node
harness proves @relavium/core runs a workflow end-to-end (live streaming + per-node-boundary
checkpointing + cross-process resume + node retry + provider failover, per-attempt cost,
gap-free sequenceNumber). The Phase-1 engine critical path is complete.

- phase-1-engine-and-llm.md: banner advanced (M2 reached, critical path complete, remaining
  Phase-1 work is the additive 1.m5/1.m6 sub-spines, Phase 2 unblocked); §1.U header + the
  Acceptance marked ✅ Done/Met (PR #27); the M2 milestone-table row → M2 ✅ (achieved
  2026-06-16); the dependency-matrix 1.U row → Done (PR #27).
- roadmap/README.md: the global milestone spine M2 row → M2 ✅ *(achieved 2026-06-16, PR #27)*,
  mirroring the M0/M1 format.
- current.md: the milestone sentence now declares M2 reached (next global checkpoint M3); the
  immediate-next-steps tail records 1.U Done + M2 reached + the additive remainder + Phase-2
  unblocked.
- CLAUDE.md / README.md / AGENTS.md: status paragraphs advanced to M2 reached.

Not over-claimed: Phase 1 is NOT "complete" — only its engine CRITICAL PATH is. The agent-first
sub-spine (1.m5: 1.W/1.X/1.Y/1.Z/1.AA) and the multimodal sub-spine (1.m6: 1.AE–1.AH) remain as
additive, off-critical-path Phase-1 work. M2 is gated solely by 1.U (now landed); those sub-spines
explicitly do not gate it.

Docs-only; Leakwatch clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire AgentSession's injected SessionEventSink onto the one shared
RunEventBus and add the session counterpart of RunHandle, completing the
1.W session-event namespace (ADR-0036 "one bus, two namespaces").

- @relavium/shared: RunOrSessionEventSchema + RunOrSessionEvent — the
  combined run+session validation gate the bus parses against.
- RunEventBus: next/emit overloads (run draft -> RunEvent, session draft
  -> SessionEvent, either -> RunOrSessionEvent) so the engine stays
  run-precise while the bus carries both families; BusEventListener for
  the wide subscriber; SessionEventDraft / BusEventDraft draft types.
- session-handle.ts (new): SessionHandle (long-lived async-iterable
  scoped to sessionId, terminal only on session:cancelled) +
  createSessionEventSink (attaches sessionId; drops the run-only
  agent:file_patch_proposed defensively at the seam).
- event-stream.ts (new): BoundedEventStream<E> extracted from RunHandle
  (the shared no-drop push->pull queue) — RunHandle now reuses it.
- per-session sequenceNumber keyed on sessionId, disjoint from runs on
  the same bus; envelope-free drafts stamped at the one translation point.

Tests: session-lifecycle bus keying, SessionHandle filtering/terminal
semantics, the sink (sessionId injection + file_patch drop), and an
AgentSession -> sink -> bus -> SessionHandle end-to-end. Full turbo gate
green (700 core tests); Leakwatch clean; coverage 100% lines on
session-handle.ts.

Refs: ADR-0036, 1.W
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai 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.

Sorry @cemililik, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d88b31cb-1712-40c0-8c7c-254a8253bf01

📥 Commits

Reviewing files that changed from the base of the PR and between d2fbe5b and 59ed0f1.

📒 Files selected for processing (2)
  • packages/core/src/engine/run-handle.test.ts
  • packages/core/src/engine/session-handle.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/engine/session-handle.test.ts

📝 Walkthrough

Walkthrough

Adds a BoundedEventStream<E> async-iterator queue with backpressure, widens RunEventBus to carry both run and session events via RunOrSessionEventSchema, refactors RunHandle onto the shared stream abstraction, and introduces SessionHandle / createSessionEventSink for session-scoped event streaming with per-session sequencing. All roadmap and status docs are updated to mark global milestone M2 (engine end-to-end) as achieved via PR #27.

Changes

Session Streaming on Shared RunEventBus

Layer / File(s) Summary
RunOrSessionEvent schema union and invariant errors
packages/shared/src/run-event.ts, packages/core/src/engine/invariant-error.ts
Adds RunOrSessionEventSchema as z.union([RunEventSchema, SessionEventSchema]) and the RunOrSessionEvent type alias for bus-level validation. Introduces RunLoopInvariantCode union and RunLoopInvariantError class for typed correlation-key and concurrent-consumer violations.
BoundedEventStream async queue with backpressure
packages/core/src/engine/event-stream.ts, packages/core/src/engine/event-stream.test.ts
Introduces DEFAULT_STREAM_CAPACITY and BoundedEventStream<E>, a bounded single-consumer no-drop AsyncIterableIterator with push, close, return, and whenDrained backpressure. Test suite covers onClose cleanup idempotency, return() early exit, optional onClose, and concurrent next() rejection via RunLoopInvariantError.
RunEventBus widened to session+run events
packages/core/src/engine/event-bus.ts, packages/core/src/engine/event-bus.test.ts
Adds SessionEventDraft, BusEventDraft, BusEventListener types; overloads next/emit for all draft types with per-correlation-key sequenceNumber stamping; widens deliver, subscribe, and onListenerError to RunOrSessionEvent; validates against RunOrSessionEventSchema; enforces XOR correlation-key invariant with RunLoopInvariantError. Bus tests verify session lifecycle stamping, per-session sequencing across in-turn dual events, disjointness from run sequences, and invariant enforcement.
RunHandle refactored onto BoundedEventStream
packages/core/src/engine/run-handle.ts, packages/core/src/engine/execution-host.ts
Removes inline RunEventStream, imports BoundedEventStream/DEFAULT_STREAM_CAPACITY, introduces isForRun type guard for bus filtering, updates createRunHandle and createClosedRunHandle to use shared stream, wires bus unsubscription via stream onClose callback. Clarifies RunStore.persistEvent docs to note session:* events are out of scope.
SessionHandle and createSessionEventSink
packages/core/src/engine/session-handle.ts, packages/core/src/engine/session-handle.test.ts, docs/reference/contracts/sse-event-schema.md
Adds SessionStreamHandleEvent union, SessionHandle interface, isForSession guard, createSessionEventSink (attaches sessionId, drops agent:file_patch_proposed), and createSessionHandle (stream filtered to sessionId, closes only on session:cancelled, exposes subscribe/cancel/whenConsumersReady). Test suite covers stream ordering across turns, non-terminal turn boundaries, session scoping on shared bus, passive subscribe filtering, cancel delegation, and sink sequencing with run-only event filtering. SSE schema doc defines session stream contract.
Public API exports and end-to-end integration
packages/core/src/index.ts, packages/core/src/engine/agent-session.test.ts
Extends public surface with createSessionHandle, createSessionEventSink, SessionHandle, SessionStreamHandleEvent, RunLoopInvariantError, and RunLoopInvariantCode. Adds end-to-end test covering AgentSessioncreateSessionEventSinkRunEventBuscreateSessionHandleConsumer pipeline with assertions on event lifecycle order, per-event sessionId, and gap-free sequenceNumber starting from 0.

M2 Milestone Documentation Updates

Layer / File(s) Summary
M2 milestone marked complete
AGENTS.md, CLAUDE.md, README.md, docs/roadmap/README.md, docs/roadmap/current.md, docs/roadmap/phases/phase-1-engine-and-llm.md
All status and roadmap documents updated to mark global milestone M2 achieved (PR #27, 2026-06-16 completion date), Phase-1 engine critical path complete with 1.U landed, remaining Phase-1 work reclassified as additive (agent-first sub-spine and multimodal sub-spine), and Phase 2 (CLI) unblocked. Updated milestones table and dependency matrix reflect completion.

Sequence Diagrams

sequenceDiagram
  participant AgentSession
  participant createSessionEventSink as SessionEventSink
  participant RunEventBus
  participant createSessionHandle as SessionHandle
  participant Consumer

  rect rgba(100, 149, 237, 0.5)
    Note over createSessionHandle,RunEventBus: Setup: subscribe before session:started
    createSessionHandle->>RunEventBus: subscribe(BusEventListener)
  end

  rect rgba(144, 238, 144, 0.5)
    Note over AgentSession,Consumer: Turn execution with streaming
    AgentSession->>createSessionEventSink: emit(session:started draft)
    createSessionEventSink->>RunEventBus: emit(draft + sessionId)
    RunEventBus->>RunEventBus: stamp timestamp + sequenceNumber[sessionId]
    RunEventBus-->>createSessionHandle: deliver(session:started)
    createSessionHandle->>Consumer: BoundedEventStream.push(session:started)

    AgentSession->>createSessionEventSink: emit(agent:token draft)
    createSessionEventSink->>RunEventBus: emit(dual draft + sessionId)
    RunEventBus-->>createSessionHandle: deliver(agent:token w/ sessionId)
    createSessionHandle->>Consumer: BoundedEventStream.push(agent:token)

    AgentSession->>createSessionEventSink: emit(session:turn_completed)
    createSessionEventSink->>RunEventBus: emit(draft + sessionId)
    RunEventBus-->>createSessionHandle: deliver(session:turn_completed)
    createSessionHandle->>Consumer: BoundedEventStream.push(session:turn_completed)
  end

  rect rgba(255, 200, 124, 0.5)
    Note over createSessionEventSink: agent:file_patch_proposed dropped here
    AgentSession->>createSessionEventSink: emit(agent:file_patch_proposed)
    createSessionEventSink-->>createSessionEventSink: drop (run-only event, no sequenceNumber advance)
  end

  rect rgba(255, 165, 0, 0.5)
    Note over AgentSession,Consumer: Session termination
    AgentSession->>createSessionEventSink: emit(session:cancelled draft)
    createSessionEventSink->>RunEventBus: emit(session:cancelled + sessionId)
    RunEventBus-->>createSessionHandle: deliver(session:cancelled)
    createSessionHandle->>Consumer: BoundedEventStream.push(session:cancelled)
    createSessionHandle->>createSessionHandle: close stream + unsubscribe from bus
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • HodeTech/Relavium#2: Both PRs modify packages/shared/src/run-event.ts and add schema contracts; this PR extends the run-event schema into the RunOrSessionEvent union built on that foundation.
  • HodeTech/Relavium#17: This PR directly extends the same packages/core/src/engine/event-bus.ts RunEventBus typing, per-correlation-key sequenceNumber stamping, and validation logic introduced in that PR to add session:* support.
  • HodeTech/Relavium#6: The RunOrSessionEventSchema union and session-stream semantics (including agent:file_patch_proposed dual-envelope behavior and SessionEventSchema) depend on the shared session event definitions and dual-envelope contract from that PR.

Poem

🐇 Hop, hop, the bus now carries sessions too,
A bounded stream keeps every event in queue.
session:cancelled—the only door that shuts,
While sequenceNumber counts without a glitch or rut.
The critical path is done, the CLI awaits—
Phase Two is unblocked, this bunny celebrates! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the primary change: wiring the session event namespace onto the shared RunEventBus. The title is specific, directly related to the main objective, and includes the workstream identifier (1.W).
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

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

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 workstream 1.W, integrating the session:* namespace onto the shared RunEventBus (ADR-0036). It introduces a generic BoundedEventStream to handle both run and session event streams, updates the RunEventBus to validate and stamp both event families with disjoint sequence counters, and implements SessionHandle and createSessionEventSink to wire AgentSession to the bus. The review feedback suggests enhancing BoundedEventStream with an onClose callback to allow proper cleanup of active bus subscriptions when a consumer abandons the stream early, thereby preventing potential memory leaks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +17 to +26
export class BoundedEventStream<E> implements AsyncIterableIterator<E> {
readonly #buffer: E[] = [];
readonly #capacity: number;
#waitingPull: ((result: IteratorResult<E>) => void) | undefined;
#drainWaiters: (() => void)[] = [];
#closed = false;

constructor(capacity: number) {
this.#capacity = capacity;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To prevent memory leaks and unnecessary CPU consumption when a consumer abandons the event stream early (e.g., via break or return in a for await loop), the stream should support an onClose callback. This allows the parent handle to unsubscribe from the shared RunEventBus and clean up any active subscribers.

Suggested change
export class BoundedEventStream<E> implements AsyncIterableIterator<E> {
readonly #buffer: E[] = [];
readonly #capacity: number;
#waitingPull: ((result: IteratorResult<E>) => void) | undefined;
#drainWaiters: (() => void)[] = [];
#closed = false;
constructor(capacity: number) {
this.#capacity = capacity;
}
export class BoundedEventStream<E> implements AsyncIterableIterator<E> {
readonly #buffer: E[] = [];
readonly #capacity: number;
readonly #onClose?: () => void;
#waitingPull: ((result: IteratorResult<E>) => void) | undefined;
#drainWaiters: (() => void)[] = [];
#closed = false;
constructor(capacity: number, onClose?: () => void) {
this.#capacity = capacity;
this.#onClose = onClose;
}

Comment on lines +43 to +54
close(): void {
if (this.#closed) {
return;
}
this.#closed = true;
if (this.#waitingPull !== undefined) {
const resolve = this.#waitingPull;
this.#waitingPull = undefined;
resolve({ value: undefined, done: true });
}
this.#wakeDrainWaiters();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Invoke the onClose callback when the stream is closed, ensuring that any cleanup logic is executed deterministically.

Suggested change
close(): void {
if (this.#closed) {
return;
}
this.#closed = true;
if (this.#waitingPull !== undefined) {
const resolve = this.#waitingPull;
this.#waitingPull = undefined;
resolve({ value: undefined, done: true });
}
this.#wakeDrainWaiters();
}
close(): void {
if (this.#closed) {
return;
}
this.#closed = true;
this.#onClose?.();
if (this.#waitingPull !== undefined) {
const resolve = this.#waitingPull;
this.#waitingPull = undefined;
resolve({ value: undefined, done: true });
}
this.#wakeDrainWaiters();
}

Comment on lines +99 to +128
export function createSessionHandle(
bus: RunEventBus,
sessionId: string,
cancel: () => void,
capacity: number = DEFAULT_STREAM_CAPACITY,
): SessionHandle {
const primary = new BoundedEventStream<SessionStreamHandleEvent>(capacity);
const unsubscribe = bus.subscribe((event) => {
if (!isForSession(event, sessionId)) {
return; // not this session's event (a run event, or another session)
}
primary.push(event);
if (TERMINAL_SESSION_TYPES.has(event.type)) {
primary.close();
unsubscribe();
}
});
return {
sessionId,
events: primary,
subscribe: (listener) =>
bus.subscribe((event) => {
if (isForSession(event, sessionId)) {
listener(event);
}
}),
cancel,
whenConsumersReady: () => primary.whenDrained(),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

When using a shared, long-lived RunEventBus, any active subscribers registered via subscribe or the primary stream itself will leak memory if they are not cleaned up when the session terminates or if the consumer abandons the loop early. By utilizing the onClose callback on BoundedEventStream and tracking active subscribers, we can guarantee that all subscriptions are automatically cleaned up.

export function createSessionHandle(
  bus: RunEventBus,
  sessionId: string,
  cancel: () => void,
  capacity: number = DEFAULT_STREAM_CAPACITY,
): SessionHandle {
  let unsubscribe: () => void = () => {};
  const activeSubscribers = new Set<() => void>();
  const primary = new BoundedEventStream<SessionStreamHandleEvent>(capacity, () => {
    unsubscribe();
    for (const unsub of activeSubscribers) {
      unsub();
    }
    activeSubscribers.clear();
  });
  unsubscribe = bus.subscribe((event) => {
    if (!isForSession(event, sessionId)) {
      return; // not this session's event (a run event, or another session)
    }
    primary.push(event);
    if (TERMINAL_SESSION_TYPES.has(event.type)) {
      primary.close();
    }
  });
  return {
    sessionId,
    events: primary,
    subscribe: (listener) => {
      const unsub = bus.subscribe((event) => {
        if (isForSession(event, sessionId)) {
          listener(event);
        }
      });
      activeSubscribers.add(unsub);
      return () => {
        unsub();
        activeSubscribers.delete(unsub);
      };
    },
    cancel,
    whenConsumersReady: () => primary.whenDrained(),
  };
}

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/engine/event-bus.ts (1)

87-95: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce an exact-one correlation key before stamping.

Line 87 and Line 123 currently allow drafts that carry both runId and sessionId, then silently key sequence on runId. In validate:false mode, that can mis-sequence events under the wrong correlation domain. Enforce XOR (runId or sessionId, never both) in the bus invariant path.

Suggested fix
-function correlationKey(draft: BusEventDraft): string | undefined {
-  if ('runId' in draft && draft.runId !== undefined) {
-    return draft.runId;
-  }
-  if ('sessionId' in draft && draft.sessionId !== undefined) {
-    return draft.sessionId;
-  }
-  return undefined;
-}
+function correlationKey(draft: BusEventDraft): string {
+  const runId = 'runId' in draft ? draft.runId : undefined;
+  const sessionId = 'sessionId' in draft ? draft.sessionId : undefined;
+  if (runId !== undefined && sessionId === undefined) return runId;
+  if (sessionId !== undefined && runId === undefined) return sessionId;
+  throw new Error('RunEventBus.next: event draft must have exactly one of runId or sessionId');
+}
@@
-    const key = correlationKey(draft);
-    if (key === undefined) {
-      // Internal invariant: the engine always sets exactly one correlation key. Guarded so a bug
-      // surfaces loudly here rather than as a mis-keyed (and therefore ungapped) sequence.
-      throw new Error('RunEventBus.next: event draft has neither runId nor sessionId');
-    }
+    const key = correlationKey(draft);

Also applies to: 123-128

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/engine/event-bus.ts` around lines 87 - 95, The
correlationKey function currently allows drafts with both runId and sessionId,
silently preferring runId without enforcement of an exclusive-or constraint.
Modify the correlationKey function to validate that exactly one of runId or
sessionId is defined (not both, not neither), and throw an error if this
invariant is violated. The same validation logic should also be applied to the
other correlation key enforcement around line 123-128 to ensure consistent XOR
enforcement across the bus invariant path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/core/src/engine/event-bus.ts`:
- Around line 87-95: The correlationKey function currently allows drafts with
both runId and sessionId, silently preferring runId without enforcement of an
exclusive-or constraint. Modify the correlationKey function to validate that
exactly one of runId or sessionId is defined (not both, not neither), and throw
an error if this invariant is violated. The same validation logic should also be
applied to the other correlation key enforcement around line 123-128 to ensure
consistent XOR enforcement across the bus invariant path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1223de33-269e-4aaf-8448-c1ab3e545182

📥 Commits

Reviewing files that changed from the base of the PR and between 64adf7e and 7302720.

📒 Files selected for processing (17)
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • docs/reference/contracts/sse-event-schema.md
  • docs/roadmap/README.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • packages/core/src/engine/agent-session.test.ts
  • packages/core/src/engine/event-bus.test.ts
  • packages/core/src/engine/event-bus.ts
  • packages/core/src/engine/event-stream.ts
  • packages/core/src/engine/execution-host.ts
  • packages/core/src/engine/run-handle.ts
  • packages/core/src/engine/session-handle.test.ts
  • packages/core/src/engine/session-handle.ts
  • packages/core/src/index.ts
  • packages/shared/src/run-event.ts

cemililik and others added 2 commits June 17, 2026 07:04
…ped errors

Fold the PR #28 review findings into 1.W:

- HIGH: BoundedEventStream now takes an onClose callback fired once on close()
  (incl. the early break/return abandon path: return() -> close()).
  createRunHandle / createSessionHandle wire their bus `unsubscribe` to it, so an
  abandoned stream detaches the subscription immediately instead of filtering
  every delivery into a closed buffer until the next terminal — a real leak for
  long-lived sessions.
- MEDIUM: correlationKey now enforces the runId XOR sessionId invariant at the bus
  (throws on BOTH, not only neither) — fail-loud even on the validate:false hot
  path, where the Zod "exactly one key" superRefine is skipped.
- LOW: the bus/stream "can never happen" asserts are now a typed, discriminated
  RunLoopInvariantError (code: both_correlation_keys / no_correlation_key /
  concurrent_consumer) per error-handling.md — the engine's only bare Errors gone.
- sonar: hoist drainSession to module scope; prefer .at(-1).
- ci: prettier --write (format:check green).

Tests: BoundedEventStream onClose (once, on close + early return) + typed
concurrent-next; the XOR both-keys guard in both validate modes. Full gate green
(705 core tests); Leakwatch clean.

Refs: ADR-0036, 1.W
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a regression guard per the PR #28 review: each handle must detach its
primary bus subscription when the consumer abandons the stream early
(`break`/`return` → BoundedEventStream.return() → close() → onClose →
unsubscribe), not only on a terminal event. Asserts the wired unsubscribe
fires for both createRunHandle and createSessionHandle, so a future refactor
that drops the onClose wiring fails loud.

Refs: ADR-0036, 1.W
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@cemililik
cemililik merged commit f7b941a into main Jun 17, 2026
9 checks passed
cemililik added a commit that referenced this pull request Jun 17, 2026
…entBus

PR #28 merged, so mark workstream 1.W ✅ Done across the canonical status docs:

- phase-1-engine-and-llm.md: §1.W heading ✅ Done; the dependency matrix row;
  and a "Landed mechanism" note recording what actually shipped (the combined
  RunOrSessionEventSchema gate + run/session-precise next/emit overloads — NOT
  the originally-planned RunEventDraft widening — the SessionHandle over the
  extracted BoundedEventStream with onClose, and the typed RunLoopInvariantError).
- current.md: Lane-C now has 1.W done, 1.X persistence next; Last updated bumped.
- CLAUDE.md / README.md: status paragraphs reflect 1.W landed.

Lane C (1.m5) continues at 1.X (session persistence). M2 already reached (1.U).

Refs: ADR-0036, 1.W
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Jun 17, 2026
…ling.md (PR #28 review)

Address a PR #28 LOW review finding: RunLoopInvariantError is a public @relavium/core
export but had no mention in the error-handling standard. Add one bullet to the
"Typed errors" section naming the engine's two code-discriminated error families —
EngineStateError (API-boundary) and RunLoopInvariantError (run-loop substrate
invariants) — citing their source (one-canonical-home; no code lists duplicated).
Covers EngineStateError too, so it reinforces the principle consistently rather than
singling out one class. The LlmError seam contract stays the one error detailed in full.

Refs: ADR-0036, 1.W
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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