Prevent repeated watch loops

This commit is contained in:
saymrwulf 2026-04-27 17:59:30 +02:00
parent a4a19f30d8
commit 70beecd615
7 changed files with 214 additions and 37 deletions

View file

@ -39,6 +39,15 @@ You do not need to babysit it. It will:
If you want the technical report, run `./scripts/ratchet report`. The normal workflow intentionally shows the cockpit first.
After a watch finishes, the cockpit enters a post-watch cooldown. That is deliberate.
Post-watch cooldown means:
1. The current experimental stage is complete.
2. Starting another identical watch immediately is not useful ratcheting.
3. The run report is the evidence artifact.
4. The next planned touch is a later fresh sample, usually `./scripts/ratchet once`.
## Research Pathway
The cockpit has two different time horizons:

View file

@ -48,6 +48,8 @@ At the end of a normal watch, inspect the ratchet record:
The ledger is the main artifact. It says what was tested, how long it ran, what actions the strategy would have considered, and what adaptation should be considered next.
After a completed watch, the cockpit should not immediately recommend another identical watch. It enters post-watch cooldown, which means the current stage is complete and the next useful operator touch is a later fresh sample.
If a run already happened before automatic bookkeeping was available, reconstruct it from the stored SQLite snapshots:
```bash

View file

@ -25,3 +25,25 @@ Karpathy-style ratchet rule: every run states a hypothesis, collects data, score
- action_counts: manual_canary=24
- report: reports/retro-2026-04-25T19-08-00-00-00.md
- adaptation: The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
## run-20260427T135327Z-222826
- status: running
- started_utc: 2026-04-27T13:53:27+00:00
- planned_cycles: 24
- interval_seconds: 300
- planned_duration_minutes: 120.0
- hypothesis: Depth-aware fillable price plus a small overpay cushion is a better canary trigger than raw best ask.
- plan: collect public Braiins depth, collect OCEAN state, compute shadow canary, store every proposal.
- operator_action: none by default; manual action only if report later says manual_canary or manual_bid and operator agrees.
- status_update: completed
- ended_utc: 2026-04-27T15:48:52+00:00
- collected_samples: 24
- action_counts: manual_canary=24
- strategy_price_min_avg_max: 0.47763 / 0.4798921698412225 / 0.48492
- expected_net_min_avg_max_btc: -0.00000383101148933686517479144602 / -0.000002821503981988926325106899014 / -0.00000236319764547711127977695701
- latest_action: manual_canary
- latest_reason: bounded research canary: profit guardrails not cleared, but expected_net=-0.00000238363529903760002629072482 is within loss budget 0.000025
- report: reports/run-20260427T135327Z-222826.md
- adaptation: The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.

View file

@ -0,0 +1,73 @@
# run-20260427T135327Z-222826
## Ratchet Question
Depth-aware fillable price plus a small overpay cushion is a better canary trigger than raw best ask.
## Run Summary
- started_utc: 2026-04-27T13:53:27+00:00
- ended_utc: 2026-04-27T15:48:52+00:00
- planned_cycles: 24
- interval_seconds: 300
- collected_samples: 24
- first_sample_utc: 2026-04-27T13:53:28+00:00
- last_sample_utc: 2026-04-27T15:48:52+00:00
- action_counts: manual_canary=24
- strategy_price_min_avg_max: 0.47763 / 0.4798921698412225 / 0.48492
- expected_net_min_avg_max_btc: -0.00000383101148933686517479144602 / -0.000002821503981988926325106899014 / -0.00000236319764547711127977695701
## Interpretation
The run repeatedly found bounded canary conditions, but modeled net was negative on average. Next ratchet: do not escalate spend; test a smaller depth target or a lower overpay cushion.
## Operator Reading
This run collected 24 public Braiins market samples. The strategy outcomes were: manual_canary=24. Expected net ranged from -0.00000383101148933686517479144602 to -0.00000236319764547711127977695701 BTC. For ratcheting, do not ask whether one line was green or red. Ask whether this run changed one control knob: depth target, overpay cushion, canary spend, duration, or timing window.
## Current Full Report Context
The block below is the latest complete human report available when this markdown was written. For retroactive reports, the authoritative reconstructed run data is the summary above.
```text
Braiins Ratchet Report
OCEAN snapshot: 2026-04-27T15:48:52+00:00
pool_hashrate_eh_s: 15.51
network_difficulty_t: 135.59
share_log_window_t: 1084.76
avg_block_time_hours: 9
Braiins market snapshot: 2026-04-27T15:48:52+00:00
status: SPOT_INSTRUMENT_STATUS_ACTIVE
best_bid_btc_per_eh_day: 0.50343
best_ask_btc_per_eh_day: 0.46075
fillable_target_ph: 2
fillable_price_btc_per_eh_day: 0.46773
suggested_bid_btc_per_eh_day: 0.47773
last_price_btc_per_eh_day: 0.47933997316187
available_hashrate_eh_s: 1.91368833114474
sampled_strategy_price_count: 50
sampled_strategy_price_min_avg_max: 0.46103 / 0.4792744415237868 / 0.48492
Strategy action: manual_canary
reason: bounded research canary: profit guardrails not cleared, but expected_net=-0.00000238363529903760002629072482 is within loss budget 0.000025
breakeven_btc_per_eh_day: 0.4663426590859076733944013201
expected_reward_btc: 0.00009761636470096239997370927518
expected_net_btc: -0.00000238363529903760002629072482
score_btc: -0.00003654936294437444001708897113
maturity: treat canary as immature for about 72 hours after spend
proposed_price_btc_per_eh_day: 0.47773
proposed_spend_btc: 0.00010
proposed_duration_minutes: 180
implied_hashrate_eh_s: 0.001674586063257488539551629582
Plain English
Decision: a tiny manual research canary is allowed by the loss budget, but this is not a profit signal.
Market depth: the visible best ask is 0.46075, but enough depth for 2 PH/s starts at 0.46773 (gap 0.00698).
Price check: proposed price is 0.47773; estimated breakeven is 0.4663426590859076733944013201; edge is -0.0113873409140923266055986799 BTC/EH/day.
Manual canary card: spend 0.00010 BTC (~10000 sats), duration 180 minutes, estimated speed 1.674586063257488539551629582 PH/s.
Expected result: -0.00000238363529903760002629072482 BTC (~-238 sats) before luck; this is a model estimate, not a promise.
Wait time: treat canary as immature for about 72 hours after spend.
Rule: manual_canary means buying information with bounded downside; manual_bid means the stricter profit-seeking guardrails cleared.
```

View file

@ -97,7 +97,7 @@ cmd_watch() {
echo
echo "Watch finished. Reading the cockpit now."
echo
run_python -m braiins_ratchet.cli next
BRAIINS_RATCHET_IGNORE_PROCESS_WATCH=1 run_python -m braiins_ratchet.cli next
return "$status"
}

View file

@ -9,6 +9,8 @@ import subprocess
from .experiments import ACTIVE_WATCH, EXPERIMENT_LOG, REPORTS_DIR
from .storage import latest_market_snapshot, latest_ocean_snapshot, latest_proposal
POST_WATCH_COOLDOWN_MINUTES = 360
def build_operator_cockpit(conn) -> str:
ocean = latest_ocean_snapshot(conn)
@ -17,6 +19,7 @@ def build_operator_cockpit(conn) -> str:
latest_report = _latest_report()
running_runs = _running_runs()
active_watch = _active_watch()
completed_watch = _recent_completed_watch(latest_report, market.timestamp_utc if market else None)
freshness = _freshness_minutes(market.timestamp_utc if market else None)
is_fresh = freshness is not None and freshness <= 30
@ -32,6 +35,7 @@ def build_operator_cockpit(conn) -> str:
f" Latest run report: {latest_report or 'none yet'}",
f" Experiment ledger: {EXPERIMENT_LOG.relative_to(REPORTS_DIR.parent) if EXPERIMENT_LOG.exists() else 'none yet'}",
f" Active watch: {active_watch or 'none detected'}",
f" Research stage: {_research_stage(active_watch, completed_watch)}",
]
if running_runs:
@ -41,6 +45,7 @@ def build_operator_cockpit(conn) -> str:
lines.extend(
_do_this_now(
active_watch=active_watch,
completed_watch=completed_watch,
has_ocean=ocean is not None,
has_market=market is not None,
is_fresh=is_fresh,
@ -51,6 +56,7 @@ def build_operator_cockpit(conn) -> str:
lines.extend(
_pathway_forecast(
active_watch=active_watch,
completed_watch=completed_watch,
has_ocean=ocean is not None,
has_market=market is not None,
is_fresh=is_fresh,
@ -83,6 +89,7 @@ def build_operator_cockpit(conn) -> str:
def _do_this_now(
active_watch: str | None,
completed_watch: tuple[str, int] | None,
has_ocean: bool,
has_market: bool,
is_fresh: bool,
@ -97,6 +104,19 @@ def _do_this_now(
" ./scripts/ratchet",
]
if completed_watch and action == "manual_canary":
report_path, age_minutes = completed_watch
remaining = max(0, POST_WATCH_COOLDOWN_MINUTES - age_minutes)
return [
" STOP.",
f" The latest 2-hour watch already finished {age_minutes} minutes ago.",
f" Report written: {report_path}",
" Do not start another identical watch now.",
f" Next planned operator touch: after about {remaining} minutes, run exactly:",
" ./scripts/ratchet once",
" Reason: this ratchet stage is complete; repeating it immediately would be loop-chasing, not research.",
]
if not has_ocean or not has_market:
return [
" Run exactly:",
@ -140,6 +160,7 @@ def _do_this_now(
def _pathway_forecast(
active_watch: str | None,
completed_watch: tuple[str, int] | None,
has_ocean: bool,
has_market: bool,
is_fresh: bool,
@ -153,6 +174,15 @@ def _pathway_forecast(
" Longterm, possible: adjust one strategy knob if the report says the run taught us something.",
]
if completed_watch and action == "manual_canary":
report_path, _age_minutes = completed_watch
return [
" Planning probabilities are workflow estimates, not profit probabilities.",
f" Immediate, certain: stop this stage and keep {report_path} as the evidence artifact.",
" Midterm, likely: after cooldown, refresh with one once command and compare against this report.",
" Longterm, possible: adjust exactly one knob only if repeated reports show the same pattern.",
]
if not has_ocean or not has_market:
return [
" Planning probabilities are workflow estimates, not profit probabilities.",
@ -228,6 +258,32 @@ def _latest_report() -> str | None:
return str(reports[0].relative_to(REPORTS_DIR.parent))
def _recent_completed_watch(latest_report: str | None, latest_market_timestamp: str | None) -> tuple[str, int] | None:
if latest_report is None:
return None
report_path = REPORTS_DIR.parent / latest_report
if not report_path.name.startswith("run-") or not report_path.exists():
return None
market_dt = _parse_utc(latest_market_timestamp)
if market_dt is not None and report_path.stat().st_mtime < market_dt.timestamp():
return None
age_seconds = datetime.now(UTC).timestamp() - report_path.stat().st_mtime
age_minutes = max(0, int(age_seconds // 60))
if age_minutes > POST_WATCH_COOLDOWN_MINUTES:
return None
return latest_report, age_minutes
def _research_stage(active_watch: str | None, completed_watch: tuple[str, int] | None) -> str:
if active_watch:
return "watch running"
if completed_watch:
report_path, age_minutes = completed_watch
remaining = max(0, POST_WATCH_COOLDOWN_MINUTES - age_minutes)
return f"post-watch cooldown ({remaining} minutes left, report {report_path})"
return "ready"
def _running_runs() -> list[str]:
if not EXPERIMENT_LOG.exists():
return []
@ -266,6 +322,8 @@ def _active_watch_from_state_file() -> str | None:
def _active_watch_from_process_table() -> str | None:
if os.environ.get("BRAIINS_RATCHET_IGNORE_PROCESS_WATCH") == "1":
return None
try:
output = subprocess.check_output(
["ps", "-axo", "pid=,command="],
@ -302,6 +360,14 @@ def _pid_exists(pid: int) -> bool:
def _freshness_minutes(timestamp_utc: str | None) -> int | None:
parsed = _parse_utc(timestamp_utc)
if parsed is None:
return None
age = datetime.now(UTC) - parsed
return max(0, int(age.total_seconds() // 60))
def _parse_utc(timestamp_utc: str | None) -> datetime | None:
if not timestamp_utc:
return None
try:
@ -310,8 +376,7 @@ def _freshness_minutes(timestamp_utc: str | None) -> int | None:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=UTC)
age = datetime.now(UTC) - parsed.astimezone(UTC)
return max(0, int(age.total_seconds() // 60))
return parsed.astimezone(UTC)
def _freshness_text(freshness: int | None) -> str:

View file

@ -3,7 +3,7 @@ from datetime import UTC, datetime
import sqlite3
import unittest
from braiins_ratchet.guidance import build_operator_cockpit
from braiins_ratchet.guidance import _do_this_now, _pathway_forecast, build_operator_cockpit
from braiins_ratchet.models import CandidateOrder, MarketSnapshot, OceanSnapshot, StrategyProposal
from braiins_ratchet.storage import init_db, save_market_snapshot, save_ocean_snapshot, save_proposal
@ -66,44 +66,50 @@ class GuidanceTests(unittest.TestCase):
self.assertIn("Longterm, possible", text)
def test_stale_market_data_routes_operator_to_once(self) -> None:
conn = sqlite3.connect(":memory:")
init_db(conn)
save_ocean_snapshot(
conn,
OceanSnapshot(
timestamp_utc="2000-01-01T00:00:00+00:00",
pool_hashrate_eh_s=Decimal("16.95"),
),
)
save_market_snapshot(
conn,
MarketSnapshot(
timestamp_utc="2000-01-01T00:00:01+00:00",
best_price_btc_per_eh_day=Decimal("0.48031"),
source="braiins-public",
),
)
save_proposal(
conn,
StrategyProposal(
action="manual_canary",
reason="inside research loss budget",
order=None,
breakeven_btc_per_eh_day=Decimal("0.46634"),
expected_reward_btc=Decimal("0.000097"),
expected_net_btc=Decimal("-0.000003"),
score_btc=Decimal("-0.000037"),
maturity_note="treat canary as immature",
),
lines = _do_this_now(
active_watch=None,
completed_watch=None,
has_ocean=True,
has_market=True,
is_fresh=False,
action="manual_canary",
)
text = build_operator_cockpit(conn)
text = "\n".join(lines)
self.assertIn("Braiins sample freshness: stale", text)
self.assertIn("./scripts/ratchet once", text)
self.assertIn("latest Braiins sample is stale", text)
self.assertIn("DO THIS NOW", text)
self.assertIn("if the fresh state still says manual_canary", text)
def test_recent_completed_watch_stops_identical_watch_loop(self) -> None:
lines = _do_this_now(
active_watch=None,
completed_watch=("reports/run-example.md", 4),
has_ocean=True,
has_market=True,
is_fresh=True,
action="manual_canary",
)
text = "\n".join(lines)
self.assertIn("STOP.", text)
self.assertIn("Do not start another identical watch now.", text)
self.assertIn("./scripts/ratchet once", text)
def test_recent_completed_watch_forecast_enters_cooldown(self) -> None:
lines = _pathway_forecast(
active_watch=None,
completed_watch=("reports/run-example.md", 4),
has_ocean=True,
has_market=True,
is_fresh=True,
action="manual_canary",
)
text = "\n".join(lines)
self.assertIn("Immediate, certain: stop this stage", text)
self.assertIn("Midterm, likely: after cooldown", text)
if __name__ == "__main__":