diff --git a/emrg/client/app.py b/emrg/client/app.py index d83189ad..1af44b72 100644 --- a/emrg/client/app.py +++ b/emrg/client/app.py @@ -1854,7 +1854,7 @@ def _is_image_token(s, i): images = _pending_images or None _pending_images = [] await conn.send_task(session_id=session_id, cwd=cwd, prompt=text, - stream=True, images=images) + images=images) logger.info("task sent, prompt_len=%d chars", len(text)) inp.text = ""; inp.cursor = 0; inp.dirty = True; term.render(); return True if b == 0x1B and len(data) >= 2 and data[1] in (0x0D, 0x0A): diff --git a/emrg/client/daemon_manager.py b/emrg/client/daemon_manager.py index 665313cf..1e4bb7c7 100644 --- a/emrg/client/daemon_manager.py +++ b/emrg/client/daemon_manager.py @@ -228,12 +228,12 @@ def __init__(self, ws): self._ws = ws async def send_task(self, session_id: str, cwd: str, prompt: str, - stream: bool = True, images: list | None = None) -> None: + images: list | None = None) -> None: """聊天发送:TaskRequest(type="task")。images 支持 /image 粘贴图。 内部 json.dumps(req.to_dict(), ensure_ascii=False) 以 str 发送(不 .encode())。 """ - req = TaskRequest(session_id=session_id, cwd=cwd, prompt=prompt, stream=stream) + req = TaskRequest(session_id=session_id, cwd=cwd, prompt=prompt) if images: req.images = images await self._ws.send(json.dumps(req.to_dict(), ensure_ascii=False)) diff --git a/emrg/gui/daemon_client.js b/emrg/gui/daemon_client.js index 1f0495e7..a0cfa721 100644 --- a/emrg/gui/daemon_client.js +++ b/emrg/gui/daemon_client.js @@ -486,11 +486,11 @@ class DaemonClient { // ── 消息发送 ──────────────────────────────────────────── - sendTask({ sessionId, cwd, prompt, stream = true, images = null, requestId = null, mode = "auto" }) { + sendTask({ sessionId, cwd, prompt, images = null, requestId = null, mode = "auto" }) { // G32:request_id 必须作为 id 字段发出(daemon 只回显不自生成) - // G96:stream 必须显式 true(daemon 读 stream 默认 False) // G143:外部预生成 requestId 优先(renderer send 前标记自有流,消除 IPC 往返竞态窗口) // WorkBuddy P2:mode="ask" → daemon 不启用工具(纯对话) + // rant 21:20:38:非 stream 路径已删除——所有 task 恒走 tool_loop(流式) const rid = requestId || crypto.randomUUID(); const payload = { type: "task", @@ -499,17 +499,14 @@ class DaemonClient { cwd, prompt, timestamp: new Date().toISOString(), - stream, images, }; if (mode && mode !== "auto") payload.mode = mode; this._setCurrentStream(rid); // G65:自有流锁——本连接发出流式 task 即标记,done/error/cancelled/断连释放 // (多会话各自独立;main.js emrg:sendMessage 的 G65 切会话检查读本字段) - if (stream) { - this.ownStream = true; - this.ownStreamRequestId = rid; - } + this.ownStream = true; + this.ownStreamRequestId = rid; this.ws.send(JSON.stringify(payload)); return rid; } diff --git a/emrg/gui/main.js b/emrg/gui/main.js index 513175ac..676e3b1d 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -287,7 +287,7 @@ vision = false // G143:renderer 预生成 requestId(send 前标记自有流,消除 IPC 往返竞态窗口) // WorkBuddy P2:mode="ask" → 纯对话(daemon 不启用工具) // G65:conn.sendTask 内部标记 ownStream(每连接独立锁) - rid = conn.sendTask({ sessionId, cwd: sessionCwd, prompt: text, stream: true, requestId, mode }); + rid = conn.sendTask({ sessionId, cwd: sessionCwd, prompt: text, requestId, mode }); } catch (e) { conn._releaseOwnStream(); // sendTask 抛异常(ws.send 失败)→ 释放锁,防 G65 锁泄漏 throw e; diff --git a/emrg/gui/test/daemon_client.test.js b/emrg/gui/test/daemon_client.test.js index 6c87872c..2fff4dd9 100644 --- a/emrg/gui/test/daemon_client.test.js +++ b/emrg/gui/test/daemon_client.test.js @@ -7,7 +7,7 @@ * - ensureConnected:port 文件读取 + auth 首帧 + auth_ok 处理 * - 坏 JSON 帧 → 忽略不崩 * - ws close → 触发 disconnected 事件(重连回调由 main 层调度) - * - sendTask:payload(type=task + session_id + prompt + images + id + stream:true) + * - sendTask:payload(type=task + session_id + prompt + images + id,无 stream 字段——非 stream 路径已删) * - sendCommand:payload(type + params);cancel 无多余字段(G24) * - 帧分类(G21+G58):tool_start/tool_end/delta/done/cancelled/error/pong/ * list_result/command_result 各帧正确分类 @@ -524,7 +524,7 @@ test("断连清 _currentStream + G94 timer(#338 回归:不弹虚假超时) const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); // 发起任务 → _currentStream 建立 - const rid = client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: tmpHome, prompt: "hi", stream: true }); + const rid = client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: tmpHome, prompt: "hi" }); assert.ok(client._currentStream, "_currentStream 已建立"); assert.ok(client._currentStream.requestId === rid); // 收到 delta → _resetStreamTimer 挂起 G94 30s timer @@ -540,7 +540,7 @@ test("断连清 _currentStream + G94 timer(#338 回归:不弹虚假超时) assert.deepStrictEqual(doneEvents, [], "断连后无虚假 done 事件"); }); -test("sendTask payload(G32/G96)", async () => { +test("sendTask payload(G32,无 stream 字段——rant 21:20:38)", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); const rid = client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hello", images: null }); @@ -550,8 +550,7 @@ test("sendTask payload(G32/G96)", async () => { assert.strictEqual(frame.session_id, "s_260803_1730_abcd1234"); assert.strictEqual(frame.cwd, "/proj"); assert.strictEqual(frame.prompt, "hello"); - assert.strictEqual(frame.stream, true, "stream 显式 true(G96)"); - assert.strictEqual(frame.images, null); + assert.strictEqual(frame.images, null); assert.ok(frame.timestamp); }); @@ -559,7 +558,7 @@ test("sendTask 外部预生成 requestId(G143)", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); const outer = "s_260803_1730_outer1234"; - const rid = client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", stream: true, requestId: outer }); + const rid = client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", requestId: outer }); assert.strictEqual(rid, outer, "返回外部预生成 id"); const frame = JSON.parse(currentMockWs.sent.at(-1)); assert.strictEqual(frame.id, outer, "payload id 用外部预生成 id"); @@ -829,11 +828,11 @@ test("18:47:37: stale projectDir port + spawn 节流失败 → probe 复用 cano // ── P2 自有流锁(G65 每连接独立;rant 15:07:19)────────────────────────── -test("P2 ownStream: sendTask(stream:true) 标记 ownStream + requestId", async () => { +test("P2 ownStream: sendTask 恒标记 ownStream + requestId(非 stream 路径已删)", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); - const rid = client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", stream: true, requestId: "req-own-1" }); - assert.strictEqual(client.ownStream, true, "stream:true must set ownStream"); + const rid = client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", requestId: "req-own-1" }); + assert.strictEqual(client.ownStream, true, "ownStream set unconditionally"); assert.strictEqual(client.ownStreamRequestId, "req-own-1"); assert.strictEqual(rid, "req-own-1"); }); @@ -841,7 +840,7 @@ test("P2 ownStream: sendTask(stream:true) 标记 ownStream + requestId", async ( test("P2 ownStream: 自有 done(request 匹配)→ 释放锁;广播 done(不匹配)→ 保持", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); - client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", stream: true, requestId: "req-own-2" }); + client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", requestId: "req-own-2" }); const send = (obj) => currentMockWs.emit("message", Buffer.from(JSON.stringify(obj))); // 广播 done(其他客户端/其他流)→ 锁保持 send({ request_id: "req-other", done: true, delta: false }); @@ -855,7 +854,7 @@ test("P2 ownStream: 自有 done(request 匹配)→ 释放锁;广播 done test("P2 ownStream: timeout 兜底 done(无匹配 request)→ 释放锁", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); - client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", stream: true, requestId: "req-own-3" }); + client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", requestId: "req-own-3" }); const send = (obj) => currentMockWs.emit("message", Buffer.from(JSON.stringify(obj))); send({ request_id: "req-stale", done: true, delta: false, timeout: true }); assert.strictEqual(client.ownStream, false, "timeout done must release own lock"); @@ -864,7 +863,7 @@ test("P2 ownStream: timeout 兜底 done(无匹配 request)→ 释放锁", as test("P2 ownStream: session busy 即发 error → 释放锁(防 G65 锁泄漏)", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); - client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", stream: true, requestId: "req-own-4" }); + client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", requestId: "req-own-4" }); const send = (obj) => currentMockWs.emit("message", Buffer.from(JSON.stringify(obj))); send({ error: "session busy: another stream running" }); assert.strictEqual(client.ownStream, false, "session busy error must release lock"); @@ -873,7 +872,7 @@ test("P2 ownStream: session busy 即发 error → 释放锁(防 G65 锁泄漏 test("P2 ownStream: cancelled(request 匹配)→ 释放锁", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); - client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", stream: true, requestId: "req-own-5" }); + client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", requestId: "req-own-5" }); const send = (obj) => currentMockWs.emit("message", Buffer.from(JSON.stringify(obj))); send({ type: "cancelled", request_id: "req-own-5" }); assert.strictEqual(client.ownStream, false, "own cancelled must release lock"); @@ -882,7 +881,7 @@ test("P2 ownStream: cancelled(request 匹配)→ 释放锁", async () => { test("P2 ownStream: 断连 → 释放锁", async () => { const client = new DaemonClient({ projectDir: tmpHome }); await connectClient(client); - client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", stream: true, requestId: "req-own-6" }); + client.sendTask({ sessionId: "s_260803_1730_abcd1234", cwd: "/proj", prompt: "hi", requestId: "req-own-6" }); assert.strictEqual(client.ownStream, true); client.close(); assert.strictEqual(client.ownStream, false, "disconnect must release lock"); diff --git a/emrg/protocol.py b/emrg/protocol.py index bbc8a9b3..c2fdbfcd 100644 --- a/emrg/protocol.py +++ b/emrg/protocol.py @@ -29,7 +29,6 @@ class TaskRequest: timestamp: str = field( default_factory=lambda: datetime.now().isoformat() ) - stream: bool = False images: Optional[list[dict]] = None def to_dict(self) -> dict: @@ -40,7 +39,6 @@ def to_dict(self) -> dict: "cwd": self.cwd, "prompt": self.prompt, "timestamp": self.timestamp, - "stream": self.stream, } if self.images: d["images"] = self.images diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 97a07ab0..d72d14f1 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -554,7 +554,6 @@ async def _handle_client(self, ws) -> None: cwd=cwd, prompt=data.get("prompt", ""), timestamp=data.get("timestamp", ""), - stream=data.get("stream", False), images=data.get("images"), ) except Exception as e: @@ -570,19 +569,14 @@ async def _handle_client(self, ws) -> None: _tool_task.cancel() session = self._get_or_create_session(session_id, Path(cwd)) logger.info( - 'task received: session=%s prompt="%s" → routing via LLM (stream=%s)', - session_id, _redact_string(req.prompt[:60]), req.stream, + 'task received: session=%s prompt="%s" → routing via LLM', + session_id, _redact_string(req.prompt[:60]), ) _cancel_event = asyncio.Event() self._session_busy[session_id] = True # lock (released in *locked wrapper) - if req.stream: - _tool_task = asyncio.create_task( - self._run_tool_loop_locked(req, ws, session, _cancel_event, allow_tools=allow_tools) - ) - else: - _tool_task = asyncio.create_task( - self._run_chat_once_locked(req, ws, session) - ) + _tool_task = asyncio.create_task( + self._run_tool_loop_locked(req, ws, session, _cancel_event, allow_tools=allow_tools) + ) continue await self._process_message(data, ws) @@ -1642,64 +1636,6 @@ def _build_user_content(text: str, images: list[dict] | None, vision: bool = Fal content.insert(0, {"type": "text", "text": "请分析这张图片"}) return content - async def _run_chat_once( - self, req: TaskRequest, ws, session: Session - ) -> None: - """Non-streaming single-turn chat (no tool loop).""" - system_prompt = self._build_system_prompt(session) - history_messages = session.get_messages_for_llm() - user_content = self._build_user_content(req.prompt, req.images, self.llm.config.vision) - messages: list[dict] = [ - {"role": "system", "content": system_prompt}, - *history_messages, - {"role": "user", "content": user_content}, - ] - tools = self.tools.to_openai_tools() - - # Persist user message (with image references if present) - user_record: dict = { - "type": "message", - "role": "user", - "content": req.prompt, - } - if req.images: - user_record["images"] = req.images - session.append_message(user_record) - - try: - msg = await self.llm.chat(messages, tools=tools) - content = msg.get("content", "") - - # Log LLM request/response - self._log_llm_exchange( - session, messages, tools, content, - finish_reason=msg.get("finish_reason", "stop"), - tool_calls=msg.get("tool_calls"), - ) - - # Persist assistant message - session.append_message({ - "type": "message", - "role": "assistant", - "content": content or "", - }) - - await self._broadcast(session.session_id, { - "request_id": req.id, - "content": content or "", - "done": True, - "delta": False, - "session_id": session.session_id, - }) - - # Fire-and-forget: reflect on whether to save memories - self._maybe_reflect_memory(session, req.prompt, content or "") - except Exception as e: - logger.exception("LLM error") - await self._broadcast(session.session_id, { - "error": f"LLM error: {e}. Check config at ~/.emrg/config.toml", - }) - # ── Phase 2 session-lock wrappers (protocol-contract §2.6.5) ── # The caller fire-and-forgets with asyncio.create_task() and never awaits, # so the lock MUST be released inside the task (wrapper finally) — not in @@ -1719,16 +1655,6 @@ async def _run_tool_loop_locked( finally: self._session_busy[session_id] = False - async def _run_chat_once_locked( - self, req: TaskRequest, ws, session: Session, - ) -> None: - """Run _run_chat_once and release the session busy lock on exit.""" - session_id = session.session_id - try: - await self._run_chat_once(req, ws, session) - finally: - self._session_busy[session_id] = False - async def _run_tool_loop( self, req: TaskRequest, ws, session: Session, cancel_event: asyncio.Event | None = None, @@ -2110,8 +2036,8 @@ def _log_llm_exchange( ) -> None: """Log a complete LLM request/response exchange to the session. - Centralizes the 4 identical append_llm patterns from _run_chat_once - and _run_tool_loop, ensuring consistent logging format. + Centralizes the identical append_llm patterns from _run_tool_loop, + ensuring consistent logging format. """ session.append_llm({ "type": "request", diff --git a/tests/test_daemon_manager.py b/tests/test_daemon_manager.py index b335cdee..fef538c3 100644 --- a/tests/test_daemon_manager.py +++ b/tests/test_daemon_manager.py @@ -298,7 +298,7 @@ def _conn(self, frames=None): def test_send_task_payload(self): conn = self._conn() asyncio.run(conn.send_task( - session_id="s1", cwd="/tmp/x", prompt="hello", stream=True, + session_id="s1", cwd="/tmp/x", prompt="hello", images=[{"path": "/tmp/a.png", "label": "[image1]"}], )) sent = json.loads(conn._ws.sent[0]) @@ -306,7 +306,7 @@ def test_send_task_payload(self): assert sent["session_id"] == "s1" assert sent["cwd"] == "/tmp/x" assert sent["prompt"] == "hello" - assert sent["stream"] is True + assert "stream" not in sent # non-stream path removed (rant 21:20:38) assert sent["images"] == [{"path": "/tmp/a.png", "label": "[image1]"}] def test_send_task_no_images(self): diff --git a/tests/test_daemon_manager_e2e.py b/tests/test_daemon_manager_e2e.py index 3f22528f..1064cefa 100644 --- a/tests/test_daemon_manager_e2e.py +++ b/tests/test_daemon_manager_e2e.py @@ -80,7 +80,7 @@ async def _test(): # streaming task → delta frames → done await conn.send_task(session_id="e2e-session", - cwd=str(tmp), prompt="你好", stream=True) + cwd=str(tmp), prompt="你好") frames = [] while True: frame = await conn.recv(timeout=5) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 63e1530e..2b716b71 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -17,7 +17,6 @@ def test_task_request_to_dict(): session_id="s_260718_1200_a3f9", cwd="/home/user/project", prompt="hello", - stream=True, ) d = req.to_dict() assert d["type"] == "task" @@ -25,7 +24,6 @@ def test_task_request_to_dict(): assert d["session_id"] == "s_260718_1200_a3f9" assert d["cwd"] == "/home/user/project" assert d["prompt"] == "hello" - assert d["stream"] is True assert "timestamp" in d @@ -35,7 +33,7 @@ def test_task_request_defaults(): assert d["type"] == "task" assert d["id"] # auto-generated UUID assert d["session_id"] == "" - assert d["stream"] is False + assert "stream" not in d # non-stream path removed (rant 21:20:38) def test_task_response_from_dict():