Skip to content

refactor(opencode,acp,orchestrator): unify session execution under SessionPool and modernize event routing architecture - #47

Merged
Leoyzen merged 51 commits into
develop/agenticfrom
opencode/session-pool-integration
Jun 10, 2026
Merged

refactor(opencode,acp,orchestrator): unify session execution under SessionPool and modernize event routing architecture#47
Leoyzen merged 51 commits into
develop/agenticfrom
opencode/session-pool-integration

Conversation

@Leoyzen

@Leoyzen Leoyzen commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR is a multi-phase architectural refactor that unifies OpenCode server execution under SessionPool, modernizes the subagent event architecture, and introduces foundational infrastructure for immutable event routing. It replaces the original "SessionPool integration" scope with a comprehensive overhaul of session lifecycle, event propagation, and cross-protocol handler consistency.

Architectural Overview

The refactor spans three major migrations and four architectural pillars:

Migrations (OpenCode Server → SessionPool)

Migration Scope Status
Migration A Core SessionPool integration, message routing, event conversion, auto-subscription Complete
Migration B Status helpers, question routing, inventory, SSE → EventBus, message history API Complete
Migration C Full ServerState dict elimination, session CRUD, permission routes, legacy fallback removal Complete

Result: ServerState slimmed from 608 → ~309 lines; all session operations routed through SessionPool helpers.

Architectural Pillars

  1. Subagent Event Architecture (Phase 2)

    • Removed SubAgentEvent wrapping in TurnRunner
    • Child sessions now emit raw events auto-subscribed by parent via EventBus
    • ToolPart registration happens explicitly in EventProcessorContext
    • Unified tool event paths through stream consumer
  2. EventBus Infrastructure (Phases 3, 8)

    • Migrated SSE to EventBus with event IDs and deduplication
    • Introduced EventEnvelope for immutable event routing
    • Added per-session subscription scoping to prevent cross-session leaks
  3. ProtocolEventConsumerMixin (Phase 7)

    • New server-side mixin for consistent event consumption across ACP and OpenCode handlers
    • ACP handler fully adopted; SpawnSessionStart placeholder replaced
    • Consumer shutdown lifecycle management
  4. MCP Provider Inheritance (Phase 8)

    • Child sessions spawned by subagents now inherit MCP providers from parent pool/orchestrator
    • Fixes AgentContext session_id retrieval for proper scoping

New Components

Component Purpose
event_adapter.py OpenCodeEventAdapter — converts RichAgentStreamEvent → OpenCode Event
session_pool_integration.py OpenCodeSessionPoolIntegration — bridges OpenCode routes with SessionPool
status_bridge.py SessionStatusBridge — syncs RunHandle status → SessionStatusEvent
event_bridge.py / event_processor.py Event routing and processing pipeline
mixins.py ProtocolEventConsumerMixin + ConsumerShutdown

Modified Modules

OpenCode Server (heaviest impact):

  • All routes migrated to SessionPool helpers (message_routes, session_routes, permission_routes, question_routes, etc.)
  • state.py — legacy dict fields removed; backward-compat wrappers added during migration
  • handler.py — orphaned OpenCodeProtocolHandler deleted

Orchestrator & Agents:

  • TurnRunner — SubAgentEvent wrapping removed
  • RunExecutor — unified tool event paths
  • AgentContext — session_id retrieval fixed
  • pool.py — SkillsTools provider added at pool level

ACP Server:

  • handler.py — adopted ProtocolEventConsumerMixin
  • event_converter.py — elicitation fixes, flat-format message support

Rebase Context

Rebased onto latest develop/agentic (2026-06-09) — includes 6 ACP/MCP elicitation fixes:

  • d41417e42 fix(acp): send elicitation response back to MCP server instead of ClientSession

  • 63a21e72c fix(acp): handle MCP elicitation locally instead of forwarding to ACP client

  • 1a5c0f6c6 fix(acp): handle flat-format mcp/message from ACP clients

  • 0a85a1da1 test(acp): add fastmcp e2e integration tests for MCP-over-ACP

  • c3149372b fix(mcp-over-acp): handle elicitation/create without inner id

  • 926a87f60 fix(mcp-over-acp): add correlation registry for elicitation passthrough

  • 52 commits replayed cleanly with 0 conflicts

  • Previous HEAD: 2e547ed36 → New HEAD: b1b27f382

