From 0024d3a8de4d2de2a4f2f810725ca565349cace7 Mon Sep 17 00:00:00 2001 From: Ziyang Guo <121015044+RerankerGuo@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:53:43 +0800 Subject: [PATCH] fix(stat): put mtime in the answer where the job description promises it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ``stat`` job is advertised to agents as "Stat path (size, mtime, exists, is_dir, is_file)", but the answer only ever carried the path, type and size — ``mtime`` lived solely in metadata, and MCP returns only ``response.answer``. So an agent asking how fresh a file is got no answer, even though the tool description told it the field was there. Render mtime in both branches of the answer: stat: topics/n.md (file, 20 bytes, mtime 2026-09-21T15:50:52.880316) stat: topics (dir, mtime 2026-09-21T15:50:52.880316) The value is hoisted into a local so the answer and ``metadata["mtime"]`` are guaranteed to come from the same ``stat()`` call rather than two reads. The module docstring claimed the agent receives "size, mtime, mime type, and ... frontmatter"; it now states that size and mtime are in the answer while mime and frontmatter stay in metadata, matching reality. Metadata is unchanged, so programmatic consumers see no difference. This is the same answer-completeness class as #564, found while auditing which advertised job fields actually reach the LLM. --- reme/steps/file_io/stat.py | 13 +++++++------ tests/unit/test_crud_steps.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/reme/steps/file_io/stat.py b/reme/steps/file_io/stat.py index 4ef31dd4..ca3c3f51 100644 --- a/reme/steps/file_io/stat.py +++ b/reme/steps/file_io/stat.py @@ -1,9 +1,9 @@ """``file_stat`` — peek at file metadata under the workspace without copying it. Cheap inspection alternative to ``file_download``: the agent gets -size, mtime, mime type, and (for markdown files) the parsed -frontmatter — enough to decide whether to download / parse / skip -without paying the copy cost. +size and mtime in the answer, with mime type and (for markdown files) +the parsed frontmatter alongside in metadata — enough to decide whether +to download / parse / skip without paying the copy cost. Returns a uniform envelope: @@ -68,12 +68,13 @@ class StatStep(BaseStep): return st = target.stat() + mtime = datetime.fromtimestamp(st.st_mtime).isoformat() payload: dict = { "path": path, "absolute_path": str(target), "exists": True, "type": "dir" if target.is_dir() else "file", - "mtime": datetime.fromtimestamp(st.st_mtime).isoformat(), + "mtime": mtime, "ctime": datetime.fromtimestamp(st.st_ctime).isoformat(), } if target != original_target: @@ -90,9 +91,9 @@ class StatStep(BaseStep): except Exception: meta = {} payload["frontmatter"] = meta - answer = f"stat: {path} (file, {st.st_size} bytes)" + answer = f"stat: {path} (file, {st.st_size} bytes, mtime {mtime})" else: - answer = f"stat: {path} (dir)" + answer = f"stat: {path} (dir, mtime {mtime})" self.context.response.success = True self.context.response.answer = answer diff --git a/tests/unit/test_crud_steps.py b/tests/unit/test_crud_steps.py index 498424bb..5d3a3184 100644 --- a/tests/unit/test_crud_steps.py +++ b/tests/unit/test_crud_steps.py @@ -150,6 +150,35 @@ def test_stat_directory_fallback(): asyncio.run(run()) +def test_stat_answer_reports_size_and_mtime(): + """The answer carries size and mtime, not just the path and type. + + The ``stat`` job description advertises "size, mtime, exists, is_dir, + is_file", but only ``answer`` reaches an MCP caller — mtime used to live + solely in metadata, so an agent could not tell how fresh a file was. + """ + + async def run(): + with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): + store = await _make_store({"topics/n.md": "---\nname: T\n---\nbody"}) + step = crud_stat.StatStep(file_store=store) + await step(path="topics/n.md") + answer = step.context.response.answer + mtime = _metadata(step)["mtime"] + assert answer.startswith("stat: topics/n.md (file, ") + assert "bytes, mtime " in answer + assert answer.endswith(f"{mtime})") + + (Path(tmp) / "topics").mkdir(parents=True, exist_ok=True) + step = crud_stat.StatStep(file_store=store) + await step(path="topics") + answer = step.context.response.answer + assert answer == f"stat: topics (dir, mtime {_metadata(step)['mtime']})" + await store.close() + + asyncio.run(run()) + + def test_stat_directory_wins_over_same_name_index(): """stat prefers an existing directory over its same-name ``.md`` index.