feat(loop): complete Issue #5 Phase 4 immutable runtime and freeze removal
- Implement eager module loading (lib_py.loop.eager) to preload all framework dependencies into sys.modules and enforce PYTHONDONTWRITEBYTECODE=1. - Synchronize runtime protocol version with MAM_VERSION constant (4.3.0). - Bypass /tmp/mam-loop-freeze.XXXXXX copy overhead by default in Python mode while preserving legacy rollback fallback (MAM_LOOP_PY=0 / MAM_LOOP_FREEZE=1). - Add tests/test_issue5_phase4_runtime.py verifying eager import completeness, mid-flight code modification immunity (B-13), and freeze bypass. - Pass full test suite (749 tests, 0 failures).
This commit is contained in:
1 parent
8cd256cc7a
commit
f413f6ada6
6 files changed
+390
-11
No files matched your search
@@ -0,0 +1,124 @@
|
||||
"""Eager module loading for immutable runtime immunity (Issue #5 Phase 4).
|
||||
|
||||
Ensures all dependencies (loop, delegation, agents, external packages)
|
||||
are loaded into sys.modules at loop startup, preventing mid-flight disk
|
||||
modifications from triggering lazy-import syntax/EOF errors (B-13).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Sequence
|
||||
|
||||
# Framework version locked with framework constants
|
||||
FRAMEWORK_VERSION = "4.3.0"
|
||||
|
||||
CORE_MODULES: Sequence[str] = (
|
||||
# Standard library modules
|
||||
"atexit",
|
||||
"argparse",
|
||||
"copy",
|
||||
"glob",
|
||||
"hashlib",
|
||||
"json",
|
||||
"os",
|
||||
"pathlib",
|
||||
"re",
|
||||
"shutil",
|
||||
"signal",
|
||||
"sqlite3",
|
||||
"subprocess",
|
||||
"sys",
|
||||
"tempfile",
|
||||
"time",
|
||||
"typing",
|
||||
# Framework loop modules
|
||||
"lib_py.loop.parser",
|
||||
"lib_py.loop.checkpoint",
|
||||
"lib_py.loop.runner",
|
||||
"lib_py.loop.attribution",
|
||||
"lib_py.loop.autocomplete",
|
||||
"lib_py.loop.evidence",
|
||||
"lib_py.loop.policy",
|
||||
"lib_py.loop.shadow",
|
||||
"lib_py.loop.store",
|
||||
"lib_py.loop.watcher",
|
||||
# Delegation and common modules
|
||||
"lib_py.paths",
|
||||
"lib_py.state",
|
||||
"lib_py.atomic_yaml",
|
||||
"lib_py.workspace_uuid",
|
||||
"lib_py.verify_session",
|
||||
"lib_py.layout",
|
||||
"lib_py.delegation.engine",
|
||||
"lib_py.delegation.finalize",
|
||||
"lib_py.delegation.job_subscriber",
|
||||
"lib_py.delegation.mqtt_common",
|
||||
"lib_py.delegation.notifier",
|
||||
"lib_py.delegation.publish_event",
|
||||
"lib_py.delegation.publisher",
|
||||
"lib_py.delegation.registry",
|
||||
"lib_py.delegation.service",
|
||||
"lib_py.delegation.cli",
|
||||
# Agent adapter registry
|
||||
"lib_py.agents.base",
|
||||
"lib_py.agents.registry",
|
||||
"lib_py.agents.input_region",
|
||||
"lib_py.agents.sanitize",
|
||||
)
|
||||
|
||||
OPTIONAL_MODULES: Sequence[str] = (
|
||||
"yaml",
|
||||
"paho.mqtt",
|
||||
"paho.mqtt.client",
|
||||
)
|
||||
|
||||
|
||||
def verify_framework_version(expected_version: str = FRAMEWORK_VERSION) -> None:
|
||||
"""Verifies that MAM_VERSION matches framework version if set."""
|
||||
env_version = os.environ.get("MAM_VERSION")
|
||||
if env_version and env_version != expected_version:
|
||||
raise RuntimeError(
|
||||
f"MAM_VERSION mismatch: expected {expected_version}, got {env_version} in environment."
|
||||
)
|
||||
|
||||
|
||||
def eager_load(
|
||||
modules: Sequence[str] = CORE_MODULES,
|
||||
optional_modules: Sequence[str] = OPTIONAL_MODULES,
|
||||
) -> List[str]:
|
||||
"""Eagerly imports all modules into sys.modules.
|
||||
|
||||
Returns the list of successfully loaded module names.
|
||||
"""
|
||||
verify_framework_version()
|
||||
|
||||
# Ensure PYTHONDONTWRITEBYTECODE is set for isolation
|
||||
sys.dont_write_bytecode = True
|
||||
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
|
||||
|
||||
loaded: List[str] = []
|
||||
for mod_name in modules:
|
||||
if mod_name not in sys.modules:
|
||||
try:
|
||||
importlib.import_module(mod_name)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"Failed to eager load {mod_name}: {exc}") from exc
|
||||
loaded.append(mod_name)
|
||||
|
||||
for opt_mod in optional_modules:
|
||||
if opt_mod not in sys.modules:
|
||||
try:
|
||||
importlib.import_module(opt_mod)
|
||||
loaded.append(opt_mod)
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass
|
||||
|
||||
return loaded
|
||||
|
||||
|
||||
def is_eager_loaded(modules: Sequence[str] = CORE_MODULES) -> bool:
|
||||
"""Returns True if all required core modules are present in sys.modules."""
|
||||
return all(mod in sys.modules for mod in modules)
|
||||
@@ -8,6 +8,7 @@ while using fence-aware strict verdict parsing and atomic checkpointing.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
@@ -21,7 +22,7 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from lib_py.loop import checkpoint, parser
|
||||
from lib_py.loop import checkpoint, eager, parser
|
||||
|
||||
# ANSI colors matching run_loop.sh
|
||||
_COLOR_INFO = "\033[1;34m[*]\033[0m"
|
||||
@@ -50,6 +51,9 @@ class LoopRunner:
|
||||
"""Orchestrates planning, code implementation, and multi-agent peer reviews."""
|
||||
|
||||
def __init__(self, argv: Optional[List[str]] = None):
|
||||
# Eagerly load all required framework modules to ensure immutable runtime (Phase 4)
|
||||
eager.eager_load()
|
||||
|
||||
self.raw_argv = list(argv) if argv is not None else sys.argv[1:]
|
||||
self.workspace_root = Path(os.environ.get("MAM_REAL_ROOT") or os.environ.get("WORKSPACE_ROOT") or Path.cwd()).resolve()
|
||||
self.mam_dir = self.workspace_root / ".mam"
|
||||
@@ -372,7 +376,6 @@ class LoopRunner:
|
||||
def run(self) -> int:
|
||||
self.parse_args()
|
||||
|
||||
import atexit
|
||||
atexit.register(self.release_guard)
|
||||
|
||||
# Signal handling
|
||||
|
||||
@@ -112,11 +112,19 @@ if [ -z "${AGENT_PYTHON_BIN:-}" ]; then
|
||||
fi
|
||||
|
||||
# --- B-13 Stage 2: freeze the runtime before the loop can be edited under us ---
|
||||
# bash keeps reading a running script from disk by byte offset, so a worker that
|
||||
# edits .agents/skills/ mid-loop can break this very file (measured: even a valid
|
||||
# replacement died with "unexpected EOF"). Re-exec once from a snapshot.
|
||||
# NOTE: log_* are not defined until :114 — use echo here, not log_warn (C2).
|
||||
if [ -z "${MAM_LOOP_FREEZE_DIR:-}" ] && [ "${MAM_LOOP_NO_FREEZE:-0}" != "1" ]; then
|
||||
# In Python mode (MAM_LOOP_PY=1, default), the Python process holds all modules in memory
|
||||
# (eager import), providing immunity against mid-flight file mutations without disk copying.
|
||||
# The legacy freeze snapshot (/tmp/mam-loop-freeze.XXXXXX) is only activated when
|
||||
# legacy shell mode (MAM_LOOP_PY=0) is selected or freeze is explicitly forced (MAM_LOOP_FREEZE=1).
|
||||
# NOTE: log_* are not defined until :163 — use echo here, not log_warn (C2).
|
||||
_should_freeze=0
|
||||
if [ "${MAM_LOOP_NO_FREEZE:-0}" != "1" ] && [ -z "${MAM_LOOP_FREEZE_DIR:-}" ]; then
|
||||
if [ "${MAM_LOOP_FREEZE:-0}" = "1" ] || [ "${MAM_LOOP_PY:-1}" = "0" ]; then
|
||||
_should_freeze=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$_should_freeze" = "1" ]; then
|
||||
_freeze=""
|
||||
_freeze="$(mktemp -d "${TMPDIR:-/tmp}/mam-loop-freeze.XXXXXX" 2>/dev/null)" || _freeze=""
|
||||
if [ -n "$_freeze" ] && mkdir -p "$_freeze/.agents" 2>/dev/null \
|
||||
|
||||
Reference in new issue
Block a user