@senpi/runtime 3.0.9-dev.feat-strategy-compos.20260722213012 → 3.0.9-dev.feat-strategy-compos.20260723050017
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/composer-commands.d.ts.map +1 -1
- package/dist/cli/composer-commands.js +37 -2
- package/dist/cli/composer-commands.js.map +1 -1
- package/dist/composer/cli.py +115 -19
- package/dist/composer/sim/timeline.py +47 -3
- package/dist/composer/validator/errors.py +2 -0
- package/dist/composer/verbs/__init__.py +3 -0
- package/dist/composer/verbs/_target.py +138 -0
- package/dist/composer/verbs/close.py +3 -1
- package/dist/composer/verbs/fund.py +78 -16
- package/dist/composer/verbs/install.py +4 -2
- package/dist/composer/verbs/ledger.py +453 -0
- package/dist/composer/verbs/review.py +280 -1
- package/dist/composer/verbs/status.py +26 -5
- package/dist/composer/verbs/update.py +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/no-reply-scoping-overlay.d.ts +75 -0
- package/dist/runtime/no-reply-scoping-overlay.d.ts.map +1 -0
- package/dist/runtime/no-reply-scoping-overlay.js +93 -0
- package/dist/runtime/no-reply-scoping-overlay.js.map +1 -0
- package/package.json +1 -1
|
@@ -37,22 +37,28 @@ anything.
|
|
|
37
37
|
|
|
38
38
|
from __future__ import annotations
|
|
39
39
|
|
|
40
|
+
import json
|
|
40
41
|
from dataclasses import dataclass, field
|
|
41
42
|
from pathlib import Path
|
|
42
43
|
from typing import Callable, Optional
|
|
43
44
|
|
|
44
45
|
from . import _box
|
|
45
46
|
from ._box import McpFn, default_mcp_call
|
|
46
|
-
from ._layout import canonical_graph_path
|
|
47
|
+
from ._layout import ARCHIVES_DIR, canonical_graph_path
|
|
47
48
|
from ._live import LiveState
|
|
49
|
+
from ._target import TargetError
|
|
48
50
|
from .portfolio import _effective_install, _resolve_wallet, enumerate_strategies
|
|
49
51
|
from .status import run_status
|
|
52
|
+
from composer.validator.errors import Location, TeachingError
|
|
50
53
|
|
|
51
54
|
# The named sources every figure is attributed to (printed so an operator always knows the producer).
|
|
52
55
|
SRC_HISTORY = "backend trade history" # discovery_get_trader_history (onchain closed ledger)
|
|
53
56
|
SRC_EVENTS = "box event log" # the runtime's on-disk event ring (dsl/position/order/…)
|
|
54
57
|
|
|
55
58
|
CLOSED_HISTORY_PULL = 100 # closed positions to pull per wallet (review.py parity)
|
|
59
|
+
EVENTS_PULL = 500 # recent-ring window per runtime (review.py's `-l 500`;
|
|
60
|
+
# mirrors composer-live-read.ts EVENTS_PULL_DEFAULT so the
|
|
61
|
+
# ARCHIVED cold path enriches over the SAME depth as live)
|
|
56
62
|
|
|
57
63
|
# EXIT attribution — the exit events a closed trade is matched against (references/event-log.md shapes).
|
|
58
64
|
_EXIT_EVENT_NAMES = ("dsl.closed", "position.closed")
|
|
@@ -722,3 +728,276 @@ def run_review(
|
|
|
722
728
|
result.strategies.append(
|
|
723
729
|
_review_one(str(state_dir), strategy, runtime_id, wallet, history_fn, events_section))
|
|
724
730
|
return result
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# =========================================================================== K7: ARCHIVED cold path
|
|
734
|
+
#
|
|
735
|
+
# The live review reads the HOT event ring served in-process by the rim (no cold-boot subprocess).
|
|
736
|
+
# But `runtime delete` (close / update teardown) ARCHIVES the whole per-runtime state dir to
|
|
737
|
+
# `<base>/archives/<runtimeId>-<wallet>-<ts>/` (src/runtime/state-layout.ts::archiveStateDir), moving
|
|
738
|
+
# the on-disk event ring with it (`<archive>/strategies/<wallet>/events/events.jsonl` +
|
|
739
|
+
# `events.1.jsonl`, src/telemetry/event-store.ts). A CLOSED / torn-down strategy's exit-attribution
|
|
740
|
+
# data SURVIVES teardown — it is just no longer on the live rim. So `composer review --archived
|
|
741
|
+
# <wallet|runtime>` does an OFFLINE FILE READ of that archived ring (NOT a subprocess, NOT a cold
|
|
742
|
+
# boot — the sanctioned exception the module header carves out) and feeds the rows through the SAME
|
|
743
|
+
# enrichment/render the live path uses. Dev-test #7 F3: the model called a closed strategy's exits
|
|
744
|
+
# "unknowable (event ring gone)" while the ground-truth DSL trail sat in the archive — this reaches it.
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _parse_archive_dir_name(name: str) -> Optional[tuple[str, str, int]]:
|
|
748
|
+
"""Parse an archive dir name ``<runtimeId>-<wallet>-<ts>`` into ``(runtimeId, wallet, ts_ms)``.
|
|
749
|
+
Mirrors TS ``listArchives`` (state-layout.ts): the trailing token is the epoch-ms timestamp, the
|
|
750
|
+
token before it is the wallet (0x… — no dashes), and everything before that is the runtime id
|
|
751
|
+
(which DOES carry dashes, e.g. ``<strategy>-<hash8>``). None when the name doesn't fit / no ts."""
|
|
752
|
+
parts = name.rsplit("-", 2)
|
|
753
|
+
if len(parts) < 3:
|
|
754
|
+
return None
|
|
755
|
+
runtime_id, wallet, ts_str = parts[0], parts[1], parts[2]
|
|
756
|
+
if not runtime_id or not wallet:
|
|
757
|
+
return None
|
|
758
|
+
try:
|
|
759
|
+
ts = int(ts_str)
|
|
760
|
+
except ValueError:
|
|
761
|
+
return None
|
|
762
|
+
if ts <= 0:
|
|
763
|
+
return None
|
|
764
|
+
return runtime_id, wallet, ts
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _archives_root(base_state_dir: str) -> Path:
|
|
768
|
+
return Path(base_state_dir) / ARCHIVES_DIR
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _list_archive_dirs(base_state_dir: str) -> list[tuple[str, str, str, int, Path]]:
|
|
772
|
+
"""Every parseable archive under ``<base>/archives`` as ``(name, runtimeId, wallet, ts, path)``,
|
|
773
|
+
newest ts first. Unparseable / non-dir entries are skipped (they are not composer's shape)."""
|
|
774
|
+
root = _archives_root(base_state_dir)
|
|
775
|
+
if not root.is_dir():
|
|
776
|
+
return []
|
|
777
|
+
out: list[tuple[str, str, str, int, Path]] = []
|
|
778
|
+
for child in sorted(root.iterdir()):
|
|
779
|
+
if not child.is_dir():
|
|
780
|
+
continue
|
|
781
|
+
parsed = _parse_archive_dir_name(child.name)
|
|
782
|
+
if parsed is None:
|
|
783
|
+
continue
|
|
784
|
+
runtime_id, wallet, ts = parsed
|
|
785
|
+
out.append((child.name, runtime_id, wallet, ts, child))
|
|
786
|
+
out.sort(key=lambda t: t[3], reverse=True)
|
|
787
|
+
return out
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _resolve_archive(base_state_dir: str, target: str) -> tuple[str, str, int, Path]:
|
|
791
|
+
"""Resolve the NEWEST archive dir matching ``target`` (a wallet OR a runtime id OR a substring of
|
|
792
|
+
the dir name). Returns ``(runtimeId, wallet, ts, path)``. Fail-loud: raises :class:`TargetError`
|
|
793
|
+
(CMP256 — the existing "target could not be resolved" class, reused per the _target.py precedent)
|
|
794
|
+
ENUMERATING the archive dirs that DO exist, mirroring ``_target.available_strategies_hint``."""
|
|
795
|
+
archives = _list_archive_dirs(base_state_dir)
|
|
796
|
+
tl = target.strip().lower()
|
|
797
|
+
matches = [
|
|
798
|
+
a for a in archives
|
|
799
|
+
if a[2].lower() == tl # wallet (case-insensitive)
|
|
800
|
+
or a[1] == target.strip() # runtime id (exact)
|
|
801
|
+
or tl in a[0].lower() # substring of the dir name (strategy-name / partial)
|
|
802
|
+
]
|
|
803
|
+
if matches:
|
|
804
|
+
best = matches[0] # already newest-first
|
|
805
|
+
_name, runtime_id, wallet, ts, path = best
|
|
806
|
+
return runtime_id, wallet, ts, path
|
|
807
|
+
|
|
808
|
+
root = _archives_root(base_state_dir)
|
|
809
|
+
if archives:
|
|
810
|
+
shown = ", ".join(a[0] for a in archives[:20]) + (", …" if len(archives) > 20 else "")
|
|
811
|
+
hint = (f"Archive dirs under {root} (newest first): {shown}. Pass a wallet (0x…) or a "
|
|
812
|
+
f"runtime id that appears in one of those names.")
|
|
813
|
+
else:
|
|
814
|
+
hint = (f"No archived runtime state dirs exist under {root} — nothing has been closed / "
|
|
815
|
+
f"torn down on this box (archives are written by `runtime delete`, i.e. `composer "
|
|
816
|
+
f"close` / `composer update`). For a LIVE strategy, run `composer review <strategy>` "
|
|
817
|
+
f"(no --archived).")
|
|
818
|
+
raise TargetError(TeachingError(
|
|
819
|
+
"CMP256",
|
|
820
|
+
f"no archived event ring matched {target!r} under {root} — it is not the wallet or runtime "
|
|
821
|
+
f"id of any archived (closed / torn-down) runtime on this box.",
|
|
822
|
+
hint,
|
|
823
|
+
Location(),
|
|
824
|
+
))
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _find_archived_events_dir(archive_dir: Path, wallet: str) -> Optional[Path]:
|
|
828
|
+
"""The events dir inside an archive: ``<archive>/strategies/<wallet>/events`` (the on-disk ring
|
|
829
|
+
layout, event-store.ts::eventsDir, keyed by the LOWERCASED wallet). Falls back to the first
|
|
830
|
+
``strategies/*/events`` dir if the exact lowercased wallet subdir isn't there (a case/normalize
|
|
831
|
+
drift). None when the archive carries no events tree at all."""
|
|
832
|
+
strategies = archive_dir / "strategies"
|
|
833
|
+
if not strategies.is_dir():
|
|
834
|
+
return None
|
|
835
|
+
exact = strategies / wallet.strip().lower() / "events"
|
|
836
|
+
if exact.is_dir():
|
|
837
|
+
return exact
|
|
838
|
+
for sub in sorted(strategies.iterdir()):
|
|
839
|
+
ev = sub / "events"
|
|
840
|
+
if sub.is_dir() and ev.is_dir():
|
|
841
|
+
return ev
|
|
842
|
+
return None
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _read_archived_ring(events_dir: Optional[Path]) -> tuple[list[dict], int]:
|
|
846
|
+
"""Read the archived 2-file ring OFFLINE: ``events.1.jsonl`` (older rotation) THEN ``events.jsonl``
|
|
847
|
+
(newer), chronological — the SAME order the runtime's ``readEvents`` uses. Each line is one JSONL
|
|
848
|
+
row (``{ts,name,body,level,attrs}``). Corrupt / non-dict rows are SKIPPED and COUNTED (never a
|
|
849
|
+
crash, never silently dropped). Applies the ``EVENTS_PULL`` recency cap (newest tail by ts).
|
|
850
|
+
Returns ``(rows, skipped_count)``; rows is ``[]`` for a genuinely empty / absent ring."""
|
|
851
|
+
rows: list[dict] = []
|
|
852
|
+
skipped = 0
|
|
853
|
+
if events_dir is None:
|
|
854
|
+
return rows, skipped
|
|
855
|
+
for fname in ("events.1.jsonl", "events.jsonl"): # archive (older) then current (newer)
|
|
856
|
+
fpath = events_dir / fname
|
|
857
|
+
if not fpath.is_file():
|
|
858
|
+
continue
|
|
859
|
+
try:
|
|
860
|
+
text = fpath.read_text(encoding="utf-8")
|
|
861
|
+
except OSError:
|
|
862
|
+
continue
|
|
863
|
+
for line in text.splitlines():
|
|
864
|
+
if not line.strip():
|
|
865
|
+
continue
|
|
866
|
+
try:
|
|
867
|
+
obj = json.loads(line)
|
|
868
|
+
except (json.JSONDecodeError, ValueError):
|
|
869
|
+
skipped += 1 # a torn / partial JSONL row — count, skip
|
|
870
|
+
continue
|
|
871
|
+
if isinstance(obj, dict):
|
|
872
|
+
rows.append(obj)
|
|
873
|
+
else:
|
|
874
|
+
skipped += 1 # a well-formed-but-non-object row
|
|
875
|
+
# Recency cap (EVENTS_PULL) — keep the newest tail by ts, matching the live per-runtime pull.
|
|
876
|
+
if len(rows) > EVENTS_PULL:
|
|
877
|
+
rows.sort(key=lambda r: _num(r.get("ts")) or 0)
|
|
878
|
+
rows = rows[-EVENTS_PULL:]
|
|
879
|
+
return rows, skipped
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
@dataclass
|
|
883
|
+
class ArchivedReviewResult:
|
|
884
|
+
"""A CLOSED / torn-down strategy's retrospective review, read from its ARCHIVED on-disk event ring
|
|
885
|
+
(K7). Wraps ONE :class:`StrategyReview` (built through the SAME enrichment/render as live) with the
|
|
886
|
+
archive provenance banner: which archive dir, when it was archived, the runtime id, and the ring's
|
|
887
|
+
read/skip counts. Exit reasons + leaks come from the archived ring; realized PnL / win rate from the
|
|
888
|
+
backend trade history (wired from the wallet parsed out of the archive name)."""
|
|
889
|
+
|
|
890
|
+
base_state_dir: str
|
|
891
|
+
target: str
|
|
892
|
+
archive_name: str
|
|
893
|
+
archive_dir: str
|
|
894
|
+
archive_ts: int
|
|
895
|
+
runtime_id: str
|
|
896
|
+
wallet: str
|
|
897
|
+
strategy: str
|
|
898
|
+
review: StrategyReview
|
|
899
|
+
events_total: int = 0
|
|
900
|
+
rows_skipped: int = 0
|
|
901
|
+
|
|
902
|
+
@property
|
|
903
|
+
def ring_empty(self) -> bool:
|
|
904
|
+
return self.events_total == 0
|
|
905
|
+
|
|
906
|
+
def _archived_at(self) -> str:
|
|
907
|
+
"""The archive timestamp as an ISO-8601 UTC string (the ts is epoch MILLISECONDS)."""
|
|
908
|
+
import datetime as _dt
|
|
909
|
+
try:
|
|
910
|
+
return _dt.datetime.fromtimestamp(self.archive_ts / 1000.0,
|
|
911
|
+
tz=_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
912
|
+
except (OverflowError, OSError, ValueError):
|
|
913
|
+
return "unknown"
|
|
914
|
+
|
|
915
|
+
def to_json(self) -> dict:
|
|
916
|
+
return {
|
|
917
|
+
"verb": "review",
|
|
918
|
+
"scope": "archived",
|
|
919
|
+
"state_dir": self.base_state_dir,
|
|
920
|
+
"target": self.target,
|
|
921
|
+
"archived": {
|
|
922
|
+
"archive_name": self.archive_name,
|
|
923
|
+
"archive_dir": self.archive_dir,
|
|
924
|
+
"archived_at_ms": self.archive_ts,
|
|
925
|
+
"archived_at": self._archived_at(),
|
|
926
|
+
"runtime_id": self.runtime_id,
|
|
927
|
+
"wallet": self.wallet,
|
|
928
|
+
"strategy": self.strategy,
|
|
929
|
+
"ring_events_read": self.events_total,
|
|
930
|
+
"ring_rows_skipped": self.rows_skipped,
|
|
931
|
+
"ring_empty": self.ring_empty,
|
|
932
|
+
},
|
|
933
|
+
"sources": {"history": SRC_HISTORY, "events": f"{SRC_EVENTS} (ARCHIVED)"},
|
|
934
|
+
"strategy": self.review.to_json(),
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
def render(self) -> str:
|
|
938
|
+
lines = [
|
|
939
|
+
f"trade review — ARCHIVED (state: {self.base_state_dir})",
|
|
940
|
+
f"reading the NEWEST archived event ring for target {self.target!r}: this strategy is "
|
|
941
|
+
f"CLOSED / torn down — its live runtime + event ring were ARCHIVED on `runtime delete` "
|
|
942
|
+
f"(`composer close` / `composer update`). Exit reasons + leaks below are from that "
|
|
943
|
+
f"archived ring [{SRC_EVENTS}]; realized PnL + win rate from {SRC_HISTORY}.",
|
|
944
|
+
f" archive : {self.archive_name} (archived {self._archived_at()})",
|
|
945
|
+
f" archive dir: {self.archive_dir}",
|
|
946
|
+
f" runtime : {self.runtime_id}",
|
|
947
|
+
]
|
|
948
|
+
if self.ring_empty:
|
|
949
|
+
lines.append(" ring : archived ring is empty — no events were recorded "
|
|
950
|
+
"(the ring WAS read; it simply held nothing — this is not an "
|
|
951
|
+
"unavailable/undetermined telemetry state)")
|
|
952
|
+
else:
|
|
953
|
+
skip = f" [{self.rows_skipped} unreadable row(s) skipped]" if self.rows_skipped else ""
|
|
954
|
+
lines.append(f" ring : {self.events_total} event(s) read from the archived "
|
|
955
|
+
f"ring{skip} [{SRC_EVENTS} (ARCHIVED)]")
|
|
956
|
+
lines.append("")
|
|
957
|
+
lines.append("strategy (closed / torn down):")
|
|
958
|
+
lines.extend(self.review.render())
|
|
959
|
+
return "\n".join(lines) + "\n"
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def run_archived_review(
|
|
963
|
+
base_state_dir: str,
|
|
964
|
+
target: str,
|
|
965
|
+
*,
|
|
966
|
+
history_fn: HistoryFn = default_closed_history,
|
|
967
|
+
) -> ArchivedReviewResult:
|
|
968
|
+
"""`composer review --archived <wallet|runtime>` — review a CLOSED / torn-down strategy from its
|
|
969
|
+
ARCHIVED on-disk event ring (K7). Resolves the NEWEST archive dir under ``<base>/archives`` matching
|
|
970
|
+
the target (wallet / runtime id / dir-name substring; :class:`TargetError` CMP256 enumerating what
|
|
971
|
+
DOES exist when nothing matches), reads the archived ring OFFLINE (``events.1.jsonl`` then
|
|
972
|
+
``events.jsonl``, EVENTS_PULL-capped, corrupt rows skipped + counted), and feeds the rows through
|
|
973
|
+
the SAME exit-reason enrichment + leak scan + render the LIVE path uses — banner-labelled ARCHIVED.
|
|
974
|
+
Closed-trade history is wired from the wallet parsed out of the archive name (an honest "unavailable
|
|
975
|
+
(reason)" degrades that side only). Never a subprocess, never a cold boot — a sanctioned file read."""
|
|
976
|
+
runtime_id, wallet, ts, archive_dir = _resolve_archive(base_state_dir, target)
|
|
977
|
+
|
|
978
|
+
events_dir = _find_archived_events_dir(archive_dir, wallet)
|
|
979
|
+
rows, skipped = _read_archived_ring(events_dir)
|
|
980
|
+
|
|
981
|
+
# Derive the strategy NAME from the runtime id (``<strategy>-<hash8>``) so the render labels it and
|
|
982
|
+
# a still-present composer/<strategy>/graph.yaml thesis is quoted (torn-down runtime, but the
|
|
983
|
+
# composer record graph often survives). Falls back to the raw runtime id when there is no dash.
|
|
984
|
+
strategy = runtime_id.rsplit("-", 1)[0] if "-" in runtime_id else runtime_id
|
|
985
|
+
|
|
986
|
+
# Feed the archived rows through the SAME machinery as live by presenting them as the resolved
|
|
987
|
+
# runtime's slice of an events section — _review_one + StrategyReview.render() are reused verbatim.
|
|
988
|
+
events_section = {"ok": True, "by_runtime": {runtime_id: rows}}
|
|
989
|
+
sr = _review_one(base_state_dir, strategy, runtime_id, wallet, history_fn, events_section)
|
|
990
|
+
|
|
991
|
+
return ArchivedReviewResult(
|
|
992
|
+
base_state_dir=str(Path(base_state_dir)),
|
|
993
|
+
target=target,
|
|
994
|
+
archive_name=archive_dir.name,
|
|
995
|
+
archive_dir=str(archive_dir),
|
|
996
|
+
archive_ts=ts,
|
|
997
|
+
runtime_id=runtime_id,
|
|
998
|
+
wallet=wallet,
|
|
999
|
+
strategy=strategy,
|
|
1000
|
+
review=sr,
|
|
1001
|
+
events_total=len(rows),
|
|
1002
|
+
rows_skipped=skipped,
|
|
1003
|
+
)
|
|
@@ -78,8 +78,27 @@ from ._layout import (
|
|
|
78
78
|
resolve_wallet_json,
|
|
79
79
|
runtime_state_dir,
|
|
80
80
|
)
|
|
81
|
+
from ._target import TargetError, available_strategies_hint
|
|
81
82
|
from .check import run_check
|
|
82
83
|
from .install import INSTALL_JSON, REGISTRY_FILENAME, compute_hash8
|
|
84
|
+
from composer.validator.errors import Location, TeachingError
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _status_read(path: Path, base_state_dir=None) -> str:
|
|
88
|
+
"""Read a user-supplied status target, wrapping any OSError (a permissions/IO fault, a vanished
|
|
89
|
+
file) as a CMP256 teaching error instead of letting a raw ``[Errno N]`` escape to the backstop
|
|
90
|
+
(K4). ``status`` shares the graph verbs' target vocabulary — a read fault teaches the same way."""
|
|
91
|
+
try:
|
|
92
|
+
return path.read_text(encoding="utf-8")
|
|
93
|
+
except OSError as exc:
|
|
94
|
+
detail = exc.strerror or str(exc)
|
|
95
|
+
raise TargetError(TeachingError(
|
|
96
|
+
"CMP256",
|
|
97
|
+
f"the status target {str(path)!r} could not be read: {detail}.",
|
|
98
|
+
"Pass the path to an existing, readable graph .yaml, a strategy directory, or a strategy "
|
|
99
|
+
"name. " + available_strategies_hint(base_state_dir),
|
|
100
|
+
Location(),
|
|
101
|
+
)) from exc
|
|
83
102
|
|
|
84
103
|
ABSENT = "absent"
|
|
85
104
|
INVALID = "invalid"
|
|
@@ -429,7 +448,7 @@ def _resolve(target, out_dir, base_state_dir=None) -> tuple[Optional[str], str,
|
|
|
429
448
|
if p.is_dir():
|
|
430
449
|
manifest = p / "manifest.json"
|
|
431
450
|
if manifest.is_file():
|
|
432
|
-
man = json.loads(manifest
|
|
451
|
+
man = json.loads(_status_read(manifest, base_state_dir))
|
|
433
452
|
strategy = str(man.get("strategy", p.name))
|
|
434
453
|
# G8b: the top-level graph SNAPSHOT is the record; fall back to the in-unit copy, then
|
|
435
454
|
# the flat legacy location for older stagings.
|
|
@@ -439,7 +458,7 @@ def _resolve(target, out_dir, base_state_dir=None) -> tuple[Optional[str], str,
|
|
|
439
458
|
if c.is_file()),
|
|
440
459
|
p / "main" / graph_name,
|
|
441
460
|
)
|
|
442
|
-
text = gpath
|
|
461
|
+
text = _status_read(gpath, base_state_dir) if gpath.is_file() else None
|
|
443
462
|
return text, str(gpath if gpath.is_file() else p), p, strategy
|
|
444
463
|
return None, str(p), None, p.name
|
|
445
464
|
if not p.is_file():
|
|
@@ -449,11 +468,11 @@ def _resolve(target, out_dir, base_state_dir=None) -> tuple[Optional[str], str,
|
|
|
449
468
|
cstaged = canonical_staged_dir(base_state_dir, target)
|
|
450
469
|
staged = cstaged if (cstaged / "manifest.json").is_file() else None
|
|
451
470
|
if cgraph.is_file():
|
|
452
|
-
return cgraph
|
|
471
|
+
return _status_read(cgraph, base_state_dir), str(cgraph), staged, target
|
|
453
472
|
if staged is not None:
|
|
454
473
|
return None, str(cgraph), staged, target
|
|
455
474
|
return None, str(p), None, p.name.replace(".graph.yaml", "")
|
|
456
|
-
text = p
|
|
475
|
+
text = _status_read(p, base_state_dir)
|
|
457
476
|
strategy = _strategy_name(text, p.stem)
|
|
458
477
|
staged = None
|
|
459
478
|
if out_dir is not None:
|
|
@@ -687,7 +706,9 @@ def _build_manual_status(label: str, strategy: str, state_dir: Path,
|
|
|
687
706
|
if wj_path is not None:
|
|
688
707
|
try:
|
|
689
708
|
rec = json.loads(wj_path.read_text(encoding="utf-8"))
|
|
690
|
-
|
|
709
|
+
# K3 follow-up: tolerant address read (hand-authored markers use the MCP key spellings).
|
|
710
|
+
from .fund import _rec_wallet
|
|
711
|
+
wallet = _rec_wallet(rec) or None
|
|
691
712
|
strategy_id = rec.get("strategyId")
|
|
692
713
|
except (OSError, json.JSONDecodeError):
|
|
693
714
|
pass
|
|
@@ -70,7 +70,7 @@ from ._layout import (
|
|
|
70
70
|
)
|
|
71
71
|
from ._unit_diff import UnitDiff, diff_unit_dirs
|
|
72
72
|
from .check import render_unit_summary
|
|
73
|
-
from .fund import _read_wallet_json
|
|
73
|
+
from .fund import _read_wallet_json, _rec_wallet
|
|
74
74
|
from .install import (
|
|
75
75
|
ALREADY_INSTALLED,
|
|
76
76
|
INSTALL_JSON,
|
|
@@ -368,11 +368,15 @@ def run_update(
|
|
|
368
368
|
# corrupt staging can never strand a live wallet.
|
|
369
369
|
staged = _resolve_staged(target, out_dir, base_state_dir)
|
|
370
370
|
if staged is None:
|
|
371
|
+
# K4: share the graph verbs' target vocabulary — a target that resolves to nothing names the
|
|
372
|
+
# strategies that DO exist under the state root, so the model corrects to a real one.
|
|
373
|
+
from ._target import available_strategies_hint
|
|
371
374
|
return _refused(target, TeachingError(
|
|
372
375
|
"CMP200", "the update target is not a staged unit (no manifest.json found).",
|
|
373
376
|
"Point `composer update` at a `composer deploy` staging dir (the one containing "
|
|
374
|
-
"manifest.json + main/),
|
|
375
|
-
"`composer deploy <graph
|
|
377
|
+
"manifest.json + main/), a graph file plus `-o <staging-dir>`, or a strategy NAME whose "
|
|
378
|
+
"canonical staged/ exists. Run `composer deploy <graph>` first. "
|
|
379
|
+
+ available_strategies_hint(base_state_dir),
|
|
376
380
|
Location(),
|
|
377
381
|
))
|
|
378
382
|
try:
|
|
@@ -416,7 +420,13 @@ def run_update(
|
|
|
416
420
|
if not wallet:
|
|
417
421
|
wj, _layout = resolve_wallet_json(base_state_dir, strategy)
|
|
418
422
|
rec = _read_wallet_json(wj) if wj is not None else None
|
|
419
|
-
|
|
423
|
+
# K4 (CMP240 fix): read the wallet ADDRESS with the SAME tolerant reader `composer fund` uses
|
|
424
|
+
# (K3 `_rec_wallet`) — a hand-authored / MCP-shaped / adopt-flow wallet.json records the
|
|
425
|
+
# address under `address` / `walletAddress` / `strategyWalletAddress`, NOT the canonical
|
|
426
|
+
# `wallet` key. Pre-K4 update read `rec.get("wallet")` raw and REFUSED CMP240 on such a
|
|
427
|
+
# marker (dev-test #7: a wallet.json with strategyId + address) though fund/install resolved
|
|
428
|
+
# it fine — the exact fund/update divergence this unifies.
|
|
429
|
+
rec_wallet = _rec_wallet(rec or {}).strip()
|
|
420
430
|
if not rec_wallet:
|
|
421
431
|
return _refused(target, TeachingError(
|
|
422
432
|
"CMP240", f"no wallet to update — `--wallet` was omitted and no funded "
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH,OAAO,EAA2B,KAAK,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAkBnG,OAAO,KAAK,EAAuB,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAOjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import { resolvePackageName } from "./runtime/auto-update/resolve-package-name.j
|
|
|
36
36
|
import { runOpenClawPluginsUpdate } from "./runtime/auto-update/run-update-command.js";
|
|
37
37
|
import { registerSpanHooks } from "./telemetry/spans/index.js";
|
|
38
38
|
import { registerComposerWriteAnnotationHook } from "./runtime/composer-managed-write.js";
|
|
39
|
+
import { registerNoReplyScopingOverlayHook } from "./runtime/no-reply-scoping-overlay.js";
|
|
39
40
|
import { initDefaultPiiRedactor } from "./telemetry/spans/redact-pii.js";
|
|
40
41
|
import { initOtelSdk, shutdownOtelSdk } from "./telemetry/otel-sdk.js";
|
|
41
42
|
import { getLogLevel, getLogLevelSource, parseLogLevel, setErrorNotifier, setLogLevel, } from "./utils/logger.js";
|
|
@@ -333,6 +334,21 @@ const senpiRuntimePlugin = {
|
|
|
333
334
|
err: err instanceof Error ? err.message : String(err),
|
|
334
335
|
});
|
|
335
336
|
}
|
|
337
|
+
// K9 — NO_REPLY scoping overlay. openclaw core teaches an unscoped "Silent
|
|
338
|
+
// Replies" rule (entire reply = NO_REPLY); a model that considers its
|
|
339
|
+
// tool-side work "already communicated" then goes silent after real work
|
|
340
|
+
// (dev-test #7 F6: silence after a successful composer update). The fleet
|
|
341
|
+
// pins openclaw, so the deployable lever is plugin-side: append a scoping
|
|
342
|
+
// correction to the cached system prompt via before_prompt_build. Additive
|
|
343
|
+
// and defensive — never blocks a turn or takes the plugin down.
|
|
344
|
+
try {
|
|
345
|
+
registerNoReplyScopingOverlayHook({ api, logger: pluginLogger });
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
pluginLogger.warn("[senpi-runtime] failed to register NO_REPLY scoping overlay hook", {
|
|
349
|
+
err: err instanceof Error ? err.message : String(err),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
336
352
|
const runtimeHandles = new Map();
|
|
337
353
|
let apiHandle;
|
|
338
354
|
// Supervised scanner state — module-scoped so stop() can reach them.
|