Skip to content

feat(sessions): session liveness + UI autostart on session start (2.128.0) - #171

Merged
drknowhow merged 1 commit into
mainfrom
feat/session-liveness-ui-autostart
Sep 8, 2026
Merged

drknowhow merged 1 commit into
mainfrom
feat/session-liveness-ui-autostart

Conversation

@drknowhow

Copy link
Copy Markdown
Owner

Why

Opening an IDE session in a project left you with no UI "in some cases", and the cases were not random: nothing in C3 connected a session starting to a UI server running. ProjectManager.launch_session had five callers — two hub endpoints, the per-project server, the TUI, and a thread in run_hub that fired ONCE, two seconds after hub startup, over projects with autostart_ui=true (true on 0 of the 60 projects registered on the machine this was found on). So when a UI was there, it was a detached pythonw server a previous session had left behind — survival, not startup. Reboot, taskkill, or the hub's Stop button, and the next session had nothing.

Liveness had the same shape of problem. _get_live_session_info read the newest session_start row and ended it only on a session_save — written solely when a human clicks end in the hub — or after 20 minutes of tool-call silence:

  • a closed IDE read "live" for up to 20 minutes — the session_end row added in 2.126.0 was read by nothing;
  • an open-but-quiet session read dead while its MCP process sat there serving;
  • two sessions on one project (two IDEs, a repo and its worktrees) collapsed into one.

And those 2.126.0 hooks were registered by c3 install-mcp only, so no already-installed project — this repo included — wrote session_open / session_end rows at all.

What changed

Liveness is a fact now. services/session_live.py keeps one heartbeat file per session (.c3/live/<id>.json = {session_id, host_session_id, ide, pid, ts}), written through services/atomic_json.write_json_atomic, beat every 60 s by the MCP lifespan's c3-session-heartbeat thread and cleared before the slow teardown steps. The timestamp decides liveness, never a pid probe (pids are reused; no psutil dependency). _get_live_sessions reads heartbeats first and falls back to the old inference for a session with no heartbeat — and that fallback finally treats session_end as terminal, matching on session_id or host_session_id. list_projects gains sessions / session_count; every singular field still describes the newest session, so existing consumers are unaffected. The hub card shows MCP×N.

The UI follows the session. cli/hook_session_open.ensure_ui launches the project's UI when one is not already running. It declines on C3_NO_UI_AUTOSTART / C3_BENCHMARK_MODE, on .c3/config.json → {"ui": {"autostart_on_session": false}}, when the project is not registered with the hub, and when the registry already has a live port for it — the last one is what keeps resume and clear from stacking a second server. source: compact never reaches it.

Ownership and reaping. Registry entries carry pid, owner (session | user) and owner_session. sweep_registry stops a session-owned server once its project has no heartbeat left and its activity log has been untouched for ui_reap_minutes (new ~/.c3/hub_config.json key, default 30, 0 disables; never on an unreachable path), and reports {"dropped", "reaped"}. A server you started with c3 ui or Open UI is never touched. The hub's one-shot autostart is now a 60 s sweep that also chases autostart_ui projects with a live session, so a hub started after the IDE, or a UI that died mid-session, is no longer a dead end.

Hook distribution. c3_hook_migrations() / migrate_hooks_for_project() turn the hub's migration into a table and ship SessionStart / SessionEnd to already-installed projects. Additive, idempotent, never creates a settings file, never drops a user's own hooks.

One bug found on the way

That migration quoted paths with shlex.quote, which on Windows produces a single-quoted backslash path. cmd.exe does not treat single quotes as quoting, so every command it wrote for a path containing a space (every path under 1. Projects, say) was dead on arrival — including the hook_c3read entry it has been writing since 2.21. The installer learned this on 2026-07-26 and has used a double-quoted forward-slash path ever since; that logic now lives in cli/_hook_utils.hook_command_arg, both writers call it, and a test pins the migration's command string against the installer's byte for byte.

Verification

  • Full suite: 4011 passed, 7 skipped. 41 new tests across tests/test_session_live.py, tests/test_hub_hook_migration.py, test_project_manager.py, test_hook_session_end.py.
  • ruff check . clean; JSX bundles transpile under real Babel (C3_REQUIRE_JSX_TOOLCHAIN=1 pytest tests/test_ui_jsx_syntax.py).
  • ensure_ui against this repo returns already-running — it correctly dedupes against the UI already live on 3334.
  • The migration, run against a copy of this project's real .claude/settings.local.json: adds both events, leaves the existing four untouched, and is a no-op on a second run.
  • c3_ci cannot reach FULL_CI_PASS locally — 3 macOS matrix cells can't run on this host.

Runtime gate: hooks and the MCP server load their code at process start, so none of this is live until the hub is restarted and a new IDE session opens. docs/session-liveness.md documents the contract and the knobs.

https://claude.ai/code/session_01FLzc3usUhbfdF9M35UUMb5

…28.0)

Starting an IDE session left you with no project UI "in some cases", and
the cases were not random: nothing in C3 connected a session starting to
a UI server running. launch_session had five callers — none in the MCP or
hook path — and the hub's autostart thread fired ONCE, two seconds after
startup, over autostart_ui projects (true on 0 of 60 registered here). A
UI that was there had been left behind by an earlier session.

Liveness had the same shape of problem: it was inferred from activity
rows, ended only by a hub "end session" click or 20 minutes of tool-call
silence. The session_end row added in 2.126.0 was read by nothing, so a
closed IDE read live for 20 minutes and a quiet-but-open session read
dead. And those hooks were installer-only, so no already-installed
project (this repo included) wrote session_open / session_end at all.

- services/session_live.py: one heartbeat file per session under
  .c3/live, atomic write, 60 s beat / 180 s TTL. Written by the MCP
  lifespan on a daemon thread and cleared before the slow teardown.
- ProjectManager._get_live_sessions: heartbeats first, activity-log
  inference as fallback, session_end honoured there (matched on
  session_id OR host_session_id). list_projects gains sessions /
  session_count; the hub card shows MCP×N.
- hook_session_open.ensure_ui: launches the project's UI, gated on
  C3_NO_UI_AUTOSTART / C3_BENCHMARK_MODE, ui.autostart_on_session, hub
  registration, and a live registry port (so resume/clear never stack a
  second server).
- Registry entries carry pid / owner / owner_session; sweep_registry
  reaps only session-owned servers, after ui_reap_minutes (default 30,
  0 disables, never on an unreachable path). The hub's one-shot
  autostart is now a 60 s sweep that also chases live-session projects.
- c3_hook_migrations + migrate_hooks_for_project: the migration is a
  table now, and it ships SessionStart / SessionEnd to existing
  projects. It also quoted commands with shlex.quote, which on Windows
  writes a single-quoted backslash path cmd.exe never unquotes — every
  entry it wrote for a path with a space was dead, hook_c3read since
  2.21 included. Both writers now share
  cli/_hook_utils.hook_command_arg, pinned against the installer.

Tests: 41 new across tests/test_session_live.py,
tests/test_hub_hook_migration.py, test_project_manager.py and
test_hook_session_end.py. Full suite 4011 passed / 7 skipped, ruff
clean. docs/session-liveness.md documents the contract and the knobs.

Claude-Session: https://claude.ai/code/session_01FLzc3usUhbfdF9M35UUMb5
@drknowhow
drknowhow merged commit de72f02 into main Sep 8, 2026
12 checks passed
@drknowhow
drknowhow deleted the feat/session-liveness-ui-autostart branch September 8, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant