From af8a3712e27ed7443ec5e6f8506a9dae18e00c54 Mon Sep 17 00:00:00 2001 From: Nam Pham Dinh Thanh Date: Wed, 26 Aug 2026 11:38:44 +0900 Subject: [PATCH] =?UTF-8?q?fix(co4e):=204=20ch=E1=BB=97=20ghi=20JSON=20c?= =?UTF-8?q?=E1=BB=A7a=20Gamma=20=C4=91i=20qua=20AtomicJsonFile=20=E2=80=94?= =?UTF-8?q?=20ti=C3=AAu=20ch=C3=AD=20nghi=E1=BB=87m=20thu=20A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Soát lại plan.md thì thấy CASAN là NĂM tiêu chí C-A-S-A-N, không phải ba. Tiêu chí A có hai vế, tôi mới đạt vế đầu: vế 1 0 API key plaintext trong JSON -> đã đạt từ 25/08 vế 2 MỌI thao tác ghi tệp đi qua AtomicJsonFile -> CHƯA Toàn repo còn 15 chỗ ghi JSON thẳng. Bốn trong đó là của Gamma (vùng Co4E): core/co4e.py:236 lưu workflow ghi thẳng, không nguyên tử gì cả core/co4e.py:310 lưu agent ghi thẳng core/co4e_run_manager.py:156 tmp + replace tự viết application/workflows/co4e_workflow_service.py:178 tmp + replace tự viết Hai chỗ đầu nguy hơn: tắt máy giữa lúc lưu là mất luôn workflow hoặc agent. Hai chỗ sau nhìn thì có vẻ ổn vì đã tmp + replace, nhưng thiếu hai thứ: * không fsync — dữ liệu có thể còn nằm trong bộ đệm ổ đĩa khi mất điện, nên "nguyên tử" chỉ đúng với crash tiến trình, không đúng với mất điện; * dùng thẳng Path.replace, đúng chỗ dính PermissionError [WinError 5] mà tôi vá hôm 25/08 — Defender giữ handle file vừa tạo. Tần suất đo được khoảng 1/140 lần lưu, nhân với số lần lưu lịch sử chạy flow. 11 chỗ còn lại thuộc team khác (accounts, admin_agents, custom_agents, flows, groups, history, projects, skills, tasks). Không đụng vào; cần báo lên vì tiêu chí A là tiêu chí TOÀN DỰ ÁN, Gamma sạch không cứu được cổng. Đã kiểm application/ vẫn không kéo PySide6 vào sau khi thêm import mới (tiêu chí C). 714 test xanh, 24/24 checker qua. Co-Authored-By: Claude Opus 5 --- application/workflows/co4e_workflow_service.py | 10 ++++++---- core/co4e.py | 8 ++++++-- core/co4e_run_manager.py | 10 ++++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/application/workflows/co4e_workflow_service.py b/application/workflows/co4e_workflow_service.py index 549c18e..3b2d3cb 100644 --- a/application/workflows/co4e_workflow_service.py +++ b/application/workflows/co4e_workflow_service.py @@ -35,6 +35,8 @@ service này. """ from __future__ import annotations +from ...infrastructure.persistence.json.atomic_json_file import AtomicJsonFile + import json import os from datetime import datetime @@ -174,10 +176,10 @@ class Co4EWorkflowService: payload = {"runs": [r.to_dict() for r in runs]} try: self._history_path_value.parent.mkdir(parents=True, exist_ok=True) - tmp = self._history_path_value.with_suffix(".json.tmp") - tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), - encoding="utf-8") - tmp.replace(self._history_path_value) # atomic — khong bao gio de lai file ghi do dang + # AtomicJsonFile thay cho tmp+replace tự viết: bản cũ thiếu fsync + # (dữ liệu có thể còn trong bộ đệm khi mất điện) và dùng thẳng + # Path.replace, vốn thỉnh thoảng bị Defender từ chối trên Windows. + AtomicJsonFile(self._history_path_value).write(payload) except OSError: # Giu dung hanh vi cu (core/co4e_run_manager.py::_save_history): # mot lan luu that bai (day dia, mat quyen...) KHONG duoc phep diff --git a/core/co4e.py b/core/co4e.py index e0337c4..46e3682 100644 --- a/core/co4e.py +++ b/core/co4e.py @@ -18,6 +18,8 @@ existing ``core/skills.py`` registry. """ from __future__ import annotations +from ..infrastructure.persistence.json.atomic_json_file import AtomicJsonFile + import json from dataclasses import asdict, dataclass, field from pathlib import Path @@ -233,7 +235,9 @@ def save_workflow(wf: Workflow, directory: Optional[Path] = None) -> Path: directory = directory or WORKFLOWS_DIR directory.mkdir(parents=True, exist_ok=True) path = directory / f"{wf.id}.json" - path.write_text(json.dumps(workflow_to_dict(wf), ensure_ascii=False, indent=2), encoding="utf-8") + # Tiêu chí nghiệm thu A: mọi thao tác ghi tệp đi qua AtomicJsonFile. Trước + # đây ghi thẳng, nên tắt máy giữa lúc lưu là mất luôn workflow. + AtomicJsonFile(path).write(workflow_to_dict(wf)) return path @@ -307,7 +311,7 @@ def save_custom_agent(agent: CustomAgent, directory: Optional[Path] = None) -> P directory = directory or AGENTS_DIR directory.mkdir(parents=True, exist_ok=True) path = directory / f"{agent.id}.json" - path.write_text(json.dumps(agent_to_dict(agent), ensure_ascii=False, indent=2), encoding="utf-8") + AtomicJsonFile(path).write(agent_to_dict(agent)) return path diff --git a/core/co4e_run_manager.py b/core/co4e_run_manager.py index c92662f..1695401 100644 --- a/core/co4e_run_manager.py +++ b/core/co4e_run_manager.py @@ -13,6 +13,8 @@ the run that is currently open. """ from __future__ import annotations +from ..infrastructure.persistence.json.atomic_json_file import AtomicJsonFile + import json from pathlib import Path from typing import Dict, List, Optional @@ -152,10 +154,10 @@ class Co4ERunManager(QObject): payload = {"runs": [h.to_record() for h in runs]} try: path.parent.mkdir(parents=True, exist_ok=True) - tmp = path.with_suffix(".json.tmp") - tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), - encoding="utf-8") - tmp.replace(path) # atomic — never leaves a half-written file + # AtomicJsonFile thay cho tmp+replace tự viết: bản cũ thiếu fsync + # (dữ liệu có thể còn trong bộ đệm khi mất điện) và dùng thẳng + # Path.replace, vốn thỉnh thoảng bị Defender từ chối trên Windows. + AtomicJsonFile(path).write(payload) except OSError: pass