From ca93102a1bbf4b27736412276430a59c8aa01eea Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Fri, 21 Aug 2026 15:07:32 +0800 Subject: [PATCH] =?UTF-8?q?emrg:=20upgrade=20banner=20=E2=80=94=20in-memor?= =?UTF-8?q?y=20run=20version=20vs=20live=20installed=5Fversion,=20dedicate?= =?UTF-8?q?d=20upgrade=20event=20(rant=202026-08-21T14:38:27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- emrg/gui/main.js | 17 +++++++++--- emrg/gui/renderer/js/app.js | 32 ++++++++++++---------- emrg/gui/test/renderer.smoke.test.js | 36 +++++++++++++++++------- emrg/server/daemon.py | 41 +++++++++++----------------- tests/test_daemon.py | 35 +++++++++++++----------- 5 files changed, 91 insertions(+), 70 deletions(-) diff --git a/emrg/gui/main.js b/emrg/gui/main.js index c7420fd..c076615 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -284,8 +284,7 @@ vision = false server_id: pong?.identity?.instance_id || "", model: pong?.model || "", evolution_count: pong?.evolution_count ?? 0, // G19:init 透传演化计数(waitForPong 已消耗 pong) - current_version: pong?.current_version || "", // rant 18:30:57:安装版本(GUI 对比显示升级横幅) - previous_version: pong?.previous_version || "", // rant 12:44:34:升级前版本(横幅 from→to) + current_version: pong?.current_version || "", // rant 18:30:57:进程实际运行版本(升级横幅对比基准;14:38:27 起为内存版本) version: APP_VERSION, // WorkBuddy P3:版本号随 package.json 走(此前 renderer 硬编码 v0.2.7) sessions, open_sessions: openSessionsList(), @@ -899,7 +898,7 @@ vision = false const sessions = await listSessions(); sendToRenderer("sessions", { sessions }); const pong = await waitForPong(); - sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" }); + sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "" }); logger.info("[gui] connManager recovery complete"); } catch (e) { logger.warn(`[gui] post-recovery refresh failed: ${e.message}`); @@ -1126,7 +1125,7 @@ vision = false const sessions = await listSessions(); sendToRenderer("sessions", { sessions }); const pong = await waitForPong(); - sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "", previous_version: pong?.previous_version || "" }); + sendToRenderer("status", { connected: true, server_id: pong?.identity?.instance_id, model: pong?.model, current_version: pong?.current_version || "" }); } }, delay); } @@ -1179,6 +1178,16 @@ vision = false stopHeartbeat(); try { conn.close(); } catch { /* ignore */ } scheduleReconnect(); + return; + } + // Rant 2026-08-21T14:38:27:升级判断——installed_version(磁盘实时)≠ + // current_version(进程内存运行版本)且 installed 非空 → 发专用 upgrade 事件 + // (不复用 status,避免 handleStatus 副作用刷屏),由 renderer 弹横幅。 + if (pong.installed_version && pong.installed_version !== pong.current_version) { + sendToRenderer("upgrade", { + current_version: pong.current_version || "", + installed_version: pong.installed_version, + }); } } diff --git a/emrg/gui/renderer/js/app.js b/emrg/gui/renderer/js/app.js index cdaaf50..77f4486 100644 --- a/emrg/gui/renderer/js/app.js +++ b/emrg/gui/renderer/js/app.js @@ -76,8 +76,7 @@ const App = (() => { state.serverId = init.server_id || ""; state.model = init.model || ""; state.version = init.version || ""; - state.currentVersion = init.current_version || ""; // rant 18:30:57:安装版本(升级横幅对比基准) - state.previousVersion = init.previous_version || ""; // rant 12:44:34:升级前版本(横幅 from→to) + state.currentVersion = init.current_version || ""; // rant 18:30:57:进程实际运行版本(升级横幅对比基准;14:38:27 起为 daemon 内存版本) state.lastKnownVersion = state.currentVersion; state.evolutionCount = init.evolution_count ?? null; state.lastKnownEvolutionCount = state.evolutionCount; @@ -1338,23 +1337,25 @@ const App = (() => { $("conn-banner").classList.add("hidden"); } - // ── 升级完成横幅(rant 2026-08-20T18:30:57 + 2026-08-21T12:44:34) ───── - function maybeShowUpgradeBanner(currentVersion, previousVersion) { - if (!currentVersion) return; // 无版本数据(dev 运行)→ 不显示 - if (currentVersion === state.lastKnownVersion) return; // 版本未变 + // ── 升级完成横幅(rant 18:30:57 + 12:44:34 + 14:38:27) ───────────── + // 14:38:27 重构:参数为 (installedVersion, runVersion)——installed 是磁盘实时 + // 安装版本(升级 agent 已更新但未重启),run 是 daemon 进程实际运行版本。 + // installed ≠ run → "已从 {run} 升级到 {installed},重启后生效"。 + function maybeShowUpgradeBanner(installedVersion, runVersion) { + if (!installedVersion) return; // 无版本数据(dev 运行)→ 不显示 + if (installedVersion === state.lastKnownVersion) return; // 已提示过该版本(防重复弹) const b = $("upgrade-banner"); if (!b) return; const msg = $("upgrade-banner-msg"); if (msg) { - // rant 12:44:34:daemon 提供升级前版本 → 显示 "from → to";否则回退旧文案 - if (previousVersion && previousVersion !== currentVersion) { - msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: previousVersion, to: currentVersion }); + if (runVersion && runVersion !== installedVersion) { + msg.textContent = _t("app.upgradeBannerMsgFromTo", { from: runVersion, to: installedVersion }); } else { - msg.textContent = _t("app.upgradeBannerMsg", { version: currentVersion }); + msg.textContent = _t("app.upgradeBannerMsg", { version: installedVersion }); } } b.classList.remove("hidden"); - state.lastKnownVersion = currentVersion; // 已提示,防重复弹 + state.lastKnownVersion = installedVersion; // 已提示,防重复弹 } function hideUpgradeBanner() { const b = $("upgrade-banner"); @@ -1494,6 +1495,9 @@ const App = (() => { case "status": handleStatus(data); break; + case "upgrade": // rant 14:38:27:心跳检测到 installed ≠ run → 弹"重启生效"横幅 + maybeShowUpgradeBanner(data.installed_version, data.current_version); + break; case "sessions": state.sessions = data.sessions || []; break; @@ -1581,10 +1585,8 @@ const App = (() => { if (data.server_id) state.serverId = data.server_id; if (data.model) state.model = data.model; state.evolutionCount = data.evolution_count ?? state.evolutionCount; - // rant 18:30:57:pong 携带 current_version → 对比已知版本,变化则弹升级横幅 - // rant 12:44:34:同时携带 previous_version → 横幅显示 from → to - if (data.previous_version) state.previousVersion = data.previous_version; - maybeShowUpgradeBanner(data.current_version || state.currentVersion, state.previousVersion); + // 14:38:27:升级判断移出 status——由 main.js 心跳对比 installed/current 发 + // 专用 upgrade 事件(本分支不再弹横幅,避免重连刷屏副作用)。 updateModelSwitcher(); updateGrowthCard(); maybeShowEvolutionToast(); diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index 584dc9e..cdf8e89 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -735,14 +735,13 @@ test("GCM rant Stage 2:演化增长 + 未认证 → GitHub 连接横幅出现 assert.ok(toastBlock.includes("maybeShowGithubBanner()"), "演化增长应触发 GitHub 横幅检查"); }); -test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ 重启按钮触发 restartDaemon(正反两态)", async () => { - // 正态:status current_version 与已知版本不同 → 横幅出现;有 previous_version → 显示 from→to +test("rant 18:30:57 + 12:44:34 + 14:38:27:升级横幅(installed≠run → from→to)+ 重启按钮(正反两态)", async () => { + // 正态(14:38:27 模型):upgrade 事件 installed≠current → 横幅出现 + from→to 文案 const { ctx } = makeSandbox({ init: async () => ({ config_exists: true, api_key_configured: true, current_version: "0.2.58", - previous_version: "", sessions: [], }), }); @@ -750,29 +749,43 @@ test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ await vm.runInContext(`(function() { document.getElementById("upgrade-banner").classList.add("hidden"); App.state.lastKnownVersion = "0.2.58"; - App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.61", previous_version: "0.2.57" } }); + App.handleEvent({ type: "upgrade", data: { current_version: "0.2.58", installed_version: "0.2.62" } }); })()`, ctx); const visible = vm.runInContext( '!document.getElementById("upgrade-banner").classList.contains("hidden")', ctx ); - assert.strictEqual(visible, true, "版本变化 → 升级横幅应出现"); + assert.strictEqual(visible, true, "installed≠run → 升级横幅应出现"); const bannerText = vm.runInContext('document.getElementById("upgrade-banner-msg").textContent', ctx); - assert.ok(bannerText.includes("0.2.57") && bannerText.includes("0.2.61"), `横幅应显示 from→to,实际 ${bannerText}`); + assert.ok(bannerText.includes("0.2.58") && bannerText.includes("0.2.62"), `横幅应显示 from→to,实际 ${bannerText}`); - // 负态:版本未变 → 横幅保持隐藏 + // 负态1:installed == lastKnown(已提示过)→ 保持隐藏(防重复弹) const { ctx: ctx2 } = makeSandbox({}); await tick(); await vm.runInContext(`(function() { document.getElementById("upgrade-banner").classList.add("hidden"); - App.state.lastKnownVersion = "0.2.58"; - App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.58" } }); + App.state.lastKnownVersion = "0.2.62"; + App.handleEvent({ type: "upgrade", data: { current_version: "0.2.58", installed_version: "0.2.62" } }); })()`, ctx2); const hidden = vm.runInContext( 'document.getElementById("upgrade-banner").classList.contains("hidden")', ctx2 ); - assert.strictEqual(hidden, true, "版本未变 → 横幅应保持隐藏"); + assert.strictEqual(hidden, true, "已提示过同版本 → 横幅应保持隐藏"); + + // 负态2:status 事件不再触发横幅(14:38:27:升级判断移出 status) + const { ctx: ctx4 } = makeSandbox({}); + await tick(); + await vm.runInContext(`(function() { + document.getElementById("upgrade-banner").classList.add("hidden"); + App.state.lastKnownVersion = "0.2.58"; + App.handleEvent({ type: "status", data: { connected: true, current_version: "0.2.62" } }); + })()`, ctx4); + const hidden2 = vm.runInContext( + 'document.getElementById("upgrade-banner").classList.contains("hidden")', + ctx4 + ); + assert.strictEqual(hidden2, true, "status 事件不应再弹升级横幅"); // 重启按钮 → restartDaemon 调用 let restarted = false; @@ -783,9 +796,12 @@ test("rant 18:30:57 + 12:44:34:版本变化 → 升级横幅(from→to)+ })()`, ctx3); const appSrc = fs.readFileSync(path.join(RENDERER_JS, "app.js"), "utf8"); assert.ok(appSrc.includes("restartDaemon"), "重启按钮应调用 window.emrg.restartDaemon"); + assert.ok(appSrc.includes('case "upgrade"'), "app.js 应处理 upgrade 事件"); const GUI_DIR = path.join(__dirname, ".."); const mainSrc = fs.readFileSync(path.join(GUI_DIR, "main.js"), "utf8"); assert.ok(mainSrc.includes("emrg:restartDaemon"), "main.js 应注册 emrg:restartDaemon IPC"); + assert.ok(mainSrc.includes('sendToRenderer("upgrade"'), "main.js 心跳应发 upgrade 事件"); + assert.ok(mainSrc.includes("pong.installed_version"), "心跳应对比 installed_version"); const preloadSrc = fs.readFileSync(path.join(GUI_DIR, "preload.js"), "utf8"); assert.ok(preloadSrc.includes("restartDaemon"), "preload 应暴露 restartDaemon"); }); diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 7e14499..e4dabac 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -227,6 +227,10 @@ def __init__(self, llm_config: LlmConfig) -> None: self._running = False self._stop_reason: str = "unknown" # shutdown_msg|cancel|sigint|bind_exit|crash (rant 2026-08-19T14:02:37) self._scheduler: Optional[TaskScheduler] = None + # Rant 2026-08-21T14:38:27:进程实际运行版本——启动时读一次 install/version.txt + # 记入内存(进程生命周期内不变)。GUI 用它对比磁盘实时 installed_version: + # 有差异 = 已装新版本但 daemon 未重启 → 弹"重启生效"横幅。 + self._run_version = self._current_installed_version() self._max_tool_rounds = llm_config.max_tool_rounds self._projects_log = runtime_dir / "projects.yml" self._rants_log = runtime_dir / "rants.jsonl" @@ -633,10 +637,12 @@ async def _run_upgrade_session(self, session_id: str, cwd: str, prompt: str) -> self._session_busy[session_id] = False def _current_installed_version(self) -> str: - """Current installed EMRG version from ~/.emrg/install/version.txt. + """Currently installed EMRG version from ~/.emrg/install/version.txt. - Rant 2026-08-20T18:30:57: raw data only, zero judgment — the GUI - compares it with its last known version and shows the upgrade banner. + Rant 2026-08-20T18:30:57 + 2026-08-21T14:38:27: live disk read — the + upgrade agent may have updated it while this daemon process still + runs the pre-upgrade code. The GUI compares it with the in-memory + ``_run_version`` and shows the upgrade banner when they differ. Returns "" when the file is missing (dev/standalone runs). """ try: @@ -645,21 +651,6 @@ def _current_installed_version(self) -> str: except (OSError, ValueError): return "" - def _previous_installed_version(self) -> str: - """Pre-upgrade EMRG version from ~/.emrg/install/previous-version.txt. - - Rant 2026-08-21T12:44:34: the upgrade agent writes the version it is - replacing into previous-version.txt before overwriting version.txt, - so the GUI banner can show "upgraded from X to Y" instead of only the - target version. Raw data only; "" when missing (dev/standalone or - first install). - """ - try: - v = (Path.home() / ".emrg" / "install" / "previous-version.txt").read_text(encoding="utf-8").strip() - return v - except (OSError, ValueError): - return "" - def _evolution_count(self) -> int: """Total completed evolution cycles across scheduler handlers + disk. @@ -1432,13 +1423,13 @@ async def _process_message( "started_at": self.start_time.isoformat(), "pid": os.getpid(), "model": self.llm.config.model, - # Rant 2026-08-20T18:30:57:并入当前安装版本——GUI 轮询 pong 时对比 - # 上次已知版本,发现变化 → 弹"已升级,重启生效"横幅。daemon 只回原始数据, - # 零判断逻辑(升级判断由 GUI 负责)。 - "current_version": self._current_installed_version(), - # Rant 2026-08-21T12:44:34:并入升级前版本——GUI 横幅显示 "from → to" - # (升级 agent 在覆盖 version.txt 前写入 previous-version.txt)。 - "previous_version": self._previous_installed_version(), + # Rant 2026-08-20T18:30:57 + 2026-08-21T14:38:27:current_version = + # 本进程实际运行版本(启动时读入内存,进程生命周期内不变); + # installed_version = 磁盘实时安装版本(升级 agent 可能已更新)。 + # GUI 对比二者:有差异 = 已装新版本但未重启 daemon → 弹横幅。 + # previous-version.txt 不再参与判断(14:38:27 重构)。 + "current_version": self._run_version, + "installed_version": self._current_installed_version(), }) return diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 9412a54..5d49dac 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -1743,7 +1743,8 @@ def test_pong_includes_current_version(tmp_path, monkeypatch): monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty) assert server._current_installed_version() == "" - # Pong payload includes the field + # Pong payload includes both fields (14:38:27 refactor: current_version = + # in-memory run version captured at startup; installed_version = live read) monkeypatch.setattr(daemon_mod.Path, "home", lambda: tmp_path) writer = _FakeWriter() import asyncio @@ -1751,38 +1752,40 @@ def test_pong_includes_current_version(tmp_path, monkeypatch): frame = _last_frame(writer) assert frame["type"] == "pong" assert frame["current_version"] == "0.2.59" + assert frame["installed_version"] == "0.2.59" -def test_pong_includes_previous_version(tmp_path, monkeypatch): - """Pong carries previous_version from ~/.emrg/install/previous-version.txt. +def test_pong_run_vs_installed_version(tmp_path, monkeypatch): + """current_version is the daemon's in-memory run version (fixed at + startup); installed_version is the live disk read — the upgrade agent can + update version.txt while the daemon still runs the pre-upgrade code. - Rant 2026-08-21T12:44:34: the upgrade agent records the pre-upgrade - version in previous-version.txt before overwriting version.txt, so the - GUI banner can show "upgraded from X to Y". Missing file → "" (dev runs - or first install), never an error. + Rant 2026-08-21T14:38:27: the GUI shows the upgrade banner when + installed_version ≠ current_version (upgrade installed, restart pending). + previous-version.txt is no longer used for the judgment. """ import emrg.server.daemon as daemon_mod install = tmp_path / ".emrg" / "install" install.mkdir(parents=True) (install / "version.txt").write_text("0.2.61\n", encoding="utf-8") - (install / "previous-version.txt").write_text("0.2.57\n", encoding="utf-8") monkeypatch.setattr(daemon_mod.Path, "home", lambda: tmp_path) server = _make_server() - assert server._previous_installed_version() == "0.2.57" + # _run_version captured at startup — mirrors the daemon's running code + assert server._run_version == "0.2.61" - # Missing file → "" (no crash) - empty = tmp_path / "no-install" - monkeypatch.setattr(daemon_mod.Path, "home", lambda: empty) - assert server._previous_installed_version() == "" + # Simulate an upgrade: version.txt updated on disk, daemon not restarted + (install / "version.txt").write_text("0.2.62\n", encoding="utf-8") + assert server._run_version == "0.2.61", "run version must stay fixed in-process" + assert server._current_installed_version() == "0.2.62" - # Pong payload includes the field - monkeypatch.setattr(daemon_mod.Path, "home", lambda: tmp_path) + # Pong: current_version = run (0.2.61), installed_version = disk (0.2.62) writer = _FakeWriter() import asyncio asyncio.run(server._process_message({"type": "ping"}, writer)) frame = _last_frame(writer) assert frame["type"] == "pong" assert frame["current_version"] == "0.2.61" - assert frame["previous_version"] == "0.2.57" + assert frame["installed_version"] == "0.2.62" + assert "previous_version" not in frame, "previous-version.txt no longer used (14:38:27)"