Several issues flagged in the morning review have been partially or fully resolved today. This update tracks the delta and surfaces what remains.
| Prior Issue | Status | What changed |
|---|---|---|
screenshot_callbacks — four concerns in one | Partially resolved | Toolbar handlers extracted to toolbar_callbacks.py. Screenshot/status/live still live together. |
/send intrusive import from screenshot_cb | Resolved | send_command.open_file_browser() is now a public API (c9a87a3). |
| SessionManager boundary violations | Resolved | session_resolver.py uses window_store.update_cwd(); session_map.py uses thread_router.set_display_name. |
Business logic in bot.py | Partially resolved | handle_new_message extracted to message_routing.py. Topic lifecycle moved to topic_lifecycle.py. |
| Scattered per-window singleton dicts | Still open | No change — WindowContext aggregation stands as long-term direction. |
| SessionManager god-object | Still open | See Issue 5 — re-scoped to write-surface problem. |
| Providers ↔ session circular import | Still open | Lazy 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.
| Integration | Strength | Distance | Volatility | Balanced? |
|---|---|---|---|---|
Handlers → session_manager (25 handlers, 62 calls) | Functional + Model | Low | High | Borderline — read-heavy, no projection type |
polling_coordinator.status_poll_loop → 9 handler modules | Functional + temporal | Low | High | Balanced by rule, low cohesion inside module |
| Provider abstraction leaks | Model + Intrusive | Low | High | No — bypassed in several paths |
hook_events ↔ message_queue | Functional | Low | Moderate | Signals misplaced code |
shell_commands ↔ shell_capture | Functional | Low | Moderate | Signals misplaced code |
message_queue.py (1184 lines, 6 concerns) | Internal (mixed) | — | Moderate | Cohesion problem |
Deferred _schedule_save wiring | Implicit globals | Low | Low | Testability hazard |
User pain: "Adding a provider is painful."
providers/base.py contains Telegram-specific constants. EXPANDABLE_QUOTE_START/END and format_expandable_quote() are Telegram blockquote syntax in a pure protocol file.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.llm/summarizer.py hardcodes Claude JSONL format. Parses message blocks directly instead of going through AgentProvider.parse_transcript_entries().providers/claude.py imports ccgram.hook.UUID_RE. Should be in base.py next to RESUME_ID_RE.transcript_discovery, recovery_callbacks, window_callbacks. Capability flags were meant to eliminate this.Practical, in order of payoff-to-cost ratio:
EXPANDABLE_QUOTE_* out of providers/base.py into a telegram formatting module. Thirty-minute change.UUID_RE into providers/base.py. Fifteen-minute change.provider_name == string checks; add capability flags as needed.providers/shell_infra.py — slim shell.py to just ShellProvider, move infrastructure elsewhere.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.
polling_coordinator is a God Loop — SignificantUser pain: "Polling/status code is hard to follow."
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.
Invert control without introducing a framework:
tick(tick_ctx) async method.strategy.tick(ctx) for each registered strategy.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."
message_queue.py mixes six concerns (1184 lines) — SignificantThis 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.
status_bubble.py — build_status_keyboard, status-bubble edit/send, text rendering.build_subagent_label / get_subagent_names into claude_task_state.py. Breaks the circular import.message_queue.py. Should be ~400–500 lines after extraction.hook_events ↔ message_queue — addressed by Issue 3.shell_commands ↔ shell_capture — these two modules are effectively one subsystem split for file-size reasons.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.
User pain: "State shape changes cascade to 25+ handlers."
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:
file_handler, shell_commands, text_handler, send_command — read cwdhistory — reads transcript_pathscreenshot_callbacks — reads notification_modetopic_emoji — reads approval_modeThese handlers depend on the whole facade shape for a single scalar.
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.
session_map Logic — Minorsession.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.
Delete the copies in session.py. SessionManager methods become thin delegators to session_map_sync. Verify with tests, then delete.
_schedule_save Wiring Is a Silent-Failure Trap — MinorFour 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.
None and raise RuntimeError("SessionManager not initialized") on call.wire_singletons(sm) helper called from __post_init__.EXPANDABLE_QUOTE_* and UUID_RE out of providers/base.py / providers/claude.py.session_map duplicates from session.py.status_bubble.py and move subagent label helpers out of hook_events._schedule_save defaults.WindowView and migrate handlers opportunistically.shell_context.py.provider_name == checks with capability flags.polling_coordinator into strategy-owned tick() methods.providers/shell_infra.py before adding the next provider.SessionManager into multiple classes.WindowContext aggregation for scattered state — still valid, but lower priority.Today's refactors resolved or partially resolved five of the six morning-review issues. Remaining work concentrates in three specific places:
WindowView solves it.None require large structural rewrites. All are incremental refactors that improve testability and cohesion without increasing effective distance.