outerloop-science 0.1.0.dev0__py3-none-any.whl
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.
- outerloop/__init__.py +18 -0
- outerloop/__main__.py +3 -0
- outerloop/appauth.py +213 -0
- outerloop/appmanifest.py +198 -0
- outerloop/attempt.py +3481 -0
- outerloop/brief.py +515 -0
- outerloop/cli.py +439 -0
- outerloop/climbboard.py +1145 -0
- outerloop/compute.py +482 -0
- outerloop/contract.py +483 -0
- outerloop/contract_cli.py +63 -0
- outerloop/disk.py +164 -0
- outerloop/dispatch.py +586 -0
- outerloop/followup.py +2143 -0
- outerloop/github.py +1486 -0
- outerloop/harness.py +1449 -0
- outerloop/housekeeping.py +167 -0
- outerloop/init.py +313 -0
- outerloop/intake.py +129 -0
- outerloop/limits.py +80 -0
- outerloop/markers.py +48 -0
- outerloop/measure.py +523 -0
- outerloop/orchestrator.py +1901 -0
- outerloop/panel.py +188 -0
- outerloop/paths.py +27 -0
- outerloop/posting.py +160 -0
- outerloop/progress.py +170 -0
- outerloop/py.typed +0 -0
- outerloop/review.py +611 -0
- outerloop/review_agent.py +263 -0
- outerloop/review_agent_cli.py +209 -0
- outerloop/review_post_cli.py +162 -0
- outerloop/review_summarize_cli.py +163 -0
- outerloop/role_runner.py +229 -0
- outerloop/roles.py +247 -0
- outerloop/rolespec.py +89 -0
- outerloop/runstate.py +385 -0
- outerloop/steward.py +852 -0
- outerloop/style.py +12 -0
- outerloop/syscall.py +977 -0
- outerloop/syscall_cli.py +531 -0
- outerloop/tick.py +3166 -0
- outerloop/verifier.py +403 -0
- outerloop/verify_agent.py +149 -0
- outerloop/verify_agent_cli.py +95 -0
- outerloop/verify_post_cli.py +116 -0
- outerloop_science-0.1.0.dev0.dist-info/METADATA +145 -0
- outerloop_science-0.1.0.dev0.dist-info/RECORD +52 -0
- outerloop_science-0.1.0.dev0.dist-info/WHEEL +4 -0
- outerloop_science-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- outerloop_science-0.1.0.dev0.dist-info/licenses/LICENSE +202 -0
- outerloop_science-0.1.0.dev0.dist-info/licenses/NOTICE +5 -0
outerloop/disk.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Disk preflight: refuse to launch work onto storage that cannot hold it.
|
|
2
|
+
|
|
3
|
+
Quota exhaustion is INVISIBLE on some clusters until a write fails: user
|
|
4
|
+
quotas on VAST/NFS homes are not exposed through statvfs (df reports the
|
|
5
|
+
whole filesystem), and rquota RPCs may be administratively blocked — the
|
|
6
|
+
first symptom is EDQUOT from a write that already lost data. (Verified on
|
|
7
|
+
Torch 2026-08-07, the day a full home quota crashed a live climb and its
|
|
8
|
+
error handling with it.)
|
|
9
|
+
|
|
10
|
+
So the preflight is built on two honest signals:
|
|
11
|
+
|
|
12
|
+
- a WRITE PROBE — create, fsync, and remove a small file; this surfaces
|
|
13
|
+
EDQUOT/ENOSPC/EROFS exactly the way real work would hit them, and is the
|
|
14
|
+
only quota check that works everywhere;
|
|
15
|
+
- statvfs free space — meaningful for cluster-level exhaustion (a shared
|
|
16
|
+
scratch filesystem filling up), used as an early-warning threshold where
|
|
17
|
+
the numbers are real.
|
|
18
|
+
|
|
19
|
+
The tick probes before launching sessions; the climb probes before touching
|
|
20
|
+
its run directory. A failed probe skips NEW work and surfaces in the
|
|
21
|
+
heartbeat — it never kills the tick chain.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import contextlib
|
|
27
|
+
import errno
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
import tempfile
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
log = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
PROBE_NAME = ".disk-probe"
|
|
37
|
+
# Enough for a clone + venv + eval scratch with margin; overridable per call.
|
|
38
|
+
DEFAULT_MIN_FREE_BYTES = 10 * 1024**3
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class MountHealth:
|
|
43
|
+
path: str
|
|
44
|
+
writable: bool
|
|
45
|
+
error: str = "" # why the probe failed, when it did
|
|
46
|
+
free_bytes: int = -1 # statvfs; -1 when the query itself failed
|
|
47
|
+
min_free_bytes: int = 0
|
|
48
|
+
|
|
49
|
+
def ok(self) -> bool:
|
|
50
|
+
if not self.writable:
|
|
51
|
+
return False
|
|
52
|
+
# Unknown free space is not a failure: the write probe is the
|
|
53
|
+
# authoritative check, the threshold is early warning on top.
|
|
54
|
+
if self.free_bytes >= 0 and self.min_free_bytes > 0:
|
|
55
|
+
return self.free_bytes >= self.min_free_bytes
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def describe(self) -> str:
|
|
59
|
+
state = "ok" if self.ok() else "BLOCKED"
|
|
60
|
+
free = f"{self.free_bytes / 1024**3:.1f}G free" if self.free_bytes >= 0 else "free=?"
|
|
61
|
+
detail = f" ({self.error})" if self.error else ""
|
|
62
|
+
return f"{self.path}: {state}, {free}{detail}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def probe_writable(path: Path) -> tuple[bool, str]:
|
|
66
|
+
"""Try to actually write in `path` the way real work would.
|
|
67
|
+
|
|
68
|
+
fsync is deliberate: quota errors on networked filesystems can be
|
|
69
|
+
deferred until the data is forced out, and a probe that skips the flush
|
|
70
|
+
reports healthy right up to the crash. mkstemp (random name) rather than
|
|
71
|
+
a PID-derived one: a stale probe left by a SIGKILLed process must never
|
|
72
|
+
alias a later PID and read as BLOCKED on healthy storage. Short writes
|
|
73
|
+
are treated as full: ENOSPC can surface as a short count before the
|
|
74
|
+
error, and a probe that ignores the count reports healthy on a full
|
|
75
|
+
mount.
|
|
76
|
+
"""
|
|
77
|
+
probe = ""
|
|
78
|
+
try:
|
|
79
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
fd, probe = tempfile.mkstemp(dir=path, prefix=f"{PROBE_NAME}.")
|
|
81
|
+
try:
|
|
82
|
+
payload = b"autoresearch disk probe\n" * 32
|
|
83
|
+
written = 0
|
|
84
|
+
while written < len(payload):
|
|
85
|
+
count = os.write(fd, payload[written:])
|
|
86
|
+
if count <= 0:
|
|
87
|
+
return False, "short write: filesystem refused data without an error"
|
|
88
|
+
written += count
|
|
89
|
+
os.fsync(fd)
|
|
90
|
+
finally:
|
|
91
|
+
os.close(fd)
|
|
92
|
+
return True, ""
|
|
93
|
+
except OSError as exc:
|
|
94
|
+
name = errno.errorcode.get(exc.errno, "") if exc.errno else ""
|
|
95
|
+
return False, f"{name or type(exc).__name__}: {exc}"
|
|
96
|
+
finally:
|
|
97
|
+
if probe:
|
|
98
|
+
with contextlib.suppress(OSError):
|
|
99
|
+
os.unlink(probe)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def free_bytes(path: Path) -> int:
|
|
103
|
+
"""Free bytes from statvfs, or -1 when the query fails. Honest for
|
|
104
|
+
cluster-level fullness; blind to per-user quotas on some filesystems —
|
|
105
|
+
that is what the write probe is for."""
|
|
106
|
+
try:
|
|
107
|
+
st = os.statvfs(path)
|
|
108
|
+
return st.f_bavail * st.f_frsize
|
|
109
|
+
except OSError:
|
|
110
|
+
return -1
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def check_mount(path: Path, min_free_bytes: int = 0) -> MountHealth:
|
|
114
|
+
writable, error = probe_writable(path)
|
|
115
|
+
return MountHealth(
|
|
116
|
+
path=str(path),
|
|
117
|
+
writable=writable,
|
|
118
|
+
error=error,
|
|
119
|
+
free_bytes=free_bytes(path),
|
|
120
|
+
min_free_bytes=min_free_bytes,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class DiskHealth:
|
|
126
|
+
"""What the tick needs: may new work launch, and what should humans see."""
|
|
127
|
+
|
|
128
|
+
state_root: MountHealth
|
|
129
|
+
home: MountHealth | None = None # warn-only: the agent barely writes there
|
|
130
|
+
|
|
131
|
+
def launch_ok(self) -> bool:
|
|
132
|
+
return self.state_root.ok()
|
|
133
|
+
|
|
134
|
+
def warnings(self) -> list[str]:
|
|
135
|
+
out = []
|
|
136
|
+
if not self.state_root.ok():
|
|
137
|
+
out.append(self.state_root.describe())
|
|
138
|
+
if self.home is not None and not self.home.ok():
|
|
139
|
+
out.append(f"home {self.home.describe()} (warn-only)")
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
def as_dict(self) -> dict[str, object]:
|
|
143
|
+
d: dict[str, object] = {
|
|
144
|
+
"launch_ok": self.launch_ok(),
|
|
145
|
+
"state_root": self.state_root.__dict__,
|
|
146
|
+
}
|
|
147
|
+
if self.home is not None:
|
|
148
|
+
d["home"] = self.home.__dict__
|
|
149
|
+
return d
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def check_disk(
|
|
153
|
+
root: Path,
|
|
154
|
+
min_free_bytes: int = DEFAULT_MIN_FREE_BYTES,
|
|
155
|
+
home: Path | None = None,
|
|
156
|
+
) -> DiskHealth:
|
|
157
|
+
"""Preflight for one tick: the state root gates launches; the home probe
|
|
158
|
+
(default: the real home) is a warn-only signal for humans, because a
|
|
159
|
+
full home breaks logins and tooling long before it breaks the agent."""
|
|
160
|
+
home_path = Path.home() if home is None else home
|
|
161
|
+
return DiskHealth(
|
|
162
|
+
state_root=check_mount(root, min_free_bytes),
|
|
163
|
+
home=check_mount(home_path, 0),
|
|
164
|
+
)
|