Skip to content

Instantly share code, notes, and snippets.

@chris-schra
Last active November 10, 2025 18:59
Show Gist options
  • Select an option

  • Save chris-schra/5e36310635f5b734e326e9938f153a2a to your computer and use it in GitHub Desktop.

Select an option

Save chris-schra/5e36310635f5b734e326e9938f153a2a to your computer and use it in GitHub Desktop.
CC Reddit

load README.md and use git diff since commit 36e1817057ce472ec854f807ccdbd84f824b1754 for paths packages/ai-adapters/codex-mcp/* and packages/ai-adapters-core/src/* to understand my recent refactoring. Note that we currently have 3 agent adapters (including the Codex Agent) with one more to come - just keep in mind during analysis.

(CC does git diff and loads relevant files)

⏺ Refactoring Analysis

  Core Architectural Shift

  You've moved from simple queue-based message handling to a sophisticated state machine with multi-queue lifecycle management. The driver appears to be the new chat.inTurnMessages capability.

  Key Changes

  1. Four-Queue Message Lifecycle

  Before: Single UserMessageQueue with states: queued → submitted → acknowledged → processing → completed

  After: Four separate tracking sets with simplified states: queued → acknowledged → completed

  enqueuedMessages        // Normal queue, processed at turn boundaries
  immediateMessages       // Interrupt current task, process ASAP
  messagesToBeDelivered   // Pre-acknowledgment staging
  messagesInProcessing    // Post-acknowledgment tracking

  The state machine drives message delivery:
  - turn_finished | active → Process enqueuedMessages
  - Any completion state → Process immediateMessages first
  - processing_finished → Transition to idle

  2. Processing State Machine (9 states)

  idle → processing_started → turn_started → step_started 
    ↓                                           ↓
  paused ← turn_finished ← processing_finished ← step_finished

  CodexAgent maps provider events to states:
  - task_started → turn_started
  - agent_reasoning_section_break / exec_command_begin → step_started
  - exec_command_end / agent_reasoning → step_finished
  - task_complete → turn_finished → processing_finished

  3. Late Immediate Messages
  if (source === this.immediateMessages && !this.pendingMessageHandle) {
    for (const handle of source) {
      handle.markAcknowledged(false); // rejected
    }
    this.handlePause('rejection');
  }

  This rejects immediate messages when there's no active task. But if user sends immediate message while idle, shouldn't we just process it normally? Or is the rule "immediate only makes sense during active processing"?

  (... more summaries ommited because it includes too much internals ...)

  Architectural Patterns

  ✅ Good SEAMS:
  - processMessages() abstracts adapter-specific delivery
  - interrupt() handles provider-specific cancellation
  - Event emitter for plugin subscriptions
  - Configurable queue behavior

  ❓ Questions:

  1. State proliferation: Why 9 states? Some feel redundant:
    - active vs processing_started?
    - turn_finished vs processing_finished?
    - Could simplify to: idle | processing | turn_boundary | paused?
  2. Batch processing: processMessages() takes array but CodexAgent processes one at a time. Is batching needed for another adapter? Or future-proofing?
  3. Processing mutex: Ensures sequential batches. But if processing one message at a time, is mutex necessary? Or for future multi-message support?
  4. immediate vs interrupt(): If immediate delivery calls interrupt(), why expose both as public APIs? Should immediate be sufficient?
  5. replace delivery mode: Still in types but not implemented. Coming soon?
  6. adapterId vs agentId: Purpose of shared adapterId? Namespace isolation? Multi-instance coordination?
  7. Resume from paused: How does caller resume after clearAfterRejection? Just send new message?
  8. Acknowledgment timeouts: 30s default feels long. Tuned for specific provider behavior?
  9. chat.inTurnMessages driver: Is this the capability driving all this complexity? Which adapter supports it? Codex doesn't seem to (based on interrupt being full cancellation, not in-turn injection).

I've answered all its questions:

1: for granularity. It's cool. 2: yeah, for MVP I went with processing only one message at a time but will soon (not today) add "message merging" so we can handle multiple messages in a batch 3: yupp, future multi-message support. 4: where do you see that immediate calls interrupt? 5: coming soon (depends on similar logic as multi-messages) 6: multi-instance coordination - they all share the same message bus and it would be noisy if we didnt have that 7: Yes, new message - nice to have: public resume() 8: It's not long, unfortunately, it's realistic 8: let's ignore that for now - share your thoughts (but goal for current discussion is to make everything clear to you, and eventually discuss how to proceed (I'll tell you how))

Just so you understand that I made sure to rebrief & iterate. It went 3 more iterations because CC didn't fully understand the logic. Then I picked up on 3 ("Late Immediate Messages") from CC's previous message and I mentioned ("I guess...") that I'm pretty sure where the root cause is:

how would you fix the issue surfaced when running 'immediate message when idle' in packages/ai-adapters/codex-mcp/src/test/codex-agent.message-lifecycle.test.ts? I guess the root cause is in updateProcessingState ?

CC read 2 files (the test and the implementation it covers) again, then:

⏺ Found it. The issue is at line 146:

  // Reject late immediate messages (no active task to integrate into)
  if (source === this.immediateMessages && !this.pendingMessageHandle) {

  Problem Flow for "immediate when idle":

  1. Agent is idle, no pendingMessageHandle
  2. sendMessage() with deliveryMode: 'immediate'
  3. addMessage() → adds to immediateMessages → triggers updateProcessingState('active')
  4. Line 146 check: immediateMessages && !pendingMessageHandle = TRUE
  5. Message rejected before processMessages() runs (which sets pendingMessageHandle)

  Root Cause:

  The rejection logic treats all immediate messages without a pending task as "late", but when transitioning idle → active, there's legitimately no pendingMessageHandle yet.

  Fix:

  Only reject immediate messages in states where processing has actually finished:

  const REJECT_LATE_IMMEDIATE_STATES: Array<AgentProcessingState> = [
    'turn_finished',
    'processing_finished'
  ];

  if (
    source === this.immediateMessages &&
    !this.pendingMessageHandle &&
    REJECT_LATE_IMMEDIATE_STATES.includes(state)
  ) {
    // reject - truly late
  } else if (source?.size) {
    // process - includes 'active' from idle
  }

  When state === 'active' (from idle), immediate messages should be processed normally, not rejected.

I thought: LGTM, so:

okay, please fix

CC implemented the "fix":

⏺ Fixed. Immediate messages sent when state === 'active' (from idle transition) will now be processed normally instead of rejected.

  The rejection logic now only applies when processing has actually finished (turn_finished or processing_finished), which correctly identifies "late" immediate messages that arrive after a task completes.

I ran tests (because I have verbose logs enabled so it'd bloat CC's conext) and prompted:

discussed test passes now, but others have a regression. Run yarn test packages/ai-adapters/codex-mcp/src/__test__/codex-agent.message-lifecycle.test.ts to see failing tests. And if you need verbose logs (and only then, since logs will bloat your context) run "DEBUG=true yarn test packages/ai-adapters/codex-mcp/src/test/codex-agent.message-lifecycle.test.ts"

Then CC read the implementation again:

⏺ I see the issue. When a late immediate message arrives after completion:
  1. Agent is idle, message added to immediateMessages
  2. addMessage() transitions to active via microtask
  3. In updateProcessingState('active'), the state is 'active' which isn't in REJECT_LATE_IMMEDIATE_STATES
  4. Message gets processed instead of rejected

  The rejection check needs to happen BEFORE transitioning to active. I'll add early rejection logic in addMessage():
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment