refactor: session debt cleanup — state machine, storage ISP, creation/close unification - #171
Conversation
There was a problem hiding this comment.
Code Review
This pull request performs a significant refactoring of the session management and storage layers, splitting monolithic files into mixins for better maintainability. Key changes include unifying session creation paths through SessionPool, replacing the deprecated SessionStore protocol with SessionPersistence and other granular ISP protocols, and updating the SQLModelProvider to implement these protocols directly. The review identified two high-severity issues where prompt stringification in SessionPoolMessagingMixin and SessionPoolRunsMixin would destroy multimodal content; these should be addressed by flattening prompt lists instead of joining them into strings.
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.
…/close unification Implements the session-debt-cleanup OpenSpec change (88/89 tasks complete). Phase 1 — State Machine Mapping & Invariants: - Fixed SessionData.status docstring (removed completed/failed, added closed) - Created SessionStateMapper with invariant checking and reconciliation - Removed all RunStatus references from AGENTS.md - 37 new tests (unit + integration) Phase 2 — Storage ISP Decomposition & Adapter: - Defined 7 @runtime_checkable Protocols (SessionPersistence, MessagePersistence, SessionMetadata, CommandLog, ProjectStoreProtocol, CheckpointStore, StatsAggregator) - Created StorageProviderAdapter implementing all 7 Protocols - 20 new tests for protocol conformance and delegation Phase 3 — Storage Bug Fixes & SQLSessionStore Elimination: - Fixed SQLModelProvider.save_session() with dialect-aware UPSERT - Fixed _session_from_db() to read status field (was always defaulting to active) - Migrated all consumers from SessionStore API to SessionPersistence API - Deleted SQLSessionStore (335 LOC) — all consumers now use SQLModelProvider - 24 new tests for round-trip, checkpoint, edge cases, E2E lifecycle Phase 4 — Creation Path Unification: - All 6 protocol servers now delegate to SessionPool.create_session() - Unified session ID generation via generate_session_id() (removed uuid usage) - Added SessionPool.create_child_session() as first-class API - ACP resume_session() now acquires _get_resume_lock() - 16 new tests for creation unification Phase 5 — Session Module Splitting: - Split session_pool.py (1844 LOC) into 4 mixin files - Split session_controller.py (1438 LOC) into 3 mixin files - Split sql_provider.py (1047 LOC) into 3 mixin files - Split session_pool_integration.py (1491 LOC) into 3 mixin files - Split acp_server/session.py (1028 LOC) into 3 mixin files - All splits are pure structural refactors with no behavior changes Phase 6 — Close Path Unification & Protocol Migration: - Standardized 7-step cleanup ordering in _close_session_unlocked() - SessionPool.close_session() delegates to SessionController.close_session() - ACPSessionManager.close_session() delegates to SessionPool.close_session() - Removed deprecated receive_request() — all callers migrated to send_message() - MCP cleanup and agent __aexit__ guaranteed in close path - 14 ACP snapshots pass, full suite: 2684 passed, 17 skipped, 1 pre-existing failure Refs: #170
d96cd79 to
a187398
Compare
45e6934 to
4a00588
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request performs a significant refactoring of the session management and storage layers to improve consistency and maintainability. Key changes include splitting monolithic modules into mixins (e.g., SessionController, ACPSession, SessionPool), unifying session creation paths, and decomposing the legacy StorageProvider into 7 focused Protocols following the Interface Segregation Principle. My feedback identifies several high-severity issues: potential TypeError exceptions when flattening multimodal prompts, resource leaks during cleanup due to improperly handled exceptions in finally blocks, and a race condition in session resumption due to improper lock handling. I have also recommended using safe lookups instead of factory methods to avoid recreating phantom sessions.
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.
…linter - Add class-level type annotations and TYPE_CHECKING method stubs to all mixin classes for attributes/methods provided by the main class (73 mypy errors fixed across 12 files) - Fix ruff lint errors in tests/ (TC001, D403, F841, PLW0108, SIM105, BLE001) - Fix ruff format on 3 test files - Remove deleted session_store from import-linter ignore list in pyproject.toml - Fix ACP turn flatten logic: add isinstance(p, list) check before extend() - Fix no-any-return errors with explicit cast() in session.py and event_bridge
…split - Restore contextlib.suppress(asyncio.CancelledError, RuntimeError) around gen.aclose() and event_bus.unsubscribe() in _run_stream_run_turn finally block — was lost when method moved to session_pool_runs.py mixin - Restore deps parameter extraction from kwargs and pass to _create_run_handle → AgentRunContext — was lost in Phase 5 split - Catch asyncio.CancelledError in shutdown() alongside Exception — CancelledError is BaseException since Python 3.8, not caught by 'except Exception'. Prevents test break behavior from crashing shutdown. - Update _create_run_handle stub in SessionPoolMessagingMixin to match new deps parameter signature Fixes 3 core test failures in test_break_behavior.py: test_simple_break_after_n_events test_break_with_exception_handling test_conversation_history_after_break
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request performs a major refactoring of the session management and orchestration layers, modularizing SessionController and SessionPool into dedicated mixins, unifying session creation paths, and integrating session persistence directly into SQLModelProvider. The review feedback highlights several critical issues that must be addressed: a race condition on RunHandle in spawn_subagent, a bug in the session close sequence that overwrites the 'checkpointed' status to 'closed' (breaking resume functionality), redundant dead code in opencode_session_routes.py, a potential resource leak when stopping child event consumers, and an unhandled asyncio.CancelledError during generator closure that could skip crucial cleanup steps.
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.
…tatus, CancelledError cleanup
Fix 1: run_handle.start("") race condition in runloop_delegation.py
- Replace dual start() call with EventBus subscription to avoid
corrupting RunHandle state from concurrent start() calls
Fix 2-4: Checkpointed status overwritten in session_controller_close.py
- Add checkpointed parameter to _close_session_unlocked()
- Skip _mark_session_closed() when session was already checkpointed
- Pass checkpointed flag from _close_session_run_turn()
Fix 5: Remove dead elif block in opencode_session_routes.py
- The elif condition was identical to the if condition (unreachable)
Fix 6: stop_event_consumer exception handling in opencode_event_bridge.py
- Wrap each child stop_event_consumer in try-except so one failure
doesn't prevent remaining children from being stopped
Fix 7: CancelledError not caught in cleanup paths
- Use save-and-re-raise pattern in session_pool_runs.py and
session_pool_messaging.py to ensure cleanup runs even when
gen.aclose() raises asyncio.CancelledError (BaseException)
…shot test Replace direct _execute_turn(agent=None) call with the existing _make_run_handle + _StubTurn pattern from tests/lifecycle/test_run_loop.py. Spy on snapshot_store.save() to capture the RUNNING snapshot (which has prompts_serialized) before the post-turn IDLE snapshot overwrites it.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the session management and orchestration layers by splitting SessionController and SessionPool into modular mixins, decomposing StorageProvider into focused protocols, unifying session creation paths with sortable IDs, and improving multimodal prompt serialization. Feedback on these changes highlights several critical improvements: using .extend() instead of .append() to prevent nested lists in recovery prompts, guarding agent.conversation access against AttributeError, wrapping cleanup operations in finally blocks with try-except blocks, restoring the 300-second timeout for subagents to prevent hangs, and optimizing the message cache eviction loop from
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.
…O(N) eviction 1. run.py: Change .append(deserialized) to .extend(deserialized) in _handle_recovery() — preserves individual prompt structure instead of nesting as a single list item. 2. runloop_delegation.py: Restore 300s timeout on EventBus subscription using asyncio.timeout(). On timeout, yield RunErrorEvent instead of raising TimeoutError so parent agent handles it gracefully. 3. session_pool.py: Optimize _evict_message_cache from O(N²) to O(N) with single-pass candidate collection and bulk eviction.
- session_controller_agent.py: Remove hasattr(self.pool, 'todos') — AgentPool.__init__ always sets self.todos = TodoTracker() - session_pool_runs.py: Replace getattr(event, 'event', event) with direct event.event access — EventEnvelope always has .event attribute
… timeout, P3: httpx read timeout)
…al CancelledError - ruff: fix lint+format on tests/orchestrator/test_aexit_hang.py - run.py: add _force_cancelling flag set by cancel() before task.cancel() - start(): only catch CancelledError when _force_cancelling is True; external task.cancel() (test cleanup) propagates normally - Fixes 2 core test timeouts: test_worker_emits_subagent_events, test_subagent_event_depth_propagation
Session Debt Cleanup
Closes #170.
Implements the
session-debt-cleanupOpenSpec change — a comprehensive refactor of the session management subsystem across 6 phases (88/89 tasks complete).Summary
SessionStateMapper)SQLSessionStoreeliminationSessionPool.create_session())Phase 1 — State Machine Mapping & Invariants
SessionData.statusdocstring (removedcompleted/failed, addedclosed)SessionStateMapperwith invariant checking and reconciliationRunStatusreferences fromAGENTS.mdPhase 2 — Storage ISP Decomposition & Adapter
@runtime_checkableProtocols:SessionPersistence,MessagePersistence,SessionMetadata,CommandLog,ProjectStoreProtocol,CheckpointStore,StatsAggregatorStorageProviderAdapterimplementing all 7 ProtocolsPhase 3 — Storage Bug Fixes & SQLSessionStore Elimination
SQLModelProvider.save_session()with dialect-aware UPSERT (was delete-then-insert)_session_from_db()to readstatusfield (was always defaulting to"active")SessionStoreAPI toSessionPersistenceAPISQLSessionStore(335 LOC) — all consumers now useSQLModelProviderPhase 4 — Creation Path Unification
SessionPool.create_session()generate_session_id()(removed alluuid.uuid4()usage)SessionPool.create_child_session()as first-class APIresume_session()now acquires_get_resume_lock()for thread safetyPhase 5 — Session Module Splitting
Pure structural refactors (no behavior changes) using mixin pattern:
session_pool.pysession_pool_messaging.py,session_pool_teams.py,session_pool_runs.py,session_pool_config.pysession_controller.pysession_controller_runs.py,session_controller_close.py,session_controller_agent.pysql_provider.pysql_messages.py,sql_sessions.py,sql_projects.pysession_pool_integration.pyopencode_session_routes.py,opencode_event_bridge.py,opencode_message_bridge.pyacp_server/session.pysession_lifecycle.py,session_events.py,session_agent_mgmt.pyPhase 6 — Close Path Unification & Protocol Migration
_close_session_unlocked(): cancel RunHandle → await completion → MCP cleanup → agent__aexit__→ session persistence → EventBus unsubscription → cascade close childrenSessionPool.close_session()delegates toSessionController.close_session()ACPSessionManager.close_session()delegates toSessionPool.close_session()receive_request()— all callers migrated tosend_message()__aexit__guaranteed in close pathtest_vision, unrelated)Test Results
test_vision, unrelated to changes)Files Changed
113 files changed, +9990 / -6190 lines
Deferred
Task 0.1 (integration test fixture) intentionally deferred — requires a dedicated CI environment with real model endpoints.