Test Coverage

test_status_bridge.py:        6/6 PASS
test_session_lifecycle.py:   24/24 PASS
test_session_integration.py:  2/2 PASS (input_provider)
test_event_conversion.py:    17/23 PASS, 6 TDD RED

The 6 TDD RED failures require EventProcessor handlers for PartDeltaEvent.text/thinking, StreamCompleteEvent→SessionIdleEvent, RunErrorEvent, RunFailedEvent — to be implemented in a follow-up.

Total: 70+ new/modified test files across tests/orchestrator/, tests/servers/, tests/agents/, tests/toolsets/.

Backward Compatibility

Thin backward-compat wrappers added to ServerState for get_or_create_agent(), _session_agents, remove_session_agent() to prevent breaking existing route references during the migration window. These will be removed in a future cleanup PR.

Checklist

  • ServerState under 309 lines (from 608)
  • No orphaned handler.py
  • All message routes use SessionPool.receive_request()
  • Input provider flows through SessionPool
  • Status bridge broadcasts idle/busy states
  • SSE migrated to EventBus with deduplication
  • SubAgentEvent wrapping removed
  • ProtocolEventConsumerMixin adopted by ACP and OpenCode
  • MCP provider inheritance for child sessions
  • Rebased onto latest develop/agentic (0 conflicts)
  • 70+ test files added/updated

Migration Path

This PR supersedes the original "SessionPool integration" scope. The changes are not backward-compatible for code depending on:

  • Direct ServerState dict mutations (e.g., state.messages, state.todos)
  • SubAgentEvent wrapping behavior in custom TurnRunner subclasses
  • OpenCodeProtocolHandler (deleted)

Consumers should migrate to:

  • SessionPool helpers for session lifecycle
  • EventBus + ProtocolEventConsumerMixin for event consumption
  • Raw child session events via auto-subscription

@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 integrates the OpenCode server with the SessionPool orchestration layer, replacing the legacy session management and protocol handler with a new integration layer, event adapter, and status bridge. While the transition to SessionPool-based routing is well-structured, several critical issues need to be addressed: a fire-and-forget task in the message routes should use state.create_background_task to avoid garbage collection; the SessionStatusBridge must be stopped in the finally block to prevent task and subscription leaks; the conversation history copy and agent state population during session forks must be restored; and asyncio.CancelledError should be excluded from broadcasting error events in the status bridge to prevent false error popups.

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 thread src/agentpool_server/opencode_server/routes/message_routes.py Outdated
Comment thread src/agentpool_server/opencode_server/routes/message_routes.py Outdated
Comment thread src/agentpool_server/opencode_server/routes/session_routes.py
Comment thread src/agentpool_server/opencode_server/routes/session_routes.py Outdated
Comment thread src/agentpool_server/opencode_server/status_bridge.py Outdated
@Leoyzen
Leoyzen force-pushed the opencode/session-pool-integration branch from 2cabbae to 37c7e92 Compare June 6, 2026 11:40
Leoyzen added a commit that referenced this pull request Jun 6, 2026
- Use state.create_background_task() for LSP warmup to prevent GC
- Stop SessionStatusBridge in finally block to prevent task/subscription leak
- Restore message copying during session fork (with session_id update)
- Skip SessionErrorEvent broadcast for asyncio.CancelledError in status bridge
- Add TODO for per-session agent conversation population during fork

@Leoyzen Leoyzen left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review Fixes Applied

All 5 review comments have been addressed in commit 94ae0be81. Below is a point-by-point response:

1. _warmup_task GC issue (message_routes.py:104)

Fixed: Replaced asyncio.create_task(warmup()) with state.create_background_task(warmup(), name="warmup_lsp"). This ensures the task is tracked in state.background_tasks with a done callback, preventing GC mid-execution.

2. SessionStatusBridge leak (message_routes.py:622)

Fixed: Added await status_bridge.stop() in the finally block before unsubscribing from the EventBus. This cancels the bridge's background consumer task and releases the EventBus subscription.

3-4. Fork session message copy (session_routes.py:893, 925)

Partially fixed: Restored message copying logic:

  • Retrieves original_messages from state.messages
  • Supports request.message_id filtering (copy up to specified message)
  • Copies messages with updated session_id references to state.messages[new_session_id]

Not applied: The suggestion to manually populate fork_agent.conversation.chat_messages was intentionally skipped because the current migration uses a shared agent instance (get_or_create_agent returns self.agent). Mutating the shared agent's conversation would pollute all sessions. Added a TODO comment to restore this once per-session agent instances are reintroduced.

5. CancelledError error broadcast (status_bridge.py:113)

Fixed: Added isinstance(exc, asyncio.CancelledError) guard before broadcasting SessionErrorEvent. Normal user cancellation/aborts no longer trigger false error popups.


All status_bridge tests pass (6/6). Ready for re-review.

@Leoyzen

Leoyzen commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

Note: develop/agentic has diverged significantly (~674 commits) since this branch was created. A clean rebase is not feasible due to file reorganization (e.g., core.py and run.py have been removed/migrated). This PR is ready for merge via GitHub's merge button rather than rebase.

All review comments from the previous review have been addressed in commit 94ae0be81.

Leoyzen added a commit that referenced this pull request Jun 6, 2026
- Use state.create_background_task() for LSP warmup to prevent GC
- Stop SessionStatusBridge in finally block to prevent task/subscription leak
- Restore message copying during session fork (with session_id update)
- Skip SessionErrorEvent broadcast for asyncio.CancelledError in status bridge
- Add TODO for per-session agent conversation population during fork
@Leoyzen
Leoyzen force-pushed the opencode/session-pool-integration branch from 94ae0be to dd29aab Compare June 6, 2026 12:37
Leoyzen added a commit that referenced this pull request Jun 6, 2026
- Use state.create_background_task() for LSP warmup to prevent GC
- Stop SessionStatusBridge in finally block to prevent task/subscription leak
- Restore message copying during session fork (with session_id update)
- Skip SessionErrorEvent broadcast for asyncio.CancelledError in status bridge
- Add TODO for per-session agent conversation population during fork
@Leoyzen
Leoyzen force-pushed the opencode/session-pool-integration branch from dd29aab to 26fc20b Compare June 6, 2026 12:37

@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 integrates the OpenCode server with the SessionPool orchestration layer, replacing the legacy protocol handler with a new OpenCodeSessionPoolIntegration class and a SessionStatusBridge to synchronize session status changes via the EventBus. It also introduces an OpenCodeEventAdapter to map AgentPool events to OpenCode SSE events and updates message routing and session lifecycle routes to delegate operations to the SessionPool. The review feedback recommends several improvements to error handling and state preservation: finalizing the assistant message with an aborted state upon request cancellation to prevent TUI lockups, populating the in-memory conversation history for forked agents, wrapping status bridge shutdown and database persistence calls in try-except blocks to allow graceful degradation to in-memory availability, and correctly converting monotonic timestamps to epoch timestamps during session state conversion.

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 thread src/agentpool_server/opencode_server/routes/message_routes.py Outdated
Comment thread src/agentpool_server/opencode_server/routes/session_routes.py Outdated
Comment thread src/agentpool_server/opencode_server/session_pool_integration.py Outdated
Comment thread src/agentpool_server/opencode_server/session_pool_integration.py Outdated
Comment thread src/agentpool_server/opencode_server/session_pool_integration.py Outdated
Comment thread src/agentpool_server/opencode_server/routes/session_routes.py Outdated
Comment thread src/agentpool_server/opencode_server/routes/session_routes.py Outdated
Leoyzen added a commit that referenced this pull request Jun 6, 2026
- Use state.create_background_task() for LSP warmup to prevent GC
- Stop SessionStatusBridge in finally block to prevent task/subscription leak
- Restore message copying during session fork (with session_id update)
- Skip SessionErrorEvent broadcast for asyncio.CancelledError in status bridge
- Add TODO for per-session agent conversation population during fork
@Leoyzen
Leoyzen force-pushed the opencode/session-pool-integration branch from 26fc20b to f10e0b3 Compare June 6, 2026 12:44
@Leoyzen

Leoyzen commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator Author

🐛 Critical Issue: Auto-resume Events Lost Due to Per-Request Event Consumer Lifecycle

After reviewing the SessionPool integration in this PR, I found a critical race condition that causes auto-resume events to be lost.

Problem

The current implementation in message_routes.py creates temporary EventBus consumer and SessionStatusBridge for each HTTP request:

# message_routes.py:_process_message_locked
status_bridge = SessionStatusBridge(...)  # per-request
await status_bridge.start()
event_queue = await session_pool.event_bus.subscribe(session_id)  # per-request
# ...
consumer_task = asyncio.create_task(_consume_events())  # per-request

But SessionPool.receive_request()TurnRunner.run_loop() may trigger auto-resume after the first turn completes:

# TurnRunner.run_loop
async with session.turn_lock:
    await self._run_turn_unlocked(session_id, *initial_prompts, **kwargs)
    await self._process_queued_work(session_id, session, **kwargs)  # auto-resume here

The complete_event is set when _run_turn_unlocked finishes (first turn), NOT when run_loop finishes. This means:

  1. _process_message_locked returns after first turn
  2. Finally block tears down consumer/bridge
  3. Auto-resume starts in run_loop but produces events to EventBus with no consumers
  4. Events are lost!

Impact

  • Subagent async tasks (via BackgroundTaskProvider.task(async_mode=true)) that complete after the lead agent's turn will inject a prompt, triggering auto-resume, but the frontend won't receive the inject_prompt notification events.
  • Any post-turn work queued via inject_prompt() or queue_prompt() loses its streaming events.

Root Cause

The fundamental issue is that event consumption lifecycle is tied to HTTP request lifecycle, but session execution lifecycle (including auto-resume) extends beyond a single request.

Proposed Solution

Session-scoped event consumer instead of request-scoped:

  1. Move EventBus consumer and SessionStatusBridge to OpenCodeSessionPoolIntegration as session-scoped resources
  2. Start consumer/bridge when session is created (create_session)
  3. Stop consumer/bridge when session is closed (close_session)
  4. In message_routes.py, only wait for complete_event (which should represent the full run_loop completion, not just first turn)

This aligns with the architecture principle: session events are a session concern, not a request concern.

Files to Modify

  • src/agentpool/orchestrator/core.py — Delay complete_event to run_loop completion
  • src/agentpool_server/opencode_server/session_pool_integration.py — Add session-scoped consumer management
  • src/agentpool_server/opencode_server/routes/message_routes.py — Remove temporary consumer/bridge creation

Verification

Need to test:

  1. Send message → start async subagent → subagent completes after lead turn → verify frontend receives auto-resume events
  2. Multiple auto-resume iterations → verify all events streamed to frontend

Status: This issue exists in the current PR implementation and needs to be addressed before merging.

Related: The full-opencode-session-pool-integration OpenSpec change already documents this migration path, but the current implementation chose per-request consumers for simplicity, which breaks auto-resume event delivery.

Leoyzen added a commit that referenced this pull request Jun 9, 2026
- Use state.create_background_task() for LSP warmup to prevent GC
- Stop SessionStatusBridge in finally block to prevent task/subscription leak
- Restore message copying during session fork (with session_id update)
- Skip SessionErrorEvent broadcast for asyncio.CancelledError in status bridge
- Add TODO for per-session agent conversation population during fork
@Leoyzen
Leoyzen force-pushed the opencode/session-pool-integration branch from 453040e to 717e0b9 Compare June 9, 2026 10:30
@Leoyzen

