| Dimension | Score | Notes |
|---|---|---|
| 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. |
parse_session_map duplicate removed from session.py ✅window_store.window_states direct dict access replaced with store API ✅_has_insert_indicator promoted to public in tmux_manager ✅_get_provider(window_id) helper extracted in window_tick.py (6 duplicates → 1) ✅session_monitor.py refactored: 4 sub-modules extracted — monitor reduced 42% ✅| Subsystem | Key Files | Domain |
|---|---|---|
| Provider abstraction | providers/ (12 files) | Core — competitive advantage, evolving |
| Session monitoring | session_monitor.py, event_reader.py, session_lifecycle.py, transcript_reader.py, idle_tracker.py | Core — actively evolving |
| Polling loop | polling_coordinator.py, window_tick.py, polling_strategies.py | Supporting — stable design |
| Message delivery | message_queue.py, status_bubble.py, tool_batch.py, message_sender.py | Supporting — stable |
| Core state | session.py, thread_router.py, window_state_store.py, claude_task_state.py | Supporting — frequently accessed |
| Shell provider | shell_infra.py, shell_commands.py, shell_capture.py, shell_context.py | Supporting — actively developed |
| Inter-agent messaging | mailbox.py, msg_broker.py, msg_spawn.py, msg_telegram.py | Core — new feature |
Structural strengths:
message_task.py — pure frozen dataclass sum type, zero project importspolling_coordinator.py — 87 lines, thin orchestration shellproviders/base.py — zero ccgram imports; clean protocol boundarytopic_state_registry.py — self-registration pattern replaces scattered lazy cleanup importshook.py — correctly isolated from config.py (deployed in agent panes)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.
| Concern | Methods |
|---|---|
| Window state | get_window_state(), view_window() |
| Mode settings | get_approval_mode(), cycle_notification_mode(), get_batch_mode()… |
| Session map | load_session_map(), register_hookless_session(), wait_for_session_map_entry() |
| Session resolve | resolve_session_for_window(), get_recent_messages() |
| Lifecycle | prune_stale_state(), audit_state(), resolve_stale_ids() |
| Provider | get_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.
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().
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.
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.
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.
| Issue | Files | Strength | Volatility | Priority | Status |
|---|---|---|---|---|---|
session.py hub — 32 handler dependents |
session.py + 32 handlers |
HIGH | HIGH | Critical | Open |
claude_task_state — no ownership discipline |
claude_task_state.py + 6 callers |
HIGH | HIGH | High | Open |
hook_events reaches into polling infra |
hook_events.py, periodic_tasks.py |
HIGH | HIGH | High | Open |
status_bubble queries polling state |
status_bubble.py, polling_strategies.py |
LOW | LOW | Medium | Open |
shell_capture ↔ shell_commands runtime cycle |
shell_capture.py, shell_commands.py |
HIGH | MEDIUM | Medium | Open |
_has_insert_indicator private API access |
window_tick.py, tmux_manager.py |
HIGH | HIGH | Critical | ✅ Fixed |
_get_provider() duplicated 6× in window_tick |
window_tick.py |
MED | HIGH | High | ✅ Fixed |
session_map bypasses WindowStateStore API |
session_map.py, window_state_store.py |
HIGH | MEDIUM | High | ✅ Fixed |
parse_session_map duplicated in two files |
session.py, session_map.py |
HIGH | MEDIUM | Medium | ✅ Fixed |
session_monitor.py monolith (was 750+ lines) |
session_monitor.py + 4 extracted modules |
HIGH | HIGH | Critical | ✅ Fixed |
claude_task_state write discipline — designate session_lifecycle.py as the single write authority; remove the hook_events → claude_task_state direct mutation path. Highest-risk shared-state pattern: volatile schema + 6 callers + violated authority claim.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.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.status_bubble → polling_strategies severance — pass rc_active: bool as a parameter to build_status_keyboard(). One-line fix, zero risk.CommandApprovalCallback protocol; route capture-pane through tmux_manager.