diff --git a/CHANGELOG.md b/CHANGELOG.md index 9da1c92b..e9205f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,18 +14,25 @@ ### Tests -4142 → **4181 passing** (+39 regression tests across `tests/test_issue1611_session_profile_filtering.py` (11), `tests/test_issue1612_renamed_root_profile.py` (11), `tests/test_issue1614_project_profile_filtering.py` (11), `tests/test_provider_management.py::test_clean_provider_key_uses_late_bound_config_path` (1), and `tests/test_version_badge.py` agent-detect chain (~5)). 0 regressions. Full suite in ~120s. +4142 → **4180 passing** (+38 regression tests across `tests/test_issue1611_session_profile_filtering.py` (11), `tests/test_issue1612_renamed_root_profile.py` (11), `tests/test_issue1614_project_profile_filtering.py` (11), `tests/test_provider_management.py::test_clean_provider_key_uses_late_bound_config_path` (1), and `tests/test_version_badge.py` agent-detect chain (~5)). 0 regressions. Full suite in ~120s. ### Pre-release verification -- Self-built fix (nesquena-hermes), Opus advisor pre-merge pass with 2 SHOULD-FIX absorbed in-PR (see Opus-applied fixes below); independent review APPROVED by nesquena pending. +- **Opus advisor on full stage-293 diff: SHIP verdict.** Two SHOULD-FIX items absorbed in-release per <20-LOC defensive policy: (a) `api/models.py:load_projects()` re-reads from disk inside `_PROJECTS_MIGRATION_LOCK` when `_projects_migrated` is found True post-wait — closes a startup-window staleness race where a thread that read pre-migration could return stale untagged rows after a peer migrated and wrote disk; (b) `_detect_agent_version()` now uses `git describe --tags --always --dirty` for symmetry with `_detect_webui_version()`. One non-blocking client-side filter cross-alias edge case deferred as follow-up issue. +- Self-built fix (#1629, nesquena-hermes), independent review **APPROVED by nesquena** with comprehensive end-to-end trace, cross-tool verification against fresh agent tarball, security audit, race/state analysis, and 13-row edge-case matrix. +- 31 dedicated regression tests for #1611/#1612/#1614 invariants. Source-string assertions pin the active-profile guards on `/api/projects/{rename,delete}` and `/api/session/move`. - `_is_root_profile` invalidation cycle exercised via test_is_root_profile_invalidation_drops_stale (cache populated, then dropped after simulated profile rename). - `ensure_cron_project` per-profile isolation exercised via test_ensure_cron_project_creates_per_profile (two profiles → two distinct project_ids). -- Legacy migration covered: untagged projects with sessions inherit session profile; orphan projects fall back to 'default'; idempotent (no-op on second call). - Cross-alias matching pinned: `_profiles_match('default', 'kinni')` returns True only when `kinni` is `is_default`. -- Source-string assertions pin the active-profile guards on `/api/projects/{rename,delete}` and `/api/session/move`. -### Opus-applied fixes (absorbed in-PR per release policy) +### Opus-applied fixes (absorbed in-release) + +**From stage-293 review:** + +- **SHOULD-FIX A (project migration startup race)**: `api/models.py:load_projects()` re-reads from disk after acquiring `_PROJECTS_MIGRATION_LOCK` and finding `_projects_migrated=True`. Without this, Thread B that read pre-migration could return stale untagged rows after Thread A migrated and wrote disk — a mutation route on those stale rows could silently overwrite the migration. Window is process-startup-only and very narrow; fix is 8 LOC. +- **SHOULD-FIX B (agent version `--dirty` symmetry)**: `_detect_agent_version()` now passes `--dirty` to `git describe --tags --always`, matching `_detect_webui_version()`. Operators with locally-modified agent checkouts now see the dirty marker. + +**Already absorbed in #1629 (in-PR Opus pre-merge pass before staging):** - **SHOULD-FIX #1 (renamed-root client cross-alias)**: removed the strict-equality client filter at `static/sessions.js:1853`. Server-side `_profiles_match` cross-aliases `'default'`-tagged rows to a renamed root `'kinni'`; a strict-equality client filter would have rejected them, dropping every legacy session for renamed-root users. Server is now solely authoritative for profile scoping. Same fix applied to the `otherProfileCount` client fallback. - **SHOULD-FIX #2 (messaging-source dedupe ordering)**: moved `_keep_latest_messaging_session_per_source(merged)` to AFTER the profile filter at `api/routes.py:2078`. Before: the dedupe ran on the merged-cross-profile list with profile-blind keys, discarding the older profile's row across profiles, then the profile filter scoped to the active profile — leaving zero rows for any messaging identity the active profile shared with another profile. After: filter first, then dedupe within scope. diff --git a/ROADMAP.md b/ROADMAP.md index 5b925bf9..8ee40741 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ > Web companion to the Hermes Agent CLI. Same workflows, browser-native. > -> Last updated: v0.50.292 (May 04, 2026) — 4142 tests collected +> Last updated: v0.50.293 (May 04, 2026) — 4180 tests collected > Test source: `pytest tests/ --collect-only -q` > Per-version detail: see [CHANGELOG.md](./CHANGELOG.md) diff --git a/TESTING.md b/TESTING.md index 9d4577cd..2a0d0c6c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1835,8 +1835,8 @@ Bridged CLI sessions: --- -*Last updated: v0.50.292, May 04, 2026* -*Total automated tests collected: 4142* +*Last updated: v0.50.293, May 04, 2026* +*Total automated tests collected: 4180* *Regression gate: tests/test_regressions.py* *Run: pytest tests/ -v --timeout=60* *Source: /* diff --git a/api/models.py b/api/models.py index 27ab5e96..36775570 100644 --- a/api/models.py +++ b/api/models.py @@ -1053,7 +1053,16 @@ def load_projects(*, _migrate: bool = True) -> list: with _PROJECTS_MIGRATION_LOCK: # Re-check inside the lock — another thread may have raced. if _projects_migrated: - return projects + # Per Opus advisor on stage-293: another thread completed + # migration and wrote new state to disk while we waited for + # the lock. Our `projects` snapshot is the pre-migration + # version; re-read so the caller doesn't see stale untagged + # rows (which a mutation route could then write back, + # silently overwriting the migration). + try: + return json.loads(PROJECTS_FILE.read_text(encoding='utf-8')) + except Exception: + return projects if _backfill_project_profiles_if_needed(projects): try: save_projects(projects) diff --git a/api/updates.py b/api/updates.py index 85d29e06..9e1d9f4e 100644 --- a/api/updates.py +++ b/api/updates.py @@ -135,7 +135,10 @@ def _detect_agent_version() -> str: # file is available (common in source checkouts and developer environments). if not Path(_AGENT_DIR).exists(): return 'not detected' - out, ok = _run_git(['describe', '--tags', '--always'], _AGENT_DIR, timeout=3) + # Symmetric with _detect_webui_version() above — `--dirty` flags a + # locally-modified checkout so operators can see when their agent has + # uncommitted changes vs a clean tag. Per Opus advisor on stage-293. + out, ok = _run_git(['describe', '--tags', '--always', '--dirty'], _AGENT_DIR, timeout=3) if ok and out: return out