Leoyzen commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

@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 unifies OpenCode Server routes under the SessionPool orchestration layer, transitions to session-scoped EventBus consumers and status bridges, and introduces EventEnvelope wrapping for EventBus events. Feedback on these changes highlights several critical issues: first, RunExecutor is not updated to handle the new ToolCallCompleteEvent returned by process_tool_event(), which will cause lost events during graph-based team execution; second, a race condition in EventBus.subscribe() can lead to duplicate events being enqueued when a concurrent publish occurs during subscription; third, in fork_session, copying messages via session_pool.copy_messages and then calling append_message_to_session results in duplicate messages in persistent storage; fourth, in the MCP prompt command route, the assistant message is finalized prematurely because the handler does not wait for the background run to complete; and finally, set_messages_for_session is left as a no-op, which can cause stale in-memory state during session revert or compaction.

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 thread src/agentpool/orchestrator/run_executor.py
Comment thread src/agentpool/orchestrator/core.py Outdated
Comment thread src/agentpool_server/opencode_server/routes/session_routes.py Outdated
Comment thread src/agentpool_server/opencode_server/routes/session_routes.py
Comment thread src/agentpool_server/opencode_server/session_pool_integration.py Outdated
@Leoyzen Leoyzen changed the title feat(opencode): integrate SessionPool for message routing and event conversion refactor(opencode,acp,orchestrator): unify session execution under SessionPool and modernize event routing architecture Jun 10, 2026
Leoyzen added a commit that referenced this pull request Jun 10, 2026
- Thread 15: Fix fork_session duplicate messages by only appending to
  SessionPool in legacy mode (session_pool is None)
- Thread 16: Fix execute_command premature completion by waiting for
  run_handle.complete_event with 30s timeout before finalizing
- Thread 17: Fix set_messages_for_session no-op by updating in-memory
  state.messages cache
@Leoyzen

Leoyzen commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

回复 Review Comments (Threads 13-17)

以下是对未解决 review comment 的回复和修复说明:


Thread 13 (CRITICAL): ToolCallCompleteEvent 丢失

结论:当前代码已正确处理,无需修改。

经核查,run_executor.py:195-204 已捕获并 enqueue process_tool_event 的返回值:

combined = await process_tool_event(...)
if combined is not None:
    await event_queue.put(combined)

agent.py 也在非 EventBus 路径做了相同处理。helpers.py 实现了正确的双路径设计:有 EventBus 时直接 publish 并返回 None,无 EventBus 时返回事件由调用者 enqueue。不存在 ToolCallCompleteEvent 丢失问题。


Thread 14: EventBus.subscribe 竞态条件 ✅ 已修复

Commit: 0d1d0520a

  • publish(): 将 replay buffer append 移入 self._lock
  • subscribe(): 将 replay buffer snapshot 与 subscriber 注册放在同一锁块内

所有 34 个 EventBus 测试通过。


Thread 15: fork_session 消息重复 ✅ 已修复

Commit: dc310555e

append_message_to_session() 现在只在 session_pool is None(legacy 模式)时调用。当 session_pool is not None 时仅更新内存缓存,避免重复写入持久层。


Thread 16: execute_command 提前完成 ✅ 已修复

Commit: dc310555e

添加 run_handle.complete_event.wait() 等待(30s 超时),防止 TUI 在后台运行完成前显示空完成消息。超时后取消 run 并返回错误。


Thread 17: set_messages_for_session 空实现 ✅ 已修复

Commit: dc310555e

pass 替换为更新 state.messages 内存缓存,与 append_message_to_session 使用相同模式。


以上修复已全部推送到 opencode/session-pool-integration 分支。

Leoyzen added 10 commits June 10, 2026 17:06
…onversion

