Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bin/render-assistant-page.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
4 changes: 3 additions & 1 deletion bin/world-scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}


Expand Down
10 changes: 10 additions & 0 deletions tests/test_renderer_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('<span class="pill scope" title="">workspace:13</span>', html)

def test_errored_session_hidden(self):
now = datetime.now(timezone.utc)
world = {"live_sessions": [
Expand Down
22 changes: 22 additions & 0 deletions tests/test_world_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading