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 number Diff line number Diff line change
Expand Up @@ -118,7 +118,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` (769) — import check: `uv run python -c "from emrg.client.app import run_client"`
Python: `uv run pytest tests/ -v` (773) — import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (231: 44 daemon_client + 19 conn-manager + 22 app-commands + 109 renderer smoke + 15 i18n + 7 integration + 3 commands + 5 build-config + 7 gui-state) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js`
CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + 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
13 changes: 13 additions & 0 deletions emrg/server/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -1747,10 +1747,23 @@ async def _process_message(
"preview": preview,
"timestamp": r.get("timestamp", ""),
})
# Optional pagination (rant 2026-08-13T14:15:12): limit/offset
# count from the NEWEST message backwards (offset=0 = latest).
# Absent limit = full list (backward compatible, used by /rewind).
limit = msg.get("limit")
offset = msg.get("offset", 0)
has_more = False
if limit is not None:
total = len(user_messages)
end = max(0, total - offset)
start = max(0, end - limit)
has_more = start > 0
user_messages = user_messages[start:end]
await self._send(ws, {
"type": "history_list",
"session_id": session_id,
"messages": user_messages,
"has_more": has_more,
})

elif msg_type == "rewind_session":
Expand Down
124 changes: 124 additions & 0 deletions tests/test_ws_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -1127,3 +1127,127 @@ async def _test():
finally:
await cleanup()
asyncio.run(_test())


class TestWSHistoryPagination:
"""list_history pagination (rant 2026-08-13T14:15:12).

limit/offset count from the NEWEST message backwards (offset=0 = latest);
absent limit keeps the full list (backward compatible, used by /rewind);
response includes has_more.
"""

@staticmethod
async def _cmd(ws, payload):
await ws.send(json.dumps(payload))
return json.loads(await asyncio.wait_for(ws.recv(), timeout=5))

@staticmethod
def _seed_history(cwd: Path, sid: str, n: int) -> None:
from emrg.session import Session
sess = Session(sid, cwd)
records = []
for i in range(n):
records.append({
"type": "message",
"role": "user",
"content": f"msg-{i:02d}",
"timestamp": f"2026-08-13T{i:02d}:00:00",
})
sess._write_history(records)

def test_full_list_without_limit(self):
"""Absent limit → full list (backward compatible)."""
async def _test():
with tempfile.TemporaryDirectory() as tmp:
cwd = Path(tmp)
sid = "s_hist"
self._seed_history(cwd, sid, 3)
_, _, cleanup = await _boot_server(cwd)
try:
ws = await connect_to_server()
try:
resp = await self._cmd(ws, {
"type": "list_history", "session_id": sid, "cwd": str(cwd),
})
assert resp["type"] == "history_list"
msgs = resp["messages"]
assert [m["content"] for m in msgs] == ["msg-00", "msg-01", "msg-02"]
# has_more present (False for full list), no error
assert resp.get("has_more") is False
finally:
await ws.close()
finally:
await cleanup()
asyncio.run(_test())

def test_limit_returns_newest(self):
"""limit=2 → newest 2 messages in time order."""
async def _test():
with tempfile.TemporaryDirectory() as tmp:
cwd = Path(tmp)
sid = "s_hist"
self._seed_history(cwd, sid, 5)
_, _, cleanup = await _boot_server(cwd)
try:
ws = await connect_to_server()
try:
resp = await self._cmd(ws, {
"type": "list_history", "session_id": sid,
"cwd": str(cwd), "limit": 2,
})
msgs = resp["messages"]
assert [m["content"] for m in msgs] == ["msg-03", "msg-04"]
assert resp.get("has_more") is True
finally:
await ws.close()
finally:
await cleanup()
asyncio.run(_test())

def test_offset_pages_older(self):
"""limit=2 offset=2 → the 2 messages before the newest 2."""
async def _test():
with tempfile.TemporaryDirectory() as tmp:
cwd = Path(tmp)
sid = "s_hist"
self._seed_history(cwd, sid, 5)
_, _, cleanup = await _boot_server(cwd)
try:
ws = await connect_to_server()
try:
resp = await self._cmd(ws, {
"type": "list_history", "session_id": sid,
"cwd": str(cwd), "limit": 2, "offset": 2,
})
msgs = resp["messages"]
assert [m["content"] for m in msgs] == ["msg-01", "msg-02"]
assert resp.get("has_more") is True
finally:
await ws.close()
finally:
await cleanup()
asyncio.run(_test())

def test_offset_beyond_all_has_more_false(self):
"""offset beyond available messages → empty + has_more False."""
async def _test():
with tempfile.TemporaryDirectory() as tmp:
cwd = Path(tmp)
sid = "s_hist"
self._seed_history(cwd, sid, 3)
_, _, cleanup = await _boot_server(cwd)
try:
ws = await connect_to_server()
try:
resp = await self._cmd(ws, {
"type": "list_history", "session_id": sid,
"cwd": str(cwd), "limit": 10, "offset": 5,
})
assert resp["messages"] == []
assert resp.get("has_more") is False
finally:
await ws.close()
finally:
await cleanup()
asyncio.run(_test())
Loading