Modularity Review — ccgram

Date: 2026-04-15 (updated post-afternoon refactoring)  |  Scope: Entire codebase (src/ccgram/ — ~90 Python files, ~18,000 lines)  |  Model: Balanced Coupling (Strength × Distance × Volatility)

Executive Summary

DimensionScoreNotes
Cohesion 6.5/10 Most modules focused. window_tick.py (610 lines) and hook_events.py are outliers. Session_monitor refactoring improved this.
Coupling Strength 5/10 session.py imported by 32 handler modules (functional coupling on volatile schema); claude_task_state accessed from 8 modules across layers.
Layer Discipline 5/10 hook_events reaches into polling infra; status_bubble queries polling state; shell logic split across provider + handler layers.
Encapsulation 6.5/10 has_insert_indicator promoted to public; store API bypasses resolved. claude_task_state ownership still undisciplined.
Volatility Management 5.5/10 Provider protocol cleanly isolated. Core volatile areas (session state, task tracking) have cascading coupling.
Testability 5/10 Global singletons (session_manager, thread_router, claude_task_state, tmux_manager) require complex test wiring.
AI Context Efficiency 5.5/10 session.py fan-out (32 modules) means any session change requires wide context. Core state changes still cascade across files.
Overall 5.5/10 Active refactoring trajectory is clearly positive — five issues resolved today. Four significant issues remain.
Progress since noon review (resolved today):

System Overview

SubsystemKey FilesDomain
Provider abstractionproviders/ (12 files)Core — competitive advantage, evolving
Session monitoringsession_monitor.py, event_reader.py, session_lifecycle.py, transcript_reader.py, idle_tracker.pyCore — actively evolving
Polling looppolling_coordinator.py, window_tick.py, polling_strategies.pySupporting — stable design
Message deliverymessage_queue.py, status_bubble.py, tool_batch.py, message_sender.pySupporting — stable
Core statesession.py, thread_router.py, window_state_store.py, claude_task_state.pySupporting — frequently accessed
Shell providershell_infra.py, shell_commands.py, shell_capture.py, shell_context.pySupporting — actively developed
Inter-agent messagingmailbox.py, msg_broker.py, msg_spawn.py, msg_telegram.pyCore — new feature

Structural strengths:

Open Issues

Issue 1 — session.py Is a De-Facto Hub with 32 Handler Dependents [Critical]

Files: session.py (783 lines), imported by 32+ handler modules.

Every Telegram handler that needs window state, mode settings, session resolution, display names, or state persistence imports session_manager directly.

ConcernMethods
Window stateget_window_state(), view_window()
Mode settingsget_approval_mode(), cycle_notification_mode(), get_batch_mode()
Session mapload_session_map(), register_hookless_session(), wait_for_session_map_entry()
Session resolveresolve_session_for_window(), get_recent_messages()
Lifecycleprune_stale_state(), audit_state(), resolve_stale_ids()
Providerget_window_provider(), set_window_provider()

Coupling: HIGH strength + HIGH volatility (WindowState schema extended repeatedly) → UNBALANCED.

AI context impact: Any session change requires loading session.py (783 lines) + every handler that mutates the relevant field. Adding a new per-window flag means auditing 32+ files for usage sites.

Recommendation: Enforce that handlers read via view_window() and write only through explicit setters. A WindowModeService wrapping approval/notification/batch modes would reduce most handlers' dependency to a 2-method slice.

Issue 2 — claude_task_state.py Is Shared Mutable State with No Ownership Discipline [High]

Files: claude_task_state.py (562 lines), write access from 6 modules across core and handler layers.

The coordination failure: session_lifecycle.py is designated the single authority for session-end cleanup. The hook_events.py SessionEnd handler correctly calls session_lifecycle.handle_session_end(window_id) — but then performs additional cleanup directly alongside it:

session_lifecycle.handle_session_end(window_id)   # designated authority
session_manager.clear_window_session(window_id)   # also done directly
clear_subagents(window_id)                        # also done directly

Cleanup is split between the authority module and the caller. Anyone adding new per-session state cleared on SessionEnd has two sites to update — miss either and state leaks.

More broadly, claude_task_state has 6 independent write paths with no coordination protocol: session_lifecycle (clear_window), transcript_reader (set_window_tasks, mark_task_completed), hook_events (add/remove/clear_subagents), plus read access from window_tick, tool_batch, and status_bubble.

