Modularity Review — Evening Update

Scope: Entire ccgram codebase (incremental review)
Date: 2026-04-12 (20:15 GMT+3)
Context: Follow-up to the morning review (modularity-review.md) after a series of refactors committed today.

Context and Status of Prior Review

Several issues flagged in the morning review have been partially or fully resolved today. This update tracks the delta and surfaces what remains.

Prior IssueStatusWhat changed
screenshot_callbacks — four concerns in onePartially resolvedToolbar handlers extracted to toolbar_callbacks.py. Screenshot/status/live still live together.
/send intrusive import from screenshot_cbResolvedsend_command.open_file_browser() is now a public API (c9a87a3).
SessionManager boundary violationsResolvedsession_resolver.py uses window_store.update_cwd(); session_map.py uses thread_router.set_display_name.
Business logic in bot.pyPartially resolvedhandle_new_message extracted to message_routing.py. Topic lifecycle moved to topic_lifecycle.py.
Scattered per-window singleton dictsStill openNo change — WindowContext aggregation stands as long-term direction.
SessionManager god-objectStill openSee Issue 5 — re-scoped to write-surface problem.
Providers ↔ session circular importStill openLazy import still in providers/__init__.py.

This update focuses on the issues the user explicitly flagged as painful — adding a provider, changing state shape, and polling/status code.

Coupling Overview (delta)

IntegrationStrengthDistanceVolatilityBalanced?
Handlers → session_manager (25 handlers, 62 calls)Functional + ModelLowHighBorderline — read-heavy, no projection type
polling_coordinator.status_poll_loop → 9 handler modulesFunctional + temporalLowHighBalanced by rule, low cohesion inside module
Provider abstraction leaksModel + IntrusiveLowHighNo — bypassed in several paths
hook_eventsmessage_queueFunctionalLowModerateSignals misplaced code
shell_commandsshell_captureFunctionalLowModerateSignals misplaced code
message_queue.py (1184 lines, 6 concerns)Internal (mixed)ModerateCohesion problem
Deferred _schedule_save wiringImplicit globalsLowLowTestability hazard

Issue 1: Provider Abstraction Leaks — Significant

User pain: "Adding a provider is painful."

Knowledge leakage

  1. providers/base.py contains Telegram-specific constants. EXPANDABLE_QUOTE_START/END and format_expandable_quote() are Telegram blockquote syntax in a pure protocol file.
  2. providers/shell.py exports module-level infrastructure. match_prompt, setup_shell_prompt, detect_pane_shell, KNOWN_SHELLS, PromptMatch are consumed directly by handlers, bypassing the provider abstraction.
  3. llm/summarizer.py hardcodes Claude JSONL format. Parses message blocks directly instead of going through AgentProvider.parse_transcript_entries().
  4. providers/claude.py imports ccgram.hook.UUID_RE. Should be in base.py next to RESUME_ID_RE.
  5. Handlers check provider names as strings in transcript_discovery, recovery_callbacks, window_callbacks. Capability flags were meant to eliminate this.

Recommended improvement

Practical, in order of payoff-to-cost ratio:

  1. Move EXPANDABLE_QUOTE_* out of providers/base.py into a telegram formatting module. Thirty-minute change.
  2. Move UUID_RE into providers/base.py. Fifteen-minute change.
  3. Grep for provider_name == string checks; add capability flags as needed.
  4. Extract providers/shell_infra.py — slim shell.py to just ShellProvider, move infrastructure elsewhere.
  5. Route summarizer.py through the provider — add AgentProvider.summarize_recent().

Don't attempt all five at once. The first two are cheap wins; others can happen opportunistically.

Issue 2: polling_coordinator is a God Loop — Significant

User pain: "Polling/status code is hard to follow."

Knowledge leakage

status_poll_loop (598 lines) orchestrates ten concerns in sequence: terminal status, pane scanning, shell output capture, RC debounce, unbound TTL, dead detection, autoclose, broker delivery, live view, transcript discovery, topic emoji. Imports nine handler modules. Ordering matters (e.g., transcript discovery must precede status scanning).

By the Balance Rule, "high strength + low distance" is nominally balanced. But internal cohesion is low — ten unrelated concerns share one function scope. The pain is internal low cohesion, not cross-module coupling.

Recommended improvement

