specmodule 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {specmodule-0.1.2/specmodule.egg-info → specmodule-0.1.3}/PKG-INFO +1 -1
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/cli.py +47 -1
- specmodule-0.1.3/module_harness/control.py +175 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/module.py +21 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/query.py +23 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/pyproject.toml +1 -1
- {specmodule-0.1.2 → specmodule-0.1.3/specmodule.egg-info}/PKG-INFO +1 -1
- {specmodule-0.1.2 → specmodule-0.1.3}/specmodule.egg-info/SOURCES.txt +1 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/LICENSE +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/README.md +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/llm/__init__.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/llm/client.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/llm/config.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/llm/mock.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/__init__.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/align.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/builtins.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/checkpoint.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/command.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/config.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/consistency.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/entry.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/events.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/feed.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/graph_builder.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/harness.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/loader.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/outputfmt.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/prompt.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/registry.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/scaffold.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/spec.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/status.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/store.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/submodule.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/templates/builtin/codereview.json +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/templates/builtin/docwrite.json +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/templates/builtin/summarize.json +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/templates/builtin/translate.json +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/module_harness/translator.py +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/setup.cfg +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/specmodule.egg-info/dependency_links.txt +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/specmodule.egg-info/entry_points.txt +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/specmodule.egg-info/requires.txt +0 -0
- {specmodule-0.1.2 → specmodule-0.1.3}/specmodule.egg-info/top_level.txt +0 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
python -m module_harness.cli snapshot [<tick>] [--run-id xxx] [--json] [--out FILE]
|
|
13
13
|
python -m module_harness.cli rollback <目标> --module <名> [--spec ...]
|
|
14
14
|
python -m module_harness.cli checkpoint <label> [<tick>] [--run-id xxx]
|
|
15
|
+
python -m module_harness.cli cancel | pause | unpause [--run-id xxx]
|
|
15
16
|
python -m module_harness.cli visualize --module <名> [--tasklist x.json | --run-id xxx] [--out FILE]
|
|
16
17
|
|
|
17
18
|
场景归属:使用者层面(usage scenario)——第二级用户只写 spec/tasklist,
|
|
@@ -430,10 +431,21 @@ def _run_resume_cmd(args: argparse.Namespace, *, require_target: bool) -> int:
|
|
|
430
431
|
_check_spec_schema(res, spec)
|
|
431
432
|
llm_client = _build_llm_client(args.mock)
|
|
432
433
|
template_name = args.template or res.default_template
|
|
434
|
+
tasklist = _load_tasklist(args.tasklist) if args.tasklist else None
|
|
433
435
|
if args.tasklist:
|
|
434
436
|
# tasklist 路径:跳过翻译,template_name 置 None(与 Module
|
|
435
437
|
# "template/tasklist 二选一"不变量对齐)
|
|
436
438
|
template_name = None
|
|
439
|
+
elif template_name is None and tasklist is None:
|
|
440
|
+
# 流程来源兜底:显式参数 > entry.default_template > module_inputs
|
|
441
|
+
# 归档 tasklist(续跑语义本该默认沿用原任务书——tasklist 通道
|
|
442
|
+
# 启动的 run 无模板可回落,此前只能靠显式 --tasklist 续跑)
|
|
443
|
+
from .query import read_module_inputs
|
|
444
|
+
|
|
445
|
+
archived = read_module_inputs(module_id)
|
|
446
|
+
if archived and archived.get("tasklist"):
|
|
447
|
+
tasklist = Tasklist.from_json(archived["tasklist"])
|
|
448
|
+
print("流程来源:沿用 module_inputs 归档 tasklist", file=sys.stderr)
|
|
437
449
|
# 回退目标:显式直传;resume 缺省(None)由库解析为最新 tick 快照
|
|
438
450
|
# (须在 Module 构造前检查——构造即写 status.json idle,会覆盖前次终态)
|
|
439
451
|
rollback_to = args.rollback
|
|
@@ -469,7 +481,7 @@ def _run_resume_cmd(args: argparse.Namespace, *, require_target: bool) -> int:
|
|
|
469
481
|
mod = res.entry.build_module(
|
|
470
482
|
spec,
|
|
471
483
|
template_name=template_name,
|
|
472
|
-
tasklist=
|
|
484
|
+
tasklist=tasklist,
|
|
473
485
|
llm_client=llm_client,
|
|
474
486
|
module_id=module_id,
|
|
475
487
|
hooks=display.hooks(),
|
|
@@ -524,6 +536,29 @@ def _latest_run_id() -> str | None:
|
|
|
524
536
|
return max(dirs, key=lambda d: d.stat().st_mtime).name
|
|
525
537
|
|
|
526
538
|
|
|
539
|
+
def _cmd_control(args: argparse.Namespace) -> int:
|
|
540
|
+
"""cancel/pause/unpause 共享实现:写控制文件(运行进程 tick 边界消费)。
|
|
541
|
+
|
|
542
|
+
纯数据操作(file 即通道),不接触运行进程;前置只校验目标 run 存在
|
|
543
|
+
(status.json 落盘)。生效时机取决于运行进程的下一 tick 边界。
|
|
544
|
+
"""
|
|
545
|
+
from .control import request_control
|
|
546
|
+
|
|
547
|
+
module_id = args.run_id or _latest_run_id()
|
|
548
|
+
if module_id is None or not (
|
|
549
|
+
_persist_dir(module_id).parent / "status.json"
|
|
550
|
+
).exists():
|
|
551
|
+
print(f"无运行记录: {module_id or '(无任何运行)'}", file=sys.stderr)
|
|
552
|
+
return 1
|
|
553
|
+
try:
|
|
554
|
+
request_control(module_id, args.command, reason=getattr(args, "reason", None))
|
|
555
|
+
except ValueError as e:
|
|
556
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
557
|
+
return 1
|
|
558
|
+
print(f"已请求 {args.command}: {module_id}(运行进程将在下一 tick 边界生效)")
|
|
559
|
+
return 0
|
|
560
|
+
|
|
561
|
+
|
|
527
562
|
def _cmd_init(args: argparse.Namespace) -> int:
|
|
528
563
|
"""生成模块脚手架:--as-dir 目录形态(pack 同构)或单文件形态 + 项目文件补齐。"""
|
|
529
564
|
try:
|
|
@@ -1303,6 +1338,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1303
1338
|
p_checkpoint.add_argument("--run-id", help="运行 id(默认最近运行)")
|
|
1304
1339
|
p_checkpoint.set_defaults(func=_cmd_checkpoint)
|
|
1305
1340
|
|
|
1341
|
+
for _name, _help in (
|
|
1342
|
+
("cancel", "请求取消运行(协作式:下一 tick 边界生效,phase→cancelled)"),
|
|
1343
|
+
("pause", "请求暂停运行(tick 边界挂起,tick 计数不前进)"),
|
|
1344
|
+
("unpause", "释放暂停,运行继续"),
|
|
1345
|
+
):
|
|
1346
|
+
_p = sub.add_parser(_name, help=_help)
|
|
1347
|
+
_p.add_argument("--run-id", help="运行 id(默认最近运行)")
|
|
1348
|
+
if _name == "cancel":
|
|
1349
|
+
_p.add_argument("--reason", help="取消原因(透传 runner.cancel)")
|
|
1350
|
+
_p.set_defaults(func=_cmd_control)
|
|
1351
|
+
|
|
1306
1352
|
p_visualize = sub.add_parser(
|
|
1307
1353
|
"visualize", help="渲染 tasklist 对应图(mermaid 导出)"
|
|
1308
1354
|
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# module_harness/control.py
|
|
2
|
+
"""跨进程运行控制——控制文件协议(cancel/pause/unpause)。
|
|
3
|
+
|
|
4
|
+
status.json 的反向通道:status.json 把运行状态带出运行进程,control.json
|
|
5
|
+
把控制请求带进运行进程——文件即协议,任何消费端(CLI/Web/TUI)可写,
|
|
6
|
+
运行进程在 tick 边界协作式消费。零依赖、不触碰运行状态的单写者规则
|
|
7
|
+
(status.json 仍只由运行进程写)。
|
|
8
|
+
|
|
9
|
+
协议:``.specmodule/runs/<run_id>/control.json``,单发一次性请求::
|
|
10
|
+
|
|
11
|
+
{"action": "cancel" | "pause" | "unpause", "reason": str|null,
|
|
12
|
+
"requested_at": float}
|
|
13
|
+
|
|
14
|
+
- 写方:``request_control`` 原子写(tmp + os.replace,与 status.json 同款)。
|
|
15
|
+
- 读方:``control_tick_start`` / ``control_tick_end`` 工厂返回 hook 回调,
|
|
16
|
+
由 Module 注册到 runner(``control=False`` 关闭):
|
|
17
|
+
- **cancel 在 ``on_tick_end`` 消费**——引擎每个 tick 结束都会无条件重写
|
|
18
|
+
``runner.status``(RUNNING/IDLE),tick_start 期设置的 CANCELLED 终态
|
|
19
|
+
会被同 tick 末尾的赋值冲掉;tick_end hook 在赋值之后运行,此刻
|
|
20
|
+
``runner.cancel()`` 设置的终态能活到 ``run_until_idle`` 的下轮
|
|
21
|
+
terminal 检查,循环即停。请求写在 tick N 内 → N(或 N+1)末尾生效,
|
|
22
|
+
即取消有一 tick 延迟(当前 tick 内已开始的 firing 会跑完)。
|
|
23
|
+
- **pause 在 ``on_tick_start`` 挂起**——即将 fire 的 tick 不启动,tick
|
|
24
|
+
计数不前进(max_ticks 不消耗),轮询等待 unpause 或 cancel;挂起期间
|
|
25
|
+
保留文件(文件本身就是"暂停中"状态,监控方 ``read_control`` 可读)。
|
|
26
|
+
挂起中见到 cancel:不清除文件、直接放行——留给 tick_end 消费(同上
|
|
27
|
+
冲掉问题),当前 tick 跑完后停。
|
|
28
|
+
- 消费即删(delete-on-consume):动作执行后删除文件,防重放。
|
|
29
|
+
- 新执行清场:``Module.run()/resume()`` 开始时 ``clear_control``——启动新
|
|
30
|
+
执行即作废陈旧请求(进程崩溃残留的 pause 不会拖住下一次运行)。
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import asyncio
|
|
36
|
+
import json
|
|
37
|
+
import logging
|
|
38
|
+
import os
|
|
39
|
+
import time
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
log = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"ACTIONS",
|
|
47
|
+
"clear_control",
|
|
48
|
+
"control_path",
|
|
49
|
+
"control_tick_end",
|
|
50
|
+
"control_tick_start",
|
|
51
|
+
"read_control",
|
|
52
|
+
"request_control",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
#: 合法控制动作。
|
|
56
|
+
ACTIONS: tuple[str, ...] = ("cancel", "pause", "unpause")
|
|
57
|
+
|
|
58
|
+
#: pause 挂起期间的轮询间隔(秒)。
|
|
59
|
+
POLL_SECONDS = 0.5
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def control_path(module_id: str, base_dir: Path | None = None) -> Path:
|
|
63
|
+
"""控制文件路径:``<base_dir>/.specmodule/runs/<run_id>/control.json``。"""
|
|
64
|
+
return (base_dir or Path.cwd()) / ".specmodule" / "runs" / module_id / "control.json"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def read_control(module_id: str, base_dir: Path | None = None) -> dict[str, Any] | None:
|
|
68
|
+
"""读当前控制请求;无请求 / 文件损坏 / action 非法 → None(容错读)。"""
|
|
69
|
+
path = control_path(module_id, base_dir)
|
|
70
|
+
if not path.exists():
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
74
|
+
except (json.JSONDecodeError, OSError):
|
|
75
|
+
log.warning("control.json 损坏或不可读(忽略): %s", path)
|
|
76
|
+
return None
|
|
77
|
+
if not isinstance(data, dict) or data.get("action") not in ACTIONS:
|
|
78
|
+
log.warning("control.json 内容非法(忽略): %s", path)
|
|
79
|
+
return None
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def request_control(
|
|
84
|
+
module_id: str,
|
|
85
|
+
action: str,
|
|
86
|
+
*,
|
|
87
|
+
reason: str | None = None,
|
|
88
|
+
base_dir: Path | None = None,
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
"""写入控制请求(原子写)。action 非法 → ValueError。返回写入的请求。"""
|
|
91
|
+
if action not in ACTIONS:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"未知控制动作: {action!r}(可用: {'/'.join(ACTIONS)})"
|
|
94
|
+
)
|
|
95
|
+
req: dict[str, Any] = {
|
|
96
|
+
"action": action,
|
|
97
|
+
"reason": reason,
|
|
98
|
+
"requested_at": time.time(),
|
|
99
|
+
}
|
|
100
|
+
path = control_path(module_id, base_dir)
|
|
101
|
+
tmp = path.with_suffix(".json.tmp")
|
|
102
|
+
try:
|
|
103
|
+
tmp.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
tmp.write_text(json.dumps(req, ensure_ascii=False), encoding="utf-8")
|
|
105
|
+
os.replace(tmp, path)
|
|
106
|
+
except OSError:
|
|
107
|
+
log.exception("写 control.json 失败: %s", path)
|
|
108
|
+
raise
|
|
109
|
+
return req
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def clear_control(module_id: str, *, base_dir: Path | None = None) -> None:
|
|
113
|
+
"""删除控制文件(消费/清场)。缺失不报错;删除失败仅 log。"""
|
|
114
|
+
try:
|
|
115
|
+
control_path(module_id, base_dir).unlink(missing_ok=True)
|
|
116
|
+
except OSError:
|
|
117
|
+
log.exception("删除 control.json 失败(忽略)")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def control_tick_start(
|
|
121
|
+
runner: Any,
|
|
122
|
+
module_id: str,
|
|
123
|
+
*,
|
|
124
|
+
base_dir: Path | None = None,
|
|
125
|
+
poll: float = POLL_SECONDS,
|
|
126
|
+
):
|
|
127
|
+
"""工厂:注册到 ``runner.on_tick_start`` 的 async pause/unpause hook。
|
|
128
|
+
|
|
129
|
+
只处理 pause 挂起(cancel 统一由 ``control_tick_end`` 消费——见模块
|
|
130
|
+
docstring 的终态冲掉说明)。``runner`` 仅用于签名一致性(本 hook 不调
|
|
131
|
+
runner 方法);由 Module 在构建 runner 后注册。
|
|
132
|
+
"""
|
|
133
|
+
async def _on_tick_start(tick: int, fireable: list[str]) -> None:
|
|
134
|
+
req = read_control(module_id, base_dir=base_dir)
|
|
135
|
+
if req is None or req["action"] == "cancel":
|
|
136
|
+
# cancel 留给 tick_end(不清文件);无请求直接放行
|
|
137
|
+
return
|
|
138
|
+
if req["action"] != "pause":
|
|
139
|
+
return
|
|
140
|
+
# pause:tick 边界挂起;文件保留 = "暂停中"状态(监控方 read_control 可读)
|
|
141
|
+
while True:
|
|
142
|
+
await asyncio.sleep(poll)
|
|
143
|
+
nxt = read_control(module_id, base_dir=base_dir)
|
|
144
|
+
if nxt is None:
|
|
145
|
+
# 挂起期间文件被外部删除(人工清理)→ 视为释放
|
|
146
|
+
return
|
|
147
|
+
if nxt["action"] == "cancel":
|
|
148
|
+
# 不清除、直接放行——tick_end 消费(终态不被本 tick 冲掉)
|
|
149
|
+
return
|
|
150
|
+
if nxt["action"] == "unpause":
|
|
151
|
+
clear_control(module_id, base_dir=base_dir)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
return _on_tick_start
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def control_tick_end(
|
|
158
|
+
runner: Any,
|
|
159
|
+
module_id: str,
|
|
160
|
+
*,
|
|
161
|
+
base_dir: Path | None = None,
|
|
162
|
+
):
|
|
163
|
+
"""工厂:注册到 ``runner.on_tick_end`` 的 cancel 消费 hook。
|
|
164
|
+
|
|
165
|
+
在引擎每 tick 末尾的状态赋值**之后**运行——此刻 ``runner.cancel()``
|
|
166
|
+
设置的 CANCELLED 能活到 ``run_until_idle`` 下轮 terminal 检查。
|
|
167
|
+
"""
|
|
168
|
+
async def _on_tick_end(tick: int, firings: list) -> None:
|
|
169
|
+
req = read_control(module_id, base_dir=base_dir)
|
|
170
|
+
if req is None or req["action"] != "cancel":
|
|
171
|
+
return
|
|
172
|
+
runner.cancel(req.get("reason") or "cancelled")
|
|
173
|
+
clear_control(module_id, base_dir=base_dir)
|
|
174
|
+
|
|
175
|
+
return _on_tick_end
|
|
@@ -28,6 +28,7 @@ from .checkpoint import (
|
|
|
28
28
|
tasklist_from_dict,
|
|
29
29
|
tasklist_to_dict,
|
|
30
30
|
)
|
|
31
|
+
from .control import clear_control, control_tick_end, control_tick_start
|
|
31
32
|
|
|
32
33
|
log = logging.getLogger(__name__)
|
|
33
34
|
|
|
@@ -72,6 +73,7 @@ class Module:
|
|
|
72
73
|
keep_records: bool = True,
|
|
73
74
|
persist: bool = True,
|
|
74
75
|
status_file: bool = True,
|
|
76
|
+
control: bool = True,
|
|
75
77
|
modules: dict[str, Any] | None = None,
|
|
76
78
|
hooks: dict | None = None,
|
|
77
79
|
) -> None:
|
|
@@ -88,6 +90,9 @@ class Module:
|
|
|
88
90
|
# True(默认):写 .specmodule/runs/<module_id>/status.json
|
|
89
91
|
# (阶段级,跨进程查询通道);False:零残留(快速模式可用)
|
|
90
92
|
self.status_file = status_file
|
|
93
|
+
# True(默认):注册控制文件 hook(control.json → cancel/pause,
|
|
94
|
+
# 见 control.py)。跨进程取消/暂停的协作式通道;False 关闭。
|
|
95
|
+
self.control = control
|
|
91
96
|
self.review_result: ConsistencyReport | None = None
|
|
92
97
|
self.module_id = module_id or f"mod_{uuid.uuid4().hex[:8]}"
|
|
93
98
|
self._base_dir = base_dir or Path.cwd()
|
|
@@ -215,6 +220,17 @@ class Module:
|
|
|
215
220
|
_register(_cb)
|
|
216
221
|
else:
|
|
217
222
|
log.warning("Module hooks: 未知 runner hook '%s'(忽略)", _hook_name)
|
|
223
|
+
if self.control:
|
|
224
|
+
# 跨进程控制通道(control.json → cancel/pause):与用户 hooks
|
|
225
|
+
# 并存——runner 的 hook 注册表是 list,追加不覆盖。cancel 在
|
|
226
|
+
# tick_end 消费(tick_start 期设终态会被引擎同 tick 赋值冲掉)、
|
|
227
|
+
# pause 在 tick_start 挂起,见 control.py 模块 docstring。
|
|
228
|
+
runner.on_tick_start(
|
|
229
|
+
control_tick_start(runner, self.module_id, base_dir=self._base_dir)
|
|
230
|
+
)
|
|
231
|
+
runner.on_tick_end(
|
|
232
|
+
control_tick_end(runner, self.module_id, base_dir=self._base_dir)
|
|
233
|
+
)
|
|
218
234
|
return runner
|
|
219
235
|
|
|
220
236
|
# ------------------------------------------------------------------
|
|
@@ -337,6 +353,11 @@ class Module:
|
|
|
337
353
|
|
|
338
354
|
async def _run_with_phases(self, runner: AsyncRunner, max_ticks: int) -> list:
|
|
339
355
|
"""归档本次输入 → 运行 → 按结果映射终态 phase(run/resume 共用)。"""
|
|
356
|
+
if self.control:
|
|
357
|
+
# 新执行清场:作废陈旧控制请求(崩溃残留的 pause 不拖住新执行)。
|
|
358
|
+
# 位于写 running phase 之前——监控方看到 running 才放开控制按钮,
|
|
359
|
+
# 此时清场已完成,清场与首请求的竞态窗口关闭。
|
|
360
|
+
clear_control(self.module_id, base_dir=self._base_dir)
|
|
340
361
|
self._archive_module_inputs()
|
|
341
362
|
self._write_phase("running")
|
|
342
363
|
try:
|
|
@@ -296,6 +296,29 @@ def load_snapshot_summary(
|
|
|
296
296
|
return out
|
|
297
297
|
|
|
298
298
|
|
|
299
|
+
def read_module_inputs(
|
|
300
|
+
module_id: str, base_dir: Path | None = None
|
|
301
|
+
) -> dict[str, Any] | None:
|
|
302
|
+
"""读运行输入存档(module_inputs 表:本次 run 使用的 spec/tasklist)。
|
|
303
|
+
|
|
304
|
+
消费场景:resume/rollback 前端预填上次输入(换 spec/tasklist 重传的
|
|
305
|
+
编辑起点)。db 缺失 / 无存档 / 读失败 → None(查询容错,同上)。
|
|
306
|
+
"""
|
|
307
|
+
db_path = run_db_path(module_id, base_dir)
|
|
308
|
+
if not db_path.exists():
|
|
309
|
+
return None
|
|
310
|
+
from .checkpoint import ModuleInputStore
|
|
311
|
+
|
|
312
|
+
store = ModuleInputStore(module_id, base_dir)
|
|
313
|
+
try:
|
|
314
|
+
return store.load_module_inputs()
|
|
315
|
+
except Exception:
|
|
316
|
+
log.exception("读取 module_inputs 失败(返回 None): %s", db_path)
|
|
317
|
+
return None
|
|
318
|
+
finally:
|
|
319
|
+
store.close()
|
|
320
|
+
|
|
321
|
+
|
|
299
322
|
def timeline_to_dict(timeline: ReviewTimeline) -> dict[str, Any]:
|
|
300
323
|
"""JSON 出口(MCP/Web 直接消费同一函数)。"""
|
|
301
324
|
return {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|