fix: isolate agent sessions and retain runtime resource ownership (#7149)

* fix: arbitrate subscription identity and restore workflow state

* fix: isolate agent sessions and retain runtime resource ownership

* test: preserve legacy empty-owner subscription transactions

* test: handle reaped terminal descendants during shutdown polling

* fix: align shared atomic configuration method with lifecycle prerequisite

* test: cover subagent cancellation and completion lifecycle boundaries

---------

Co-authored-by: jxxghp <jxxghp@gmail.com>
This commit is contained in:
Aqr-K
2026-10-23 08:20:59 +08:00
committed by GitHub
parent 951cdc5b27
commit 7584d4b9fc
33 changed files with 1345 additions and 88 deletions

View File

@@ -5,7 +5,25 @@ Agent 文件写入工具的共享辅助函数。
import hashlib
import os
import tempfile
import threading
from pathlib import Path
from weakref import WeakValueDictionary
_write_locks: WeakValueDictionary[str, threading.Lock] = WeakValueDictionary()
_write_locks_guard = threading.Lock()
def _write_lock(path: Path) -> threading.Lock:
"""
为同一规范目标保留进程内互斥,闲置后自动回收路径锁。
"""
key = os.path.normcase(str(path.resolve()))
with _write_locks_guard:
lock = _write_locks.get(key)
if lock is None:
lock = threading.Lock()
_write_locks[key] = lock
return lock
class FileVersionConflictError(RuntimeError):
@@ -29,9 +47,26 @@ def atomic_write_text(
path: Path,
content: str,
expected_sha256: str | None = None,
) -> None:
*,
overwrite: bool = True,
) -> str:
"""
校验目标版本后,在同目录写入临时文件并原子替换文本。
在同目标写锁内校验版本并原子替换,返回本次提交内容的SHA256。
:param path: 已通过权限检查的目标路径
:param content: 本次完整文本内容
:param expected_sha256: 当前版本摘要;None表示未提供版本前提
:param overwrite: False使用原子排他创建,已有目标不得替换
:return: 本次实际提交文本的SHA256,不重新读取可能被后续调用修改的文件
:raises FileVersionConflictError: 版本变化或排他创建时目标已存在
"""
with _write_lock(path):
return _write_text_locked(path, content, expected_sha256, overwrite=overwrite)
def _write_text_locked(path: Path, content: str, expected_sha256: str | None, *, overwrite: bool) -> str:
"""
在已持有目标锁时写入临时文件,禁止覆盖使用原子link拒绝已存在目标。
"""
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temp_name = tempfile.mkstemp(
@@ -55,7 +90,14 @@ def atomic_write_text(
raise FileVersionConflictError(str(path))
if path.exists():
os.chmod(temp_path, path.stat().st_mode)
os.replace(temp_path, path)
if overwrite:
os.replace(temp_path, path)
else:
try:
os.link(temp_path, path)
except FileExistsError as error:
raise FileVersionConflictError(str(path)) from error
return hashlib.sha256(content.encode("utf-8")).hexdigest()
finally:
if temp_path.exists():
temp_path.unlink()

View File

@@ -348,11 +348,9 @@ class ApplyPatchTool(MoviePilotTool):
f"错误:文件 {resolved_path} 在应用补丁期间被创建,拒绝覆盖。"
"请确认文件状态后重新应用补丁。"
)
await self.run_blocking(
"default", atomic_write_text, local_path, new_content, sha256
)
new_sha256 = await self.run_blocking(
"default", calculate_file_sha256, local_path
"default", atomic_write_text, local_path, new_content, sha256,
overwrite=file_patch.operation != "add",
)
verb = "新增" if file_patch.operation == "add" else "更新"
results.append(f"{verb} {resolved_path}(sha256={new_sha256})")

View File

@@ -151,16 +151,13 @@ class EditFileTool(MoviePilotTool):
new_text,
-1 if replace_all else 1,
)
await self.run_blocking(
new_sha256 = await self.run_blocking(
"default",
atomic_write_text,
local_path,
new_content,
current_sha256,
)
new_sha256 = await self.run_blocking(
"default", calculate_file_sha256, local_path
)
logger.info(
f"成功编辑文件 {resolved_path},替换了 {replacement_count} 处内容"

View File

@@ -224,7 +224,7 @@ class ViewImageTool(MoviePilotTool):
if has_file_path:
resolved_path, access_error = await self._check_local_file_access(
clean_file_path,
operation="读取图片",
operation="读取",
)
if access_error:
return self._failure("local_path_access_denied", access_error, source_type)

View File

@@ -129,15 +129,13 @@ class WriteFileTool(MoviePilotTool):
"请重新读取文件并基于最新内容写入。"
)
await self.run_blocking(
new_sha256 = await self.run_blocking(
"default",
atomic_write_text,
local_path,
content,
current_sha256,
)
new_sha256 = await self.run_blocking(
"default", calculate_file_sha256, local_path
overwrite=overwrite,
)
logger.info(f"成功写入文件 {resolved_path}")