Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ public CLI/import or serialized contracts. Retain public behavior tests; remove
only characterization scaffolding whose retired implementation has no consumer.
Deletion is code retirement, not deletion of users' state, receipts or backups.

### Permanent document IO separation

The durable text effects formerly defined in `todos/active_state_editing.py`
now live unchanged in `runtime/document_io.py`. This is retained Python Host IO,
not a new semantic owner or a Python-retirement count. The caller inventory is:

| Caller family | Retained obligation |
| --- | --- |
| Canonical Todo projection, completion validation store, team plan | Complete document/declaration publication, exclusive rebuild and durable retry; authority decisions remain typed |
| Project registry, source-session registration/registry/Turn effects, supervisor log | Atomic publication and file/directory durability with original identity/retry contracts |
| Bootstrap, runtime shadow writer, feedback, legacy state migration | Existing source/prose effects and upgrade recovery; supported source writers remain reachable |

Failure injection targets the new owner, including the embedded real recovery
probe. Real File/SQLite projection/replay and source writer tests retain their
authority, crash and no-duplicate-effect assertions. Removing the old three
definitions does not remove the editor's live read/edit helpers. Reverting this
package changes code ownership only, without a state conversion. Last source
writer/outbox exits, installed adoption, D2 and release-default qualification
remain separate acceptance boundaries.

### Merged T4 slice: unused Python lease/handoff facades

The caller audit at `e240730ec` led to #5395, merged at `8474c8d86`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ owner、持久兼容义务、正反例证据及回退方式,和不可变基线
内部删除必要但不充分,不能忽略公开 CLI/import 和序列化契约。保留公共行为测试,
只删没有消费者的旧实现专属 characterization。删的是代码,不是用户状态、回执和备份。

### 永久文档 IO 解耦

原 `todos/active_state_editing.py` 的三个持久化文本函数原样移入
`runtime/document_io.py`。这保留 Python Host IO,不新增语义 owner,也不计入
Python 退役收益。调用方清单如下:

| 调用家族 | 保留义务 |
| --- | --- |
| Canonical Todo 投影、completion validation store、team plan | 完整文档/声明落盘、排他重建和持久化重试;权威决策仍由 typed owner 负责 |
| Project registry、source-session registration/registry/Turn effects、supervisor 日志 | 原身份/重试契约下的原子发布及文件/目录持久化 |
| Bootstrap、runtime shadow writer、feedback、旧状态迁移 | 现有源/叙述写入和升级恢复;受支持的源 writer 仍可达 |

故障注入转向新 owner,包括嵌入的真实恢复 probe。真实 File/SQLite 投影/重放
和源 writer 测试继续保留权威、崩溃和副作用不重复的断言。删除三个旧定义不删除
编辑模块仍活跃的读取/编辑函数。回退本包只改变代码归属,无需数据转换。
最后源 writer/outbox 退出、安装采用、D2 和发布默认资格仍分别验收。

### 已合入的 T4 切片:已无调用方的 Python lease/handoff facade

在 `e240730ec` 核对调用方后,#5395 已于 `8474c8d86` 合入,退役了下列
Expand Down
19 changes: 19 additions & 0 deletions docs/reference/canonical-todo-completion-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,25 @@ transport plumbing; the TS edit decoder/materializer is no longer owned only
by the ordinary update transaction. Permanent rendering and private command
execution still have real Python callers and are not retirement candidates.

Durable text publication is owned by
`control_plane/runtime/document_io.py`, independently of Todo editing.
`atomic_write_state_text`, `verify_state_text_durable` and
`fsync_state_directory` keep their existing locking precondition, exact UTF-8
newlines, permissions, exclusive create, atomic replace and durability barriers.
Canonical projections and validator declarations, registry/session publication,
supervisor logs, feedback, migration and team-plan adapters use that same Host IO
owner. The old definitions in `todos/active_state_editing.py` are removed;
its live source editing/read helpers remain. Publication or readback failure
still propagates to the caller's existing recovery contract. This changes no
provider, default, format, authority policy or supported legacy upgrade route.

永久文本落盘由 `control_plane/runtime/document_io.py` 负责,解除与 Todo 编辑
模块的依赖。三个原函数保留锁前提、UTF-8 换行字节、权限、排他创建、原子替换和
文件/目录持久化屏障;投影、验证声明、registry/session、supervisor 日志、
feedback、迁移和 team-plan 调用方复用同一 Host IO owner。旧编辑模块只删除这
三个定义,仍活跃的源编辑/读取函数保留。故障仍进入原调用方恢复契约,不切换
provider、默认值、格式或权威策略,也不强制旧 Goal 升级。

Canonical Todo creation, update, completion, supersession and archive do not
import the Markdown line writer or source Todo capture producers during CLI
registration. Bootstrap loads capture producers only for a source-state write;
Expand Down
2 changes: 1 addition & 1 deletion loopx/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

