File size: 8,474 Bytes
d6da243
 
b8fadbf
 
 
 
 
 
d6da243
b8fadbf
 
 
 
d6da243
b8fadbf
 
 
 
d6da243
 
 
 
b8fadbf
 
 
 
 
 
 
 
 
 
 
 
d6da243
 
 
 
b8fadbf
 
d6da243
 
 
 
b8fadbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6da243
 
 
 
 
b8fadbf
d6da243
b8fadbf
d6da243
 
b8fadbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6da243
b8fadbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6da243
 
 
b8fadbf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#!/usr/bin/env python3
"""
Persistent continual 54D learning loop.

Watches Cosmos/data/cosmos/experience_corpus.txt and runs one bounded CPU
training burst only after enough genuinely new lines arrive. The last trained
line count survives restarts, a process lock prevents duplicate learners, and a
parent/stop-file contract lets WAKE_HER shut the learner down with Cosmos.
"""
from __future__ import annotations

import json
import os
from pathlib import Path
import subprocess
import sys
import time


PR = Path(__file__).resolve().parents[1]
CORPUS = PR / "Cosmos" / "data" / "cosmos" / "experience_corpus.txt"
TRAINER = PR / "scripts" / "train_cosmos_from_play.py"
CHECKPOINT = PR / "Cosmos" / "checkpoints" / "cosmos" / "cosmos_play.pt"
STATE = PR / "Cosmos" / "checkpoints" / "cosmos" / "cosmos_54d_loop_state.json"
LOCK = PR / "Cosmos" / "checkpoints" / "cosmos" / "cosmos_54d_loop.lock"
INTERVAL = max(30, int(os.getenv("COSMOS_54D_TRAIN_INTERVAL_S", "1800")))
MIN_NEW = max(1, int(os.getenv("COSMOS_54D_TRAIN_MIN_NEW", "12")))
START_DELAY = max(0, int(os.getenv("COSMOS_54D_TRAIN_START_DELAY_S", "0")))
PARENT_PID = int(os.getenv("COSMOS_54D_PARENT_PID", "0") or 0)
STOP_FILE = Path(os.getenv("COSMOS_54D_STOP_FILE", "")) if os.getenv("COSMOS_54D_STOP_FILE") else None


def _say(message: str) -> None:
    print(f"[54D-LOOP] {message}", flush=True)


def _lines() -> int:
    try:
        with CORPUS.open("r", encoding="utf-8", errors="ignore") as handle:
            return sum(1 for _ in handle)
    except Exception:
        return 0


def _parent_alive() -> bool:
    """Return whether the wake process still exists on every supported OS.

    ``os.kill(pid, 0)`` is not a reliable Windows existence probe: on some
    Python builds it raises ``SystemError``/WinError 87 even for a live PID.
    Use a real process handle on Windows, then retain the POSIX probe elsewhere.
    """
    if PARENT_PID <= 0:
        return True
    if os.name == "nt":
        try:
            import ctypes

            access = 0x00100000 | 0x00001000  # SYNCHRONIZE | QUERY_LIMITED_INFORMATION
            handle = ctypes.windll.kernel32.OpenProcess(access, False, PARENT_PID)
            if handle:
                ctypes.windll.kernel32.CloseHandle(handle)
                return True
            return False
        except Exception:
            # A probe failure must never crash the learner. The stop file is
            # still authoritative when WAKE_HER shuts down.
            return True
    try:
        os.kill(PARENT_PID, 0)
        return True
    except (OSError, SystemError, ValueError):
        return False


def _stopping() -> bool:
    return bool((STOP_FILE and STOP_FILE.exists()) or not _parent_alive())


def _sleep(seconds: int) -> bool:
    deadline = time.monotonic() + max(0, seconds)
    while time.monotonic() < deadline:
        if _stopping():
            return False
        time.sleep(min(2.0, max(0.0, deadline - time.monotonic())))
    return not _stopping()


def _single_instance():
    LOCK.parent.mkdir(parents=True, exist_ok=True)
    handle = LOCK.open("a+b")
    handle.seek(0, os.SEEK_END)
    if handle.tell() == 0:
        handle.write(b"0")
        handle.flush()
    handle.seek(0)
    try:
        if os.name == "nt":
            import msvcrt

            msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
        else:
            import fcntl

            fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
    except (OSError, IOError):
        handle.close()
        return None
    return handle


