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
2 changes: 1 addition & 1 deletion Agent.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,7 +93,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design:
pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (652) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (670) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (107: 29 daemon_client + 22 app-commands + 31 renderer smoke + 15 i18n + 7 integration + 3 commands) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文)
Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响)
Expand Down
2 changes: 1 addition & 1 deletion README.cn.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -274,7 +274,7 @@ EMRG 不只是追赶——它自己追上来。
git clone https://github.com/argszero/emrg.git
cd emrg
uv sync # 安装依赖
uv run pytest tests/ -v # 跑测试(当前 652 项)
uv run pytest tests/ -v # 跑测试(当前 670 项)
uv run python -m emrg # 启动 TUI
# CI 含 actionlint workflow 门禁(#444):workflow 解析错误在 PR 即失败

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -273,7 +273,7 @@ EMRG doesn't just keep up — it catches up on its own.
git clone https://github.com/argszero/emrg.git
cd emrg
uv sync # install deps
uv run pytest tests/ -v # run tests (currently 652 items)
uv run pytest tests/ -v # run tests (currently 670 items)
uv run python -m emrg # launch TUI
# CI includes actionlint workflow gate (#444): workflow parse errors fail PR CI

Expand Down
30 changes: 30 additions & 0 deletions emrg/client/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -356,6 +356,13 @@ async def _reconnect():
import emrg
ver = getattr(emrg, "__version__", "dev")
chat.add("system", f"EMRG {ver} | {server_id}\nType /help for shortcuts, or just start chatting.")
# Auto update-check prompt (rant 2026-08-10T07:12:12):
# one-time, non-blocking — query daemon's cached latest
# release, show a status line, mark prompted (idempotent).
try:
await conn.send_command("update_check")
except Exception:
pass # never block chat on update check
status.update(left=_status_left(session_title, session_id), center=server_id)
term.set_title(f"{session_title or session_id} @ {project_name}")
term.render(); continue
Expand DownExpand Up@@ -906,6 +913,29 @@ async def _reconnect():
continue

# Memory content (read)
if data.get("type") == "update_check":
# Auto update-check prompt (rant 2026-08-10T07:12:12):
# one-time, non-blocking status line; idempotent per version.
if data.get("has_update") and data.get("enabled"):
latest = data.get("latest_version", "")
prompted = data.get("prompted_version", "")
if latest and latest != prompted:
import emrg
ver = getattr(emrg, "__version__", "dev")
chat.add("system",
f"New version v{latest} available (current v{ver}) — "
f"https://github.com/argszero/emrg/releases")
status.update(center=server_id or "emrg")
try:
await conn.send_command(
"update_check_prompted",
{"version": latest},
)
except Exception:
pass
term.render()
continue

if data.get("type") == "memory_content":
err = data.get("error", "")
if err:
Expand Down
43 changes: 42 additions & 1 deletion emrg/config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,9 +37,23 @@ class LlmConfig:
stream_options: Optional[dict] = field(default_factory=lambda: {"include_usage": False})


@dataclass
class UpdateConfig:
"""Auto update-check settings (rant 2026-08-10T07:12:12).

check: master switch — when false, the daemon never queries GitHub.
ttl_hours: how often to re-check the latest release (default 24h).
Prompting is always display-only (no auto download/install).
"""

check: bool = True
ttl_hours: int = 24


@dataclass
class EmrgConfig:
llm: LlmConfig = field(default_factory=LlmConfig)
update: UpdateConfig = field(default_factory=UpdateConfig)


def config_dir() -> Path:
Expand DownExpand Up@@ -92,7 +106,34 @@ def load_config() -> EmrgConfig:
var_name = llm.api_key[2:-1]
llm.api_key = os.environ.get(var_name, llm.api_key)

return EmrgConfig(llm=llm)
update_data = data.get("update", {})
update = UpdateConfig(
check=update_data.get("check", True),
ttl_hours=update_data.get("ttl_hours", 24),
)

return EmrgConfig(llm=llm, update=update)


def load_update_config() -> UpdateConfig:
"""Load only the [update] section (rant 2026-08-10T07:12:12).

The daemon is constructed with just LlmConfig; this helper lets it read
the update-check switch without parsing the full config. Missing config
file or missing section → defaults (check=True, ttl=24h).
"""
cfg_path = config_path()
if not cfg_path.exists():
return UpdateConfig()
try:
data = tomllib.loads(cfg_path.read_text(encoding="utf-8"))
except (OSError, tomllib.TOMLDecodeError):
return UpdateConfig()
update_data = data.get("update", {})
return UpdateConfig(
check=update_data.get("check", True),
ttl_hours=update_data.get("ttl_hours", 24),
)


def ensure_config() -> None:
Expand Down
26 changes: 26 additions & 0 deletions emrg/gui/main.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -459,6 +459,32 @@ vision = false
return { authenticated: Boolean(frame.authenticated), user: frame.user || null };
});