from .control_plane.runtime.time import now_local_iso
from .control_plane.runtime.public_safety import public_safe_compact_text
from .control_plane.runtime.document_io import atomic_write_state_text
from .control_plane.todos.active_state_editing import (
TODO_SECTION_HEADINGS,
atomic_write_state_text,
insertion_anchor,
section_bounds,
)
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/agents/supervisor_event_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from ..effect_runtime import effect_runtime_result
from ..runtime.time import now_utc_iso
from ..todos.active_state_editing import atomic_write_state_text, verify_state_text_durable
from ..runtime.document_io import atomic_write_state_text, verify_state_text_durable

SUPERVISOR_EVENT_SCHEMA = "supervisor_log_event_v0"
LOCAL_PRIVATE_PRIVACY = "local_private"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def write_captured_todo_state(
) -> None:
"""Under the primary lock, prepare before replacement and mark only after durability."""

from ..todos.active_state_editing import atomic_write_state_text
from ..runtime.document_io import atomic_write_state_text

def write() -> None:
capture.prepare(text)
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/goals/source_session_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
source_session_registry_transaction,
)
from ..runtime.time import now_local_iso
from ..todos.active_state_editing import atomic_write_state_text
from ..runtime.document_io import atomic_write_state_text
from .source_session_registry_state import (
GOAL_INSTANCE_ID,
alias_digest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ...registry import atomic_write_json
from ..projects.registry_codec import SOURCE_SESSION_PROFILE_ID
from ..todos.active_state_editing import fsync_state_directory
from ..runtime.document_io import fsync_state_directory
from .goal_ref_validation import (
GOAL_INSTANCE_ID,
exact_goal_ref,
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/goals/source_session_turn_effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
SOURCE_SESSION_PROFILE_ID,
load_project_registry,
)
from ..todos.active_state_editing import fsync_state_directory
from ..runtime.document_io import fsync_state_directory
from .source_session_registry_state import (
alias_digest,
canonical_digest,
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/projects/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
resolve_runtime_root,
select_default_runtime_root,
)
from ..todos.active_state_editing import atomic_write_state_text as _atomic_write_text
from ..runtime.document_io import atomic_write_state_text as _atomic_write_text
from ..coordination.legacy_writer_fence import legacy_todo_write_transaction, require_legacy_state_replacement_allowed
from ..goals.source_session_services import (
FreshSourceSessionRegistration,
Expand Down
58 changes: 58 additions & 0 deletions loopx/control_plane/runtime/document_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Durable local document effects, independent of Todo editing or authority.

Callers own locking, admission and recovery policy. These Python Host IO
primitives preserve UTF-8 bytes, permissions and file/directory durability;
they neither choose an authority provider nor authorize a mutation.
"""
from __future__ import annotations

import os
import stat
import tempfile
from pathlib import Path


def atomic_write_state_text(path: Path, text: str, *, create_only: bool = False) -> None:
"""Persist complete UTF-8 state while the caller holds its sibling lock."""

path.parent.mkdir(parents=True, exist_ok=True)
mode = stat.S_IMODE(path.stat().st_mode) if not create_only and path.exists() else 0o600
descriptor, temporary = tempfile.mkstemp(
prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
)
temporary_path = Path(temporary)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle:
os.chmod(temporary_path, mode)
handle.write(text)
handle.flush()
os.fsync(handle.fileno())
if create_only:
os.link(temporary_path, path)
else:
os.replace(temporary_path, path)
fsync_state_directory(path)
finally:
temporary_path.unlink(missing_ok=True)


def fsync_state_directory(path: Path) -> None:
if os.name == "posix":
parent = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(parent)
finally:
os.close(parent)


def verify_state_text_durable(path: Path, text: str) -> None:
"""Re-establish durability after a previous publish may have failed at fsync.

Equality of visible bytes alone does not prove the directory entry persisted.
The caller holds the same document lock as the state writer.
"""
with path.open("r+" if os.name == "nt" else "r", encoding="utf-8", newline="") as handle:
if handle.read() != text:
raise RuntimeError("Todo Markdown projection readback mismatch")
os.fsync(handle.fileno())
fsync_state_directory(path)
6 changes: 3 additions & 3 deletions loopx/control_plane/testing/authority_e2e_rows_stage2c2.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
from loopx.cli import main
from loopx.control_plane.coordination import local_authority_shadow_adapter as adapter
from loopx.control_plane.coordination import local_authority_shadow_outbox as outbox
from loopx.control_plane.todos import active_state_editing
from loopx.control_plane.runtime import document_io
window, state = sys.argv[1], pathlib.Path(sys.argv[2])
def pause(payload=None):
print('BARRIER ' + json.dumps(payload or {}), flush=True)
Expand Down Expand Up @@ -139,14 +139,14 @@ def write_json(path, value):
if window == 'before_marker' and path.name.endswith('.committed.json'): pause()
return actual_json(path, value)
outbox.durable_write_json = write_json
actual_replace = active_state_editing.os.replace
actual_replace = document_io.os.replace
def replace(source, target):
is_primary = pathlib.Path(target) == state
if is_primary and window == 'before_replace': pause()
result = actual_replace(source, target)
if is_primary and window == 'after_replace': pause()
return result
active_state_editing.os.replace = replace
document_io.os.replace = replace
actual_unlink = pathlib.Path.unlink
def unlink(path, *args, **kwargs):
result = actual_unlink(path, *args, **kwargs)
Expand Down
49 changes: 0 additions & 49 deletions loopx/control_plane/todos/active_state_editing.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
from __future__ import annotations

import os
import re
import stat
import tempfile
from pathlib import Path
from typing import Any

Expand All @@ -29,52 +26,6 @@
COMPLETED_WORK_ARCHIVE_HEADING = "Completed Work Archive"


def atomic_write_state_text(path: Path, text: str, *, create_only: bool = False) -> None:
"""Persist complete UTF-8 state while the caller holds its sibling lock."""

path.parent.mkdir(parents=True, exist_ok=True)
mode = stat.S_IMODE(path.stat().st_mode) if not create_only and path.exists() else 0o600
descriptor, temporary = tempfile.mkstemp(
prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
)
temporary_path = Path(temporary)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle:
os.chmod(temporary_path, mode)
handle.write(text)
handle.flush()
os.fsync(handle.fileno())
if create_only:
os.link(temporary_path, path)
else:
os.replace(temporary_path, path)
fsync_state_directory(path)
finally:
temporary_path.unlink(missing_ok=True)


def fsync_state_directory(path: Path) -> None:
if os.name == "posix":
parent = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(parent)
finally:
os.close(parent)


def verify_state_text_durable(path: Path, text: str) -> None:
"""Re-establish durability after a previous publish may have failed at fsync.

Equality of visible bytes alone does not prove the directory entry persisted.
The caller holds the same document lock as the state writer.
"""
with path.open("r+" if os.name == "nt" else "r", encoding="utf-8", newline="") as handle:
if handle.read() != text:
raise RuntimeError("Todo Markdown projection readback mismatch")
os.fsync(handle.fileno())
fsync_state_directory(path)


def section_bounds(lines: list[str], role: str) -> tuple[int, int, str] | None:
region = next((region for region in find_todo_source_regions(lines) if region.role == role), None)
return (region.start, region.body_end, region.heading) if region else None
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/todos/completion_validation_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from ...registry import atomic_write_json, read_json
from ...file_lock import exclusive_cross_runtime_file_lock
from .active_state_editing import fsync_state_directory
from ..runtime.document_io import fsync_state_directory
from .completion_validation_projection import (
completion_validation_declaration,
completion_validation_declaration_sha256,
Expand Down
2 changes: 1 addition & 1 deletion loopx/control_plane/todos/provider_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
render_canonical_todo_sections,
)
from .completion_validation_store import load_completion_validation_declarations
from .active_state_editing import atomic_write_state_text, verify_state_text_durable
from ..runtime.document_io import atomic_write_state_text, verify_state_text_durable
from .projection_document import recovered_todo_projection_skeleton


Expand Down
3 changes: 2 additions & 1 deletion loopx/control_plane/work_items/team_plan_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
settle_todo_runtime_shadow_capture,
)
from ..todos.path_resolution import resolve_todo_state_path
from ..runtime.document_io import verify_state_text_durable
from ..todos.active_state_editing import (
insert_into_existing_section, insert_new_section, section_bounds,
verify_state_text_durable, replace_updated_at,
replace_updated_at,
)
from ..todos.contract import (
TODO_ACTION_KIND_ADVANCEMENT_VALUES, format_todo_metadata_line,
Expand Down
2 changes: 1 addition & 1 deletion loopx/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from .file_lock import exclusive_cross_runtime_file_lock
from .control_plane.coordination.runtime_shadow_writer_adapter import require_prose_state_write_allowed
from .control_plane.todos.active_state_editing import atomic_write_state_text
from .control_plane.runtime.document_io import atomic_write_state_text

import json
import re
Expand Down
4 changes: 2 additions & 2 deletions loopx/semantics/project_registry_io_manifest_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -1791,15 +1791,15 @@
},
{
"site": "loopx/control_plane/work_items/team_plan_adapter.py::<module>.apply_team_plan::codec_read:load_registry#1",
"line": 139,
"line": 140,
"column": 31,
"kind": "codec_read",
"api": "load_registry",
"classification": "codec_api"
},
{
"site": "loopx/control_plane/work_items/team_plan_adapter.py::<module>.team_plan_state_fingerprint::codec_read:load_registry#1",
"line": 78,
"line": 79,
"column": 31,
"kind": "codec_read",
"api": "load_registry",
Expand Down
2 changes: 1 addition & 1 deletion loopx/state_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
project_registry_transaction,
require_runtime_compatible_project_registry,
)
from .control_plane.todos.active_state_editing import atomic_write_state_text
from .control_plane.runtime.document_io import atomic_write_state_text
from .control_plane.coordination.legacy_writer_fence import legacy_coordination_todo_lock_path, require_legacy_state_replacement_allowed
from .control_plane.work_items.task_lease import task_lease_lock_path
from .registry import registry_goals
Expand Down
Loading
Loading