Skip to content

refactor: eliminate all Tier 1 architectural violations (reducer-effect pipeline)#138

Merged
teng-lin merged 6 commits intomainfrom
worktree-arch-tier-1-violation
Feb 25, 2026
Merged

refactor: eliminate all Tier 1 architectural violations (reducer-effect pipeline)#138
teng-lin merged 6 commits intomainfrom
worktree-arch-tier-1-violation

Conversation

@teng-lin
Copy link
Copy Markdown
Owner

Summary

  • Tier 1 feat: add agent team observation support #1: Route local slash command results through SessionReducerSLASH_LOCAL_RESULT/SLASH_LOCAL_ERROR signals instead of calling broadcaster/emitter directly from SlashCommandChain
  • Tier 1 feat: slash command handling with autocomplete in consumer UI #2: Move capabilities NDJSON send and pending-initialize timer into a SessionRuntime post-reducer hook triggered by the new CAPABILITIES_INIT_REQUESTED signal, replacing direct I/O in CapabilitiesPolicy
  • Tier 1 fix: production hardening — security, resilience, and API surface #3: Move CONSUMER_CONNECTED/CONSUMER_DISCONNECTED consumer event emissions into reducer EMIT_EVENT effects; runtime enriches signals with counts and identity before the reducer call
  • Tier 1 feat: rebuild web consumer as React 19 + Zustand + Tailwind v4 + Vite app #4 + P3: Add SEND_TO_CONSUMER effect type; route QUEUE_ERROR signals and handleInboundCommand rejections through the effect pipeline instead of calling broadcaster.sendTo directly
  • Post-review fixes: Guard CAPABILITIES_INIT_REQUESTED against no_backend result (prevented phantom capability timeouts); add ws !== null guards for programmatic sendUserMessage calls; log warning on unregistered socket disconnect; add SEND_TO_CONSUMER effect test + sendTo to makeDeps(); document SEND_TO_CONSUMER exception to the plain-data contract in effect-types.ts

Test Plan

  • pnpm test — all 2923 unit + integration tests pass
  • pnpm typecheck — no TypeScript errors
  • pnpm check:fix — no lint errors
  • Verify CAPABILITIES_INIT_REQUESTED only arms the pending-initialize timer after a successful backend send (not on no_backend/unsupported)
  • Verify QUEUE_ERROR path: queueing a second message while one is already queued sends an error only to the requesting consumer socket
  • Verify handleInboundCommand rejections (session closing, adapter lock) send targeted errors via SEND_TO_CONSUMER effect

Previously handleSystemSignal() called emitEvent() directly for
CONSUMER_CONNECTED and CONSUMER_DISCONNECTED signals, bypassing the
reducer-effect pipeline.