Invert control without introducing a framework:

  1. Give each strategy a tick(tick_ctx) async method.
  2. Replace the inlined per-binding loop with a dispatcher that calls strategy.tick(ctx) for each registered strategy.
  3. Move run_periodic_tasks / run_lifecycle_tasks contents into strategy tick() methods.

Same 1-second cadence. No DI. Coordinator shrinks to ~100 lines. Adding a new polling concern becomes "add a strategy, register it" instead of "edit the god loop."

Issue 3: message_queue.py mixes six concerns (1184 lines) — Significant

Knowledge leakage

This file owns per-user FIFO queues, the worker task, merging / batching, the pinned status-bubble rendering, tool_use↔tool_result pairing, and the status inline keyboard builder.

The circular import hook_events ↔ message_queue exists because message_queue needs build_subagent_label / get_subagent_names from hook_events, and hook_events needs enqueue_status_update from message_queue. Subagent labels are metadata about claude_task_state, not hook events.

Recommended improvement

  1. Extract status_bubble.pybuild_status_keyboard, status-bubble edit/send, text rendering.
  2. Move build_subagent_label / get_subagent_names into claude_task_state.py. Breaks the circular import.
  3. Leave queue primitives in message_queue.py. Should be ~400–500 lines after extraction.

Issue 4: Circular Dependencies Signal Misplaced Code — Minor (as signal: Moderate)

Recommended improvement

Extract shell_context.py with gather_llm_context, redact_for_llm, mark_telegram_command, and pending-command tracking. Both shell_commands (approval) and shell_capture (relay) depend on shell_context but not on each other.

Issue 5: SessionManager Write Surface — Significant (re-scoped)

User pain: "State shape changes cascade to 25+ handlers."

Knowledge leakage

25 handlers, 62 direct calls. The widest consumers (sync_command: 10, transcript_discovery: 7, recovery_callbacks: 6) are legitimate window lifecycle managers. But seven handlers have exactly one call that returns a WindowState for a single-field read:

These handlers depend on the whole facade shape for a single scalar.

Recommended improvement

Don't split SessionManager. Introduce a read-only projection:

@dataclass(frozen=True)
class WindowView:
    window_id: str
    cwd: str
    provider_name: str
    approval_mode: str
    notification_mode: str
    transcript_path: Path | None

Add session_manager.view_window(wid) -> WindowView | None. Handlers that only read migrate to this. Gives an explicit projection contract, a testability win (tests build WindowView literals), and no behavior change. Migrate one handler at a time.

Issue 6: Duplicated session_map Logic — Minor

session.py still contains full copies of load_session_map, register_hookless_session, write_hookless_session_map, prune_session_map that also exist in session_map.py. Incomplete extraction.

Recommended improvement

Delete the copies in session.py. SessionManager methods become thin delegators to session_map_sync. Verify with tests, then delete.

Issue 7: Deferred _schedule_save Wiring Is a Silent-Failure Trap — Minor

Four singletons initialize with _schedule_save = lambda: None. SessionManager.__post_init__() replaces them. Tests that mutate state without first instantiating SessionManager silently lose the save path.

Recommended improvement

  1. Replace the no-op lambda with None and raise RuntimeError("SessionManager not initialized") on call.
  2. Centralize the wiring in a single visible place — a wire_singletons(sm) helper called from __post_init__.

Priority Ranking (Practical)

Do first — high payoff, low cost

  1. Move EXPANDABLE_QUOTE_* and UUID_RE out of providers/base.py / providers/claude.py.
  2. Delete session_map duplicates from session.py.
  3. Extract status_bubble.py and move subagent label helpers out of hook_events.
  4. Fail-loud _schedule_save defaults.

Do when touching the affected code

  1. Introduce WindowView and migrate handlers opportunistically.
  2. Extract shell_context.py.
  3. Replace handler provider_name == checks with capability flags.

Do before the next big feature in that area

  1. Invert polling_coordinator into strategy-owned tick() methods.
  2. Extract providers/shell_infra.py before adding the next provider.

Don't do

Summary

Today's refactors resolved or partially resolved five of the six morning-review issues. Remaining work concentrates in three specific places:

None require large structural rewrites. All are incremental refactors that improve testability and cohesion without increasing effective distance.


This analysis uses the Balanced Coupling model by Vlad Khononov. See modularity-review.md for the morning baseline analysis.