Skip to content

Commit 00e4728

Browse files
committed
v73: isolate unavailable fee evidence from paper observer lifecycle
Request English fee publication with canonical redirect receipts. Classify unavailable and unrecognized fee publications explicitly, record blocked evidence and retry after one hour while continuing price observations. Keep fee expiry and execution gates, integrity failures, accepted weights and archived reports unchanged. Correct misleading remote-helper fallback phase. Add profile-preserving pinned updater with bounded mission startup verification. Validation: 1267 passed, 65 skipped; existing PEFT warning. Ruff, shell syntax and diff checks passed. Operator archived failed HTML and live hardware are not independently verified.
1 parent f36c4ba commit 00e4728

9 files changed

Lines changed: 281 additions & 13 deletions

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11

22
---
33

4+
Synta v73 keeps paper observations running when the fee publication cannot be verified,
5+
without extending expired fees or admitting fills. See [SYNTA_V73.md](SYNTA_V73.md).
6+
47
Synta v72 adds public web/form tools with explicit one-shot operator approval and
58
fixes historical trade markers to use execution-time valuations. Read-only questions
69
can inspect actual evidence. See [SYNTA_V72.md](SYNTA_V72.md).

‎SYNTA_V73.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Synta v73: fee observer recovery
2+
3+
A missing or unrecognized fee publication previously raised a generic exception and terminated the paper observer. The controller could then restart despite a healthy inference server.
4+
5+
- `rlm/v100/spot_bootstrap.py`: requests the English Kraken publication, permits its canonical redirect, retains the actual source URL and archived body, and raises `FeeSourceUnavailable` for unavailable or unrecognized fee evidence. Rates are parsed, never substituted with guessed values.
6+
- `rlm/v100/paper_learning.py`: records `fee-refresh-blocked`, retries after one hour, and continues price observations and independent verified work. Existing fee expiry and ledger execution gates remain enforced. Other integrity errors still propagate.
7+
- `rlm/v100/competition.py`: reports `research-on-current-master` when an unavailable Windows helper causes immediate use of the accepted local master. No backend routing behavior changed.
8+
- `tools/update-synta-fee-observer.sh`: installs a pinned v73 revision and preserves the active profile, goals, weights, ledgers, Windows policy and archived reports.
9+
10+
The operator's unrecognized archived HTML has not been inspected. The locale request does not guarantee recognition of every future page layout. Failure remains visible and blocks fee-dependent fills; it no longer terminates unrelated observations. This release does not establish strategy profitability or completed optimizer updates.

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "rlms"
33
authors = [
44
{name = "Alex Zhang", email = "altzhang@mit.edu"},
55
]
6-
version = "0.1.3+v100.72"
6+
version = "0.1.3+v100.73"
77
description = "Recursive Language Models."
88
readme = "README.md"
99
license = "MIT"

