From 3abbf98b038356961e244d9f7227837e6c9d08f0 Mon Sep 17 00:00:00 2001 From: Mukul Sharma Date: Sun, 18 Oct 2026 18:00:01 -0700 Subject: [PATCH] Read the session folder from cmux 0.65 restore records Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++++++ bin/render-assistant-page.py | 2 +- bin/world-scanner.py | 4 +++- tests/test_renderer_tabs.py | 10 ++++++++++ tests/test_world_scanner.py | 22 ++++++++++++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd76202..fdd4faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,12 @@ The version is carried in `pyproject.toml` and `src/assistant/__init__.py` and reminders to finish older pending work before starting another task. ### Fixed +- Stop the dashboard from crashing on sessions with no working folder. cmux + 0.65 lists a `restore_record` before the `resume_binding`, so + `world-scanner.py` read the folder from a record that names it + `working_directory` and saved no folder for those sessions. The renderer then + failed on every run. The scanner now reads either name, and the open-sessions + list shows a session even when its folder is unknown. - Stop two time-of-day test failures. The Observer audit tests failed in the last minute of a two-hour window, and five Strategist tests that count calls per UTC day failed in the last minutes of the day. Each test now starts its clock at the diff --git a/bin/render-assistant-page.py b/bin/render-assistant-page.py index e154dac..b0286c9 100755 --- a/bin/render-assistant-page.py +++ b/bin/render-assistant-page.py @@ -312,7 +312,7 @@ def render_live_sessions(world): sessions.append({ "ws_ref": sess.get("ws_ref"), "ws_title": sess.get("ws_title", ""), - "cwd": sess.get("cwd", ""), + "cwd": sess.get("cwd") or "", "role": role, "ts": ts, "text": text or "", "age_sec": age_sec, "user_unanswered": sess.get("user_unanswered"), diff --git a/bin/world-scanner.py b/bin/world-scanner.py index c2a1f73..030aee2 100755 --- a/bin/world-scanner.py +++ b/bin/world-scanner.py @@ -257,7 +257,9 @@ def surface_resume_binding(surface_ref, ws_ref): return { "session_id": str(sid), "provider": "droid" if kind in {"factory", "droid"} else "claude", - "cwd": binding.get("cwd"), + # cmux 0.65 lists a restore_record, which names it working_directory, + # before the resume_binding. + "cwd": binding.get("cwd") or binding.get("working_directory"), } diff --git a/tests/test_renderer_tabs.py b/tests/test_renderer_tabs.py index 0805e9d..d145553 100644 --- a/tests/test_renderer_tabs.py +++ b/tests/test_renderer_tabs.py @@ -350,6 +350,16 @@ def test_quiet_pill_for_very_old_session(self): self.assertEqual(n, 1) self.assertIn(">quiet<", html) + def test_session_with_unknown_cwd_renders(self): + now = datetime.now(timezone.utc) + world = {"live_sessions": [ + {"ws_ref": "workspace:13", "cwd": None, "is_cron": False, + "last_assistant": {"ts": _iso(now - timedelta(seconds=30)), "text": "no folder"}}, + ]} + html, n = self.mod.render_live_sessions(world) + self.assertEqual(n, 1) + self.assertIn('workspace:13', html) + def test_errored_session_hidden(self): now = datetime.now(timezone.utc) world = {"live_sessions": [ diff --git a/tests/test_world_scanner.py b/tests/test_world_scanner.py index 6c0eda4..29f7044 100644 --- a/tests/test_world_scanner.py +++ b/tests/test_world_scanner.py @@ -467,6 +467,28 @@ def test_build_identity_keeps_same_cwd_sessions_separate(ws, monkeypatch): assert session["last_user"]["text"] == f"Context for {expected['session_id']}" +def test_build_reads_cwd_from_the_cmux_065_restore_record(ws, monkeypatch): + records, _, _, _ = _seed_identity_world(ws, monkeypatch, ("claude",)) + sid = records[0]["session_id"] + original_run = ws.subprocess.run + + def run(args, **kwargs): + if args[1:4] != ["surface", "resume", "show"]: + return original_run(args, **kwargs) + # cmux 0.65 order: the restore_record comes before the resume_binding. + return fake_completed(json.dumps({ + "cleared": False, "pane_ref": "pane:1", + "restore_record": {"checkpoint_id": sid, "kind": "claude", + "working_directory": "/work/repo"}, + "resume_binding": {"checkpoint_id": sid, "kind": "claude", "cwd": "/work/repo"}, + })) + + monkeypatch.setattr(ws.subprocess, "run", run) + ws.build() + world = json.loads(ws.OUT_PATH.read_text()) + assert [s["cwd"] for s in world["live_sessions"]] == ["/work/repo"] + + @pytest.mark.parametrize("kind", ["claude", "droid"]) @pytest.mark.parametrize("process_state", ["unrelated", "background", "ambiguous", "mixed"]) def test_native_binding_requires_verified_provider_process(ws, monkeypatch, kind, process_state):