Enrich signals with consumerCountAfter and identity before reducer call,
then produce EMIT_EVENT effects from the pure reducer. Runtime post-reducer
hook now only manages the WebSocket handle map (non-serializable state).
…ONSUMER effect (Tier 1 #4 + P3)

Add SEND_TO_CONSUMER effect type so targeted per-consumer errors go
through the reducer-effect pipeline instead of being sent inline.

Tier 1 #4: MessageQueueHandler.sendTo() calls replaced with QUEUE_ERROR
signal dispatches; reducer produces SEND_TO_CONSUMER effects.

P3 fix: handleInboundCommand() post-reducer user_message and set_adapter
rejections now append SEND_TO_CONSUMER effects to the effect list instead
of calling broadcaster.sendTo() directly.
Update docs/architecture.md:
- Replace Tier 1 table with resolved summary (#1–4 all fixed)
- Update Tier 2 #6 (pendingInitialize now set in correct post-reducer hook)
- Update Tier 3 #14 (handleInboundCommand rejections fixed via SEND_TO_CONSUMER)
- Update Tier 3 #16 (trySendRawToBackend now runtime-internal only)

Also narrow SLASH_LOCAL_RESULT signal source type to "emulated" | "cli"
to match ConsumerMessage contract (caught by typecheck).
…ND_TO_CONSUMER test)

- Guard CAPABILITIES_INIT_REQUESTED against no_backend result so the
  pending-initialize timer is only armed after a successful send
- Add ws !== null guards in handleInboundCommand extraEffects to avoid
  producing SEND_TO_CONSUMER effects for programmatic (null ws) calls
- Log a warning on CONSUMER_DISCONNECTED when the socket is unregistered
- Add sendTo to makeDeps() in effect-executor.test.ts and cover
  SEND_TO_CONSUMER with a dedicated test
- Update effect-types.ts module header to document the SEND_TO_CONSUMER
  exception to the "plain data, no dependencies" contract
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @teng-lin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors core session management logic to align with the established reducer-effect architectural principles. By routing various direct I/O operations and event emissions through a standardized signal-reducer-effect pipeline, the changes enhance the system's testability, predictability, and overall architectural integrity. The introduction of new system signals and an effect type for targeted consumer messages streamlines error handling and event propagation, ensuring a more consistent and robust flow of information within the application.

Highlights

  • Tier 1 Architectural Violations (Reducer-Effect Pipeline) Resolved: All four identified Tier 1 architectural violations related to direct I/O and bypassing the reducer-effect pipeline have been eliminated, improving testability and traceability.
  • Slash Command Handling Refactored: Local and adapter-native slash command results (successes and errors) now route through SLASH_LOCAL_RESULT and SLASH_LOCAL_ERROR system signals, processed by the reducer to produce BROADCAST and EMIT_EVENT effects.
  • Capabilities Initialization Centralized: The capabilities NDJSON send and pending-initialize timer management have been moved into a SessionRuntime post-reducer hook, triggered by a new CAPABILITIES_INIT_REQUESTED signal, replacing direct I/O in CapabilitiesPolicy.
  • Consumer Event Emissions Standardized: CONSUMER_CONNECTED and CONSUMER_DISCONNECTED event emissions now occur as EMIT_EVENT effects produced by the reducer, with the runtime enriching signals with consumer counts and identity before the reducer call.
  • Targeted Error Reporting via New Effect Type: A new SEND_TO_CONSUMER effect type has been introduced to handle targeted error messages. QUEUE_ERROR signals and handleInboundCommand rejections now route through this effect pipeline instead of direct broadcaster.sendTo calls.
  • Post-Review Fixes Implemented: Guards were added for CAPABILITIES_INIT_REQUESTED against no_backend results, ws !== null checks for programmatic sendUserMessage calls, logging for unregistered socket disconnects, and documentation for the SEND_TO_CONSUMER effect's exception to the plain-data contract.
Changelog
  • docs/architecture.md
    • Updated the 'Violations to Core Design Principles' section to mark Tier 1 violations as resolved and detail the applied fixes.
  • src/core/capabilities/capabilities-policy.integration.test.ts
    • Updated createMockRuntime to accept config and added logic to handle CAPABILITIES_INIT_REQUESTED signals, including timer management and raw backend send simulation.
    • Modified createDeps to pass config to createMockRuntime.
  • src/core/capabilities/capabilities-policy.test.ts
    • Refactored the test to verify that sendInitializeRequest dispatches a CAPABILITIES_INIT_REQUESTED signal via the runtime's process method, rather than directly sending raw data.
  • src/core/capabilities/capabilities-policy.ts
    • Removed the randomUUID import and the trySendRawToBackend method.
    • Added a initializeTimeoutMs getter to expose the configuration value.
    • Modified sendInitializeRequest to dispatch a CAPABILITIES_INIT_REQUESTED system signal instead of directly managing timers and sending raw NDJSON to the backend.
  • src/core/session-coordinator.ts
    • Removed the direct broadcaster.sendTo dependency from the MessageQueueHandler constructor.
    • Refactored LocalHandler, AdapterNativeHandler, and UnsupportedHandler instantiations to use a new processSignal function, eliminating direct broadcaster and emitEvent dependencies.
    • Removed the call to capabilitiesPolicy.cancelPendingInitialize during SESSION_CLOSING as this logic is now handled within the runtime's post-reducer orchestration.
  • src/core/session/effect-executor.test.ts
    • Imported WebSocketLike type.
    • Added sendTo to the mock broadcaster in makeDeps.
    • Added a test case to verify that the SEND_TO_CONSUMER effect correctly calls broadcaster.sendTo.
  • src/core/session/effect-executor.ts
    • Added sendTo to the broadcaster interface in EffectExecutorDeps.
    • Implemented the execution logic for the new SEND_TO_CONSUMER effect type, calling deps.broadcaster.sendTo.
  • src/core/session/effect-types.ts
    • Imported WebSocketLike type.
    • Added documentation explaining that SEND_TO_CONSUMER is an exception to the plain-data contract due to carrying a WebSocketLike handle.
    • Extended SystemSignal to include consumerCountAfter for CONSUMER_CONNECTED and consumerCountAfter and identity for CONSUMER_DISCONNECTED.
    • Added new SystemSignal types: CAPABILITIES_INIT_REQUESTED, SLASH_LOCAL_RESULT, SLASH_LOCAL_ERROR, and QUEUE_ERROR.
  • src/core/session/message-queue-handler.test.ts
    • Updated createMockRuntime to include handling for the QUEUE_ERROR system signal, which now uses broadcaster.sendTo.
    • Modified setup and other test functions to remove the direct broadcaster.sendTo callback from the MessageQueueHandler constructor.
  • src/core/session/message-queue-handler.ts
    • Removed the ConsumerMessage import and the sendTo parameter from the constructor.
    • Replaced direct calls to this.sendTo with dispatching QUEUE_ERROR system signals via this.getRuntime(session).process for various queue-related error conditions.
  • src/core/session/session-event.ts
    • Added consumerCountAfter to CONSUMER_CONNECTED signal and consumerCountAfter and identity to CONSUMER_DISCONNECTED signal.
    • Introduced new system signals: CAPABILITIES_INIT_REQUESTED, SLASH_LOCAL_RESULT, SLASH_LOCAL_ERROR, and QUEUE_ERROR.
  • src/core/session/session-reducer.ts
    • Added reduction logic for SLASH_LOCAL_RESULT and SLASH_LOCAL_ERROR signals, producing BROADCAST and EMIT_EVENT effects.
    • Modified CONSUMER_CONNECTED and CONSUMER_DISCONNECTED signal reduction to produce EMIT_EVENT effects, utilizing the newly enriched signal data.
    • Added reduction logic for QUEUE_ERROR signals, producing a SEND_TO_CONSUMER effect.
    • Marked CAPABILITIES_INIT_REQUESTED as a no-op signal for the reducer, as its logic is handled in the runtime's post-reducer orchestration.
  • src/core/session/session-runtime.ts
    • Imported randomUUID.
    • Modified handleInboundCommand to use SEND_TO_CONSUMER effects for user_message and set_adapter rejections, guarding against null WebSockets.
    • Enriched CONSUMER_CONNECTED and CONSUMER_DISCONNECTED signals with consumerCountAfter and identity before passing them to the reducer.
    • Added a warning log for CONSUMER_DISCONNECTED of unregistered sockets.
    • Removed direct emitEvent calls for consumer connection/disconnection events.
    • Moved the logic for CAPABILITIES_INIT_REQUESTED (sending raw NDJSON, timer management) and SESSION_CLOSING (clearing pending initialize timer) into the post-reducer handle mutations section.
  • src/core/slash/slash-command-chain.test.ts
    • Removed imports for createTestSocket, findMessage, noopLogger, and ConsumerBroadcaster.
    • Updated makeLocalSetup to remove broadcaster and emitEvent dependencies, and to use processSignal.
    • Refactored tests for LocalHandler, AdapterNativeHandler, and UnsupportedHandler to assert calls to processSignal with the new SLASH_LOCAL_RESULT and SLASH_LOCAL_ERROR signals, instead of direct broadcaster or emitEvent calls.
  • src/core/slash/slash-command-chain.ts
    • Removed imports for BridgeEventMap and ConsumerBroadcaster.
    • Introduced ProcessSignal type using SystemSignal.
    • Updated LocalHandlerDeps, AdapterNativeHandlerDeps, and UnsupportedHandlerDeps interfaces to replace broadcaster and emitEvent with processSignal.
    • Modified LocalHandler, AdapterNativeHandler, and UnsupportedHandler to dispatch SLASH_LOCAL_RESULT or SLASH_LOCAL_ERROR system signals via this.deps.processSignal instead of directly broadcasting messages or emitting events.
  • src/testing/adapter-test-helpers.ts
    • Updated the MessageQueueHandler constructor call to remove the sendTo callback.
    • Introduced a processSignal helper function that dispatches system signals via the runtime.
    • Modified the instantiation of LocalHandler, AdapterNativeHandler, and UnsupportedHandler to use the new processSignal dependency instead of broadcaster and emitEvent.
Activity
  • The author, teng-lin, has implemented a series of refactorings to address Tier 1 architectural violations.
  • The changes involve significant restructuring of how slash commands, capabilities initialization, consumer events, and targeted error messages are handled, moving towards a more pure reducer-effect pipeline.
  • Extensive unit and integration tests have been updated or added to reflect and validate these architectural changes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This is an excellent and thorough refactoring that successfully eliminates the Tier 1 architectural violations by moving side effects into a proper reducer-effect pipeline. The changes are consistent across the codebase, including production code, tests, and documentation, which significantly improves the architectural purity and maintainability. The minor suggestion to simplify a newly added logic block for improved readability remains relevant.

Comment on lines +318 to +350
// Augment reducer effects with SEND_TO_CONSUMER effects for rejection cases.
// These require the live ws handle, which the pure reducer cannot access.
// ws is null as never for programmatic sendUserMessage() / sendPermissionResponse() calls —
// skip the targeted error in those cases (the caller checks the boolean return value instead).
const extraEffects: import("./effect-types.js").Effect[] = [];
if (msg.type === "user_message" && nextData === prevData && ws !== null) {
// Reducer no-op means closed/closing session — send targeted error.
extraEffects.push({
type: "SEND_TO_CONSUMER",
ws,
message: {
type: "error",
message: "Session is closing or closed and cannot accept new messages.",
},
});
} else if (msg.type === "set_adapter" && ws !== null) {
// Rejected for active sessions — send targeted error to the requesting consumer only.
extraEffects.push({
type: "SEND_TO_CONSUMER",
ws,
message: {
type: "error",
message:
"Adapter cannot be changed on an active session. Create a new session with the desired adapter.",
});
break;
},
});
}

executeEffects(
extraEffects.length > 0 ? [...effects, ...extraEffects] : effects,
this.session,
this.effectDeps(),
);
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

For improved readability and to reduce verbosity, the logic for augmenting the effects array can be simplified. Instead of creating a separate extraEffects array and then conditionally merging it, you can directly push the new effects onto the effects array returned by the reducer. Since effects is a mutable local variable, this is a safe and more direct approach.

    // Augment reducer effects with SEND_TO_CONSUMER effects for rejection cases.
    // These require the live ws handle, which the pure reducer cannot access.
    // ws is null for programmatic sendUserMessage() / sendPermissionResponse() calls —
    // skip the targeted error in those cases (the caller checks the boolean return value instead).
    if (msg.type === "user_message" && nextData === prevData && ws !== null) {
      // Reducer no-op means closed/closing session — send targeted error.
      effects.push({
        type: "SEND_TO_CONSUMER",
        ws,
        message: {
          type: "error",
          message: "Session is closing or closed and cannot accept new messages.",
        },
      });
    } else if (msg.type === "set_adapter" && ws !== null) {
      // Rejected for active sessions — send targeted error to the requesting consumer only.
      effects.push({
        type: "SEND_TO_CONSUMER",
        ws,
        message: {
          type: "error",
          message:
            "Adapter cannot be changed on an active session. Create a new session with the desired adapter.",
        },
      });
    }

    executeEffects(effects, this.session, this.effectDeps());

@teng-lin teng-lin merged commit 509886f into main Feb 25, 2026
6 checks passed
@teng-lin teng-lin deleted the worktree-arch-tier-1-violation branch February 25, 2026 04:46
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