@trac3er/oh-my-god 2.0.0-beta.2 → 2.1.0-alpha
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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/OMG-setup.sh +33 -0
- package/README.md +43 -0
- package/claude_experimental/__init__.py +27 -0
- package/claude_experimental/_compat.py +12 -0
- package/claude_experimental/_degradation.py +210 -0
- package/claude_experimental/_flags.py +35 -0
- package/claude_experimental/_lifecycle.py +122 -0
- package/claude_experimental/integration/__init__.py +16 -0
- package/claude_experimental/integration/_placeholder.py +7 -0
- package/claude_experimental/integration/autotuner.py +182 -0
- package/claude_experimental/integration/checkpoints.py +327 -0
- package/claude_experimental/integration/experiments.py +302 -0
- package/claude_experimental/integration/openapi_gen.py +428 -0
- package/claude_experimental/integration/streaming.py +131 -0
- package/claude_experimental/integration/telemetry.py +287 -0
- package/claude_experimental/memory/__init__.py +16 -0
- package/claude_experimental/memory/_placeholder.py +7 -0
- package/claude_experimental/memory/api.py +434 -0
- package/claude_experimental/memory/augmented_generation.py +142 -0
- package/claude_experimental/memory/episodic.py +190 -0
- package/claude_experimental/memory/failure_learning.py +183 -0
- package/claude_experimental/memory/migrate.py +169 -0
- package/claude_experimental/memory/procedural.py +212 -0
- package/claude_experimental/memory/semantic.py +376 -0
- package/claude_experimental/memory/store.py +310 -0
- package/claude_experimental/parallel/__init__.py +17 -0
- package/claude_experimental/parallel/_placeholder.py +7 -0
- package/claude_experimental/parallel/aggregation.py +131 -0
- package/claude_experimental/parallel/api.py +244 -0
- package/claude_experimental/parallel/executor.py +110 -0
- package/claude_experimental/parallel/ralph_bridge.py +164 -0
- package/claude_experimental/parallel/sandbox.py +314 -0
- package/claude_experimental/parallel/scaling.py +171 -0
- package/claude_experimental/parallel/ultraworker.py +285 -0
- package/claude_experimental/patterns/__init__.py +16 -0
- package/claude_experimental/patterns/_placeholder.py +7 -0
- package/claude_experimental/patterns/antipatterns.py +454 -0
- package/claude_experimental/patterns/api.py +196 -0
- package/claude_experimental/patterns/extractor.py +163 -0
- package/claude_experimental/patterns/mining.py +219 -0
- package/claude_experimental/patterns/refactoring.py +234 -0
- package/hooks/_common.py +3 -1
- package/package.json +1 -1
- package/runtime/subagent_dispatcher.py +109 -9
- package/settings.json +6 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""AutoTuner — Automatic parameter tuning from telemetry signals.
|
|
2
|
+
|
|
3
|
+
Reads P99 latency and pool utilization from TelemetryCollector,
|
|
4
|
+
adjusts DynamicPool worker count using simple threshold-based rules:
|
|
5
|
+
- Scale up: P99 > 2× target_p99_ms → increase workers (up to max_workers)
|
|
6
|
+
- Scale down: utilization < 30% for >30s → decrease workers (down to min_workers)
|
|
7
|
+
|
|
8
|
+
All tuning decisions are logged as ``autotuner_adjustment`` counter metrics.
|
|
9
|
+
|
|
10
|
+
Feature-gated via OMG_ADVANCED_INTEGRATION_ENABLED.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
import time
|
|
16
|
+
from typing import TYPE_CHECKING, cast
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from claude_experimental.integration.telemetry import TelemetryCollector
|
|
20
|
+
from claude_experimental.parallel.scaling import DynamicPool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AutoTuner:
|
|
24
|
+
"""Automatic parameter tuning from telemetry signals.
|
|
25
|
+
|
|
26
|
+
Monitors P99 latency of ``task_duration_ms`` histogram metrics and
|
|
27
|
+
pool utilization, adjusting DynamicPool worker count with simple
|
|
28
|
+
threshold rules. No Bayesian optimization — just reliable thresholds.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
pool: DynamicPool instance to tune.
|
|
32
|
+
telemetry: TelemetryCollector for reading metrics and logging decisions.
|
|
33
|
+
min_workers: Minimum worker count floor (default 1).
|
|
34
|
+
max_workers: Maximum worker count ceiling (default 8).
|
|
35
|
+
target_p99_ms: Target P99 latency in milliseconds (default 5000).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_SUSTAINED_LOW_UTIL_SECONDS: float = 30.0
|
|
39
|
+
_LOW_UTIL_THRESHOLD: float = 0.30 # 30%
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
pool: "DynamicPool",
|
|
44
|
+
telemetry: "TelemetryCollector",
|
|
45
|
+
min_workers: int = 1,
|
|
46
|
+
max_workers: int = 8,
|
|
47
|
+
target_p99_ms: float = 5000,
|
|
48
|
+
) -> None:
|
|
49
|
+
from claude_experimental.integration import _require_enabled
|
|
50
|
+
|
|
51
|
+
_require_enabled()
|
|
52
|
+
|
|
53
|
+
if min_workers < 1:
|
|
54
|
+
raise ValueError("min_workers must be >= 1")
|
|
55
|
+
if max_workers < min_workers:
|
|
56
|
+
raise ValueError("max_workers must be >= min_workers")
|
|
57
|
+
if target_p99_ms <= 0:
|
|
58
|
+
raise ValueError("target_p99_ms must be > 0")
|
|
59
|
+
|
|
60
|
+
self._pool = pool
|
|
61
|
+
self._telemetry = telemetry
|
|
62
|
+
self._min_workers = min_workers
|
|
63
|
+
self._max_workers = max_workers
|
|
64
|
+
self._target_p99_ms = target_p99_ms
|
|
65
|
+
|
|
66
|
+
# Hysteresis for sustained low-utilization scale-down
|
|
67
|
+
self._low_util_since: float | None = None
|
|
68
|
+
|
|
69
|
+
def tune_cycle(self) -> dict[str, int]:
|
|
70
|
+
"""Run one tuning cycle.
|
|
71
|
+
|
|
72
|
+
Reads P99 latency and pool utilization, applies scaling rules,
|
|
73
|
+
and returns a dict of parameter changes applied.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
``{"max_workers": N}`` when an adjustment was made.
|
|
77
|
+
Empty dict ``{}`` if no adjustment was needed.
|
|
78
|
+
"""
|
|
79
|
+
from claude_experimental.integration import _require_enabled
|
|
80
|
+
|
|
81
|
+
_require_enabled()
|
|
82
|
+
|
|
83
|
+
stats = self._pool.pool_stats()
|
|
84
|
+
current_size: int = stats["pool_size"]
|
|
85
|
+
|
|
86
|
+
# --- Rule 1: P99 > 2× target → scale UP ---
|
|
87
|
+
p99 = self._compute_p99()
|
|
88
|
+
if p99 is not None and p99 > 2 * self._target_p99_ms:
|
|
89
|
+
new_size = min(current_size + 1, self._max_workers)
|
|
90
|
+
if new_size > current_size:
|
|
91
|
+
self._apply_resize(
|
|
92
|
+
new_size,
|
|
93
|
+
direction="up",
|
|
94
|
+
reason=f"p99={p99:.0f}ms > 2x target={self._target_p99_ms:.0f}ms",
|
|
95
|
+
)
|
|
96
|
+
self._low_util_since = None
|
|
97
|
+
return {"max_workers": new_size}
|
|
98
|
+
|
|
99
|
+
# --- Rule 2: utilization < 30% sustained → scale DOWN ---
|
|
100
|
+
utilization = self._compute_utilization(stats)
|
|
101
|
+
if utilization < self._LOW_UTIL_THRESHOLD and current_size > self._min_workers:
|
|
102
|
+
now = time.monotonic()
|
|
103
|
+
if self._low_util_since is None:
|
|
104
|
+
self._low_util_since = now
|
|
105
|
+
elif now - self._low_util_since >= self._SUSTAINED_LOW_UTIL_SECONDS:
|
|
106
|
+
new_size = max(current_size - 1, self._min_workers)
|
|
107
|
+
if new_size < current_size:
|
|
108
|
+
self._apply_resize(
|
|
109
|
+
new_size,
|
|
110
|
+
direction="down",
|
|
111
|
+
reason=(
|
|
112
|
+
f"utilization={utilization:.0%} < 30% "
|
|
113
|
+
f"for >{self._SUSTAINED_LOW_UTIL_SECONDS:.0f}s"
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
self._low_util_since = None
|
|
117
|
+
return {"max_workers": new_size}
|
|
118
|
+
else:
|
|
119
|
+
# Reset hysteresis if utilization recovered
|
|
120
|
+
self._low_util_since = None
|
|
121
|
+
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
def get_current_params(self) -> dict[str, object]:
|
|
125
|
+
"""Return current tunable parameters.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dict with ``min_workers``, ``max_workers``, ``current_workers``,
|
|
129
|
+
and ``target_p99_ms``.
|
|
130
|
+
"""
|
|
131
|
+
from claude_experimental.integration import _require_enabled
|
|
132
|
+
|
|
133
|
+
_require_enabled()
|
|
134
|
+
|
|
135
|
+
stats = self._pool.pool_stats()
|
|
136
|
+
return {
|
|
137
|
+
"min_workers": self._min_workers,
|
|
138
|
+
"max_workers": self._max_workers,
|
|
139
|
+
"current_workers": stats["pool_size"],
|
|
140
|
+
"target_p99_ms": self._target_p99_ms,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# Internal helpers
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def _compute_p99(self) -> float | None:
|
|
148
|
+
"""Compute P99 latency from ``task_duration_ms`` histogram metrics.
|
|
149
|
+
|
|
150
|
+
Queries the last 5 minutes of histogram data and computes the
|
|
151
|
+
99th percentile from raw values.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
P99 latency in milliseconds, or ``None`` if no data available.
|
|
155
|
+
"""
|
|
156
|
+
rows = self._telemetry.query(
|
|
157
|
+
"task_duration_ms", metric_type="histogram", since_minutes=5
|
|
158
|
+
)
|
|
159
|
+
if not rows:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
values = sorted(cast(float, row["value"]) for row in rows)
|
|
163
|
+
# 99th percentile: ceiling-based index
|
|
164
|
+
idx = int(math.ceil(len(values) * 0.99)) - 1
|
|
165
|
+
idx = max(0, min(idx, len(values) - 1))
|
|
166
|
+
return values[idx]
|
|
167
|
+
|
|
168
|
+
def _compute_utilization(self, stats: dict[str, int]) -> float:
|
|
169
|
+
"""Compute pool utilization as ``active / pool_size``."""
|
|
170
|
+
pool_size = stats["pool_size"]
|
|
171
|
+
if pool_size == 0:
|
|
172
|
+
return 0.0
|
|
173
|
+
return stats["active"] / pool_size
|
|
174
|
+
|
|
175
|
+
def _apply_resize(self, new_size: int, *, direction: str, reason: str) -> None:
|
|
176
|
+
"""Resize the pool and log the tuning decision as telemetry."""
|
|
177
|
+
self._pool._resize(new_size)
|
|
178
|
+
self._telemetry.record_counter(
|
|
179
|
+
"autotuner_adjustment",
|
|
180
|
+
value=1,
|
|
181
|
+
tags={"direction": direction, "reason": reason},
|
|
182
|
+
)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Human-in-the-loop checkpoint system for pausing execution at critical decision points.
|
|
2
|
+
|
|
3
|
+
Provides a polling-based checkpoint mechanism that allows agents to pause at
|
|
4
|
+
critical decision points (verification, decision, clarification) and wait for
|
|
5
|
+
human input before proceeding. Checkpoints are persisted as JSON files in
|
|
6
|
+
`.omg/state/checkpoints/` with atomic writes for crash safety.
|
|
7
|
+
|
|
8
|
+
Feature-gated behind OMG_ADVANCED_INTEGRATION_ENABLED=1.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import tempfile
|
|
15
|
+
import uuid
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Constants
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
_CHECKPOINT_DIR = os.path.join(".omg", "state", "checkpoints")
|
|
26
|
+
_SCHEMA_VERSION = 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CheckpointType(str, Enum):
|
|
30
|
+
"""Supported checkpoint types."""
|
|
31
|
+
|
|
32
|
+
VERIFICATION = "VERIFICATION"
|
|
33
|
+
DECISION = "DECISION"
|
|
34
|
+
CLARIFICATION = "CLARIFICATION"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CheckpointStatus(str, Enum):
|
|
38
|
+
"""Checkpoint lifecycle states."""
|
|
39
|
+
|
|
40
|
+
PENDING = "pending"
|
|
41
|
+
RESOLVED = "resolved"
|
|
42
|
+
EXPIRED = "expired"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Helpers
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def _require_integration() -> None:
|
|
50
|
+
"""Gate all public operations behind the ADVANCED_INTEGRATION flag."""
|
|
51
|
+
from claude_experimental.integration import _require_enabled
|
|
52
|
+
_require_enabled()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _now_iso() -> str:
|
|
56
|
+
return datetime.now(timezone.utc).isoformat()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_iso(iso_str: str) -> datetime:
|
|
60
|
+
# Handle both +00:00 and Z suffixes
|
|
61
|
+
s = iso_str.replace("Z", "+00:00")
|
|
62
|
+
return datetime.fromisoformat(s)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _atomic_json_write(path: str, data: dict[str, Any]) -> None:
|
|
66
|
+
"""Write JSON atomically: temp file → os.replace().
|
|
67
|
+
|
|
68
|
+
Creates parent directories if needed.
|
|
69
|
+
"""
|
|
70
|
+
parent = os.path.dirname(path)
|
|
71
|
+
if parent:
|
|
72
|
+
os.makedirs(parent, exist_ok=True)
|
|
73
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
74
|
+
dir=parent, suffix=".tmp", prefix=".ckpt_"
|
|
75
|
+
)
|
|
76
|
+
try:
|
|
77
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
78
|
+
json.dump(data, f, indent=2, sort_keys=True)
|
|
79
|
+
os.replace(tmp_path, path)
|
|
80
|
+
except BaseException:
|
|
81
|
+
# Clean up temp file on any failure
|
|
82
|
+
try:
|
|
83
|
+
os.unlink(tmp_path)
|
|
84
|
+
except OSError:
|
|
85
|
+
pass
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _read_checkpoint_file(path: str) -> dict[str, Any] | None:
|
|
90
|
+
"""Read and parse a checkpoint JSON file. Returns None on failure."""
|
|
91
|
+
try:
|
|
92
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
93
|
+
return json.load(f) # type: ignore[no-any-return]
|
|
94
|
+
except (OSError, json.JSONDecodeError):
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# CheckpointManager
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
class CheckpointManager:
|
|
103
|
+
"""Manages human-in-the-loop checkpoints for pausing execution.
|
|
104
|
+
|
|
105
|
+
Checkpoints are persisted as individual JSON files under
|
|
106
|
+
``{base_dir}/.omg/state/checkpoints/{checkpoint_id}.json``.
|
|
107
|
+
|
|
108
|
+
Usage::
|
|
109
|
+
|
|
110
|
+
mgr = CheckpointManager()
|
|
111
|
+
cid = mgr.create_checkpoint(
|
|
112
|
+
checkpoint_type="DECISION",
|
|
113
|
+
description="Choose deployment target",
|
|
114
|
+
options=["staging", "production"],
|
|
115
|
+
)
|
|
116
|
+
# ... poll or wait ...
|
|
117
|
+
mgr.resume_checkpoint(cid, decision="staging")
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, base_dir: str = ".") -> None:
|
|
121
|
+
self._checkpoint_dir = os.path.join(base_dir, _CHECKPOINT_DIR)
|
|
122
|
+
|
|
123
|
+
# -- public API ---------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def create_checkpoint(
|
|
126
|
+
self,
|
|
127
|
+
checkpoint_type: str,
|
|
128
|
+
description: str,
|
|
129
|
+
options: list[str] | None = None,
|
|
130
|
+
timeout_seconds: int = 3600,
|
|
131
|
+
) -> str:
|
|
132
|
+
"""Create a new pending checkpoint.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
checkpoint_type: One of VERIFICATION, DECISION, CLARIFICATION.
|
|
136
|
+
description: Human-readable description of what needs attention.
|
|
137
|
+
options: Optional list of valid choices (useful for DECISION type).
|
|
138
|
+
timeout_seconds: Seconds until the checkpoint auto-expires (default 3600).
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
checkpoint_id: UUID string for the new checkpoint.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
RuntimeError: If the feature flag is disabled.
|
|
145
|
+
ValueError: If checkpoint_type is invalid.
|
|
146
|
+
"""
|
|
147
|
+
_require_integration()
|
|
148
|
+
|
|
149
|
+
# Validate type
|
|
150
|
+
try:
|
|
151
|
+
ct = CheckpointType(checkpoint_type)
|
|
152
|
+
except ValueError:
|
|
153
|
+
valid = ", ".join(t.value for t in CheckpointType)
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Invalid checkpoint type '{checkpoint_type}'. "
|
|
156
|
+
f"Must be one of: {valid}"
|
|
157
|
+
) from None
|
|
158
|
+
|
|
159
|
+
checkpoint_id = str(uuid.uuid4())
|
|
160
|
+
now = datetime.now(timezone.utc)
|
|
161
|
+
expires_at = datetime.fromtimestamp(
|
|
162
|
+
now.timestamp() + timeout_seconds, tz=timezone.utc
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
checkpoint_data: dict[str, Any] = {
|
|
166
|
+
"checkpoint_id": checkpoint_id,
|
|
167
|
+
"type": ct.value,
|
|
168
|
+
"description": description,
|
|
169
|
+
"options": options if options is not None else [],
|
|
170
|
+
"status": CheckpointStatus.PENDING.value,
|
|
171
|
+
"decision": None,
|
|
172
|
+
"created_at": now.isoformat(),
|
|
173
|
+
"expires_at": expires_at.isoformat(),
|
|
174
|
+
"schema_version": _SCHEMA_VERSION,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
path = os.path.join(self._checkpoint_dir, f"{checkpoint_id}.json")
|
|
178
|
+
_atomic_json_write(path, checkpoint_data)
|
|
179
|
+
return checkpoint_id
|
|
180
|
+
|
|
181
|
+
def get_checkpoint(self, checkpoint_id: str) -> dict[str, Any]:
|
|
182
|
+
"""Retrieve the current state of a checkpoint.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
checkpoint_id: UUID of the checkpoint.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Checkpoint dict with all fields.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
RuntimeError: If the feature flag is disabled.
|
|
192
|
+
KeyError: If checkpoint not found.
|
|
193
|
+
"""
|
|
194
|
+
_require_integration()
|
|
195
|
+
|
|
196
|
+
path = os.path.join(self._checkpoint_dir, f"{checkpoint_id}.json")
|
|
197
|
+
data = _read_checkpoint_file(path)
|
|
198
|
+
if data is None:
|
|
199
|
+
raise KeyError(f"Checkpoint not found: {checkpoint_id}")
|
|
200
|
+
|
|
201
|
+
# Auto-expire if past deadline
|
|
202
|
+
if data.get("status") == CheckpointStatus.PENDING.value:
|
|
203
|
+
expires_at = _parse_iso(str(data["expires_at"]))
|
|
204
|
+
if datetime.now(timezone.utc) > expires_at:
|
|
205
|
+
data["status"] = CheckpointStatus.EXPIRED.value
|
|
206
|
+
_atomic_json_write(path, data)
|
|
207
|
+
|
|
208
|
+
return data
|
|
209
|
+
|
|
210
|
+
def resume_checkpoint(self, checkpoint_id: str, decision: str) -> bool:
|
|
211
|
+
"""Resolve a pending checkpoint with a human decision.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
checkpoint_id: UUID of the checkpoint to resume.
|
|
215
|
+
decision: The decision string from the human.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
True if the checkpoint was successfully resolved.
|
|
219
|
+
False if the checkpoint was not in pending state.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
RuntimeError: If the feature flag is disabled.
|
|
223
|
+
KeyError: If checkpoint not found.
|
|
224
|
+
"""
|
|
225
|
+
_require_integration()
|
|
226
|
+
|
|
227
|
+
path = os.path.join(self._checkpoint_dir, f"{checkpoint_id}.json")
|
|
228
|
+
data = _read_checkpoint_file(path)
|
|
229
|
+
if data is None:
|
|
230
|
+
raise KeyError(f"Checkpoint not found: {checkpoint_id}")
|
|
231
|
+
|
|
232
|
+
# Can only resume pending checkpoints
|
|
233
|
+
if data.get("status") != CheckpointStatus.PENDING.value:
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
# Check expiry
|
|
237
|
+
expires_at = _parse_iso(str(data["expires_at"]))
|
|
238
|
+
if datetime.now(timezone.utc) > expires_at:
|
|
239
|
+
data["status"] = CheckpointStatus.EXPIRED.value
|
|
240
|
+
_atomic_json_write(path, data)
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
data["status"] = CheckpointStatus.RESOLVED.value
|
|
244
|
+
data["decision"] = decision
|
|
245
|
+
_atomic_json_write(path, data)
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
def list_pending(self) -> list[dict[str, Any]]:
|
|
249
|
+
"""List all checkpoints currently in pending state.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of checkpoint dicts with status=pending.
|
|
253
|
+
Expired checkpoints are auto-transitioned and excluded.
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
RuntimeError: If the feature flag is disabled.
|
|
257
|
+
"""
|
|
258
|
+
_require_integration()
|
|
259
|
+
|
|
260
|
+
pending: list[dict[str, Any]] = []
|
|
261
|
+
|
|
262
|
+
if not os.path.isdir(self._checkpoint_dir):
|
|
263
|
+
return pending
|
|
264
|
+
|
|
265
|
+
now = datetime.now(timezone.utc)
|
|
266
|
+
|
|
267
|
+
for filename in os.listdir(self._checkpoint_dir):
|
|
268
|
+
if not filename.endswith(".json"):
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
path = os.path.join(self._checkpoint_dir, filename)
|
|
272
|
+
data = _read_checkpoint_file(path)
|
|
273
|
+
if data is None:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
if data.get("status") != CheckpointStatus.PENDING.value:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# Auto-expire
|
|
280
|
+
expires_at = _parse_iso(str(data["expires_at"]))
|
|
281
|
+
if now > expires_at:
|
|
282
|
+
data["status"] = CheckpointStatus.EXPIRED.value
|
|
283
|
+
_atomic_json_write(path, data)
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
pending.append(data)
|
|
287
|
+
|
|
288
|
+
return pending
|
|
289
|
+
|
|
290
|
+
def cleanup_expired(self) -> int:
|
|
291
|
+
"""Remove checkpoint files that have expired.
|
|
292
|
+
|
|
293
|
+
Deletes files where expires_at < now, regardless of current status.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Number of expired checkpoint files removed.
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
RuntimeError: If the feature flag is disabled.
|
|
300
|
+
"""
|
|
301
|
+
_require_integration()
|
|
302
|
+
|
|
303
|
+
removed = 0
|
|
304
|
+
|
|
305
|
+
if not os.path.isdir(self._checkpoint_dir):
|
|
306
|
+
return removed
|
|
307
|
+
|
|
308
|
+
now = datetime.now(timezone.utc)
|
|
309
|
+
|
|
310
|
+
for filename in os.listdir(self._checkpoint_dir):
|
|
311
|
+
if not filename.endswith(".json"):
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
path = os.path.join(self._checkpoint_dir, filename)
|
|
315
|
+
data = _read_checkpoint_file(path)
|
|
316
|
+
if data is None:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
expires_at = _parse_iso(str(data["expires_at"]))
|
|
320
|
+
if now > expires_at:
|
|
321
|
+
try:
|
|
322
|
+
os.unlink(path)
|
|
323
|
+
removed += 1
|
|
324
|
+
except OSError:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
return removed
|