‎rlm/v100/competition.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,10 +317,12 @@ def waiting_researcher(
317317
raise
318318
attempt += 1
319319
event = {
320-
"phase": "waiting-for-remote-helper",
320+
"phase": "research-on-current-master"
321+
if fallback is not None
322+
else "waiting-for-remote-helper",
321323
"endpoint": profile["runtime"]["base_url"],
322324
"attempt": attempt,
323-
"retry_seconds": 30,
325+
"retry_seconds": 0 if fallback is not None else 30,
324326
"error": type(error).__name__,
325327
"detail": str(error)[:400],
326328
"checkpoints": "retained; no candidate accepted",

‎rlm/v100/paper_learning.py‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import re
55
import threading
6+
import time
67
from contextlib import ExitStack
78
from pathlib import Path
89

@@ -105,6 +106,7 @@ def __init__(self, root: Path, path: Path):
105106
self.settings = load_settings(self.path)
106107
self.settings_sha = sha(self.settings)
107108
self.stop_event = threading.Event()
109+
self.fee_retry_after = 0.0
108110
self.failure: Exception | None = None
109111
self.worker: threading.Thread | None = None
110112
self.scope = ExitStack()
@@ -155,16 +157,28 @@ def tick(self) -> None:
155157
if kraken:
156158
from datetime import UTC, datetime, timedelta
157159

158-
from rlm.v100.spot_bootstrap import prepare
160+
from rlm.v100.spot_bootstrap import FeeSourceUnavailable, prepare
159161

160-
if any(
162+
if time.time() >= self.fee_retry_after and any(
161163
datetime.fromisoformat(
162164
state["fee_profiles"][item["fee_profile"]]["valid_until"]
163165
)
164166
<= datetime.now(UTC) + timedelta(hours=1)
165167
for item in kraken
166168
):
167-
prepare(self.root, refresh=True)
169+
try:
170+
prepare(self.root, refresh=True)
171+
except FeeSourceUnavailable as error:
172+
self.fee_retry_after = time.time() + 3600
173+
self.note(
174+
"fee-refresh-blocked",
175+
{
176+
"error": type(error).__name__,
177+
"detail": str(error)[:500],
178+
"retry_at": self.fee_retry_after,
179+
"scope": "Existing fee expiry and fill gates unchanged. Quotes, research and independent verified learning may continue; no guessed fees or expiry extension.",
180+
},
181+
)
168182
poll_crypto(book, self.stop_event.is_set)
169183
from rlm.v100.provider_registry import poll as poll_registered
170184
from rlm.v100.reward_policy import train as train_reward

‎rlm/v100/research_tools.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,9 +513,13 @@ def public_origin(url: str) -> tuple[object, str]:
513513
return origin, sorted(addresses)[0]
514514

515515

516-
def download_page(url: str, max_bytes: int = 262144) -> tuple[str, str]:
516+
def download_page(
517+
url: str, max_bytes: int = 262144, *, language: str | None = None
518+
) -> tuple[str, str]:
517519
if type(max_bytes) is not int or not 262144 <= max_bytes <= 2 * 2**20:
518520
raise ValueError("Invalid bounded source read budget")
521+
if language is not None and not re.fullmatch(r"[a-z]{2}(?:-[A-Z]{2})?", language):
522+
raise ValueError("Invalid source language")
519523
for _ in range(3):
520524
origin, address = public_origin(url)
521525
# Connect to the already checked IP, retaining hostname verification and
@@ -541,6 +545,7 @@ def download_page(url: str, max_bytes: int = 262144) -> tuple[str, str]:
541545
"Host": origin.hostname,
542546
"User-Agent": "V100-Research/0.1",
543547
"Accept-Encoding": "identity",
548+
**({"Accept-Language": language} if language is not None else {}),
544549
},
545550
redirect=False,
546551
retries=False,

‎rlm/v100/spot_bootstrap.py‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,21 @@
77
from decimal import Decimal
88
from pathlib import Path
99

10+
from urllib3.exceptions import HTTPError
11+
1012
from rlm.v100.common import atomic_json
1113
from rlm.v100.paper import FEE_FIELDS, PaperBook
1214
from rlm.v100.paper_feeds import poll_crypto, public_json
1315
from rlm.v100.research_tools import TextOnly, download_page
1416

15-
FEES_URL = "https://www.kraken.com/features/fee-schedule"
17+
FEES_URL = "https://www.kraken.com/en-us/features/fee-schedule"
18+
FEE_SOURCE_URLS = {FEES_URL, "https://www.kraken.com/features/fee-schedule"}
19+
20+
21+
class FeeSourceUnavailable(ValueError):
22+
"""An unverified fee publication blocks fills, not unrelated observations."""
23+
24+
1625
PAIRS_URL = "https://api.kraken.com/0/public/AssetPairs?pair=XBTUSD,ETHUSD"
1726

1827

@@ -141,9 +150,24 @@ def symbol_for(code):
141150
"status": "existing paper spot configuration retained",
142151
"weights_changed": False,
143152
}
144-
url, body = download_page(FEES_URL, max_bytes=2 * 2**20)
145-
if url != FEES_URL:
146-
raise ValueError("Unexpected fee-source redirect")
153+
try:
154+
url, body = download_page(FEES_URL, max_bytes=2 * 2**20, language="en-US")
155+
except (ValueError, OSError, HTTPError) as error:
156+
atomic_json(
157+
root / "research/paper/fee-source-status.json",
158+
{
159+
"source_url": FEES_URL,
160+
"state": "blocked",
161+
"error": str(error)[:500],
162+
"updated": time.time(),
163+
"scope": "Fee publication unavailable; existing expiry unchanged, no guessed replacement",
164+
},
165+
)
166+
raise FeeSourceUnavailable(
167+
"Fee publication unavailable: " + str(error)[:400]
168+
) from error
169+
if url not in FEE_SOURCE_URLS:
170+
raise FeeSourceUnavailable("Unexpected fee-source redirect")
147171
digest = hashlib.sha256(body.encode()).hexdigest()
148172
snapshot = root / "research/paper/sources" / (digest + ".html")
149173
snapshot.parent.mkdir(parents=True, exist_ok=True)
@@ -162,7 +186,7 @@ def symbol_for(code):
162186
root / "research/paper/fee-source-status.json",
163187
{**fee_receipt, "state": "blocked", "error": str(error)},
164188
)
165-
raise ValueError(f"{error}; archived source {digest}") from error
189+
raise FeeSourceUnavailable(f"{error}; archived source {digest}") from error
166190
atomic_json(
167191
root / "research/paper/fee-source-status.json",
168192
{**fee_receipt, "state": "parsed", "taker_fee_bps": taker},
@@ -177,7 +201,7 @@ def symbol_for(code):
177201
):
178202
raise ValueError("Kraken instrument rules unavailable")
179203
identity = "kraken-tier1-" + str(time.time_ns())
180-
evidence = {"source_url": FEES_URL, "source_sha256": digest, "available_at": observed}
204+
evidence = {"source_url": url, "source_sha256": digest, "available_at": observed}
181205
profile = {
182206
**dict.fromkeys(FEE_FIELDS, "0"),
183207
**evidence,
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import json
2+
from types import SimpleNamespace
3+
4+
import pytest
5+
6+
from rlm.v100 import paper_learning, spot_bootstrap
7+
from rlm.v100.common import atomic_json
8+
from tests.test_v100_paper_learning import settings
9+
10+
11+
@pytest.fixture
12+
def observer(tmp_path, monkeypatch):
13+
from rlm.v100 import market_adapters, provider_registry, reward_policy
14+
15+
ledger = tmp_path / "research/paper/ledger.sqlite3"
16+
ledger.parent.mkdir(parents=True)
17+
ledger.touch()
18+
state = {
19+
"instruments": {
20+
"BTC": {
21+
"feed_id": "kraken:XBTUSD",
22+
"fee_profile": "old",
23+
"market": "crypto",
24+
"product": "spot",
25+
}
26+
},
27+
"fee_profiles": {"old": {"valid_until": "2020-01-01T00:00:00+00:00"}},
28+
}
29+
notes, quotes = [], []
30+
31+
class Book:
32+
def __init__(self, root):
33+
pass
34+
35+
def state(self):
36+
return state
37+
38+
def note(self, branch, payload):
39+
notes.append(payload)
40+
41+
def close(self):
42+
pass
43+
44+
monkeypatch.setattr(paper_learning, "PaperBook", Book)
45+
monkeypatch.setattr(paper_learning, "poll_crypto", lambda *a: quotes.append("price"))
46+
monkeypatch.setattr(provider_registry, "poll", lambda *a: None)
47+
monkeypatch.setattr(market_adapters, "poll", lambda *a: None)
48+
monkeypatch.setattr(reward_policy, "train", lambda *a: {"state": "no labels"})
49+
monkeypatch.setattr(paper_learning, "scheduled_reports", lambda *a: [])
50+
now = [1791748000.0]
51+
monkeypatch.setattr(paper_learning, "time", SimpleNamespace(time=lambda: now[0]))
52+
config = tmp_path / "settings.json"
53+
atomic_json(config, {**settings(), "crypto": True})
54+
session = paper_learning.PaperLearning(tmp_path, config)
55+
return session, state, notes, quotes, now
56+
57+
58+
def test_fee_source_failure_preserves_observations_and_backs_off(observer, monkeypatch):
59+
session, state, notes, quotes, now = observer
60+
calls = []
61+
62+
def unavailable(*args, **kwargs):
63+
calls.append(1)
64+
raise spot_bootstrap.FeeSourceUnavailable("Archived unrecognized fee table")
65+
66+
monkeypatch.setattr(spot_bootstrap, "prepare", unavailable)
67+
session.tick()
68+
session.tick()
69+
session.check()
70+
assert calls == [1] and quotes == ["price", "price"]
71+
assert state["fee_profiles"]["old"]["valid_until"] == "2020-01-01T00:00:00+00:00"
72+
assert session.failure is None and notes[0]["status"] == "fee-refresh-blocked"
73+
assert session.fee_retry_after == now[0] + 3600
74+
now[0] += 3601
75+
session.tick()
76+
assert len(calls) == 2
77+
78+
79+
def test_non_fee_integrity_errors_are_not_downgraded(observer, monkeypatch):
80+
session, _, _, quotes, _ = observer
81+
82+
def invalid(*a, **k):
83+
raise ValueError("Spot execution rules changed")
84+
85+
monkeypatch.setattr(spot_bootstrap, "prepare", invalid)
86+
with pytest.raises(ValueError, match="rules changed"):
87+
session.tick()
88+
assert not quotes
89+
90+
91+
def test_repaired_fee_source_is_retried_without_resetting_session(observer, monkeypatch):
92+
session, _, _, quotes, now = observer
93+
calls = []
94+
95+
def refresh(*a, **k):
96+
calls.append(1)
97+
if len(calls) == 1:
98+
raise spot_bootstrap.FeeSourceUnavailable("Temporary layout failure")
99+
return {"status": "paper spot sources configured"}
100+
101+
monkeypatch.setattr(spot_bootstrap, "prepare", refresh)
102+
session.tick()
103+
now[0] += 3601
104+
session.tick()
105+
session.check()
106+
assert len(calls) == 2 and len(quotes) == 2 and session.failure is None
107+
108+
109+
def test_public_fee_locale_and_redirect_receipt_are_exact(tmp_path, monkeypatch):
110+
state = {"currency": "PLN", "instruments": {}, "fee_profiles": {}}
111+
112+
class Book:
113+
def __init__(self, root):
114+
pass
115+
116+
def state(self):
117+
return state
118+
119+
def close(self):
120+
pass
121+
122+
monkeypatch.setattr(spot_bootstrap, "PaperBook", Book)
123+
seen = []
124+
125+
def page(url, **kwargs):
126+
seen.append((url, kwargs))
127+
return "https://www.kraken.com/features/fee-schedule", "<p>Unrecognized publication</p>"
128+
129+
monkeypatch.setattr(spot_bootstrap, "download_page", page)
130+
with pytest.raises(spot_bootstrap.FeeSourceUnavailable, match="archived source"):
131+
spot_bootstrap.prepare(tmp_path, refresh=True)
132+
assert seen[0][1]["language"] == "en-US"
133+
receipt = json.loads((tmp_path / "research/paper/fee-source-status.json").read_text())
134+
assert receipt["source_url"] == "https://www.kraken.com/features/fee-schedule"
135+
assert receipt["state"] == "blocked"

‎tools/update-synta-fee-observer.sh‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
# Install fee observer recovery; preserve configuration.
3+
set -euo pipefail
4+
REV="${1:?Pass the pinned 40-character commit}"
5+
[[ "$REV" =~ ^[0-9a-f]{40}$ ]] || { echo 'Invalid revision' >&2; exit 1; }
6+
source "$HOME/ai-v100/env.sh"
7+
PY="$AI_V100_ROOT/venvs/v100-continual/bin/python"
8+
export SYNTA_FEE_RECOVERY_REV="$REV"
9+
10+
"$PY" <<'PY'
11+
import os
12+
import subprocess
13+
import time
14+
from importlib.metadata import version
15+
from pathlib import Path
16+
from rlm.v100.common import atomic_json, load_profile
17+
from rlm.v100.mission import status, stop
18+
19+
installed = version('rlms')
20+
if installed not in tuple(f'0.1.3+v100.{number}' for number in range(67, 74)):
21+
raise SystemExit(f'Targeted fee recovery update requires v67-v73; installed: {installed}')
22+
subprocess.run(['systemctl', '--user', 'is-active', '--quiet', 'v100-mission.service'], check=True)
23+
root = Path(os.environ['AI_V100_ROOT'])
24+
record = status(root)
25+
if not record.get('run'):
26+
raise SystemExit('No mission profile to preserve; no service changed')
27+
for line in subprocess.check_output(['ps', '-eo', 'pid=,args='], text=True).splitlines():
28+
if 'rlm.v100.cli' in line and any(' '+mode+' ' in line+' ' for mode in
29+
('train', 'mission-prepare', 'optimize-mtp', 'calibrate-training', 'test-mtp')):
30+
raise SystemExit('Exclusive experiment still runs; no service changed. '+line)
31+
source = Path(record['learning']['live_profile']) if record.get('learning', {}).get('live_profile') else Path(record['run']) / 'input-profile.json'
32+
profile = load_profile(source, root)
33+
folder = root / 'research/fee-recovery-update' / str(time.time_ns())
34+
folder.mkdir(parents=True)
35+
atomic_json(folder / 'before.json', record)
36+
atomic_json(folder / 'profile.json', profile)
37+
atomic_json(root / 'research/fee-recovery-update/current.json', {'profile': str(folder / 'profile.json')})
38+
print('Preserved profile:', folder / 'profile.json', flush=True)
39+
print(stop(root), flush=True)
40+
subprocess.run(['systemctl', '--user', 'stop', 'v100-mission.service'], check=True, timeout=90)
41+
if status(root)['running']:
42+
raise SystemExit('Mission still running; package not changed')
43+
PY
44+
45+
export UV_HTTP_TIMEOUT=600 UV_HTTP_RETRIES=5
46+
uv --no-config pip install --python "$PY" --no-deps --reinstall-package rlms \
47+
"rlms @ git+https://github.com/Mantiee/rlm.git@$REV"
48+
uv --no-config pip check --python "$PY"
49+
"$PY" <<'PY'
50+
import json
51+
import os
52+
from importlib.metadata import version
53+
from pathlib import Path
54+
from rlm.v100.supervisor import install
55+
from rlm.v100.mission import status
56+
import time
57+
58+
root = Path(os.environ['AI_V100_ROOT'])
59+
if version('rlms') != '0.1.3+v100.73':
60+
raise SystemExit('Pinned revision is not the v73 fee observer fix; mission remains stopped')
61+
path = Path(json.loads((root / 'research/fee-recovery-update/current.json').read_text())['profile'])
62+
print(json.dumps(install(root, path), indent=2), flush=True)
63+
deadline = time.monotonic() + 30
64+
while time.monotonic() < deadline:
65+
record = status(root)
66+
if record.get('running'):
67+
print(json.dumps(record, indent=2), flush=True)
68+
break
69+
time.sleep(0.5)
70+
else:
71+
raise SystemExit('Supervisor installed, but mission did not start within 30 seconds; inspect v100-mission.service journal')
72+
PY
73+
systemctl --user try-restart v100-dashboard.service
74+
systemctl --user try-restart synta-remote.service
75+
echo 'Fee observer recovery updated. Current goals, weights, Windows policy, ledgers and archived reports retained.'

0 commit comments

Comments
 (0)