ipcMain.handle("emrg:updateCheck", async () => {
// Auto update-check prompt (rant 2026-08-10T07:12:12): query daemon's
// cached latest release; display-only, no auto download/install.
try {
const frame = await client.sendCommandAndWait("update_check", {}, 10000);
return {
current_version: frame.current_version || "",
latest_version: frame.latest_version || "",
has_update: Boolean(frame.has_update),
prompted_version: frame.prompted_version || "",
enabled: Boolean(frame.enabled),
};
} catch {
return { has_update: false, enabled: false, latest_version: "", current_version: "" };
}
});

ipcMain.handle("emrg:updateCheckPrompted", async (_e, { version }) => {
// Idempotency (rant 07:12:12 §4): record that the GUI showed the prompt
// for this version — same version never re-prompted.
try {
await client.sendCommandAndWait("update_check_prompted", { version: String(version || "") }, 5000);
} catch { /* best-effort */ }
return { ok: true };
});

ipcMain.handle("emrg:githubConnect", async (_e, { token }) => {
// Windows GCM rant Stage 2:PAT 授权 + setup-git(daemon github_connect)
const frame = await client.sendCommandAndWait("github_connect", { token: String(token || "").trim() }, 40000);
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/preload.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,8 @@ const api = {
sendRant: (payload) => ipcRenderer.invoke("emrg:sendRant", payload),
evolutionSummary: (payload) => ipcRenderer.invoke("emrg:evolutionSummary", payload),
githubStatus: () => ipcRenderer.invoke("emrg:githubStatus"),
updateCheck: () => ipcRenderer.invoke("emrg:updateCheck"),
updateCheckPrompted: (payload) => ipcRenderer.invoke("emrg:updateCheckPrompted", payload),
githubConnect: (payload) => ipcRenderer.invoke("emrg:githubConnect", payload),
githubDisconnect: () => ipcRenderer.invoke("emrg:githubDisconnect"),
githubConnectWeb: () => ipcRenderer.invoke("emrg:githubConnectWeb"),
Expand Down
1 change: 1 addition & 0 deletions emrg/gui/renderer/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -164,6 +164,7 @@ <h2 data-i18n="settings.title">设置</h2>
<div class="settings-group-title" data-i18n="settings.aboutTitle">关于</div>
<div class="about-box">
<div class="about-line">EMRG <span id="about-version">v0.2.8</span> · <span id="about-evolutions">🌱 <span data-i18n="copy.growthCountPrefix">已自我进化</span> 0 <span data-i18n="copy.times">次</span></span></div>
<div class="about-line hidden" id="about-update"></div>
<div class="hint" style="margin-top:6px;" data-i18n="settings.aboutDesc">EMRG 是一个会自我进化的 AI 智能体——每次改进都会自动汇报,你可以随时在这里看到它的成长。</div>
</div>
</div>
Expand Down
36 changes: 36 additions & 0 deletions emrg/gui/renderer/js/dialogs.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,9 @@ const Dialogs = (() => {
} catch { /* 元素缺失(测试桩)时忽略 */ }
// Windows GCM rant Stage 2:GitHub 连接状态随设置面板打开时刷新
await refreshGithubStatus();
// Auto update-check prompt (rant 2026-08-10T07:12:12): about area shows
// a one-time non-intrusive line when a newer release exists.
await refreshUpdateCheck();
} catch (e) {
Chat.addSystemMessage(_t("settings.readFailed", { msg: e.message }));
}
Expand DownExpand Up@@ -485,6 +488,39 @@ const Dialogs = (() => {
}
}

// Auto update-check prompt (rant 2026-08-10T07:12:12): display-only, one
// line in the about area, never a modal — no auto download/install.
async function refreshUpdateCheck() {
const el = $("about-update");
if (!el) return; // 元素缺失(测试桩)时忽略
try {
const u = await window.emrg.updateCheck();
if (!u || !u.enabled || !u.has_update || !u.latest_version) {
el.classList.add("hidden");
el.textContent = "";
return;
}
if (u.latest_version === u.prompted_version) {
el.classList.add("hidden");
el.textContent = "";
return;
}
const link = el("a", {
href: "https://github.com/argszero/emrg/releases",
target: "_blank",
rel: "noopener",
}, _t("settings.updateAvailable", { latest: u.latest_version }));
el.textContent = "";
el.appendChild(link);
el.classList.remove("hidden");
// 幂等:同版本只提示一次
try { await window.emrg.updateCheckPrompted({ version: u.latest_version }); } catch { /* ignore */ }
} catch {
el.classList.add("hidden");
el.textContent = "";
}
}

// ── 确认对话框(替代 confirm/alert) ────
let confirmCb = null;
function showConfirm(title, message, opts = {}) {
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/renderer/js/i18n.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ const I18N = (() => {
"settings.githubConnectedStatus": "已连接 @{user}",
"settings.githubNotConnected": "未连接",
"settings.githubStatusFailed": "状态获取失败",
"settings.updateAvailable": "发现新版本 v{latest} —— 点击前往 Releases 下载(不会自动安装)",
"settings.githubTokenEmpty": "请先粘贴 GitHub Personal Access Token",
"settings.githubConnecting": "连接中…",
"settings.githubConnected": "已连接 GitHub:@{user}(gh auth setup-git 已执行)",
Expand DownExpand Up@@ -380,6 +381,7 @@ const I18N = (() => {
"settings.githubConnectedStatus": "Connected as @{user}",
"settings.githubNotConnected": "Not connected",
"settings.githubStatusFailed": "Failed to load status",
"settings.updateAvailable": "New version v{latest} available — click to visit Releases (no auto-install)",
"settings.githubTokenEmpty": "Please paste a GitHub Personal Access Token first",
"settings.githubConnecting": "Connecting…",
"settings.githubConnected": "Connected to GitHub: @{user} (gh auth setup-git done)",
Expand Down
74 changes: 74 additions & 0 deletions emrg/server/daemon.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -292,12 +292,19 @@ async def serve(self) -> None:
# host-modified skill copies.
self._skills_ttl_task = asyncio.create_task(self._skills_ttl_loop())

# Auto update-check prompt (rant 2026-08-10T07:12:12): runs at startup
# + every [update] ttl_hours (default 24h). ONLY checks the latest
# release and persists state — clients (TUI/GUI) decide how to show
# the one-time prompt. No auto download/install, silent on failure.
self._update_check_task = asyncio.create_task(self._update_check_loop())

try:
await self._server.serve_forever()
except asyncio.CancelledError:
pass
finally:
self._skills_ttl_task.cancel()
self._update_check_task.cancel()
try:
await self._skills_ttl_task
except (asyncio.CancelledError, Exception):
Expand DownExpand Up@@ -369,6 +376,38 @@ async def _skills_ttl_loop(self) -> None:
logger.warning("skills update errors: %s", result["errors"])
await asyncio.sleep(_UPDATE_TTL_SECONDS)

async def _update_check_loop(self) -> None:
"""Background auto update-check prompt (rant 2026-08-10T07:12:12).

Runs at startup + every [update] ttl_hours (default 24h). ONLY checks
the latest release via api.github.com and persists state to
~/.emrg/.last_update_check.json — no auto download/install. Failures
are silent (never crash, never log noise); the next TTL retries.
Disabled entirely when [update] check = false in config.toml.
"""
from emrg.config import load_update_config
from emrg.update_check import (
load_state,
run_update_check_once,
should_check,
)

update_cfg = load_update_config()
if not update_cfg.check:
logger.debug("auto update-check disabled by config ([update] check=false)")
return

ttl = max(3600, int(update_cfg.ttl_hours or 24) * 3600)
while True:
state = load_state()
if should_check(state, ttl):
result = await run_update_check_once()
if result.get("checked"):
logger.debug(
"update check: latest=%s", result.get("latest_version")
)
await asyncio.sleep(ttl)

def _evolution_count(self) -> int:
"""Total completed evolution cycles across scheduler handlers + disk.

Expand DownExpand Up@@ -1354,6 +1393,41 @@ async def _process_message(
auth = await self._check_github_auth()
await self._send(ws, {"type": "github_status", **auth})

elif msg_type == "update_check":
# Auto update-check prompt (rant 2026-08-10T07:12:12): TUI/GUI
# query the daemon's latest known release. Returns the cached
# latest_version (populated at startup + every TTL) plus a
# has_update flag computed against the running version. No auto
# download/install; the client shows a one-time prompt.
import emrg
from emrg.config import load_update_config
from emrg.update_check import is_newer, load_state, parse_version

current = getattr(emrg, "__version__", "0")
state = load_state()
latest = state.get("latest_version") or ""
has_update = bool(
latest and is_newer(parse_version(latest), parse_version(current))
)
await self._send(ws, {
"type": "update_check",
"current_version": current,
"latest_version": latest,
"has_update": has_update,
"prompted_version": state.get("prompted_version") or "",
"enabled": load_update_config().check,
})

elif msg_type == "update_check_prompted":
# Idempotency (rant 07:12:12 §4): a client records that it showed
# the prompt for this version — same version never re-prompted.
from emrg.update_check import load_state, mark_prompted

version = msg.get("version", "")
if version:
mark_prompted(load_state(), version)
await self._send(ws, {"type": "update_check_prompted", "ok": True})

elif msg_type == "github_connect":
# Windows GCM rant Stage 2: GUI PAT auth — gh auth login
# --with-token + gh auth setup-git (git no longer touches GCM).
Expand Down
Loading
Loading