- Add OpenCodeEventAdapter for AgentPool->OpenCode event conversion
- Add OpenCodeSessionPoolIntegration for SessionPool orchestration
- Add SessionStatusBridge for RunHandle->SessionStatus sync
- Route message handling through SessionPool.receive_request()
- Delegate session CRUD (create/load/delete) to SessionPool
- Delegate abort and fork to SessionPool
- Wire input provider flow through SessionPool
- Remove ServerState per-session agent creation, async queue
- Remove orphaned OpenCodeProtocolHandler (handler.py)
- Slim ServerState to SSE/connection-only (~309 lines)
- Add comprehensive TDD tests for session integration and event conversion
- Fix backward-compat references and lint issues
- Use state.create_background_task() for LSP warmup to prevent GC
- Stop SessionStatusBridge in finally block to prevent task/subscription leak
- Restore message copying during session fork (with session_id update)
- Skip SessionErrorEvent broadcast for asyncio.CancelledError in status bridge
- Add TODO for per-session agent conversation population during fork
When SpawnSessionStart is received, automatically subscribe to child
session EventBus and forward events as SubAgentEvent to frontend.

- Add _consume_child_events() to subscribe and forward child events
- Detect SpawnSessionStart in parent event stream
- Handle nested subagents recursively
- Clean up subscriptions on StreamCompleteEvent/RunErrorEvent
… session-scoped event consumer refactor

- Add OpenCodeSessionPoolIntegration to ServerState and create_app()
- Use integration for session creation, message routing, and cleanup
- Add close_session() to OpenCodeSessionPoolIntegration for proper cleanup
- Update delete_session route to use integration.close_session()
- Add backward compatibility when integration is not available
- Fix CancelledError handling to not re-raise (matching old behavior)
- Add conversation history preservation in CancelledError/ Exception handlers
- Update test fixture to mock SessionPool for new architecture
- Add session-scoped consumer tests

Completes openspec change: session-scoped-event-consumer
…agents

Per-session agents created by SessionController.get_or_create_session_agent()
were missing pool-level resource providers (MCP aggregating provider and
skills instruction provider), causing them to have zero tools when the
agent config had no agent-level tools. This led to models outputting
pseudo-tool-call text instead of executing actual function calls.

The fix adds the same pool-level providers to per-session agents that
shared agents already receive in AgentPool.__aenter__().
…esume

Event consumer loop now creates a proper AssistantMessage (was UserMessage)
and registers it in state.messages before processing the first event.
Without this, PartUpdatedEvents were ignored by the TUI because the
message store lacked the parent message entry.

Red flag test: test_auto_resume_events_create_message_in_state
…l level

SkillsTools provides list_skills and load_skill tools. Previously, these
were only available to agents that explicitly configured
in their agent config. Agents without this config (like librarian with all
tools commented out) would receive skill metadata via SkillsInstructionProvider
but had no actual tools to call, causing pseudo-tool-call output.

This fix adds SkillsTools as a pool-level provider alongside MCP and
skills_instruction providers, ensuring all agents (shared and per-session)
have access to skill discovery and loading tools.

Changes:
- AgentPool.__init__(): create self.skills_tools_provider
- AgentPool.__aenter__(): add skills_tools_provider to all shared agents
- AgentPool.__aexit__(): remove skills_tools_provider during cleanup
- SessionController.get_or_create_session_agent(): add skills_tools_provider
  to per-session agents
Unify all pydantic-ai stream events into a single FIFO path in SessionPool mode,
eliminating the dual-path architecture that caused race conditions.

Core changes:
- process_tool_event(): Remove direct EventBus publish, always return combined
  ToolCallCompleteEvent. Caller decides routing.
- _run_agentlet_core(): Add ToolCallStartEvent mapping for FunctionToolCallEvent
  and PartStartEvent(BaseToolCallPart), with deduplication by tool_call_id.
  Capture process_tool_event() return value and enqueue into local queue.
- EventBusHooksAdapter: Disable before_tool_execute/after_tool_execute event
  publishing (now transparent passthroughs). Tool events come exclusively from
  the stream path.

