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
17 changes: 13 additions & 4 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
});
}
}

Expand Down
32 changes: 17 additions & 15 deletions emrg/gui/renderer/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
36 changes: 26 additions & 10 deletions emrg/gui/test/renderer.smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -735,44 +735,57 @@ 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: [],
}),
});
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.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;
Expand All @@ -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");
});
Expand Down
41 changes: 16 additions & 25 deletions emrg/server/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -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.

Expand Down Expand Up @@ -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

Expand Down
35 changes: 19 additions & 16 deletions tests/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1743,46 +1743,49 @@ 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
asyncio.run(server._process_message({"type": "ping"}, writer))
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)"
Loading