-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauto_dream.py
More file actions
220 lines (192 loc) · 7.59 KB
/
Copy pathauto_dream.py
File metadata and controls
220 lines (192 loc) · 7.59 KB
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
"""Staging-only dream cycle for the PT agent-memory lifecycle.
Responsibilities, in order:
1. Load episodic JSONL under the shared sidecar lock.
2. Replacement-decode malformed UTF-8 and skip malformed JSON rows.
3. Cluster episodes and extract structured candidate patterns.
4. Stage candidates with lifecycle metadata; never graduate subjectively.
5. Heuristically reject obvious junk while preserving host review authority.
6. Decay low-salience episodes and archive stale workspace artifacts.
7. Atomically rewrite sanitized episodic state through fsynced temp replacement.
8. Refresh REVIEW_QUEUE.md so the next host session sees pending work.
Durability and safety invariants:
- One stable sidecar lock spans the complete read-modify-write cycle.
- Every persisted JSON string passes through the tracked-path sanitizer.
- The destination is replaced only after the temporary file is flushed/fsynced.
- This module never graduates lessons and never commits unattended changes.
"""
from __future__ import annotations
import contextlib
import json
import os
import sys
import tempfile
from archive import archive_stale_workspace
from decay import decay_old_entries
from promote import cluster_and_extract, write_candidates
from review_state import mark_rejected, write_review_queue_summary
from validate import heuristic_check
ROOT = os.path.abspath(os.path.dirname(__file__))
AGENT_ROOT = os.path.dirname(ROOT)
HARNESS_HOOKS = os.path.join(AGENT_ROOT, "harness", "hooks")
if HARNESS_HOOKS not in sys.path:
sys.path.insert(0, HARNESS_HOOKS)
from _episodic_io import episodic_lock, is_legacy_episodic_row # noqa: E402
from path_hygiene import sanitize_json_strings # noqa: E402
EPISODIC = os.path.join(ROOT, "episodic/AGENT_LEARNINGS.jsonl")
CANDIDATES = os.path.join(ROOT, "candidates")
SEMANTIC = os.path.join(ROOT, "semantic")
REVIEW_QUEUE = os.path.join(ROOT, "working/REVIEW_QUEUE.md")
PROMOTION_THRESHOLD = 7.0
CLUSTER_SIMILARITY = 0.3
@contextlib.contextmanager
def _episodic_locked():
"""Hold the shared sidecar lock across the dream read-modify-write cycle."""
with episodic_lock(EPISODIC, exclusive=True):
yield None
def _load_entries_locked(_fd):
"""Read a replacement-decoded snapshot and skip malformed JSONL rows."""
entries = []
try:
with open(EPISODIC, encoding="utf-8", errors="replace") as stream:
for line in stream:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if is_legacy_episodic_row(entry):
continue
entries.append(entry)
except FileNotFoundError:
pass
return entries
def _write_all(fd, payload):
"""Write every byte, preserving the pre-atomic-rewrite helper contract.
The production episodic rewrite uses temp-file persistence plus ``os.replace``.
This helper remains for callers/tests that need a correct descriptor write-all
primitive and for other durability code that may reuse it.
"""
view = memoryview(payload)
written = 0
while written < len(view):
try:
count = os.write(fd, view[written:])
except InterruptedError:
continue
if count <= 0:
raise OSError("descriptor write made no progress")
written += count
def _write_entries_locked(_fd, entries):
"""Atomically replace episodic JSONL after durable temp-file persistence.
The sidecar lock remains stable across ``os.replace``. The temporary file is
created in the destination directory so replacement stays on one filesystem.
"""
os.makedirs(os.path.dirname(EPISODIC), exist_ok=True)
payload = "".join(
json.dumps(sanitize_json_strings(entry)) + "\n" for entry in entries
)
temp_path = None
try:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
errors="strict",
dir=os.path.dirname(EPISODIC),
prefix=".agent-learnings-",
suffix=".tmp",
delete=False,
) as stream:
temp_path = stream.name
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
os.replace(temp_path, EPISODIC)
temp_path = None
try:
dir_fd = os.open(os.path.dirname(EPISODIC), os.O_RDONLY)
except OSError:
dir_fd = None
if dir_fd is not None:
try:
os.fsync(dir_fd)
finally:
os.close(dir_fd)
finally:
if temp_path is not None:
try:
os.unlink(temp_path)
except FileNotFoundError:
pass
def _load_entries():
with _episodic_locked() as fd:
return _load_entries_locked(fd)
def _write_entries(entries):
with _episodic_locked() as fd:
_write_entries_locked(fd, entries)
def _heuristic_prefilter(candidates_dir, semantic_dir):
"""Move obvious junk to rejected while leaving subjective review to hosts."""
if not os.path.isdir(candidates_dir):
return 0
lessons_path = os.path.join(semantic_dir, "LESSONS.md")
if os.path.exists(lessons_path):
with open(lessons_path, encoding="utf-8", errors="replace") as stream:
existing = stream.read()
else:
existing = ""
rejected = 0
for fname in sorted(os.listdir(candidates_dir)):
if not fname.endswith(".json"):
continue
path = os.path.join(candidates_dir, fname)
if not os.path.isfile(path):
continue
try:
with open(path, encoding="utf-8", errors="replace") as stream:
candidate = json.load(stream)
except (OSError, json.JSONDecodeError):
continue
check = heuristic_check(candidate, existing)
if not check["passed"]:
mark_rejected(
candidate["id"],
"heuristic_prefilter",
", ".join(check["reasons"]),
candidates_dir,
duplicate_claims=check.get("duplicates", []),
)
rejected += 1
return rejected
def run_dream_cycle():
"""Run one locked staging, decay, archive, and review-queue cycle."""
with _episodic_locked() as fd:
entries = _load_entries_locked(fd)
if not entries:
pending = write_review_queue_summary(CANDIDATES, REVIEW_QUEUE)
print(f"dream cycle: no entries (queue has {pending} pending)")
return
patterns = cluster_and_extract(entries, threshold=CLUSTER_SIMILARITY)
promotable = {
key: pattern
for key, pattern in patterns.items()
if pattern.get("canonical_salience", 0) >= PROMOTION_THRESHOLD
}
staged = write_candidates(promotable, CANDIDATES)
prefiltered = _heuristic_prefilter(CANDIDATES, SEMANTIC)
kept, archived = decay_old_entries(
entries, archive_dir=os.path.join(ROOT, "episodic/snapshots")
)
_write_entries_locked(fd, kept)
archive_stale_workspace(
working_dir=os.path.join(ROOT, "working"),
archive_dir=os.path.join(ROOT, "episodic/snapshots"),
)
pending = write_review_queue_summary(CANDIDATES, REVIEW_QUEUE)
print(
f"dream cycle: patterns={len(patterns)} staged={staged} "
f"prefiltered_out={prefiltered} pending_review={pending} "
f"archived={len(archived)} kept={len(kept)}"
)
if __name__ == "__main__":
run_dream_cycle()