def _load_last_trained(current_lines: int) -> int:
    try:
        value = json.loads(STATE.read_text(encoding="utf-8"))
        return max(0, int(value.get("last_trained_lines", 0)))
    except Exception:
        pass
    # Migrate safely: if the checkpoint is newer than the corpus, it already
    # represents the current data. Otherwise the accumulated new corpus is due.
    try:
        if CHECKPOINT.is_file() and CHECKPOINT.stat().st_mtime_ns >= CORPUS.stat().st_mtime_ns:
            return current_lines
    except Exception:
        pass
    return 0


def _save_state(lines: int) -> None:
    STATE.parent.mkdir(parents=True, exist_ok=True)
    value = {
        "schema": "cosmos.54d_continual_state.v1",
        "last_trained_lines": int(lines),
        "checkpoint": str(CHECKPOINT),
        "checkpoint_mtime_ns": CHECKPOINT.stat().st_mtime_ns if CHECKPOINT.is_file() else None,
        "updated_at": time.time(),
    }
    temp = STATE.with_name(f"{STATE.name}.tmp.{os.getpid()}")
    temp.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
    os.replace(temp, STATE)


def _mem_too_high() -> bool:
    try:
        import psutil

        vm = psutil.virtual_memory()
        return vm.percent >= 90.0 or vm.available < 1_500_000_000
    except Exception:
        return False


def _run_burst() -> tuple[int | None, str]:
    env = dict(os.environ)
    env.setdefault("COSMOS_PLAY_TRAIN_STEPS", "120")
    env.setdefault("COSMOS_PLAY_TRAIN_SEC", "360")
    env.setdefault("COSMOS_PLAY_TRAIN_THREADS", "2")
    env.setdefault("PYTHONIOENCODING", "utf-8")
    proc = subprocess.Popen(
        [sys.executable, str(TRAINER)],
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        encoding="utf-8",
        errors="replace",
    )
    deadline = time.monotonic() + 900
    while proc.poll() is None:
        if _stopping():
            proc.terminate()
            try:
                proc.wait(timeout=10)
            except Exception:
                proc.kill()
            output, _ = proc.communicate()
            return None, output or ""
        if time.monotonic() >= deadline:
            proc.kill()
            output, _ = proc.communicate()
            return -1, (output or "") + "\ntrainer timeout"
        time.sleep(1)
    output, _ = proc.communicate()
    return proc.returncode, output or ""


def main() -> int:
    lock_handle = _single_instance()
    if lock_handle is None:
        _say("another continual learner already owns the lock; this copy exits")
        return 0
    try:
        _say(
            f"armed: {MIN_NEW}+ new lines, check every {INTERVAL}s, "
            f"start delay {START_DELAY}s"
        )
        if START_DELAY and not _sleep(START_DELAY):
            return 0
        current = _lines()
        last = _load_last_trained(current)
        if last > current:
            last = current
            _save_state(last)
        elif not STATE.exists() and last == current:
            _save_state(last)
        _say(f"persistent baseline: corpus={current}, last_trained={last}")

        while not _stopping():
            current = _lines()
            new_lines = current - last
            if new_lines >= MIN_NEW:
                if _mem_too_high():
                    _say(
                        f"corpus {current} (+{new_lines}) - RAM guard active; "
                        "training deferred"
                    )
                else:
                    before = CHECKPOINT.stat().st_mtime_ns if CHECKPOINT.is_file() else 0
                    _say(f"corpus {current} (+{new_lines}) - bounded training burst")
                    code, output = _run_burst()
                    tail = "\n".join(output.strip().splitlines()[-8:])
                    if tail:
                        _say("trainer tail:\n" + tail)
                    after = CHECKPOINT.stat().st_mtime_ns if CHECKPOINT.is_file() else 0
                    if code == 0 and after != before:
                        last = current
                        _save_state(last)
                        _say(f"checkpoint advanced; durable baseline={last}")
                    elif code is None:
                        _say("stop requested; active trainer terminated cleanly")
                        break
                    else:
                        _say(
                            f"burst did not advance checkpoint (return={code}); "
                            "new lines remain due for retry"
                        )
            else:
                _say(f"corpus {current} (+{new_lines}) - waiting for {MIN_NEW - new_lines} more")
            if not _sleep(INTERVAL):
                break
        _say("stopped with Cosmos; checkpoint and corpus remain persistent")
        return 0
    finally:
        lock_handle.close()


if __name__ == "__main__":
    raise SystemExit(main())