Modularity Review

Scope: Entire ccgram codebase — fourth pass, 2026-04-16
Date: 2026-04-16 (v4 — follows 2026-04-16/, -v2/, and -v3/)

Executive Summary

ccgram is a single-process Python bot (~47k lines) that routes Telegram messages to AI coding agent CLIs running in tmux panes. Three refactoring passes have moved the overall modularity score from 4.8 to 5.6/10 (+0.8). The third pass was incremental cleanup: four redundant read patterns were replaced with WindowView fields, and two new accessor methods (window_count, iter_window_ids) eliminated direct window_states dict access from handler modules.

The score holds at 5.6/10 — this pass addressed low-hanging fruit within the existing architecture rather than making structural changes. The SessionManager dependency hub (85 call sites across 30 handler files) remains the dominant coupling issue. Every other dimension is either at its natural floor (acceptable given low distance) or blocked by this single bottleneck. The codebase is now at the point of diminishing returns for targeted fixes: the next meaningful improvement requires a dedicated pass to migrate the 10 highest-call-count handlers to read through WindowView exclusively.

Four-Version Progression

Dimensionv1 (baseline)v2 (+0.6)v3 (+0.2)v4 (this)Net Δ
Encapsulation / Information Hiding4/105/106/106/10+2
Cohesion5/105/105/105/10
Coupling Discipline4/105/105/105/10+1
Contract Stability6/106/106/106/10
Testability5/106/107/107/10+2
Volatility Alignment4/105/105/105/10+1
Module Size Distribution6/106/106/106/10
Dependency Direction4/105/105/105/10+1
Overall4.8/105.4/105.6/105.6/10+0.8

What Was Resolved This Pass

ChangeFilesEffect
restore_command.pyget_approval_mode()view.approval_mode1Eliminated redundant read where view_window() was already in scope
recovery_callbacks.pyget_window_provider()view.provider_name1Same pattern; view guaranteed non-None by cwd guard
recovery_callbacks.py — two getters consolidated into one view_window()1Two session_manager calls → one, yielding both fields
resume_command.py — same two-getter consolidation1Identical pattern
session.py — added window_count + iter_window_ids()1New contract accessors for handler use
msg_spawn.pywindow_states dict → window_count1Eliminated direct dict access + check_max_windows indirection
sync_command.pywindow_states.keys()iter_window_ids()1Eliminated direct dict key access

Net session_manager call site reduction: 89 → 85 (–4 in handlers)

What Remains — Diminishing Returns

Categoryv1 statusv4 status
Import cycles4 cycles3 cycles (one broken by monitor_events.py)
claude_task_state write authority6 uncoordinated sites3 trigger-type authorities (by design)
Polling state encapsulationDirect access from 4 handler filesFacade (reset_window_polling_state) fully adopted
Infrastructure → domain dependenciestmux_managerproviders at module levelLocal import only
Provider testabilityAll 4 shell_infra functions + claude.py hardwired to tmuxInjectable capture_fn/send_keys_fn on entry points
SessionManager read coupling89 call sites, 30 files85 call sites, 30 files (–4.5%)

The remaining 85 call sites across 30 files are the natural floor for targeted fixes. Reducing this number further requires systematically migrating handler files to receive WindowView through their call chains rather than importing session_manager directly.

Coupling Overview

Integration Strength Distance Volatility Balanced?
30 handler modules → SessionManager (85 calls)FunctionalSame serviceHighNo — ceiling for further improvement
window_tickpolling_strategies (30 calls)FunctionalSame service, low distanceHighTolerable — poll executor owns transitions
3 deferred-import cyclesFunctional (bidirectional)Same serviceHighNo — low priority
window_tickclaude_task_state (3 mutations)FunctionalSame serviceHighYes — poll-cycle authority
transcript_readerclaude_task_state (2 mutations)FunctionalSame serviceHighYes — transcript-parse authority
hook_eventssession_lifecycle (5 facade calls)ContractSame serviceHighYes
reset_window_polling_state()ContractSame serviceHighYes — no bypasses
window_count + iter_window_ids() (new)ContractSame serviceModerateYes — replaces raw dict access
has_prompt_marker + setup_shell_promptContractSame serviceModerateYes — injectable callables
monitor_events.py, IdleTracker, event_reader, base.pyContractSame serviceLow/ModerateYes — design exemplars

Issue 1: SessionManager Dependency Hub — Natural Floor Reached

Integration: 30 handler modules → session.py:SessionManager (85 call sites)  |  Severity: Significant

Knowledge Leakage

SessionManager is still directly imported by every handler module. The WindowView contract now covers 10 fields and is used correctly by handlers that call view_window(), but the remaining call sites use getter methods (get_window_provider, get_approval_mode, get_notification_mode, get_session_id_for_window) and write methods (set_window_provider, set_window_approval_mode) that expose the internal state model directly.

What remains: 13 standalone get_window_provider calls (no prior view in scope), 11 set_window_provider writes (legitimate), 18 view_window calls (correct pattern), and ~43 infrastructure/lifecycle calls (permanent floor).

Complexity Impact

The 85-call breadth means session state model changes still require broad audits. However, the accidental volatility is now lower than baseline: WindowView covers frequently-read fields, session_lifecycle owns all mutation-authority methods, and reset_window_polling_state seals polling internals. A developer adding a new per-window field now has a clear contract path.

Recommended Next Step

Targeted fixes have reached diminishing returns. The next improvement is a bulk migration pass: for each of the 10 highest-call-count handler files, replace standalone get_window_provider(wid) / get_approval_mode(wid) calls with view = session_manager.view_window(wid); view.field if view else default. This converts functional coupling reads to contract coupling reads through WindowView.


Issue 2: Three Deferred-Import Cycles Remain

Integration: session.pysession_resolver.py, session_map.pywindow_state_store.py / thread_router.py  |  Severity: Minor

Knowledge Leakage

Three bidirectional dependency cycles survive, suppressed by deferred imports. The monitor_events.py extraction proved the fix pattern. These cycles are not blocking feature work or causing test problems.

Recommended Improvement

Address opportunistically when touching the affected modules. The recipe is proven: extract shared types to a dependency-free module.


Retrospective: Four-Pass Series

The Balanced Coupling model guided prioritization across all four passes:

DimensionWhat moved itWhat blocks further improvement
Encapsulation (+2)session_lifecycle write authority, reset_window_polling_state facade, WindowView.batch_mode, injectable capture_fnSessionManager getter proliferation across 30 files
Testability (+2)Injectable capture_fn/send_keys_fn in claude.py + shell_infra.py, monitor_events.py extractionshell_infra internal functions still hardwire tmux
Coupling Discipline (+1)tmux_manager module-level import removed, one cycle broken3 remaining deferred-import cycles
Dependency Direction (+1)tmux_managerproviders inverted dependency resolvedproviders/claude.py still lazily imports tmux_manager
Volatility Alignment (+1)Mutations consolidated per trigger type; facade seals polling internalsSessionManager read coupling in volatile handler layer
Cohesion (=)No module splits needed; large files are legitimate
Contract Stability (=)WindowView, facades, accessors all stableAlready at 6/10; further gains need protocol-level contracts
Module Size (=)tmux_manager.py (1175 lines) is infrastructure; splitting adds complexity

The trajectory — 4.8 → 5.4 → 5.6 → 5.6 — shows the characteristic diminishing-returns curve of incremental refactoring. The v1→v2 jump came from addressing four distinct coupling patterns in parallel. Each subsequent pass had fewer high-impact targets. The codebase is at a stable plateau where remaining issues are either Minor or require a different class of effort.