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
4 changes: 2 additions & 2 deletions Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ EMRG is a self-evolving AI agent architecture experiment. Python implementation,
- Streaming chat with delta rendering (16ms batching), markdown on done (marked + DOMPurify + local highlight.js subset), tool call status cards (2000-char truncation + expand)
- Session list/switch/new/delete + right-click rename (context menu, #423) synced with daemon; own-stream busy lock (G65); broadcast streams from other clients tagged "来自其他客户端"
- Disconnect/reconnect: red status dot, auto daemon respawn (stale-port detection), session resume, input bar restored on disconnect (no 30s fake-timeout)
- Unit tests `npm test` (111: 29 daemon_client + 22 app-commands + 32 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- Unit tests `npm test` (113: 31 daemon_client + 22 app-commands + 32 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config); RESPONSE_TYPES mirror daemon protocol verified against `daemon.py`
- **Auto project tracking** — Automatically detects and records working directories; project-scoped sessions
- **Rant-driven evolution** — User feedback via `/rant` drives automatic self-improvement cycles
- **Headless GitHub auth** — Non-interactive evolution auto-extracts `GH_TOKEN` from git credential store (osxkeychain / credential helper); PR comment/LGTM queries fall back to REST API (GraphQL needs `read:org` scope)
Expand Down Expand Up @@ -94,7 +94,7 @@ pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.port; python -m emrg
```

Python: `uv run pytest tests/ -v` (680) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (111: 29 daemon_client + 22 app-commands + 32 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
GUI: `cd emrg/gui && npm test` (113: 31 daemon_client + 22 app-commands + 32 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config) — 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 number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ uv run python -m emrg # 启动 TUI
cd emrg/gui
npm ci # 安装依赖(生产模式可 --omit=dev)
npm start # 启动 GUI(自动拉起 daemon)
npm test # 运行 Node 测试(111 项:29 daemon_client + 22 app-commands + 32 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config;集成测试在 CI 跑,本地可 npm run test:integration)
npm test # 运行 Node 测试(113 项:31 daemon_client + 22 app-commands + 32 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config;集成测试在 CI 跑,本地可 npm run test:integration)
```

CI 通过 GitHub Actions 自动运行测试并检查冲突标记(`.github/workflows/test.yml`)。
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ uv run python -m emrg # launch TUI
cd emrg/gui
npm ci # install deps (production: --omit=dev)
npm start # launch GUI (auto-starts daemon)
npm test # run Node tests (111: 29 daemon_client + 22 app-commands + 32 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config; integration runs in CI, local: npm run test:integration)
npm test # run Node tests (113: 31 daemon_client + 22 app-commands + 32 renderer smoke + 15 i18n + 7 integration + 3 commands + 3 build-config; integration runs in CI, local: npm run test:integration)
```

CI runs tests and checks for conflict markers automatically via GitHub Actions (`.github/workflows/test.yml`).
Expand Down
16 changes: 15 additions & 1 deletion emrg/gui/daemon_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,10 +301,12 @@ class DaemonClient {
return pt;
}

async ensureConnected() {
async ensureConnected({ skipStart = false } = {}) {
// Rant 2026-08-09T18:47:37:1. 读 port 文件(projectDir → 规范 ~/.emrg 回退)→
// 无则拉 daemon;spawn 失败先探测已有 daemon,活着直接复用,不再盲报
// "failed to start after 3 attempts"。每步打结构化诊断日志(B1-B5)。
// P2 connManager(rant 2026-08-10T15:07:19):skipStart=true 时 daemon 生命周期
// 由 connManager 独占管理——本实例只连接**已运行**的 daemon,绝不自行拉起。
let port, token;
const pt = this._readPortToken();
if (pt) {
Expand All @@ -314,6 +316,11 @@ class DaemonClient {
`[gui] ensureConnected: port_file_exists=true, port_file_content=${port}, source=${pt.source}`
);
} else {
if (skipStart) {
throw new Error(
`daemon not running (skipStart): no port file at ${PORT_FILE(this.projectDir)}`
);
}
this.logger.info(`[gui] ensureConnected: port_file_exists=false — spawning daemon`);
const r = await this._spawnOrProbe();
port = r.port;
Expand Down Expand Up @@ -341,6 +348,13 @@ class DaemonClient {
try { this.ws.close(); } catch { /* ignore */ }
throw new Error(`daemon unreachable (pid alive): ${e.message}`);
}
if (skipStart) {
this.logger.warn(
`[gui] ws connect failed: ${e.message} — stale port, daemon dead (skipStart: not respawning)`
);
try { this.ws.close(); } catch { /* ignore */ }
throw new Error(`daemon unreachable (skipStart): ${e.message}`);
}
this.logger.warn(`[gui] ws connect failed: ${e.message} — stale port, respawning daemon`);
try { this.ws.close(); } catch { /* ignore */ }
try { fs.unlinkSync(PORT_FILE(this.projectDir)); } catch { /* ignore */ }
Expand Down
37 changes: 37 additions & 0 deletions emrg/gui/test/daemon_client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,43 @@ test("ensureConnected: port 文件缺失 → 拉起 daemon(spawn 参数正确
assert.strictEqual(spawnCalls.projectDir, tmpHome);
});

test("P2 skipStart: port 文件缺失 → 抛错不拉起 daemon(connManager 独占 daemon 生命周期)", async () => {
fs.rmSync(PORT_FILE(tmpHome), { force: true });
const client = new DaemonClient({ projectDir: tmpHome });
let spawnCalls = 0;
client.startDaemon = async function () {
spawnCalls += 1;
throw new Error("startDaemon must not be called with skipStart");
};
await assert.rejects(
() => client.ensureConnected({ skipStart: true }),
/daemon not running \(skipStart\): no port file/
);
assert.strictEqual(spawnCalls, 0, "startDaemon must never be called");
assert.strictEqual(client.connected, false);
});

test("P2 skipStart: stale port + daemon 已死 → 抛错不重拉(不删文件不 spawn)", async () => {
// 预写 port 文件指向一个无人监听的端口(连接必然失败)
fs.writeFileSync(PORT_FILE(tmpHome), "1\nseekrit-token"); // 127.0.0.1:1 拒绝连接
const client = new DaemonClient({ projectDir: tmpHome });
// daemon 进程已死(无 pid 文件)→ 旧路径会删文件重拉;skipStart 必须拒绝
let spawnCalls = 0;
client.startDaemon = async function () {
spawnCalls += 1;
throw new Error("startDaemon must not be called with skipStart");
};
const p = client.ensureConnected({ skipStart: true });
await waitForWs();
const firstWs = currentMockWs;
firstWs.emit("error", new Error("connect ECONNREFUSED"));
await assert.rejects(p, /daemon unreachable \(skipStart\)/);
assert.strictEqual(spawnCalls, 0, "startDaemon must never be called");
// port 文件保留(connManager 重启恢复依赖它判断 daemon 状态)
assert.ok(fs.existsSync(PORT_FILE(tmpHome)), "port file must be kept");
assert.strictEqual(client.connected, false);
});

test("Phase4: _findDaemonExecutable 打包模式定位捆绑 emrgd(POSIX)", async () => {
const client = new DaemonClient({ projectDir: tmpHome, isPackaged: true });
const exe = client._findDaemonExecutable();
Expand Down
Loading