Coupling: HIGH strength + HIGH distance (monitoring layer ↔ display/handler layer) + HIGH volatility → UNBALANCED. Any schema change requires auditing 6 callers across 2 layers.

Recommendation: Consolidate all write access in session_lifecycle.py. Have hook_events.py delegate fully — remove the parallel cleanup block alongside the handle_session_end() call. Give display modules a read-only interface via get_claude_task_snapshot().

Issue 3 — hook_events.py Crosses the Hook→Polling Boundary [High]

Files: handlers/hook_events.py, handlers/periodic_tasks.py

# hook_events.py:180-182
from .periodic_tasks import run_broker_cycle
await run_broker_cycle(bot, idle_windows=frozenset({event.window_key}))

9 of hook_events.py's imports are inside function bodies. The same message_queue import appears in 4 separate functions — hidden fan-out invisible to static analysis.

Coupling: HIGH strength (cross-subsystem direct call) + HIGH volatility → UNBALANCED.

Recommendation: Replace the direct run_broker_cycle call with a registered callback. Consolidate deferred message_queue imports to top-level.

Issue 4 — status_bubble.py Queries Polling State Directly [Medium]

Files: handlers/status_bubble.py, handlers/polling_strategies.py

from .polling_strategies import terminal_screen_buffer
rc_active=terminal_screen_buffer.is_rc_active(window_id)

Display logic (status_bubble) depends on polling state (polling_strategies) for a single boolean. Low volatility makes it tolerable today; architecturally surprising.

Recommendation: Pass rc_active: bool as a parameter to build_status_keyboard(). One-line fix.

Issue 5 — Shell Feature Has Runtime Circular Dependency [Medium]

Files: handlers/shell_capture.py, handlers/shell_commands.py

shell_capture._maybe_suggest_fix() calls shell_commands.show_command_approval() via a deferred runtime import. Additionally, shell_capture.py bypasses tmux_manager with a direct asyncio.create_subprocess_exec("tmux", ...) call.

Recommendation: Introduce a CommandApprovalCallback protocol. Route the capture-pane call through tmux_manager.

Summary Matrix

IssueFilesStrengthVolatilityPriorityStatus
session.py hub — 32 handler dependents session.py + 32 handlers HIGHHIGH CriticalOpen
claude_task_state — no ownership discipline claude_task_state.py + 6 callers HIGHHIGH HighOpen
hook_events reaches into polling infra hook_events.py, periodic_tasks.py HIGHHIGH HighOpen
status_bubble queries polling state status_bubble.py, polling_strategies.py LOWLOW MediumOpen
shell_captureshell_commands runtime cycle shell_capture.py, shell_commands.py HIGHMEDIUM MediumOpen
_has_insert_indicator private API access window_tick.py, tmux_manager.py HIGHHIGH Critical✅ Fixed
_get_provider() duplicated 6× in window_tick window_tick.py MEDHIGH High✅ Fixed
session_map bypasses WindowStateStore API session_map.py, window_state_store.py HIGHMEDIUM High✅ Fixed
parse_session_map duplicated in two files session.py, session_map.py HIGHMEDIUM Medium✅ Fixed
session_monitor.py monolith (was 750+ lines) session_monitor.py + 4 extracted modules HIGHHIGH Critical✅ Fixed

Recommended Priorities

  1. claude_task_state write discipline — designate session_lifecycle.py as the single write authority; remove the hook_eventsclaude_task_state direct mutation path. Highest-risk shared-state pattern: volatile schema + 6 callers + violated authority claim.
  2. hook_events.py boundary crossing — replace direct run_broker_cycle call with a callback. Consolidate 4× deferred message_queue imports to top-level. Hidden runtime dependencies are the most expensive pattern for AI context loading.
  3. session.py interface narrowing — enforce view_window() for reads and explicit setters for writes. The 32-file fan-out is a symptom; narrowing each handler's dependency to the methods it actually uses is the fix.
  4. status_bubblepolling_strategies severance — pass rc_active: bool as a parameter to build_status_keyboard(). One-line fix, zero risk.
  5. Shell circular dependency — introduce CommandApprovalCallback protocol; route capture-pane through tmux_manager.