diff --git a/integrations/qoder/README.md b/integrations/qoder/README.md index 28305a1d..59bad738 100644 --- a/integrations/qoder/README.md +++ b/integrations/qoder/README.md @@ -9,53 +9,116 @@ directory packages ReMe automatic recall, completed-turn recording, MCP memory t ## Requirements - Qoder CLI 1.1.41 or newer with plugin support. -- Python 3.11 or newer as `python3` in the Qoder environment, with FastMCP 3.1 or newer. -- A running ReMe service exposing the MCP endpoint and the HTTP jobs `search` and `auto_memory`. +- Python 3.11 or newer available as `python3` in the environment Qoder uses for plugin hooks and + MCP servers. +- FastMCP 3.1 or newer in that same Python environment. The `reme-ai[core]` install includes it. +- A separately running ReMe service exposing the MCP endpoint and the HTTP jobs `search` and + `auto_memory`. -The default split development setup uses: +## 1. Prepare ReMe -- MCP: `http://127.0.0.1:2333/mcp` -- HTTP Job API: `http://127.0.0.1:2334` +Install ReMe and its Python dependencies from this repository. Use the Python interpreter that +Qoder can access as `python3`. -## Install +macOS or Linux: -From this checkout: +```bash +cd /path/to/ReMe +python3 -m pip install -e ".[core]" +``` + +Windows PowerShell: ```powershell -qodercli plugins marketplace add E:\path\to\ReMe\integrations\qoder +cd E:\path\to\ReMe +python -m pip install -e ".[core]" +``` + +Automatic memory generation requires model credentials. Configure `LLM_API_KEY` and +`LLM_BASE_URL` as described in the [Quick Start](../../README.md#environment-variables). + +Start ReMe in a separate terminal and leave it running: + +```bash +reme start +``` + +The default HTTP service serves both the Job API and streamable HTTP MCP on +`http://127.0.0.1:2333`. The default Qoder configuration therefore uses: + +- MCP: `http://127.0.0.1:2333/mcp` +- HTTP Job API: derived from `mcp_url` as `http://127.0.0.1:2333` + +If the deployment intentionally uses separate ports, set `api_url` explicitly. + +## 2. Install the plugin + +The plugin is distributed from the repository-local marketplace. Keep the checkout available when +installing or updating it. + +macOS or Linux: + +```bash +cd /path/to/ReMe +reme_root="$(pwd)" +qodercli plugins validate "$reme_root/integrations/qoder/reme" --strict +qodercli plugins marketplace add "$reme_root/integrations/qoder" qodercli plugins install reme@reme-marketplace --scope user ``` -Direct local installation is also supported: +Windows PowerShell: ```powershell -qodercli plugins install E:\path\to\ReMe\integrations\qoder\reme --scope user +cd E:\path\to\ReMe +$remeRoot = (Resolve-Path .).Path +qodercli plugins validate "$remeRoot\integrations\qoder\reme" --strict +qodercli plugins marketplace add "$remeRoot\integrations\qoder" +qodercli plugins install reme@reme-marketplace --scope user ``` -Validate the package before installing: +Direct installation from the plugin directory is also supported: -```powershell -qodercli plugins validate E:\path\to\ReMe\integrations\qoder\reme --strict +```bash +qodercli plugins install /path/to/ReMe/integrations/qoder/reme --scope user ``` -## Configure +## 3. Configure -The plugin reads `%USERPROFILE%\.qoder\reme\config.json`, or -`${QODER_CONFIG_DIR}/reme/config.json` when `QODER_CONFIG_DIR` is set: +The plugin reads `~/.qoder/reme/config.json`, or `${QODER_CONFIG_DIR}/reme/config.json` when +`QODER_CONFIG_DIR` is set. + +macOS or Linux: + +```bash +mkdir -p "$HOME/.qoder/reme" +cp integrations/qoder/reme/config.example.json "$HOME/.qoder/reme/config.json" +``` + +Windows PowerShell: ```powershell New-Item -ItemType Directory -Force "$HOME\.qoder\reme" -Copy-Item integrations\qoder\reme\config.example.json "$HOME\.qoder\reme\config.json" +Copy-Item .\integrations\qoder\reme\config.example.json "$HOME\.qoder\reme\config.json" ``` -`mcp_url` controls the MCP tool connection. `api_url` controls the HTTP job API used by lifecycle -hooks. The two must not be conflated when the ReMe deployment uses separate ports. +The relevant defaults are: -Set `memory_interval` to `1` for immediate per-turn writes. The remaining keys and limits match the -Claude Code integration: +```json +{ + "mcp_url": "http://127.0.0.1:2333/mcp", + "api_url": "", + "memory_interval": 1 +} +``` + +An empty `api_url` is derived from `mcp_url` when that URL ends in `/mcp`. Set `api_url` only for a +split-port or reverse-proxy deployment. The shipped example sets `memory_interval` to `1` for +immediate per-turn writes; the code default is `5`. | Option | Default | Meaning | | --- | --- | --- | +| `mcp_url` | `http://127.0.0.1:2333/mcp` | ReMe MCP endpoint | +| `api_url` | `""` | Optional HTTP Job API override; derived from `mcp_url` when empty | | `auto_recall` | `true` | Search before a user prompt | | `auto_memory` | `true` | Capture and submit completed turns | | `recall_limit` | `5` | Search result limit | @@ -67,6 +130,9 @@ Claude Code integration: | `context_max_chars` | `8000` | Maximum recalled context | | `timezone` | `Asia/Shanghai` | Daily batch timezone | +Restart Qoder after installation, or run `/plugins reload`. Reconnect MCP after changing +`mcp_url`; hook configuration changes apply on the next invocation. + ## Lifecycle `UserPromptSubmit` performs bounded recall and injects results as untrusted historical evidence. @@ -77,7 +143,8 @@ endpoint and protected by per-session locks. Stable message IDs make repeated St Qoder transcript records use Claude-style `user` and `assistant` JSONL rows. Tool calls, tool results, reasoning blocks, metadata messages, subagents, and injected `` are excluded. -The hook accepts complete final JSONL rows while Qoder finishes materializing the transcript. +The hook waits for the current final JSONL row instead of treating an older identical reply as the +new turn. ## Migration from manual hooks @@ -88,16 +155,25 @@ memory-gate hooks should remain untouched. ## Verify -```powershell -qodercli plugins list -qodercli mcp list +First verify that Qoder discovered the plugin resources: + +```bash +qodercli plugins list --json ``` -Open a new Qoder conversation through CodexHost or directly. Success requires: +The `reme` entry should be enabled and report one skill, one `reme` MCP server, and five hook +registrations. `qodercli mcp list` does not report servers bundled by plugins, so it is not a valid +connection test. -1. a `memory_saved` row in `%USERPROFILE%\.qoder\reme\hooks.log`; -2. the corresponding queue `.done` receipt; -3. one new `session/dialog/qoder-*.jsonl` file with a user and final-assistant row. +Open a fresh Qoder session and ask it to call the ReMe `health_check` and `version` MCP tools. Then +finish one memory-worthy turn with `memory_interval` set to `1` and verify all three signals: + +1. a content-free `memory_saved` row in `~/.qoder/reme/hooks.log`; +2. the corresponding queue `.done` receipt under `~/.qoder/reme/queue/`; +3. a new `session/dialog/qoder-*.jsonl` file containing the user and final-assistant rows. + +Open a second Qoder session and ask for the fact from the first session. Confirm that recall returns +it before considering the installation complete. The service-owned `auto_dream` consolidation job remains enabled according to the ReMe service configuration; the plugin does not start another scheduler. diff --git a/integrations/qoder/README_ZH.md b/integrations/qoder/README_ZH.md index 327415c4..65f2c8cb 100644 --- a/integrations/qoder/README_ZH.md +++ b/integrations/qoder/README_ZH.md @@ -3,47 +3,139 @@ [English](./README.md) Qoder 支持把 Skills、MCP、Hooks 和配置打包成插件。本目录提供 ReMe 的 Qoder 整包集成: -自动召回、每轮完成的对话采集、MCP 记忆工具,以及 `reme-memory` Skill。 +自动召回、完成轮次记录、MCP 记忆工具,以及 `reme-memory` Skill。 -## 安装 +## 环境要求 + +- Qoder CLI 1.1.41 或更新版本,并支持插件。 +- Qoder 执行 Hook 和 MCP Server 的环境中可以调用 `python3`,且 Python 版本为 3.11 或更新版本。 +- 同一个 Python 环境中安装 FastMCP 3.1 或更新版本;`reme-ai[core]` 会一并安装。 +- 单独运行一个 ReMe 服务,提供 MCP endpoint 以及 `search`、`auto_memory` HTTP Job。 + +## 1. 准备 ReMe + +在本仓库中安装 ReMe 和 Python 依赖。请使用 Qoder 能通过 `python3` 调用的同一个解释器。 + +macOS 或 Linux: + +```bash +cd /path/to/ReMe +python3 -m pip install -e ".[core]" +``` + +Windows PowerShell: ```powershell -qodercli plugins marketplace add E:\path\to\ReMe\integrations\qoder +cd E:\path\to\ReMe +python -m pip install -e ".[core]" +``` + +自动生成记忆需要模型凭据。请按照 [快速开始](../../README_ZH.md#环境变量配置) 配置 +`LLM_API_KEY` 和 `LLM_BASE_URL`。 + +在另一个终端启动 ReMe,并保持服务运行: + +```bash +reme start +``` + +默认 HTTP 服务在同一端口提供 Job API 和 streamable HTTP MCP: + +- MCP:`http://127.0.0.1:2333/mcp` +- HTTP Job API:从 `mcp_url` 推导为 `http://127.0.0.1:2333` + +如果确实使用拆分端口部署,才需要显式设置 `api_url`。 + +## 2. 安装插件 + +插件从仓库内的本地 marketplace 发布。安装或升级时需要使用这个 ReMe checkout。 + +macOS 或 Linux: + +```bash +cd /path/to/ReMe +reme_root="$(pwd)" +qodercli plugins validate "$reme_root/integrations/qoder/reme" --strict +qodercli plugins marketplace add "$reme_root/integrations/qoder" +qodercli plugins install reme@reme-marketplace --scope user +``` + +Windows PowerShell: + +```powershell +cd E:\path\to\ReMe +$remeRoot = (Resolve-Path .).Path +qodercli plugins validate "$remeRoot\integrations\qoder\reme" --strict +qodercli plugins marketplace add "$remeRoot\integrations\qoder" qodercli plugins install reme@reme-marketplace --scope user ``` 也可以直接从插件目录安装: -```powershell -qodercli plugins install E:\path\to\ReMe\integrations\qoder\reme --scope user +```bash +qodercli plugins install /path/to/ReMe/integrations/qoder/reme --scope user ``` -安装前可运行: +## 3. 配置 -```powershell -qodercli plugins validate E:\path\to\ReMe\integrations\qoder\reme --strict +用户配置位于 `~/.qoder/reme/config.json`,也可以通过 `QODER_CONFIG_DIR` 改配置根。 + +macOS 或 Linux: + +```bash +mkdir -p "$HOME/.qoder/reme" +cp integrations/qoder/reme/config.example.json "$HOME/.qoder/reme/config.json" ``` -## 配置 +Windows PowerShell: -用户配置位于 `%USERPROFILE%\.qoder\reme\config.json`,也可以通过 `QODER_CONFIG_DIR` 改配置根。 -默认拆分服务为: +```powershell +New-Item -ItemType Directory -Force "$HOME\.qoder\reme" +Copy-Item .\integrations\qoder\reme\config.example.json "$HOME\.qoder\reme\config.json" +``` -- MCP:`http://127.0.0.1:2333/mcp` -- Hook HTTP Job API:`http://127.0.0.1:2334` +关键默认配置如下: -把 `memory_interval` 设为 `1` 可在每轮结束后立即写入。`auto_recall` 控制提醒前召回, -`auto_memory` 控制完成轮次采集;超时与上下文上限和 Claude Code 集成一致。 +```json +{ + "mcp_url": "http://127.0.0.1:2333/mcp", + "api_url": "", + "memory_interval": 1 +} +``` + +当 `mcp_url` 以 `/mcp` 结尾时,空的 `api_url` 会自动推导为同一服务地址。只有拆分端口或 +反向代理部署才需要设置 `api_url`。仓库中的示例把 `memory_interval` 设为 `1`,用于每轮立即写入; +代码默认值为 `5`。 + +| 配置项 | 默认值 | 含义 | +| --- | --- | --- | +| `mcp_url` | `http://127.0.0.1:2333/mcp` | ReMe MCP 地址 | +| `api_url` | `""` | 可选的 HTTP Job API 地址;为空时从 `mcp_url` 推导 | +| `auto_recall` | `true` | 用户提问前自动召回 | +| `auto_memory` | `true` | 采集并提交完成轮次 | +| `recall_limit` | `5` | 召回结果上限 | +| `recall_min_score` | `0` | 最低召回分数 | +| `recall_timeout` | `5` | 前台召回超时,最多 10 秒 | +| `request_timeout` | `600` | 记忆写入超时,最多 600 秒 | +| `memory_interval` | `5` | 多少轮完成后批量写入 | +| `shutdown_timeout` | `2` | 会话结束时尽力排空,最多 2 秒 | +| `context_max_chars` | `8000` | 注入的召回上下文上限 | +| `timezone` | `Asia/Shanghai` | 按天分批使用的时区 | + +安装后重启 Qoder,或执行 `/plugins reload`。修改 `mcp_url` 后需要重新连接 MCP;Hook 配置会在 +下次调用时生效。 ## 工作方式 -`UserPromptSubmit` 同步召回并注入不受信任的历史证据;`Stop` 先同步捕获本轮 user/最终 -assistant 文本到本地队列,再由并行异步 Hook 调用 `POST {api_url}/auto_memory`。 -`SessionStart` 重试残留队列,`SessionEnd` 做短时排空。 +`UserPromptSubmit` 执行有上限的召回,并作为不受信任的历史证据注入。`Stop` 先同步捕获本轮 +user/最终 assistant 文本到本地队列,再由并行异步 Hook 调用 `POST {api_url}/auto_memory`。 +`SessionStart` 重试残留队列,`SessionEnd` 做短时排空。队列按服务地址隔离,并带进程锁和 +`.done` 回执;稳定的消息 ID 保证重复 Stop 不会重复写入。 -队列按服务地址隔离,带进程锁和 `.done` 回执;工具输出、推理、子代理、注入的 -`` 都会被排除。Qoder 使用 Claude 风格 JSONL transcript,Hook 支持等到最后一行 -完整落盘后再捕获。 +Qoder 使用 Claude 风格 JSONL transcript。工具输出、推理、元数据消息、子代理和注入的 +`` 都会被排除。当连续两轮最终回复相同时,插件会等待当前轮的新 transcript +行,而不会把上一轮的相同回复当成新轮次。 ## 从手工 Hook 迁移 @@ -53,11 +145,22 @@ Qoder、Orca、记忆库门禁等其他 Hook 不要删除。 ## 验收 -```powershell -qodercli plugins list -qodercli mcp list +首先确认 Qoder 发现了插件资源: + +```bash +qodercli plugins list --json ``` -新开一个 Qoder 会话后,应同时看到 `%USERPROFILE%\.qoder\reme\hooks.log` 中的 -`memory_saved`、队列 `.done` 回执,以及新的 `session/dialog/qoder-*.jsonl` user/assistant 两行。 +`reme` 条目应当已启用,并显示 1 个 Skill、1 个 `reme` MCP Server 和 5 个 Hook 注册项。 +`qodercli mcp list` 不会列出插件内置的 MCP Server,因此不能作为连接是否正常的判断依据。 + +新开一个 Qoder 会话,让 Agent 实际调用 ReMe 的 `health_check` 和 `version` MCP 工具。然后把 +`memory_interval` 设为 `1`,完成一轮值得记忆的对话,并同时确认: + +1. `~/.qoder/reme/hooks.log` 中出现不包含正文的 `memory_saved`; +2. `~/.qoder/reme/queue/` 下出现对应的 `.done` 回执; +3. 新的 `session/dialog/qoder-*.jsonl` 中包含该轮 user 和最终 assistant 两行。 + +最后新开第二个 Qoder 会话,询问第一轮中的事实,确认自动召回成功后再认为安装完成。 + `auto_dream` 仍由 ReMe 服务自身调度,插件不会额外启动调度器。 diff --git a/integrations/qoder/reme/config.example.json b/integrations/qoder/reme/config.example.json index 69618c83..bf5764da 100644 --- a/integrations/qoder/reme/config.example.json +++ b/integrations/qoder/reme/config.example.json @@ -1,6 +1,6 @@ { "mcp_url": "http://127.0.0.1:2333/mcp", - "api_url": "http://127.0.0.1:2334", + "api_url": "", "auto_recall": true, "auto_memory": true, "recall_limit": 5, diff --git a/integrations/qoder/reme/hooks/auto_memory.py b/integrations/qoder/reme/hooks/auto_memory.py index b2ce5fbc..e8014610 100644 --- a/integrations/qoder/reme/hooks/auto_memory.py +++ b/integrations/qoder/reme/hooks/auto_memory.py @@ -265,9 +265,15 @@ def capture(config: dict, payload: dict) -> Path | None: if root is None: return None deadline = time.monotonic() + CAPTURE_SETTLE_SECONDS + messages = [] while True: messages = completed_turn(payload, root.name) - if len(messages) == 2 or time.monotonic() >= deadline: + if len(messages) == 2: + # An identical reply may still resolve to the previous turn while Qoder appends this turn. + key = digest(messages[-1]["id"]) + if not (root / f"{key}.done").exists() and not (root / f"{key}.json").exists(): + break + if time.monotonic() >= deadline: break time.sleep(0.1) if len(messages) != 2: diff --git a/tests/unit/test_coding_agent_plugins.py b/tests/unit/test_coding_agent_plugins.py index 8e99562c..87446d9f 100644 --- a/tests/unit/test_coding_agent_plugins.py +++ b/tests/unit/test_coding_agent_plugins.py @@ -139,6 +139,44 @@ def test_claude_synchronous_capture_preserves_identical_replies_before_delayed_d assert len(list(adapter.queue_root(adapter.load_config()).rglob("*.done"))) == 2 +def test_qoder_capture_waits_for_delayed_identical_reply(adapter, tmp_path, monkeypatch): + require_host(adapter, "qoder") + monkeypatch.setattr(adapter, "CAPTURE_SETTLE_SECONDS", 0.5) + config = adapter.load_config() + path = tmp_path / "transcript.jsonl" + + first = transcript(adapter, path, [("first", "Saved.")]) + root = adapter.capture(config, first) + first_pending = next(root.glob("*.json")) + adapter.write_json(first_pending.with_suffix(".done"), {}) + first_pending.unlink() + + second = transcript(adapter, path, [("first", "Saved."), ("second", "Saved.")]) + lines = path.read_text(encoding="utf-8").splitlines(keepends=True) + delayed_reply = lines[-1] + path.write_text("".join(lines[:-1]), encoding="utf-8") + + def append_delayed_reply(): + with path.open("a", encoding="utf-8") as handle: + handle.write(delayed_reply) + + captured = [] + worker = threading.Thread(target=lambda: captured.append(adapter.capture(config, second))) + timer = threading.Timer(0.05, append_delayed_reply) + timer.start() + worker.start() + worker.join(3) + timer.join(3) + + assert not worker.is_alive() + assert not timer.is_alive() + assert captured == [root] + pending = list(root.glob("*.json")) + assert len(pending) == 1 + messages = json.loads(pending[0].read_text(encoding="utf-8"))["messages"] + assert [message["content"][0]["text"] for message in messages] == ["second", "Saved."] + + def test_claude_delivery_waits_for_parallel_synchronous_capture(adapter, tmp_path, monkeypatch): require_structured_transcript_host(adapter) write_config(adapter, {"memory_interval": 1})