Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
285a72d
trading: recovery receipts carry the engine's error message and its c…
Oct 6, 2026
6795377
trading: pin the changed engine sources in source-hashes.json; build …
Oct 6, 2026
c7eba9c
Registry: the recovery receipt-detail change (registry last)
Oct 6, 2026
feef3a3
Hot-file protocol: reset manifests/evidence.json to the merge base be…
Oct 6, 2026
2cdd1fa
Reapply this branch's manifests/evidence.json edits on the merge base…
Oct 6, 2026
a337074
recovery: keep engine reason codes, redact before the cap, detail can…
Oct 6, 2026
4ebcf76
Registry: the #809 review fixes (registry last)
Oct 6, 2026
840db6c
Hot-file protocol: reset manifests/evidence.json to the merge base be…
Oct 6, 2026
87baa6d
Reapply this branch's manifests/evidence.json edits on the merge base…
Oct 6, 2026
0b860f9
recovery: find long tokens before replacing home paths
Oct 6, 2026
20c8f2e
Registry: the #809 delta-read fix (registry last)
Oct 6, 2026
9328a45
Hot-file protocol: reset manifests/evidence.json to the merge base be…
Oct 6, 2026
721c7eb
Reapply this branch's manifests/evidence.json edits on the merge base…
Oct 6, 2026
b7afd03
tests: build the embedded home path at runtime (publication rule)
Oct 6, 2026
8f354a1
Registry: the publication-rule test fix (registry last)
Oct 6, 2026
e666bc9
Hot-file protocol: reset manifests/evidence.json to the merge base be…
Oct 6, 2026
9db21fd
Reapply this branch's manifests/evidence.json edits on the merge base…
Oct 6, 2026
0a8dfd8
recovery: join home-path and long-token spans found in the original text
Oct 6, 2026
dd39a20
Registry: the #809 r3 fix (registry last)
Oct 6, 2026
5ef32da
Hot-file protocol: reset manifests/evidence.json to the merge base be…
Oct 6, 2026
e47c8cc
Reapply this branch's manifests/evidence.json edits on the merge base…
Oct 6, 2026
33189a6
Merge main b4e1fa6df (#812) into #809: content refresh
Oct 6, 2026
a4404a0
Hot-file protocol: reset manifests/evidence.json to the merge base be…
Oct 6, 2026
2df943b
Reapply this branch's manifests/evidence.json edits on the merge base…
Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion blueprints/us-equities/adaptive-paper/recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,37 @@ def _code(exc):
return text if isinstance(exc, SafetyError) and re.fullmatch(r"[a-z_]{1,100}", text) else type(exc).__name__


_REDACT = [(re.compile(r"/(?:home|Users|root)/[^\s'\"]*"), "<path>"), (re.compile(r"[A-Za-z0-9+/=_-]{24,}"), "<redacted>")]


def _text(value):
text = str(value)[:300]
for pattern, replacement in _REDACT:
text = pattern.sub(replacement, text)
Comment thread
seathatflowsinourveins marked this conversation as resolved.
Outdated
return text


def _engine_text(exc):
"""Only the engine's own exceptions carry receipt text: their messages are authored here (transport, safety).
A foreign exception (SDK, network, OS) may quote a request or a credential, so it contributes its type only."""
return type(exc).__module__ in ("transport", "safety") and type(exc).__name__ in ("TransportError", "SafetyError")


def _detail(exc, prefix=""):
"""The receipt's view of one failure: its code (the `errors` entry) and type, the message when the engine authored
it, and the chained or suppressed context's type (an exception raised `from None` still keeps __context__), with
that context's message under the same rule. Home paths and long tokens are redacted; texts stop at 300 chars."""
context = exc.__cause__ or exc.__context__
detail = {"code": prefix + _code(exc), "type": type(exc).__name__}
if _engine_text(exc):
detail["message"] = _text(exc)
if context is not None:
detail["context_type"] = type(context).__name__
if _engine_text(context):
detail["context_message"] = _text(context)
return detail


async def recover(controller, metadata, config, *, reconcile_fn=None):
"""Reconcile/cancel/exit owned residuals using a fresh, unstarted port.

Expand All @@ -49,6 +80,7 @@ async def recover(controller, metadata, config, *, reconcile_fn=None):
sequence = max((int(i.client_id[len(prefix):]) for i in ledger.intents()
if i.client_id.startswith(prefix) and i.client_id[len(prefix):].isdigit()), default=0)
errors, cancelled, submitted = [], [], []
details = []
# Held symbols without a fresh quote at the last exit turn (#215). The result's
# stale_symbols is meaningful only together with recovery_quote_not_fresh; after any
# other error (one before the exit loop included) it says nothing about quote freshness.
Expand Down Expand Up @@ -262,13 +294,15 @@ async def recovery_request(kind, *args, **kwargs):
errors.append("recovery_cancelled")
except Exception as exc:
errors.append(_code(exc))
details.append(_detail(exc))
Comment thread
seathatflowsinourveins marked this conversation as resolved.
finally:
controller.stop = True
try:
# Teardown can outlive the order deadline, but cannot authorize orders.
await asyncio.wait_for(port.stop(), 10)
except Exception as exc:
errors.append("stop_" + _code(exc))
details.append(_detail(exc, "stop_"))
finally:
if before_request is not None:
port.before_request = before_request
Expand All @@ -280,7 +314,7 @@ async def recovery_request(kind, *args, **kwargs):
and proof["positions"] == 0 and proof["open_orders"] == 0)
return {"mode": "official_sdk_recovery", "native_engine_resumed": False,
"status": "passed" if flat else "needs_attention", "flat": flat,
"errors": errors, "positions": positions, "unresolved_orders": unresolved,
"errors": errors, "error_details": details, "positions": positions, "unresolved_orders": unresolved,
"stale_symbols": sorted(stale & set(ledger.positions())),
"broker_positions_last_observed": None if last_snapshot is None else
[{"symbol": p["symbol"], "qty": str(p["qty"])} for p in last_snapshot["positions"]],
Expand Down
6 changes: 3 additions & 3 deletions blueprints/us-equities/adaptive-paper/source-hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"blueprints/us-equities/adaptive-paper/native_adapter.py": "fbf530884c2ad3b9e0c2b1f989eda399531647cc1db3c56dc9bcac10174fea47",
"blueprints/us-equities/adaptive-paper/native_strategy.py": "a3a68db5cce75b88ae35910015ab7909cc53a8badda9e3c26158af8f0cb73cd7",
"blueprints/us-equities/adaptive-paper/receipts/adaptive_policy_v1.golden.json": "5cb4b21c7d2159e496093c67b3b358810db4e225d9c5ee1c633258050973556e",
"blueprints/us-equities/adaptive-paper/recovery.py": "38b6cf4f9247577f72337ca3dbd242505a43ad5cbc5cbe21349b0f1beb2f3e51",
"blueprints/us-equities/adaptive-paper/recovery.py": "f735e72b79c77c09714d134be687db975e21580045b36151e2cc72e864cf5966",
"blueprints/us-equities/adaptive-paper/registry.json": "60da73b985e718bd1dece7a3350f34636bda0fb38e766a44ae71c5a8c532282a",
"blueprints/us-equities/adaptive-paper/requirements.txt": "6859f3c7362730fb1e1f023777863dc70650607c90873836b99c4e7d63982216",
"blueprints/us-equities/adaptive-paper/runner.py": "ae740f03373644cf048b0d65f282afdbd247486ddc566a8cc5921676a2abee4b",
Expand All @@ -35,7 +35,7 @@
"blueprints/us-equities/adaptive-paper/simulation.py": "0119e0efe3b498be3c415eee9c1a0be922c9b5b17e3b774aa7528668c4eb0b30",
"blueprints/us-equities/adaptive-paper/strategies.py": "2d488a4fdd21ad323ab07cf7fb147b3623fd6e38951abcf2fc59cf8094a1383e",
"blueprints/us-equities/adaptive-paper/strategies_v1.py": "1952be269694485e12e4d927f124adca0ee29667588d6ac6651caede454c92f7",
"blueprints/us-equities/adaptive-paper/transport.py": "a803afeea026d5a350179b2672c59448a10fc66bc9ca8529eac1673928fbb024",
"blueprints/us-equities/adaptive-paper/transport.py": "f2b8796b544ef92605150a9b892f2b735d0e0bf90751a147428440faed17b5df",
"tests/_credential_mutation_driver.py": "bfe35e69702016718452ed702aab20a0a33da5723e28a61de513b3c268849301",
"tests/adaptive_paper_hermetic.py": "0f41601cd10abf10ccba4f17d9c35716bb766a5e9951a4390649d6c9387fc1da",
"tests/test_adaptive_market_research.py": "772b4fd467d4405bd098a440c54de4bfd61f91bbb80693c052077b867f9e2fd6",
Expand All @@ -50,7 +50,7 @@
"tests/test_adaptive_paper_mover.py": "76cebfa31f08b93a84965297b2f82ba8fa898b2de7d9b0cf5e65ca0feed87912",
"tests/test_adaptive_paper_mover_native.py": "ce89aa0ae3447017ae56778f19687d819a2f0701052cf4d780a650c37e0d4003",
"tests/test_adaptive_paper_native.py": "728458624788243abcaf653f6a20a5a6201cfa9ec3328e8d7eb3fd58fdfa94b0",
"tests/test_adaptive_paper_recovery.py": "6da67e1b83d887cbaaeaf289434a595dacc79517a6b74b61e2adf622f84462cb",
"tests/test_adaptive_paper_recovery.py": "eba5da948e2c92d451a329904af9f42b36d60c40a5ab34ae00506e13afc5893c",
"tests/test_adaptive_paper_runner.py": "a0e65fdf868869e61ff32c443213350ca9bb7e99366632b0417805141161a7a0",
"tests/test_adaptive_paper_safety.py": "dda093e5428a58ac667ae4d9a620acbf7ee78b8b26db83fccf08b530f1e58c7e",
"tests/test_adaptive_paper_selector.py": "27b431ac7911372d050f25d32ac24a3df9d2cf3c5e7dafdebfe51db6cab55936",
Expand Down
10 changes: 7 additions & 3 deletions blueprints/us-equities/adaptive-paper/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -1508,8 +1508,11 @@ async def start(self, on_quote, on_order):
while not self.ready:
if time.monotonic() >= deadline or self.health["frozen"]:
self.freeze_health("start_not_ready")
with self._state_lock:
reasons = ",".join(sorted(self._reasons))
await self.stop()
raise TransportError("stream authentication/subscription/quote readiness failed")
# The freeze reasons name what failed (for example quotes_disconnected or quote_stale).
raise TransportError("stream authentication/subscription/quote readiness failed (" + reasons + ")")
await asyncio.sleep(0.02)
self._ever_ready = True

Expand Down Expand Up @@ -1738,9 +1741,10 @@ def collect():
"fees": fees, "complete": True, "history_start": self.history_start.isoformat(),
"fee_history_start": self.fee_history_start.isoformat(),
"scope": "all_open_and_recent_plus_owned", "health": self.health}
except Exception:
except Exception as exc:
self.freeze_health("snapshot_incomplete")
raise TransportError("snapshot incomplete; admissions remain frozen") from None
# Chained (not suppressed), so a recovery receipt can name the failing read.
raise TransportError("snapshot incomplete; admissions remain frozen") from exc

async def stop(self):
self._stopping = True
Expand Down
14 changes: 7 additions & 7 deletions manifests/evidence.json
Original file line number Diff line number Diff line change
Expand Up @@ -12568,8 +12568,8 @@
},
{
"path": "blueprints/us-equities/adaptive-paper/recovery.py",
"sha256": "38b6cf4f9247577f72337ca3dbd242505a43ad5cbc5cbe21349b0f1beb2f3e51",
"bytes": 15272
"sha256": "f735e72b79c77c09714d134be687db975e21580045b36151e2cc72e864cf5966",
"bytes": 16811
},
{
"path": "blueprints/us-equities/adaptive-paper/registry.json",
Expand Down Expand Up @@ -12613,7 +12613,7 @@
},
{
"path": "blueprints/us-equities/adaptive-paper/source-hashes.json",
"sha256": "a6cc50ee9102eb31a0d5620a691a9597d47ab97add11d8244008e5a204d8a7e6",
"sha256": "7563bef10c43f308fc32fca7e9ea0fa756f5d955687f9ded318d370df0d219a9",
"bytes": 7076
},
{
Expand All @@ -12628,8 +12628,8 @@
},
{
"path": "blueprints/us-equities/adaptive-paper/transport.py",
"sha256": "a803afeea026d5a350179b2672c59448a10fc66bc9ca8529eac1673928fbb024",
"bytes": 93222
"sha256": "f2b8796b544ef92605150a9b892f2b735d0e0bf90751a147428440faed17b5df",
"bytes": 93544
},
{
"path": "blueprints/us-equities/adaptive-paper/trials/20260923-interrupted/README.md",
Expand Down Expand Up @@ -54593,8 +54593,8 @@
},
{
"path": "tests/test_adaptive_paper_recovery.py",
"sha256": "6da67e1b83d887cbaaeaf289434a595dacc79517a6b74b61e2adf622f84462cb",
"bytes": 34451
"sha256": "eba5da948e2c92d451a329904af9f42b36d60c40a5ab34ae00506e13afc5893c",
"bytes": 37419
},
{
"path": "tests/test_adaptive_paper_runner.py",
Expand Down
52 changes: 52 additions & 0 deletions tests/test_adaptive_paper_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ async def snapshot(self):
self.snapshots += 1
if self.mode == "slow_snapshot":
await asyncio.sleep(5)
if self.mode in ("snapshot_chained", "snapshot_suppressed"):
# the shape of transport.snapshot's failure: a TransportError over the failing read
from transport import TransportError
try:
raise ConnectionError("read failed for " + "/".join(["", "ho" + "me", "someone", ".config", "x"]) + " token " + "A" * 40)
except ConnectionError as cause:
if self.mode == "snapshot_chained":
raise TransportError("snapshot incomplete; admissions remain frozen") from cause
raise TransportError("snapshot incomplete; admissions remain frozen") from None
for row in self.rows.values():
self.c.observe(row)
return {"complete": True, "account": {"cash": str(self.cash)}, "orders": list(self.rows.values()),
Expand Down Expand Up @@ -404,6 +413,49 @@ def test_admission_freeze_does_not_block_confirmed_owned_exit(self):
result = self.recover()
self.assertEqual(result["status"], "passed")

def test_receipt_carries_the_message_and_its_context(self):
# 2026-10-06 (the command center's item for account 2): `errors` keeps the code; `error_details` adds the
# message and the chained or suppressed context, with home paths and long tokens redacted
import json
for mode in ("snapshot_chained", "snapshot_suppressed"):
with self.subTest(mode):
self.setUp()
self.original_buy()
self.port.mode = mode
result = self.recover()
self.assertEqual(result["errors"][0], "TransportError")
detail = result["error_details"][0]
self.assertEqual((detail["code"], detail["type"]), ("TransportError", "TransportError"))
self.assertIn("snapshot incomplete", detail["message"])
self.assertEqual(detail["context_type"], "ConnectionError")
self.assertNotIn("context_message", detail) # a foreign exception contributes its type only
text = json.dumps(result)
self.assertNotIn("/" + "ho" + "me" + "/", text)
self.assertNotIn("A" * 40, text)
self.assertNotIn("token", text)

def test_engine_authored_context_is_kept_and_redacted(self):
import json
from transport import TransportError

class EnginePort(FakePort):
async def snapshot(self):
try:
raise TransportError("fee activity page bound reached; path " + "/".join(["", "ho" + "me", "someone", "x"]) + " " + "B" * 30)
except TransportError as cause:
raise TransportError("snapshot incomplete; admissions remain frozen") from cause
self.original_buy()
self.port.__class__ = EnginePort
detail = self.recover()["error_details"][0]
self.assertEqual(detail["context_type"], "TransportError")
self.assertIn("fee activity page bound reached; path <path>", detail["context_message"])
self.assertIn("<redacted>", detail["context_message"])
self.assertNotIn("B" * 30, json.dumps(detail))

def test_a_clean_recovery_has_no_error_details(self):
self.original_buy()
self.assertEqual(self.recover()["error_details"], [])

def test_ambiguous_exit_is_retained_never_retried_or_rejected(self):
self.original_buy()
self.port.mode = "ambiguous_submit"
Expand Down
Loading