This fixes two observable bugs:
1. Missing ToolCallStartEvent in opencode TUI (start event wasn't mapped)
2. Race condition where ToolCallCompleteEvent arrived before start event and
   was silently dropped by event_processor (dual-path ordering hazard)

Test coverage:
- Updated red flag tests to verify fixed behavior
- Added FIFO ordering test
- Added duplicate suppression test
- Added process_tool_event() no-direct-publish test
- Added RunExecutor event_bus integration test
- Added multiple tool calls ordering test

Spec: Updated openspec/specs/unified-event-routing/spec.md with new scenarios
for stream consumer flow, duplicate suppression, and PartStartEvent mapping.
Remove manual event routing from business layer tools:
- TurnRunner now wraps child session events in SubAgentEvent via metadata
- SessionPool.run_stream() supports scope parameter (session/descendants/subtree)
- subagent_tools.py: remove manual EventBus subscription and SubAgentEvent wrapping
- workers.py: use session_pool.run_stream(), remove manual event emission
- agentpool_commands/pool.py: use session_pool.run_stream() for subagent spawning
- create_child_session() now accepts **metadata for child session tracking

Tests updated for unified event routing via EventBus descendants scope.

Archives cleanup-business-layer-event-routing openspec change.
Leoyzen and others added 23 commits June 10, 2026 17:06
Recreate the thin-agentpool-core OpenSpec change after accidental deletion.
Includes proposal, design, 3 delta specs, and tasks for thinning
AgentPool core to native+acp agents only.
…to SessionPool

- Remove 5 legacy fields from ServerState: messages, session_status, todos,
  input_providers, pending_questions
- Migrate all route files to SessionPool/SessionController helpers:
  - message_routes.py: get_messages_for_session, append_message_to_session
  - session_routes.py: status + message helpers for all CRUD/ops
  - permission_routes.py: SessionController exclusively
  - question_routes.py: SessionController exclusively
- Migrate non-route files: event_processor.py, status_bridge.py,
  session_pool_integration.py fallback removal
- Update 33+ test files with backward-compat fixtures and helper usage
- Centralize SessionStatusEvent handling in broadcast_event()

Test results: 730 passed, 1 failed (pre-existing flaky timeout)
- Add EventEnvelope dataclass with source_session_id + transparent __getattr__ forwarding
- Wrap all EventBus events in EventEnvelope at publish time
- Remove session_id injection from producers (RunExecutor, helpers, event_emitter)
- Adapt all consumers to unwrap EventEnvelope before type checks:
  - ACPProtocolHandler, ProtocolEventConsumerMixin
  - OpenCode server (status_bridge, session_pool_integration)
  - Claude Code Agent, ACP Agent, BaseAgent
- Migrate tests to assert EventEnvelope wrapping behavior
- Add dedicated EventEnvelope integration tests
- Fix all ruff/mypy issues and e2e regressions

All 112 key tests pass.
…umers

- Override _get_subscription_scope to 'session' to prevent event interleaving
- Add _on_spawn_session_start to create child consumers for sync subagents
- Skip child consumer creation for background tasks (spawn_mechanism='task')
Use self.node._events.session_id instead of getattr fallback.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Inherit MCP tool providers (kind==mcp) when creating child sessions.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Extends exception handling in load_rules() to catch RequestError
(and other exceptions) from remote filesystem calls via MCP/ACP.
Previously only OSError/UnicodeDecodeError were caught, causing
task failures when remote workspace-fs returned errors.
…e condition

Moves replay buffer snapshot capture inside the same lock as subscriber
registration in subscribe(), and moves replay buffer append inside the
lock in publish(). This prevents concurrent publish() from delivering
events to both the historical replay and the live queue.
- Thread 15: Fix fork_session duplicate messages by only appending to
  SessionPool in legacy mode (session_pool is None)
- Thread 16: Fix execute_command premature completion by waiting for
  run_handle.complete_event with 30s timeout before finalizing
- Thread 17: Fix set_messages_for_session no-op by updating in-memory
  state.messages cache
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