Refactor core implementation and simplify related code

This commit is contained in:
酱牛肉 2026-08-06 16:30:32 +08:00
parent 61ee89161c
commit 78217b2f52
12 changed files with 1660 additions and 10 deletions

3
.gitignore vendored
View file

@ -72,7 +72,8 @@ benchmark/*/results/
# integration tests outputs
tests/integration/logs/
sandbox/tmp_test_trajectories/
daily/
#meta-reme
meta-reme/bundles/
meta-reme/bundles/

View file

@ -5,8 +5,6 @@ version: 1
# copied.
common:
include:
- LICENSE
- README.md
- pyproject.toml
- reme/__init__.py
- reme/application.py
@ -24,6 +22,32 @@ common:
- reme/steps/file_io
- reme/steps/index
- reme/steps/transfer
exclude:
- reme/components/client/http_client.py
- reme/components/client/mcp_client.py
- reme/components/file_graph/neo4j_file_graph.py
- reme/components/file_graph/nx_file_graph.py
- reme/components/file_store/faiss_local_file_store.py
- reme/components/file_store/zvec_local_file_store.py
- reme/components/service/http_service.py
- reme/components/service/mcp_service.py
- reme/components/tokenizer/jieba_tokenizer.py
- reme/schema/auto_fin.py
- reme/schema/daily_paper.py
- reme/steps/common/add.py
- reme/steps/common/demo.py
- reme/steps/common/llm_demo.py
- reme/steps/common/stream_demo.py
- reme/steps/common/stream_llm_demo.py
- reme/steps/common/version.py
- reme/steps/file_io/read_image.py
- reme/utils/arxiv.py
- reme/utils/huggingface_papers.py
config_transform:
service_backend: cli
remove_jobs:
- read_image
- version
step_packages:
- common
- evolve
@ -38,8 +62,50 @@ common:
- auto_memory_step
- search_step
- search_v2_step
forbidden_backends:
- as_embedding:dashscope_multimodal
- client:http
- client:mcp
- file_graph:neo4j
- file_graph:nx
- file_store:faiss
- file_store:zvec
- service:http
- service:mcp
- step:add_step
- step:demo_echo_step1
- step:demo_echo_step2
- step:llm_demo_step
- step:read_image_step
- step:stream_demo_step1
- step:stream_demo_step2
- step:stream_llm_demo_step
- step:version_step
- tokenizer:jieba
forbidden_paths:
- LICENSE
- README.md
- reme/steps/cookbook
- reme/components/client/http_client.py
- reme/components/client/mcp_client.py
- reme/components/file_graph/neo4j_file_graph.py
- reme/components/file_graph/nx_file_graph.py
- reme/components/file_store/faiss_local_file_store.py
- reme/components/file_store/zvec_local_file_store.py
- reme/components/service/http_service.py
- reme/components/service/mcp_service.py
- reme/components/tokenizer/jieba_tokenizer.py
- reme/schema/auto_fin.py
- reme/schema/daily_paper.py
- reme/steps/common/add.py
- reme/steps/common/demo.py
- reme/steps/common/llm_demo.py
- reme/steps/common/stream_demo.py
- reme/steps/common/stream_llm_demo.py
- reme/steps/common/version.py
- reme/steps/file_io/read_image.py
- reme/utils/arxiv.py
- reme/utils/huggingface_papers.py
targets:
default:
@ -61,6 +127,8 @@ targets:
- reme/steps/benchmark/base
- reme/steps/benchmark/lme
benchmark_package: lme
exclude:
- reme/steps/evolve/auto_memory_cc.py
validation:
imports:
- reme.steps.benchmark.lme
@ -68,7 +136,10 @@ targets:
- lme_auto_memory_step
- lme_agentic_answer_step
- lme_answer_judge_step
forbidden_backends:
- step:auto_memory_cc_step
forbidden_paths:
- reme/steps/evolve/auto_memory_cc.py
- reme/steps/benchmark/beam
beam:
@ -77,6 +148,8 @@ targets:
- reme/steps/benchmark/base
- reme/steps/benchmark/beam
benchmark_package: beam
exclude:
- reme/steps/evolve/auto_memory_cc.py
validation:
imports:
- reme.steps.benchmark.beam
@ -84,5 +157,8 @@ targets:
- beam_auto_memory_step
- beam_agentic_answer_step
- beam_rubric_judge_step
forbidden_backends:
- step:auto_memory_cc_step
forbidden_paths:
- reme/steps/evolve/auto_memory_cc.py
- reme/steps/benchmark/lme

View file

@ -10,6 +10,7 @@ from __future__ import annotations
import argparse
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
@ -80,13 +81,25 @@ def _resolve_source_path(source_repo: Path, relative: str) -> Path:
return resolved
def _iter_files(source_repo: Path, entries: Iterable[str]) -> list[tuple[Path, Path]]:
def _is_within(relative: Path, parent: Path) -> bool:
"""Return whether ``relative`` is ``parent`` or one of its descendants."""
return relative == parent or parent in relative.parents
def _iter_files(
source_repo: Path,
entries: Iterable[str],
exclusions: Iterable[str] = (),
) -> list[tuple[Path, Path]]:
excluded = [Path(value) for value in exclusions]
files: dict[Path, Path] = {}
for entry in entries:
source = _resolve_source_path(source_repo, entry)
candidates = [source] if source.is_file() else sorted(path for path in source.rglob("*") if path.is_file())
for candidate in candidates:
relative = candidate.relative_to(source_repo)
if any(_is_within(relative, path) for path in excluded):
continue
if any(part in _IGNORED_PARTS for part in relative.parts) or candidate.suffix in _IGNORED_SUFFIXES:
continue
if candidate.is_symlink():
@ -95,6 +108,145 @@ def _iter_files(source_repo: Path, entries: Iterable[str]) -> list[tuple[Path, P
return [(relative, files[relative]) for relative in sorted(files)]
def _write_pruned_package_initializers(bundle_root: Path) -> None:
"""Write registration imports for packages whose optional modules were pruned."""
initializers = {
"reme/components/client/__init__.py": '''"""Client abstractions included in this generated bundle."""
from .base_client import BaseClient
__all__ = ["BaseClient"]
''',
"reme/components/file_graph/__init__.py": '''"""File graph implementations included in this generated bundle."""
from .base_file_graph import BaseFileGraph
from .local_file_graph import LocalFileGraph
__all__ = ["BaseFileGraph", "LocalFileGraph"]
''',
"reme/components/file_store/__init__.py": '''"""File store implementations included in this generated bundle."""
from .base_file_store import BaseFileStore
from .local_file_store import LocalFileStore
__all__ = ["BaseFileStore", "LocalFileStore"]
''',
"reme/components/job/__init__.py": '''"""Job implementations included in this generated bundle."""
from .background_job import BackgroundJob
from .base_job import BaseJob
from .cron_job import CronJob
from .stream_job import StreamJob
__all__ = ["BackgroundJob", "BaseJob", "CronJob", "StreamJob"]
''',
"reme/components/service/__init__.py": '''"""Service implementations included in this generated bundle."""
from .base_service import BaseService
from .cli_service import CliService
__all__ = ["BaseService", "CliService"]
''',
"reme/components/tokenizer/__init__.py": '''"""Tokenizer implementations included in this generated bundle."""
from .base_tokenizer import BaseTokenizer
from .regex_tokenizer import RegexTokenizer
__all__ = ["BaseTokenizer", "RegexTokenizer"]
''',
}
for relative, content in initializers.items():
(bundle_root / relative).write_text(content, encoding="utf-8")
file_io_init = bundle_root / "reme/steps/file_io/__init__.py"
content = file_io_init.read_text(encoding="utf-8")
content = content.replace("from .read_image import ReadImageStep\n", "")
content = content.replace(' "ReadImageStep",\n', "")
file_io_init.write_text(content, encoding="utf-8")
common_init = bundle_root / "reme/steps/common/__init__.py"
content = common_init.read_text(encoding="utf-8")
for import_line in (
"from .add import AddStep\n",
"from .demo import DemoEchoStep1, DemoEchoStep2\n",
"from .llm_demo import LLMDemoStep\n",
"from .stream_demo import StreamDemoStep1, StreamDemoStep2\n",
"from .stream_llm_demo import StreamLLMDemoStep\n",
"from .version import VersionStep\n",
):
content = content.replace(import_line, "")
for exported_name in (
"AddStep",
"DemoEchoStep1",
"DemoEchoStep2",
"LLMDemoStep",
"StreamDemoStep1",
"StreamDemoStep2",
"StreamLLMDemoStep",
"VersionStep",
):
content = content.replace(f' "{exported_name}",\n', "")
common_init.write_text(content, encoding="utf-8")
auto_memory_cc = bundle_root / "reme/steps/evolve/auto_memory_cc.py"
if not auto_memory_cc.exists():
evolve_init = bundle_root / "reme/steps/evolve/__init__.py"
content = evolve_init.read_text(encoding="utf-8")
content = content.replace("from .auto_memory_cc import AutoMemoryCCStep\n", "")
content = content.replace(' "AutoMemoryCCStep",\n', "")
evolve_init.write_text(content, encoding="utf-8")
schema_init = bundle_root / "reme/schema/__init__.py"
content = schema_init.read_text(encoding="utf-8")
auto_fin_import = re.search(r"from \.auto_fin import \((.*?)\)\n", content, flags=re.DOTALL)
if auto_fin_import is None:
raise BundleBuildError("Could not prune auto-fin schema exports")
auto_fin_names = re.findall(r"\b[A-Za-z_][A-Za-z0-9_]*\b", auto_fin_import.group(1))
content = content[: auto_fin_import.start()] + content[auto_fin_import.end() :]
daily_paper_import = re.search(r"from \.daily_paper import ([^\n]+)\n", content)
if daily_paper_import is None:
raise BundleBuildError("Could not prune daily-paper schema exports")
daily_paper_names = [name.strip() for name in daily_paper_import.group(1).split(",")]
content = content[: daily_paper_import.start()] + content[daily_paper_import.end() :]
for exported_name in auto_fin_names + daily_paper_names:
content = content.replace(f' "{exported_name}",\n', "")
schema_init.write_text(content, encoding="utf-8")
embedding_init = bundle_root / "reme/components/as_embedding/__init__.py"
content = embedding_init.read_text(encoding="utf-8")
content, count = re.subn(
r'\n@R\.register\("dashscope_multimodal"\)\nclass DashScopeMultiModalAsEmbedding\b.*?'
r'(?=\n@R\.register\("gemini"\))',
"",
content,
flags=re.DOTALL,
)
if count != 1:
raise BundleBuildError("Could not prune the multimodal embedding backend")
content = content.replace(' "DashScopeMultiModalAsEmbedding",\n', "")
embedding_init.write_text(content, encoding="utf-8")
def _transform_target_config(bundle_root: Path, entries: list[str], transform: Mapping[str, Any]) -> None:
"""Apply the sealed-bundle service and job restrictions to its one target config."""
config_entries = [Path(value) for value in entries if Path(value).parent == Path("reme/config")]
config_entries = [value for value in config_entries if value.suffix in {".yaml", ".yml"}]
if len(config_entries) != 1:
raise BundleBuildError(f"Expected exactly one target YAML config, found: {config_entries}")
config_path = bundle_root / config_entries[0]
config = _mapping(yaml.safe_load(config_path.read_text(encoding="utf-8")), "target config")
config = dict(config)
service = dict(_mapping(config.get("service", {}), "target config.service"))
service["backend"] = transform.get("service_backend", service.get("backend"))
config["service"] = service
jobs = dict(_mapping(config.get("jobs", {}), "target config.jobs"))
for name in _string_list(transform.get("remove_jobs"), "common.config_transform.remove_jobs"):
jobs.pop(name, None)
config["jobs"] = jobs
config_path.write_text(yaml.safe_dump(config, sort_keys=False, allow_unicode=True), encoding="utf-8")
def _write_step_initializers(bundle_root: Path, step_packages: list[str], benchmark_package: str | None) -> None:
steps_dir = bundle_root / "reme" / "steps"
imports = list(step_packages)
@ -134,7 +286,7 @@ def _merged_validation(common: Mapping[str, Any], target_spec: Mapping[str, Any]
common_validation = _mapping(common.get("validation", {}), "common.validation")
target_validation = _mapping(target_spec.get("validation", {}), "target.validation")
result: dict[str, list[str]] = {}
for field in ("imports", "required_steps", "forbidden_paths"):
for field in ("imports", "required_steps", "forbidden_backends", "forbidden_paths"):
result[field] = _string_list(common_validation.get(field), f"common.validation.{field}") + _string_list(
target_validation.get(field),
f"target.validation.{field}",
@ -143,6 +295,9 @@ def _merged_validation(common: Mapping[str, Any], target_spec: Mapping[str, Any]
def _validate_bundle(bundle_root: Path, validation: Mapping[str, list[str]]) -> None:
cached = [path.relative_to(bundle_root) for path in bundle_root.rglob("*") if path.name == "__pycache__"]
if cached:
raise BundleBuildError(f"Python cache directories were included in bundle: {cached}")
for relative in validation["forbidden_paths"]:
if (bundle_root / relative).exists():
raise BundleBuildError(f"Forbidden path was included in bundle: {relative}")
@ -154,10 +309,14 @@ def _validate_bundle(bundle_root: Path, validation: Mapping[str, list[str]]) ->
"from reme.enumeration import ComponentEnum\n"
f"imports = {validation['imports']!r}\n"
f"required = {validation['required_steps']!r}\n"
f"forbidden = {validation['forbidden_backends']!r}\n"
"for name in imports: importlib.import_module(name)\n"
"registered = R.get_all(ComponentEnum.STEP)\n"
"missing = [name for name in required if name not in registered]\n"
"if missing: raise RuntimeError(f'Missing registered steps: {missing}')\n"
"unexpected = [entry for entry in forbidden "
"if R.get(ComponentEnum(entry.split(':', 1)[0]), entry.split(':', 1)[1]) is not None]\n"
"if unexpected: raise RuntimeError(f'Forbidden registered backends: {unexpected}')\n"
)
environment = os.environ.copy()
environment["PYTHONPATH"] = str(bundle_root)
@ -192,12 +351,17 @@ def build_bundle(
target_spec.get("include"),
f"targets.{target}.include",
)
exclusions = _string_list(common.get("exclude"), "common.exclude") + _string_list(
target_spec.get("exclude"),
f"targets.{target}.exclude",
)
step_packages = _string_list(common.get("step_packages"), "common.step_packages")
benchmark_package = target_spec.get("benchmark_package")
if benchmark_package is not None and not isinstance(benchmark_package, str):
raise BundleBuildError(f"'targets.{target}.benchmark_package' must be a string")
files = _iter_files(source_repo, entries)
files = _iter_files(source_repo, entries, exclusions)
validation = _merged_validation(common, target_spec)
config_transform = _mapping(common.get("config_transform", {}), "common.config_transform")
output_dir.mkdir(parents=True, exist_ok=True)
temporary = Path(tempfile.mkdtemp(prefix=f".{target}-bundle-", dir=output_dir))
@ -209,6 +373,8 @@ def build_bundle(
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination)
_write_step_initializers(staged_root, step_packages, benchmark_package)
_write_pruned_package_initializers(staged_root)
_transform_target_config(staged_root, entries, config_transform)
if validate:
_validate_bundle(staged_root, validation)

View file

@ -51,6 +51,7 @@ meta-reme/
- auto-memory 正式修改白名单;
- debug 阶段允许使用的临时目录;
- sandbox 镜像、超时、并发和重试策略;
- validation 的默认抽样策略、允许指定的 case/query 以及升级为全量评测的条件;
- proposer 模型、搜索预算和 top-K 数量;
- 唯一目标 `mean_query_score`。
@ -185,6 +186,8 @@ prepare → install → ingest → index → answer → export
Agent 可以自主选择完整 case、指定 session、session prefix 或少量 query 进行 debug。部分 session 结果只用于排查语法、配置、记忆写入和运行错误,不进入正式评分。
除 debug 外,validation 还支持显式传入 case ID 列表,并可为每个 case 指定 query ID 子集。该能力用于在全量评测代价过高时先执行 targeted screening,例如优先验证 weakness report 中的失败 query 和少量回归 case。筛选范围必须在运行前固化为 `ValidationSpec`,记录选择理由及其 fingerprint;不得在看到本次得分后追加或替换 query 来美化结果。
现有 sandbox 需要补充失败导出能力:即使在安装、初始化或 ingest 阶段失败,也应 best-effort 导出 `failure.json`、stdout、stderr、Python traceback、action log、已完成阶段和宿主异常。每次运行结束后,宿主下载原始压缩包,并安全解压到:
```text
@ -195,13 +198,18 @@ evaluations/<commit>/<mode>/cases/<case>/attempt-<n>/
## 10. Validation 与结果记录
正式 validation 必须从 frozen commit 创建全新 SourceCandidate。不同 harness、case 之间使用独立 runtime workspace;同一 case 的多个 query 应从相同的 post-ingestion memory snapshot 启动,避免 query 顺序造成状态污染。
Validation 分为 targeted screening 和 full validation。两者都必须从 frozen commit 创建全新 SourceCandidate,并使用同一套 sandbox、失败分类和结果 schema。不同 harness、case 之间使用独立 runtime workspace;同一 case 本次选中的多个 query 应从相同的 post-ingestion memory snapshot 启动,避免 query 顺序造成状态污染。
每个 harness 的 `search.json` 保存:
`ValidationSpec` 明确记录目标 commit、mode、选中的 case/query、选择理由和 dataset/code/config/model/image fingerprint。未指定 case 表示覆盖全部 case,某个 case 未指定 query 子集表示覆盖该 case 的全部 query。系统在执行前根据 dataset manifest 展开并固化选择结果;空集合、未知 ID、重复 ID,以及不属于所选 case 的 query 均应直接报错。
部分 validation 的聚合结果必须同时记录已选数量、数据集总数量、覆盖率和 `is_full=false`。其平均分只描述本次固定子集,不得与其他选择范围的分数直接比较,也不得进入正式 leaderboard 或用于选择 winner。编排器可以依据预先配置的筛选规则决定淘汰候选或将其升级为 full validation;只有覆盖完整 search set 且没有 `infra_error` 的结果才具有正式可比性。Baseline 至少执行一次 full validation,最终 winner 仍需进行第 11 节规定的 clean replay。
每次 targeted screening 的聚合结果写入 `evaluations/<commit>/screening/<validation_id>.json`;完整 search validation 仍发布为 harness 级 `evaluations/<commit>/search.json`。两类结果均保存:
- commit、parents、changed paths 和 scope 结果;
- dataset、代码、配置、模型和镜像 fingerprint;
- 总平均分、query 数、失败数和运行状态;
- validation spec、选择理由、case/query 覆盖数量、覆盖率和是否具有正式可比性;
- 每个 case/session 的处理结果;
- 每个 query 的问题、golden answer、模型回答、score、Judge 结论、异常、token 和耗时;
- artifacts 路径及 hash。
@ -219,10 +227,60 @@ evaluations/<commit>/<mode>/cases/<case>/attempt-<n>/
→ 修改和 sandbox debug
→ 冻结 commit
→ 白名单及接口检查
→ 完整 search validation
→ 可选:指定 case/query 的 targeted screening
→ 通过预设升级条件后执行完整 search validation
→ 更新 registry、summary 和 leaderboard
```
Targeted screening 是可选的成本控制阶段。未通过筛选的 candidate 记录结果和淘汰原因,但不消耗“完整 validation 数”预算;其实际 token、时间和费用仍计入对应总预算。筛选规则、样本范围和升级阈值必须在 domain spec 或 proposal 执行前确定,避免根据中间结果反复挑选有利样本。
达到 proposal 数、完整 validation 数、token 或时间预算后,系统选择 search set 平均分最高的候选。完全同分时保留较早的 incumbent,不引入其他优化指标。
最终 winner 必须从 commit SHA 重新构建,并在完整 search set 上进行 clean replay。只有 replay 成功后才加载 test set,分别评测 baseline 和 winner,生成最终报告。整个过程中 test 结果不反馈给 agent,从而保证 test set 只用于衡量泛化效果。
## 12. 中断与恢复
搜索可能持续数小时乃至数天,进程崩溃、机器重启或用户主动终止都不应破坏已完成的成果。恢复机制必须满足三个要求:持久化状态可校验、恢复操作幂等、同一 workspace 同时只有一个编排器写入。
### 12.1 持久化与一致性边界
`events.jsonl` 是搜索状态的事实来源,`run.json` 是由事件日志生成的加速快照。每条事件包含单调递增的 `seq`、唯一 `event_id`、时间戳和关联的 round、proposal、commit、validation、case、attempt 标识。写入事件后再原子替换 `run.json`;恢复时从快照记录的 `last_event_seq` 继续回放。若快照缺失、落后或校验失败,则完全从事件日志重建,不以快照覆盖较新的事件。
`run.json`、case 结果、聚合结果和 registry 均采用“写临时文件 → flush/fsync → 同目录原子 rename”的方式发布。`events.jsonl` 尾部若存在崩溃造成的不完整 JSON 行,恢复时忽略该行并记录诊断信息;中间行损坏则停止恢复并明确报错,避免静默跳过历史。启动时通过 workspace lock 拒绝第二个并发编排器,lock 中记录 PID、主机和启动时间,并允许在确认所属进程已不存在后接管 stale lock。
### 12.2 Case 原子性与结果复用
每个 case 的完整 pipeline(`prepare → install → ingest → index → answer → export`)是最小恢复单元。中断时不尝试复用半写入的记忆、索引或回答;下一次 attempt 在全新的 runtime workspace 中从头执行。中断或基础设施失败的 attempt 目录及原始 artifacts 保留用于审计,不原地覆盖,也不作为有效结果参与聚合;可归因于候选本身的确定性失败仍按第 3 节记为 0 分。
完成的 case 结果发布到第 9 节约定的 attempt 目录,并包含 `case_result.json` 和 `complete.json`。`complete.json` 最后写入,记录结果文件及 artifacts 的 SHA256、dataset/code/config/model/image fingerprint、case ID、attempt ID、所选 query 集合的 fingerprint 和完成状态。仅当完成标志可解析、hash 正确、状态为成功或确定性的候选失败,且所有 fingerprint 与当前 validation 完全一致时,结果才可复用;部分 query 的结果不能冒充该 case 的全量结果。目录或某个 `search.json` 文件的存在本身不能作为完成依据。基础设施故障耗尽重试后仍保留为 `infra_error`,但不把 validation 错误地标记为可聚合完成。
`ValidationSpec` 选中的全部 case/query 达到可聚合状态后,编排器确定性地生成对应聚合结果;targeted screening 写入其 validation ID 对应的结果文件,只有 full validation 才生成 harness 级 `evaluations/<commit>/search.json`。随后记录 `validation_completed` 事件。若聚合阶段中断,可从具有相同选择 fingerprint 的 case 结果重新生成,不重复执行已验证的选择范围。
### 12.3 搜索、Proposal 与 Git 续跑
恢复以最后一个已提交的阶段事件为准,而不是无条件进入“下一轮”:
- **搜索循环**:已记录 `round_completed` 的轮次不再执行;进行中的轮次从其最后一个完成阶段继续。只有尚未产生可恢复产物的 agent 调用才重新发起。
- **Proposal**:已落盘的 weakness 和 proposal 文件必须具有唯一 ID、fingerprint 和对应的完成事件。仅有临时文件或缺少完成事件的 agent 输出视为未提交,可保留为诊断材料,但不进入搜索状态。
- **Harness**:commit 只有通过 scope、import、smoke 检查并写入 registry 后才成为 frozen harness。存在 commit 但尚未登记时,恢复流程根据事件和检查结果决定继续冻结或放弃,不能仅凭分支名推断状态。
- **Git 管理**:恢复时先核对 registry 中的 commit 和 snapshot hash,再清理可证明由本次运行创建且不再被引用的 worktree。未登记分支和 debug commit 默认保留或归档;不得自动删除用户分支、受保护分支或仍被事件、proposal、registry 引用的对象。
- **Validation**:进行中的 validation 只调度缺少有效完成标志的 case/query 选择;恢复时必须使用原 `ValidationSpec`,不能重新抽样。部分 validation 完成后记录覆盖范围和筛选结论;只有 full validation 才更新正式 leaderboard。
预算只由去重后的完成事件结算,`event_id` 重放不得重复计费。已发起但没有产生完成事件的模型调用或 sandbox attempt 仍记录实际可观测的 token、费用和耗时;若外部系统无法确认消耗,则记为 `unknown` 并按 domain spec 的保守策略处理。时间预算使用累计运行时,不包含进程停止期间的墙钟时间,除非 domain spec 明确配置绝对截止时间。
### 12.4 恢复流程
`run.py` 启动时执行以下恢复流程:
```text
1. 获取 workspace lock;若已有存活 owner,则拒绝启动
2. 校验 events.jsonl,并以 run.json 的 last_event_seq 为起点回放;必要时从头重建快照
3. 校验 registry 中的 commit、snapshot hash 和 frozen harness 状态
4. 根据阶段事件定位进行中的 round、proposal 或 validation
5. 校验 case 的 complete.json、fingerprint 和 artifacts hash,复用有效结果
6. 为未完成 case 创建新的 attempt;从有效 case 结果重新执行缺失的聚合
7. 清理无引用且可安全识别的临时 worktree,保留或归档其他 Git 状态
8. 原子写入新快照并追加 resume_completed 事件,从最后一个未完成阶段继续
```
`resume_started` 和 `resume_completed` 事件记录恢复前后的状态摘要及本次采取的动作。上述流程可重复执行:再次中断不会重复登记 harness、重复结算预算或覆盖已有 attempt。无法验证的状态应停止并给出可操作的错误,不得通过猜测或静默删除数据来推进搜索。

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 140 KiB

507
meta-reme/models.py Normal file
View file

@ -0,0 +1,507 @@
"""Public data contracts shared by the Meta-ReMe orchestration modules.
The models in this module describe persisted state and values crossing module
boundaries. File I/O, event reduction, scoring, and orchestration belong in
their respective modules rather than here.
"""
# Persisted schema classes are intentionally named and self-describing.
# pylint: disable=missing-class-docstring,missing-function-docstring
from __future__ import annotations
from datetime import datetime, timezone
from enum import StrEnum
import hashlib
import json
from pathlib import Path
from typing import Any, Mapping
from uuid import uuid4
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
SCHEMA_VERSION = 1
def utc_now() -> datetime:
"""Return an aware UTC timestamp."""
return datetime.now(timezone.utc)
class ContractModel(BaseModel):
"""Base class for strict, JSON-serializable persisted contracts."""
model_config = ConfigDict(extra="forbid", use_enum_values=False)
def canonical_json_bytes(value: Any) -> bytes:
"""Serialize a supported value deterministically for hashing."""
if isinstance(value, BaseModel):
value = value.model_dump(mode="json", exclude_none=False)
return json.dumps(
value,
allow_nan=False,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
default=_json_default,
).encode("utf-8")
def _json_default(value: Any) -> Any:
if isinstance(value, Path):
return value.as_posix()
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, StrEnum):
return value.value
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
def fingerprint(value: Any) -> str:
"""Return the SHA256 of a canonical JSON representation."""
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
class DatasetName(StrEnum):
LONGMEMEVAL = "longmemeval"
BEAM = "beam"
class RunStatus(StrEnum):
INITIALIZING = "initializing"
SEARCHING = "searching"
REPLAYING = "replaying"
TESTING = "testing"
COMPLETED = "completed"
FAILED = "failed"
class EvaluationMode(StrEnum):
DEBUG = "debug"
SCREENING = "screening"
SEARCH = "search"
REPLAY = "replay"
TEST = "test"
class AttemptStatus(StrEnum):
COMPLETED = "completed"
CANDIDATE_FAILURE = "candidate_failure"
INFRA_ERROR = "infra_error"
INTERRUPTED = "interrupted"
class ValidationStatus(StrEnum):
RUNNING = "running"
COMPLETED = "completed"
INFRA_ERROR = "infra_error"
class HarnessStatus(StrEnum):
DEBUG = "debug"
FROZEN = "frozen"
REJECTED = "rejected"
class EventType(StrEnum):
RUN_INITIALIZED = "run_initialized"
RESUME_STARTED = "resume_started"
RESUME_COMPLETED = "resume_completed"
ROUND_STARTED = "round_started"
WEAKNESS_COMPLETED = "weakness_completed"
PROPOSAL_COMPLETED = "proposal_completed"
HARNESS_COMMITTED = "harness_committed"
HARNESS_FROZEN = "harness_frozen"
HARNESS_REJECTED = "harness_rejected"
VALIDATION_STARTED = "validation_started"
CASE_ATTEMPT_COMPLETED = "case_attempt_completed"
VALIDATION_COMPLETED = "validation_completed"
LEADERBOARD_UPDATED = "leaderboard_updated"
ROUND_COMPLETED = "round_completed"
WINNER_SELECTED = "winner_selected"
RUN_COMPLETED = "run_completed"
RUN_FAILED = "run_failed"
class DatasetSpec(ContractModel):
name: DatasetName
source: str
fingerprint: str = Field(min_length=1)
class ScopeSpec(ContractModel):
harness_paths: list[str] = Field(default_factory=list)
debug_paths: list[str] = Field(default_factory=lambda: ["meta_debug"])
config_paths: dict[str, list[str]] = Field(default_factory=dict)
class SandboxSpec(ContractModel):
image: str = Field(min_length=1)
timeout_seconds: float = Field(gt=0)
concurrency: int = Field(default=1, ge=1)
max_retries: int = Field(default=2, ge=0)
max_artifact_bytes: int = Field(default=1_073_741_824, gt=0)
class ProposerSpec(ContractModel):
model: str = Field(min_length=1)
top_k: int = Field(default=5, ge=1)
class ValidationPolicySpec(ContractModel):
screening_enabled: bool = True
default_case_ids: list[str] | None = None
default_query_ids: dict[str, list[str]] = Field(default_factory=dict)
promotion_min_mean_query_score: float | None = None
require_full_for_leaderboard: bool = True
@field_validator("require_full_for_leaderboard")
@classmethod
def require_full_results(cls, value: bool) -> bool:
if not value:
raise ValueError("partial validations cannot be admitted to the leaderboard")
return value
@model_validator(mode="after")
def validate_default_selection(self) -> "ValidationPolicySpec":
ValidationSelection(
case_ids=self.default_case_ids,
query_ids=self.default_query_ids,
reason="domain default screening selection",
)
return self
class BudgetSpec(ContractModel):
max_proposals: int | None = Field(default=None, ge=1)
max_validations: int | None = Field(default=None, ge=1)
max_tokens: int | None = Field(default=None, ge=1)
max_runtime_seconds: float | None = Field(default=None, gt=0)
deadline: datetime | None = None
@model_validator(mode="after")
def require_a_limit(self) -> "BudgetSpec":
if all(
value is None
for value in (
self.max_proposals,
self.max_validations,
self.max_tokens,
self.max_runtime_seconds,
self.deadline,
)
):
raise ValueError("at least one search budget limit must be configured")
return self
class DomainSpec(ContractModel):
schema_version: int = Field(default=SCHEMA_VERSION, ge=1)
dataset: DatasetSpec
bundle_target: str = Field(pattern=r"^(default|lme|beam)$")
benchmark_runner: str = Field(min_length=1)
scorer: str = Field(min_length=1)
objective: str = Field(default="mean_query_score", pattern=r"^mean_query_score$")
baseline_files: list[str] = Field(default_factory=list)
scope: ScopeSpec
sandbox: SandboxSpec
proposer: ProposerSpec
validation: ValidationPolicySpec = Field(default_factory=ValidationPolicySpec)
budget: BudgetSpec
class QuerySpec(ContractModel):
query_id: str = Field(min_length=1)
question: str
golden_answer: Any
metadata: dict[str, Any] = Field(default_factory=dict)
class SessionSpec(ContractModel):
session_id: str = Field(min_length=1)
messages: list[dict[str, Any]]
metadata: dict[str, Any] = Field(default_factory=dict)
class CaseSpec(ContractModel):
case_id: str = Field(min_length=1)
sessions: list[SessionSpec]
queries: list[QuerySpec]
metadata: dict[str, Any] = Field(default_factory=dict)
class DatasetManifest(ContractModel):
schema_version: int = Field(default=SCHEMA_VERSION, ge=1)
dataset: DatasetName
source_fingerprint: str = Field(min_length=1)
normalized_fingerprint: str = Field(min_length=1)
case_count: int = Field(ge=0)
query_count: int = Field(ge=0)
created_at: datetime = Field(default_factory=utc_now)
class Fingerprints(ContractModel):
dataset: str = Field(min_length=1)
code: str = Field(min_length=1)
config: str = Field(min_length=1)
model: str = Field(min_length=1)
image: str = Field(min_length=1)
class ValidationSelection(ContractModel):
"""A fixed case/query subset; no selection means the complete dataset."""
case_ids: list[str] | None = None
query_ids: dict[str, list[str]] = Field(default_factory=dict)
reason: str = Field(default="full dataset", min_length=1)
@model_validator(mode="after")
def validate_selection(self) -> "ValidationSelection":
if self.case_ids is not None:
if not self.case_ids:
raise ValueError("case_ids cannot be an empty selection")
if len(self.case_ids) != len(set(self.case_ids)):
raise ValueError("case_ids must be unique")
unknown_cases = set(self.query_ids) - set(self.case_ids)
if unknown_cases:
raise ValueError(f"query selections reference unselected cases: {sorted(unknown_cases)}")
for case_id, query_ids in self.query_ids.items():
if not case_id or not query_ids:
raise ValueError("query_ids must contain non-empty case and query selections")
if len(query_ids) != len(set(query_ids)):
raise ValueError(f"query IDs must be unique for case {case_id!r}")
return self
@property
def requests_full_dataset(self) -> bool:
return self.case_ids is None and not self.query_ids
class ValidationSpec(ContractModel):
validation_id: str = Field(default_factory=lambda: uuid4().hex)
commit_sha: str = Field(min_length=1)
mode: EvaluationMode
selection: ValidationSelection = Field(default_factory=ValidationSelection)
selection_fingerprint: str = ""
fingerprints: Fingerprints
@model_validator(mode="after")
def validate_selection_fingerprint(self) -> "ValidationSpec":
expected = model_fingerprint(self.selection)
if not self.selection_fingerprint:
self.selection_fingerprint = expected
elif self.selection_fingerprint != expected:
raise ValueError("selection_fingerprint does not match validation selection")
return self
class ValidationCoverage(ContractModel):
selected_cases: int = Field(ge=0)
total_cases: int = Field(ge=0)
selected_queries: int = Field(ge=0)
total_queries: int = Field(ge=0)
is_full: bool
@model_validator(mode="after")
def validate_coverage(self) -> "ValidationCoverage":
if self.selected_cases > self.total_cases or self.selected_queries > self.total_queries:
raise ValueError("selected validation coverage cannot exceed dataset totals")
expected_full = self.selected_cases == self.total_cases and self.selected_queries == self.total_queries
if self.is_full != expected_full:
raise ValueError("is_full does not match selected and total coverage")
return self
class BudgetUsage(ContractModel):
proposals: int = Field(default=0, ge=0)
validations: int = Field(default=0, ge=0)
tokens: int | None = Field(default=0, ge=0)
runtime_seconds: float = Field(default=0, ge=0)
has_unknown_usage: bool = False
class RunState(ContractModel):
schema_version: int = Field(default=SCHEMA_VERSION, ge=1)
run_id: str = Field(default_factory=lambda: uuid4().hex)
status: RunStatus = RunStatus.INITIALIZING
last_event_seq: int = Field(default=0, ge=0)
current_round: int = Field(default=0, ge=0)
baseline_commit: str | None = None
best_commit: str | None = None
usage: BudgetUsage = Field(default_factory=BudgetUsage)
created_at: datetime = Field(default_factory=utc_now)
updated_at: datetime = Field(default_factory=utc_now)
class SearchEvent(ContractModel):
schema_version: int = Field(default=SCHEMA_VERSION, ge=1)
seq: int = Field(ge=1)
event_id: str = Field(default_factory=lambda: uuid4().hex)
event_type: EventType
timestamp: datetime = Field(default_factory=utc_now)
round_id: int | None = Field(default=None, ge=0)
proposal_id: str | None = None
commit_sha: str | None = None
validation_id: str | None = None
case_id: str | None = None
attempt_id: str | None = None
payload: dict[str, Any] = Field(default_factory=dict)
class WeaknessEvidence(ContractModel):
query_id: str
observation: str
class WeaknessReport(ContractModel):
weakness_id: str = Field(default_factory=lambda: uuid4().hex)
round_id: int = Field(ge=0)
patterns: list[str]
evidence: list[WeaknessEvidence]
possible_causes: list[str]
regression_risks: list[str] = Field(default_factory=list)
input_fingerprint: str
class Proposal(ContractModel):
proposal_id: str = Field(default_factory=lambda: uuid4().hex)
round_id: int = Field(ge=0)
parent_commits: list[str] = Field(min_length=1)
hypothesis: str
expected_improvements: list[str]
merge_rationale: str | None = None
input_fingerprint: str
class ScopeCheckResult(ContractModel):
passed: bool
changed_paths: list[str]
violations: list[str] = Field(default_factory=list)
class HarnessManifest(ContractModel):
commit_sha: str = Field(min_length=1)
parent_commits: list[str]
snapshot_sha256: str = Field(min_length=1)
config_fingerprint: str = Field(min_length=1)
proposal_id: str | None = None
status: HarnessStatus
scope: ScopeCheckResult
created_at: datetime = Field(default_factory=utc_now)
class QueryResult(ContractModel):
query_id: str
question: str
golden_answer: Any
answer: str | None = None
score: float
judge: dict[str, Any] = Field(default_factory=dict)
error: str | None = None
tokens: int | None = Field(default=None, ge=0)
duration_seconds: float | None = Field(default=None, ge=0)
class CaseResult(ContractModel):
case_id: str
attempt_id: str
status: AttemptStatus
selected_query_ids: list[str] = Field(default_factory=list)
queries: list[QueryResult] = Field(default_factory=list)
completed_stages: list[str] = Field(default_factory=list)
error: str | None = None
artifacts: dict[str, str] = Field(default_factory=dict)
@model_validator(mode="after")
def validate_terminal_result(self) -> "CaseResult":
if self.status is AttemptStatus.COMPLETED and not self.queries:
raise ValueError("completed case result must contain at least one query")
if len(self.selected_query_ids) != len(set(self.selected_query_ids)):
raise ValueError("selected_query_ids must be unique")
result_query_ids = {query.query_id for query in self.queries}
if self.queries and result_query_ids != set(self.selected_query_ids):
raise ValueError("case query results must match selected_query_ids")
return self
class AttemptCompletion(ContractModel):
schema_version: int = Field(default=SCHEMA_VERSION, ge=1)
case_id: str
attempt_id: str
status: AttemptStatus
fingerprints: Fingerprints
selection_fingerprint: str = Field(min_length=1)
result_file: str = "case_result.json"
result_sha256: str = Field(min_length=1)
artifact_sha256: dict[str, str] = Field(default_factory=dict)
completed_at: datetime = Field(default_factory=utc_now)
@field_validator("status")
@classmethod
def require_reusable_status(cls, value: AttemptStatus) -> AttemptStatus:
if value not in (AttemptStatus.COMPLETED, AttemptStatus.CANDIDATE_FAILURE):
raise ValueError("complete marker requires a reusable terminal status")
return value
class ValidationResult(ContractModel):
validation_id: str
commit_sha: str
mode: EvaluationMode
status: ValidationStatus
fingerprints: Fingerprints
selection: ValidationSelection
coverage: ValidationCoverage
comparable: bool = False
mean_query_score: float | None = None
query_count: int = Field(default=0, ge=0)
failure_count: int = Field(default=0, ge=0)
cases: list[CaseResult] = Field(default_factory=list)
created_at: datetime = Field(default_factory=utc_now)
@model_validator(mode="after")
def validate_score(self) -> "ValidationResult":
if self.status is ValidationStatus.COMPLETED and self.mean_query_score is None:
raise ValueError("completed validation must contain mean_query_score")
if self.status is ValidationStatus.INFRA_ERROR and self.mean_query_score is not None:
raise ValueError("infra_error validation cannot have a comparable score")
expected_comparable = (
self.status is ValidationStatus.COMPLETED
and self.mode in (EvaluationMode.SEARCH, EvaluationMode.REPLAY)
and self.coverage.is_full
)
if self.comparable != expected_comparable:
raise ValueError("comparable requires a completed full search or replay validation")
return self
class LeaderboardEntry(ContractModel):
commit_sha: str
validation_id: str
mean_query_score: float
completed_at: datetime
proposal_id: str | None = None
class WorkspaceManifest(ContractModel):
schema_version: int = Field(default=SCHEMA_VERSION, ge=1)
workspace_id: str = Field(default_factory=lambda: uuid4().hex)
domain_fingerprint: str
created_at: datetime = Field(default_factory=utc_now)
class WorkspaceLockOwner(ContractModel):
token: str = Field(default_factory=lambda: uuid4().hex)
pid: int = Field(gt=0)
hostname: str
started_at: datetime = Field(default_factory=utc_now)
def model_fingerprint(model: BaseModel | Mapping[str, Any]) -> str:
"""Convenience alias documenting fingerprints derived from contracts."""
return fingerprint(model)

317
meta-reme/workspace.py Normal file
View file

@ -0,0 +1,317 @@
"""Filesystem boundary and lifecycle management for a Meta-ReMe workspace."""
from __future__ import annotations
from contextlib import AbstractContextManager
import errno
import json
import os
from pathlib import Path
import shutil
import socket
import stat
import tempfile
from typing import Any
from pydantic import BaseModel, ValidationError
import yaml
from models import DomainSpec, WorkspaceLockOwner, WorkspaceManifest, canonical_json_bytes, model_fingerprint
WORKSPACE_MANIFEST = ".meta-reme-workspace.json"
WORKSPACE_LOCK = ".meta-reme.lock"
_DIRECTORIES = (
"code/repo",
"code/worktrees",
"datasets/search/cases",
"harnesses",
"weaknesses",
"proposals",
"evaluations",
"logs",
)
class WorkspaceError(RuntimeError):
"""Base error for invalid or unsafe workspace operations."""
class WorkspaceLockedError(WorkspaceError):
"""Raised when another process owns a workspace lock."""
class WorkspaceFormatError(WorkspaceError):
"""Raised when a directory is not a valid compatible workspace."""
class Workspace:
"""A validated Meta-ReMe workspace rooted at an absolute path."""
def __init__(self, root: Path, manifest: WorkspaceManifest) -> None:
self.root = Path(root).resolve()
self.manifest = manifest
@classmethod
def create(cls, root: Path, domain_spec: DomainSpec) -> "Workspace":
"""Create a new workspace without overwriting an existing directory."""
root = Path(root).resolve()
if root.exists() and (not root.is_dir() or any(root.iterdir())):
raise WorkspaceError(f"Refusing to initialize non-empty workspace: {root}")
root.mkdir(parents=True, exist_ok=True)
manifest = WorkspaceManifest(domain_fingerprint=model_fingerprint(domain_spec))
workspace = cls(root, manifest)
try:
for relative in _DIRECTORIES:
(root / relative).mkdir(parents=True, exist_ok=False)
workspace.atomic_write_text(
"domain_spec.yaml",
yaml.safe_dump(
domain_spec.model_dump(mode="json", exclude_none=False),
allow_unicode=True,
sort_keys=True,
),
)
# The manifest is the publication marker and must be written last.
workspace.atomic_write_json(WORKSPACE_MANIFEST, manifest)
return workspace
except BaseException:
# Only remove a directory created by this call and only while it has
# not yet become a published workspace.
if not (root / WORKSPACE_MANIFEST).exists():
shutil.rmtree(root, ignore_errors=True)
raise
@classmethod
def open(cls, root: Path, domain_spec: DomainSpec | None = None) -> "Workspace":
"""Open and validate an existing workspace."""
root = Path(root).resolve()
manifest_path = root / WORKSPACE_MANIFEST
if not root.is_dir() or not manifest_path.is_file():
raise WorkspaceFormatError(f"Not a Meta-ReMe workspace: {root}")
try:
manifest = WorkspaceManifest.model_validate_json(manifest_path.read_text(encoding="utf-8"))
except (OSError, ValidationError) as exc:
raise WorkspaceFormatError(f"Invalid workspace manifest: {manifest_path}") from exc
for relative in _DIRECTORIES:
if not (root / relative).is_dir():
raise WorkspaceFormatError(f"Workspace directory is missing: {relative}")
if domain_spec is not None:
actual = model_fingerprint(domain_spec)
if actual != manifest.domain_fingerprint:
raise WorkspaceFormatError("Domain spec fingerprint does not match this workspace")
return cls(root, manifest)
def path(self, relative: str | Path) -> Path:
"""Resolve a safe workspace-relative path."""
relative = Path(relative)
if relative.is_absolute() or ".." in relative.parts:
raise WorkspaceError(f"Path must be workspace-relative: {relative}")
resolved = (self.root / relative).resolve()
try:
resolved.relative_to(self.root)
except ValueError as exc:
raise WorkspaceError(f"Path escapes workspace: {relative}") from exc
return resolved
def entity_path(self, base: str | Path, identifier: str) -> Path:
"""Resolve a path for an externally supplied case/commit/attempt ID."""
if not identifier or identifier in {".", ".."} or Path(identifier).name != identifier:
raise WorkspaceError(f"Unsafe workspace identifier: {identifier!r}")
if any(character in identifier for character in ("/", "\\", "\x00")):
raise WorkspaceError(f"Unsafe workspace identifier: {identifier!r}")
return self.path(Path(base) / identifier)
def case_attempt_dir(
self,
commit_sha: str,
mode: str,
case_id: str,
attempt_id: str,
*,
create: bool = False,
) -> Path:
"""Return the canonical attempt directory described by the design."""
current = self.entity_path("evaluations", commit_sha)
for identifier in (mode, "cases", case_id, attempt_id):
current = self.entity_path(current.relative_to(self.root), identifier)
if create:
current.mkdir(parents=True, exist_ok=False)
return current
def atomic_write_json(self, relative: str | Path, value: BaseModel | Any) -> Path:
"""Atomically publish JSON in the workspace."""
if isinstance(value, BaseModel):
value = value.model_dump(mode="json", exclude_none=False)
content = json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True, allow_nan=False) + "\n"
return self.atomic_write_text(relative, content)
def atomic_write_text(self, relative: str | Path, content: str) -> Path:
"""Atomically publish UTF-8 text in the workspace."""
return self.atomic_write_bytes(relative, content.encode("utf-8"))
def atomic_write_bytes(self, relative: str | Path, content: bytes) -> Path:
"""Write, fsync, and atomically replace a file in its destination directory."""
destination = self.path(relative)
destination.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{destination.name}.", dir=destination.parent)
temporary = Path(temporary_name)
try:
with os.fdopen(descriptor, "wb") as stream:
stream.write(content)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, destination)
_fsync_directory(destination.parent)
return destination
except BaseException:
temporary.unlink(missing_ok=True)
raise
def acquire_lock(self) -> "WorkspaceLock":
"""Acquire the single-writer workspace lock."""
lock = WorkspaceLock(self.path(WORKSPACE_LOCK))
lock.acquire()
return lock
def install_search_dataset(self, source: Path) -> Path:
"""Copy a normalized search dataset into an empty workspace and make it read-only."""
source = Path(source).resolve()
destination = self.path("datasets/search")
if not source.is_dir():
raise WorkspaceError(f"Normalized search dataset is not a directory: {source}")
if any(path.is_file() or path.is_symlink() for path in destination.rglob("*")):
raise WorkspaceError(f"Search dataset has already been installed: {destination}")
for path in source.rglob("*"):
if path.is_symlink():
raise WorkspaceError(f"Dataset may not contain symbolic links: {path.relative_to(source)}")
temporary = self.path("datasets/.search-installing")
if temporary.exists():
raise WorkspaceError(f"Incomplete dataset installation requires inspection: {temporary}")
shutil.copytree(source, temporary, symlinks=True)
try:
shutil.rmtree(destination)
os.replace(temporary, destination)
_make_tree_read_only(destination)
_fsync_directory(destination.parent)
return destination
except BaseException:
if not destination.exists():
shutil.rmtree(temporary, ignore_errors=True)
destination.mkdir(parents=True, exist_ok=True)
raise
class WorkspaceLock(AbstractContextManager["WorkspaceLock"]):
"""An atomic, ownership-checked filesystem lock."""
def __init__(self, path: Path) -> None:
self.path = path
self.owner: WorkspaceLockOwner | None = None
def acquire(self) -> None:
"""Acquire this lock, taking over only a confirmed stale local owner."""
if self.owner is not None:
raise WorkspaceLockedError(f"Lock is already held by this object: {self.path}")
owner = WorkspaceLockOwner(pid=os.getpid(), hostname=socket.gethostname())
for _ in range(2):
try:
descriptor = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError as exc:
existing = self._read_owner()
if not _is_confirmed_stale(existing):
raise WorkspaceLockedError(_lock_message(self.path, existing)) from exc
try:
self.path.unlink()
except FileNotFoundError:
pass
continue
try:
with os.fdopen(descriptor, "wb") as stream:
stream.write(canonical_json_bytes(owner) + b"\n")
stream.flush()
os.fsync(stream.fileno())
_fsync_directory(self.path.parent)
except BaseException:
self.path.unlink(missing_ok=True)
raise
self.owner = owner
return
raise WorkspaceLockedError(f"Could not acquire workspace lock: {self.path}")
def _read_owner(self) -> WorkspaceLockOwner | None:
"""Read the current lock owner, returning None for an invalid lock."""
try:
return WorkspaceLockOwner.model_validate_json(self.path.read_text(encoding="utf-8"))
except (OSError, ValidationError):
return None
def release(self) -> None:
"""Release the lock only if its ownership token is unchanged."""
if self.owner is None:
return
existing = self._read_owner()
if existing is None or existing.token != self.owner.token:
raise WorkspaceLockedError(f"Workspace lock ownership changed: {self.path}")
self.path.unlink()
_fsync_directory(self.path.parent)
self.owner = None
def __enter__(self) -> "WorkspaceLock":
return self
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
self.release()
def _is_confirmed_stale(owner: WorkspaceLockOwner | None) -> bool:
if owner is None or owner.hostname != socket.gethostname():
return False
try:
os.kill(owner.pid, 0)
except ProcessLookupError:
return True
except PermissionError:
return False
except OSError as exc:
return exc.errno == errno.ESRCH
return False
def _lock_message(path: Path, owner: WorkspaceLockOwner | None) -> str:
if owner is None:
return f"Workspace has an unreadable lock requiring manual inspection: {path}"
return f"Workspace is locked by pid {owner.pid} on {owner.hostname} since {owner.started_at.isoformat()}"
def _fsync_directory(directory: Path) -> None:
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
descriptor = os.open(directory, flags)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
def _make_tree_read_only(root: Path) -> None:
for path in sorted(root.rglob("*"), reverse=True):
mode = stat.S_IMODE(path.stat().st_mode)
if path.is_dir():
path.chmod(mode & ~0o222 | 0o555)
else:
path.chmod(mode & ~0o222 | 0o444)
mode = stat.S_IMODE(root.stat().st_mode)
root.chmod(mode & ~0o222 | 0o555)

View file

@ -150,6 +150,37 @@ and initializes a fresh repository for the next case.
For an ephemeral Docker workspace, always call `export()` or `export_full()`
before `close()`.
## Reuse one built memory for parallel queries
Use `export_workspace()` after constructing the memory, then give the snapshot
to each independent query case with `upload_workspace()`. Upload clears the
target runtime workspace by default, so the cases start from the same memory
state but never share mutable files or indexes.
```python
memory_case = await factory.create_case("build-memory")
try:
await memory_case.ingest_session(session_id="source", messages=[...])
await memory_case.run_job("index_update")
snapshot = await memory_case.export_workspace("artifacts/built-memory.tar.gz")
finally:
await memory_case.close()
query_cases = await factory.create_cases(["query-1", "query-2"])
try:
await asyncio.gather(*(case.upload_workspace(snapshot) for case in query_cases))
answers = await asyncio.gather(
*(case.answer(query=query) for case, query in zip(query_cases, queries))
)
finally:
await asyncio.gather(*(case.close() for case in query_cases))
```
`upload_workspace()` also accepts a host directory. It validates and repacks
the directory or input archive before extraction; symbolic links, special
files, and traversal paths are rejected. Pass `clear=False` only when an
intentional merge into the target workspace is required.
## Reuse one container for sequential cases
When container startup and source installation dominate the benchmark, one

View file

@ -5,10 +5,13 @@ from __future__ import annotations
import asyncio
from dataclasses import asdict
from datetime import datetime, timezone
import gzip
import io
import json
from pathlib import Path
from pathlib import PurePosixPath
import re
import tarfile
from typing import Any, Callable, Literal, Protocol
import uuid
@ -21,6 +24,8 @@ CANDIDATE_VENV = "/workspace/candidate-venv"
HARNESS_ROOT = "/workspace/harness"
SOURCE_ARCHIVE = "/workspace/candidate.tar.gz"
EXPORT_ARCHIVE = "/tmp/reme-case-export.tar.gz"
WORKSPACE_ARCHIVE = "/tmp/reme-workspace-upload.tar.gz"
WORKSPACE_EXPORT_ARCHIVE = "/tmp/reme-workspace-export.tar.gz"
RUNTIME_LAYOUT = f"{CASE_ROOT}/runtime-layout.json"
_CASE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
_GIT_AUTHOR_NAME = "ReMe Sandbox"
@ -90,6 +95,107 @@ def _decode(value: bytes | str | None) -> str:
return value or ""
def _workspace_member_path(name: str) -> PurePosixPath | None:
"""Validate and normalize one relative path in a workspace archive."""
path = PurePosixPath(name)
if not name or path.is_absolute() or ".." in path.parts or "\\" in name:
raise ValueError(f"workspace archive contains unsafe path: {name!r}")
if path == PurePosixPath("."):
return None
return path
def _archive_workspace(source: str | Path) -> bytes:
"""Create a safe, deterministic gzip tar archive from a host workspace.
``source`` may be a workspace directory or an archive previously returned
by :meth:`DockerReMeSandbox.export_workspace`. Incoming archives are read
and rebuilt so links, special files, and traversal paths never reach the
sandbox extractor.
"""
supplied_path = Path(source).expanduser()
if supplied_path.is_symlink():
raise ValueError("workspace source must not be a symbolic link")
source_path = supplied_path.resolve(strict=True)
entries: list[tuple[PurePosixPath, tarfile.TarInfo, bytes | None]] = []
if source_path.is_dir():
for item in sorted(source_path.rglob("*"), key=lambda value: value.relative_to(source_path).as_posix()):
relative = item.relative_to(source_path)
if item.is_symlink():
raise ValueError(f"workspace source contains a symbolic link: {relative}")
path = _workspace_member_path(relative.as_posix())
assert path is not None
if item.is_dir():
info = tarfile.TarInfo(path.as_posix())
info.type = tarfile.DIRTYPE
info.mode = item.stat().st_mode & 0o777
entries.append((path, info, None))
elif item.is_file():
info = tarfile.TarInfo(path.as_posix())
info.size = item.stat().st_size
info.mode = item.stat().st_mode & 0o777
entries.append((path, info, item.read_bytes()))
else:
raise ValueError(f"workspace source contains unsupported file: {relative}")
elif source_path.is_file():
with tarfile.open(source_path, mode="r:*") as archive:
members = archive.getmembers()
normalized_paths = [
path for member in members if (path := _workspace_member_path(member.name)) is not None
]
# ``export()`` archives the whole case, while ``export_workspace``
# archives only the workspace contents. Accept both forms so an
# existing case artifact can seed independent query sandboxes.
case_export = (
PurePosixPath("manifest.json") in normalized_paths
and any(path.parts and path.parts[0] == "reme_workspace" for path in normalized_paths)
)
seen: set[PurePosixPath] = set()
for member in members:
path = _workspace_member_path(member.name)
if path is None:
continue
if case_export:
if not path.parts or path.parts[0] != "reme_workspace":
continue
if len(path.parts) == 1:
if not member.isdir():
raise ValueError("case export has a non-directory reme_workspace entry")
continue
path = PurePosixPath(*path.parts[1:])
if path in seen:
raise ValueError(f"workspace archive contains duplicate path: {path}")
seen.add(path)
if member.isdir():
info = tarfile.TarInfo(path.as_posix())
info.type = tarfile.DIRTYPE
info.mode = member.mode & 0o777
entries.append((path, info, None))
elif member.isfile():
content = archive.extractfile(member)
if content is None:
raise ValueError(f"workspace archive cannot read file: {path}")
data = content.read()
info = tarfile.TarInfo(path.as_posix())
info.size = len(data)
info.mode = member.mode & 0o777
entries.append((path, info, data))
else:
raise ValueError(f"workspace archive contains unsupported entry: {member.name!r}")
else:
raise ValueError(f"workspace source is neither a directory nor a file: {source_path}")
raw = io.BytesIO()
with gzip.GzipFile(fileobj=raw, mode="wb", mtime=0) as compressed:
with tarfile.open(fileobj=compressed, mode="w") as archive:
for _path, info, data in entries:
info.mtime = 0
info.uid = info.gid = 0
info.uname = info.gname = ""
archive.addfile(info, io.BytesIO(data) if data is not None else None)
return raw.getvalue()
def _default_workspace_builder(case_id: str, image: str, env: dict[str, str]) -> Workspace:
"""Create the exact AgentScope 2.0.4.post1 Docker backend lazily."""
import agentscope
@ -492,6 +598,54 @@ class DockerReMeSandbox:
"""Download every file under the disposable case root."""
return await self.export(destination, profile="full", include_candidate=include_candidate)
async def export_workspace(self, destination: str | Path) -> Path:
"""Download a portable snapshot of this runtime workspace to the host.
The resulting gzip tar archive can be passed directly to
:meth:`upload_workspace` on any initialized case. It contains the
workspace contents (including its local Git history), not the enclosing
case directory, logs, or candidate source.
"""
async with self._operation_lock:
assert self.backend is not None, "sandbox is not initialized"
await self._exec_checked(
"export_workspace",
["tar", "-czf", WORKSPACE_EXPORT_ARCHIVE, "-C", self.runtime_workspace, "."],
)
target = Path(destination).expanduser().resolve()
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(await self.backend.read_file(WORKSPACE_EXPORT_ARCHIVE))
return target
async def upload_workspace(self, source: str | Path, *, clear: bool = True) -> None:
"""Copy a host workspace directory or exported archive into this case.
By default the existing runtime workspace is deleted first, ensuring a
query case receives only the supplied memory state. Set ``clear=False``
only when intentionally merging files into the current workspace.
The copy is made through a validated archive, so each sandbox receives
an independent filesystem tree and can run queries in parallel.
"""
archive = _archive_workspace(source)
async with self._operation_lock:
assert self.backend is not None, "sandbox is not initialized"
if clear:
await self._exec_checked("clear_runtime_workspace", ["rm", "-rf", self.runtime_workspace])
await self._exec_checked("create_runtime_workspace", ["mkdir", "-p", self.runtime_workspace])
await self.backend.write_file(WORKSPACE_ARCHIVE, archive)
await self._exec_checked(
"upload_workspace",
["tar", "-xzf", WORKSPACE_ARCHIVE, "-C", self.runtime_workspace],
)
# A directory copied from the host may not have Git metadata. Git
# init is idempotent and preserves history when the snapshot has it.
await self._exec_checked(
"initialize_uploaded_memory_history",
["git", "init", "--quiet"],
cwd=self.runtime_workspace,
)
await self._exec_checked("remove_workspace_upload", ["rm", "-f", WORKSPACE_ARCHIVE])
@staticmethod
def _validated_workspace_path(value: Any, workspace_root: PurePosixPath, *, allow_root: bool) -> str:
"""Validate one case-relative path received from candidate-controlled state."""
@ -612,7 +766,7 @@ class DockerReMeSandbox:
assert self.backend is not None, "sandbox is not initialized"
await self._exec_checked(
"clear_case",
["rm", "-rf", CASE_ROOT, EXPORT_ARCHIVE],
["rm", "-rf", CASE_ROOT, EXPORT_ARCHIVE, WORKSPACE_ARCHIVE, WORKSPACE_EXPORT_ARCHIVE],
)
await self._create_case_layout()
self.case_id = case_id

View file

@ -0,0 +1,68 @@
"""Focused coverage for the sealed benchmark bundle."""
from __future__ import annotations
import importlib.util
from pathlib import Path
import yaml
PROJECT_ROOT = Path(__file__).resolve().parents[2]
BUILDER_PATH = PROJECT_ROOT / "meta-reme/bundle_builder.py"
def _load_builder():
spec = importlib.util.spec_from_file_location("reme_bundle_builder", BUILDER_PATH)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_bundle_prunes_unused_runtime_backends(tmp_path: Path) -> None:
"""Generated benchmark bundles omit backends outside the sealed runtime contract."""
builder = _load_builder()
bundle = builder.build_bundle("default", tmp_path, validate=True)
forbidden = (
"reme/components/client/http_client.py",
"reme/components/client/mcp_client.py",
"reme/components/file_graph/neo4j_file_graph.py",
"reme/components/file_graph/nx_file_graph.py",
"reme/components/file_store/faiss_local_file_store.py",
"reme/components/file_store/zvec_local_file_store.py",
"reme/components/service/http_service.py",
"reme/components/service/mcp_service.py",
"reme/components/tokenizer/jieba_tokenizer.py",
"reme/schema/auto_fin.py",
"reme/schema/daily_paper.py",
"reme/steps/common/add.py",
"reme/steps/common/demo.py",
"reme/steps/common/llm_demo.py",
"reme/steps/common/stream_demo.py",
"reme/steps/common/stream_llm_demo.py",
"reme/steps/common/version.py",
"reme/steps/file_io/read_image.py",
"reme/utils/arxiv.py",
"reme/utils/huggingface_papers.py",
)
assert all(not (bundle / path).exists() for path in forbidden)
config = yaml.safe_load((bundle / "reme/config/default.yaml").read_text(encoding="utf-8"))
assert config["service"]["backend"] == "cli"
assert "read_image" not in config["jobs"]
assert "version" not in config["jobs"]
assert {"dream_cron", "optimize_index_cron"}.issubset(config["jobs"])
assert not list(bundle.rglob("__pycache__"))
assert not (bundle / "README.md").exists()
assert not (bundle / "LICENSE").exists()
assert (bundle / "pyproject.toml").is_file()
embedding_source = (bundle / "reme/components/as_embedding/__init__.py").read_text(encoding="utf-8")
assert "dashscope_multimodal" not in embedding_source
assert (bundle / "reme/steps/evolve/auto_memory_cc.py").is_file()
benchmark_bundle = builder.build_bundle("lme", tmp_path / "lme", validate=True)
assert not (benchmark_bundle / "reme/steps/evolve/auto_memory_cc.py").exists()
assert "AutoMemoryCCStep" not in (benchmark_bundle / "reme/steps/evolve/__init__.py").read_text(encoding="utf-8")
assert not list(benchmark_bundle.rglob("__pycache__"))

View file

@ -0,0 +1,177 @@
"""Focused contract and filesystem tests for Meta-ReMe's initial foundation."""
# pylint: disable=missing-function-docstring
from __future__ import annotations
import importlib
import json
import os
from pathlib import Path
import socket
import sys
import pytest
from pydantic import ValidationError
META_REME = Path(__file__).resolve().parents[2] / "meta-reme"
sys.path.insert(0, str(META_REME))
models = importlib.import_module("models")
workspace_module = importlib.import_module("workspace")
def domain_spec():
return models.DomainSpec(
dataset=models.DatasetSpec(name="longmemeval", source="dataset.json", fingerprint="dataset-sha"),
bundle_target="lme",
benchmark_runner="benchmark.longmemeval.run",
scorer="lme_answer_judge_step",
scope=models.ScopeSpec(harness_paths=["reme/steps/benchmark/lme/auto_memory.py"]),
sandbox=models.SandboxSpec(image="reme:test", timeout_seconds=60),
proposer=models.ProposerSpec(model="test-model"),
budget=models.BudgetSpec(max_proposals=2),
)
def test_fingerprint_is_stable_for_mapping_order() -> None:
assert models.fingerprint({"a": 1, "b": [2, 3]}) == models.fingerprint({"b": [2, 3], "a": 1})
def test_contracts_reject_unknown_fields_and_invalid_terminal_states() -> None:
with pytest.raises(ValidationError):
models.DatasetSpec(name="longmemeval", source="x", fingerprint="sha", typo=True)
with pytest.raises(ValidationError, match="at least one search budget"):
models.BudgetSpec()
with pytest.raises(ValidationError, match="at least one query"):
models.CaseResult(case_id="case", attempt_id="attempt", status="completed")
with pytest.raises(ValidationError, match="reusable terminal status"):
models.AttemptCompletion(
case_id="case",
attempt_id="attempt",
status="infra_error",
fingerprints=models.Fingerprints(dataset="d", code="c", config="f", model="m", image="i"),
selection_fingerprint="selection-sha",
result_sha256="sha",
)
def test_partial_validation_selection_is_not_comparable() -> None:
selection = models.ValidationSelection(
case_ids=["case-1"],
query_ids={"case-1": ["query-1"]},
reason="target known failure",
)
coverage = models.ValidationCoverage(
selected_cases=1,
total_cases=10,
selected_queries=1,
total_queries=100,
is_full=False,
)
common = {
"validation_id": "validation-1",
"commit_sha": "commit",
"mode": "screening",
"status": "completed",
"fingerprints": models.Fingerprints(dataset="d", code="c", config="f", model="m", image="i"),
"selection": selection,
"coverage": coverage,
"mean_query_score": 1.0,
"query_count": 1,
}
result = models.ValidationResult(**common)
assert not result.comparable
spec = models.ValidationSpec(
commit_sha="commit",
mode="screening",
selection=selection,
fingerprints=common["fingerprints"],
)
assert spec.selection_fingerprint == models.model_fingerprint(selection)
with pytest.raises(ValidationError, match="comparable requires"):
models.ValidationResult(**common, comparable=True)
def test_validation_selection_rejects_queries_from_unselected_cases() -> None:
with pytest.raises(ValidationError, match="unselected cases"):
models.ValidationSelection(case_ids=["case-1"], query_ids={"case-2": ["query-1"]}, reason="targeted")
def test_workspace_create_open_and_domain_fingerprint(tmp_path: Path) -> None:
root = tmp_path / "workspace"
spec = domain_spec()
workspace = workspace_module.Workspace.create(root, spec)
assert workspace.path("code/repo").is_dir()
assert workspace.path("datasets/search/cases").is_dir()
assert workspace_module.Workspace.open(root, spec).manifest == workspace.manifest
changed = spec.model_copy(update={"scorer": "different"})
with pytest.raises(workspace_module.WorkspaceFormatError, match="fingerprint"):
workspace_module.Workspace.open(root, changed)
with pytest.raises(workspace_module.WorkspaceError, match="non-empty"):
workspace_module.Workspace.create(root, spec)
def test_workspace_rejects_escaping_paths_and_ids(tmp_path: Path) -> None:
workspace = workspace_module.Workspace.create(tmp_path / "workspace", domain_spec())
with pytest.raises(workspace_module.WorkspaceError):
workspace.path("../outside")
with pytest.raises(workspace_module.WorkspaceError):
workspace.entity_path("evaluations", "../case")
def test_atomic_write_replaces_content_and_leaves_no_temporary_file(tmp_path: Path) -> None:
workspace = workspace_module.Workspace.create(tmp_path / "workspace", domain_spec())
destination = workspace.atomic_write_json("run.json", {"value": 1})
workspace.atomic_write_json("run.json", {"value": 2})
assert json.loads(destination.read_text(encoding="utf-8")) == {"value": 2}
assert not list(destination.parent.glob(".run.json.*"))
def test_install_search_dataset_replaces_empty_skeleton_and_is_read_only(tmp_path: Path) -> None:
workspace = workspace_module.Workspace.create(tmp_path / "workspace", domain_spec())
source = tmp_path / "normalized"
(source / "cases").mkdir(parents=True)
(source / "manifest.json").write_text("{}\n", encoding="utf-8")
(source / "cases/case-1.json").write_text("{}\n", encoding="utf-8")
installed = workspace.install_search_dataset(source)
assert (installed / "cases/case-1.json").is_file()
assert not (installed / "manifest.json").stat().st_mode & 0o222
with pytest.raises(workspace_module.WorkspaceError, match="already been installed"):
workspace.install_search_dataset(source)
def test_workspace_lock_rejects_live_owner_and_releases(tmp_path: Path) -> None:
workspace = workspace_module.Workspace.create(tmp_path / "workspace", domain_spec())
lock = workspace.acquire_lock()
with pytest.raises(workspace_module.WorkspaceLockedError, match="locked by pid"):
workspace.acquire_lock()
lock.release()
with workspace.acquire_lock():
assert workspace.path(workspace_module.WORKSPACE_LOCK).exists()
assert not workspace.path(workspace_module.WORKSPACE_LOCK).exists()
def test_workspace_lock_recovers_confirmed_stale_local_owner(tmp_path: Path) -> None:
workspace = workspace_module.Workspace.create(tmp_path / "workspace", domain_spec())
stale = models.WorkspaceLockOwner(pid=_missing_pid(), hostname=socket.gethostname())
workspace.atomic_write_json(workspace_module.WORKSPACE_LOCK, stale)
with workspace.acquire_lock() as lock:
assert lock.owner is not None
assert lock.owner.token != stale.token
def _missing_pid() -> int:
candidate = max(os.getpid() + 1_000_000, 10_000_000)
while True:
try:
os.kill(candidate, 0)
except ProcessLookupError:
return candidate
candidate += 1

View file

@ -230,6 +230,96 @@ async def test_image_candidate_skips_source_upload_and_runs_jobs(tmp_path):
assert "--exclude=reme_workspace/session" not in export_command
@pytest.mark.asyncio
async def test_workspace_can_be_cleared_and_uploaded_from_host(tmp_path):
"""A completed memory can be copied into an independent query case."""
source = tmp_path / "built-memory"
(source / "daily").mkdir(parents=True)
(source / "daily" / "2026-08-06.md").write_text("remember this\n")
workspace = FakeWorkspace("query-1", "candidate:test", {})
factory = DockerReMeSandboxFactory(
ImageCandidate("candidate:test"),
workspace_builder=lambda *_args: workspace,
)
case = await factory.create_case("query-1")
old_path = f"{case.runtime_workspace}/daily/old.md"
workspace.backend.files[old_path] = b"old memory"
await case.upload_workspace(source)
payload = workspace.backend.files["/tmp/reme-workspace-upload.tar.gz"]
with tarfile.open(fileobj=io.BytesIO(payload), mode="r:gz") as archive:
assert archive.getnames() == ["daily", "daily/2026-08-06.md"]
assert archive.extractfile("daily/2026-08-06.md").read() == b"remember this\n"
clear_index = next(
index
for index, (command, _, _) in enumerate(workspace.backend.commands)
if command == ["rm", "-rf", case.runtime_workspace]
)
upload_index = next(
index
for index, (command, _, _) in enumerate(workspace.backend.commands)
if command == ["tar", "-xzf", "/tmp/reme-workspace-upload.tar.gz", "-C", case.runtime_workspace]
)
assert clear_index < upload_index
assert old_path not in workspace.backend.files
assert (["git", "init", "--quiet"], case.runtime_workspace, case.command_timeout) in workspace.backend.commands
@pytest.mark.asyncio
async def test_workspace_upload_rejects_unsafe_host_archive(tmp_path):
"""Host archives cannot use traversal paths to write outside the workspace."""
source = tmp_path / "unsafe.tar.gz"
with tarfile.open(source, mode="w:gz") as archive:
info = tarfile.TarInfo("../outside.md")
info.size = 4
archive.addfile(info, io.BytesIO(b"nope"))
workspace = FakeWorkspace("query-1", "candidate:test", {})
factory = DockerReMeSandboxFactory(
ImageCandidate("candidate:test"),
workspace_builder=lambda *_args: workspace,
)
case = await factory.create_case("query-1")
commands_before = list(workspace.backend.commands)
with pytest.raises(ValueError, match="unsafe path"):
await case.upload_workspace(source)
assert workspace.backend.commands == commands_before
@pytest.mark.asyncio
async def test_workspace_upload_accepts_a_complete_case_export(tmp_path):
"""A previous ``export()`` archive restores only its runtime workspace."""
source = tmp_path / "case-export.tar.gz"
with tarfile.open(source, mode="w:gz") as archive:
manifest = b'{"case_id": "source"}'
manifest_info = tarfile.TarInfo("manifest.json")
manifest_info.size = len(manifest)
archive.addfile(manifest_info, io.BytesIO(manifest))
note = b"remember this\n"
note_info = tarfile.TarInfo("reme_workspace/daily/2023-05-30.md")
note_info.size = len(note)
archive.addfile(note_info, io.BytesIO(note))
log = b"do not import case logs"
log_info = tarfile.TarInfo("logs/actions.jsonl")
log_info.size = len(log)
archive.addfile(log_info, io.BytesIO(log))
workspace = FakeWorkspace("query-1", "candidate:test", {})
factory = DockerReMeSandboxFactory(
ImageCandidate("candidate:test"),
workspace_builder=lambda *_args: workspace,
)
case = await factory.create_case("query-1")
await case.upload_workspace(source)
payload = workspace.backend.files["/tmp/reme-workspace-upload.tar.gz"]
with tarfile.open(fileobj=io.BytesIO(payload), mode="r:gz") as archive:
assert archive.getnames() == ["daily/2023-05-30.md"]
assert archive.extractfile("daily/2023-05-30.md").read() == note
@pytest.mark.asyncio
async def test_host_selects_when_to_commit_configured_daily_directory():
"""Several ingested sessions can be grouped into one explicit host checkpoint."""
@ -329,6 +419,8 @@ async def test_one_container_can_reset_and_run_another_case(tmp_path):
backend.files["/workspace/candidate/reme/__init__.py"] = b"candidate-code"
old_git_object = "/workspace/case/reme_workspace/.git/objects/old-case-object"
backend.files[old_git_object] = b"old case history"
backend.files["/tmp/reme-workspace-upload.tar.gz"] = b"uploaded workspace"
backend.files["/tmp/reme-workspace-export.tar.gz"] = b"exported workspace"
assert any(path.startswith("/workspace/case/") for path in backend.files)
await case.reset_case("case-2")
@ -341,6 +433,8 @@ async def test_one_container_can_reset_and_run_another_case(tmp_path):
assert backend.files["/workspace/case/reme_workspace/.git/HEAD"].startswith(b"ref:")
assert not any(path.startswith("/workspace/case/results/") for path in backend.files)
assert "/tmp/reme-case-export.tar.gz" not in backend.files
assert "/tmp/reme-workspace-upload.tar.gz" not in backend.files
assert "/tmp/reme-workspace-export.tar.gz" not in backend.files
git_init_positions = [
index for index, (command, _, _) in enumerate(backend.commands) if command == ["git", "init", "--quiet"]
]