@senpi/runtime 3.0.0-dev.runtime-phase-2.20260623210537 → 3.0.0-dev.runtime-phase-2.20260625112547
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/README.md +10 -9
- package/dist/actions/action-runtime-module.d.ts.map +1 -1
- package/dist/actions/action-runtime-module.js +1 -1
- package/dist/actions/action-runtime-module.js.map +1 -1
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +3 -0
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +54 -14
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/types.d.ts +10 -2
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +8 -0
- package/dist/actions/types.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +80 -74
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +0 -108
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +2 -0
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +2 -0
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/config/parse-tiers.d.ts.map +1 -1
- package/dist/dsl/config/parse-tiers.js +6 -2
- package/dist/dsl/config/parse-tiers.js.map +1 -1
- package/dist/dsl/engine-client/config-mapping.d.ts +7 -0
- package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/config-mapping.js +16 -2
- package/dist/dsl/engine-client/config-mapping.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +18 -2
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +302 -213
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +15 -0
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +46 -9
- package/dist/dsl/monitor/create-monitor.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +138 -77
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +16 -1
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +35 -4
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/dsl/plugin/options.d.ts +18 -0
- package/dist/dsl/plugin/options.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +4 -3
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +14 -5
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.js +12 -1
- package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
- package/dist/runtime/load-runtime-config.js +1 -1
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/map-strategies-registration.js +7 -8
- package/dist/runtime/map-strategies-registration.js.map +1 -1
- package/dist/runtime/map-strategies.d.ts.map +1 -1
- package/dist/runtime/map-strategies.js +12 -3
- package/dist/runtime/map-strategies.js.map +1 -1
- package/dist/runtime/reconcile-decide.d.ts +73 -0
- package/dist/runtime/reconcile-decide.d.ts.map +1 -0
- package/dist/runtime/reconcile-decide.js +186 -0
- package/dist/runtime/reconcile-decide.js.map +1 -0
- package/dist/runtime/reconcile-run.d.ts +62 -0
- package/dist/runtime/reconcile-run.d.ts.map +1 -0
- package/dist/runtime/reconcile-run.js +173 -0
- package/dist/runtime/reconcile-run.js.map +1 -0
- package/dist/runtime/reconcile-state.d.ts +44 -0
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +80 -44
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/risk-guard.d.ts.map +1 -1
- package/dist/runtime/risk-guard.js +17 -12
- package/dist/runtime/risk-guard.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +2912 -1052
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +136 -42
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +26 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +254 -8
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scanner-registry.d.ts +1 -1
- package/dist/runtime/scanner-registry.d.ts.map +1 -1
- package/dist/runtime/scanner-registry.js +3 -12
- package/dist/runtime/scanner-registry.js.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
- package/dist/runtime-api/intake/acceptance-record.d.ts +10 -0
- package/dist/runtime-api/intake/acceptance-record.d.ts.map +1 -1
- package/dist/runtime-api/intake/acceptance-record.js +28 -0
- package/dist/runtime-api/intake/acceptance-record.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +28 -36
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts +20 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +57 -6
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts +18 -4
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +24 -3
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +16 -0
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +4 -0
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +12 -0
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/scanners/external/scaffold/__main__.py +65 -24
- package/dist/scanners/external/scaffold/ctx.py +78 -0
- package/dist/scanners/external/scaffold/delivery.py +56 -22
- package/dist/scanners/external/scaffold/entrypoint_loader.py +42 -48
- package/dist/scanners/external/scaffold/journal.py +13 -2
- package/dist/scanners/external/scaffold/requirements.txt +11 -0
- package/dist/scanners/external/scaffold/scaffold.py +159 -128
- package/dist/scanners/external/scaffold/senpi_mcp.py +250 -0
- package/dist/scanners/external/scaffold/signal_envelope.py +177 -34
- package/dist/scanners/external/scaffold/state_manager.py +175 -0
- package/dist/scanners/external-scanner-receiver.d.ts +6 -0
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +14 -1
- package/dist/scanners/external-scanner-receiver.js.map +1 -1
- package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
- package/dist/scripts/run-dsl-standalone-live.js +5 -3
- package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
- package/dist/senpi/client.d.ts +37 -4
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +101 -24
- package/dist/senpi/client.js.map +1 -1
- package/dist/state/state-manager.d.ts +17 -0
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +50 -4
- package/dist/state/state-manager.js.map +1 -1
- package/dist/strategy/strategy-state.d.ts +9 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/strategy/strategy-state.js +62 -9
- package/dist/strategy/strategy-state.js.map +1 -1
- package/dist/tracking/constants.d.ts.map +1 -1
- package/dist/tracking/constants.js +2 -0
- package/dist/tracking/constants.js.map +1 -1
- package/dist/types/dsl/index.d.ts +1 -1
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/types/scanner.d.ts +7 -0
- package/dist/types/scanner.d.ts.map +1 -1
- package/dist/types/strategy.d.ts +3 -1
- package/dist/types/strategy.d.ts.map +1 -1
- package/dist/utils/keyed-mutex.d.ts +37 -0
- package/dist/utils/keyed-mutex.d.ts.map +1 -0
- package/dist/utils/keyed-mutex.js +77 -0
- package/dist/utils/keyed-mutex.js.map +1 -0
- package/examples/strategies/README.md +17 -12
- package/examples/strategies/iguana/README.md +135 -0
- package/examples/strategies/iguana/recipe.yaml +137 -0
- package/examples/strategies/iguana/scanners/scan.py +243 -0
- package/examples/strategies/iguana/scanners/scoring.py +152 -0
- package/examples/strategies/raptor/README.md +156 -0
- package/examples/strategies/raptor/recipe.yaml +154 -0
- package/examples/strategies/raptor/scanners/scan.py +349 -0
- package/examples/strategies/raptor/scanners/scoring.py +452 -0
- package/examples/strategies/spider/README.md +89 -0
- package/examples/strategies/spider/scalp/recipe.yaml +129 -0
- package/examples/strategies/spider/scalp/scanners/scan.py +219 -0
- package/examples/strategies/spider/scalp/scanners/scoring.py +179 -0
- package/examples/strategies/spider/swing/recipe.yaml +137 -0
- package/examples/strategies/spider/swing/scanners/scan.py +334 -0
- package/examples/strategies/spider/swing/scanners/scoring.py +184 -0
- package/examples/strategies/turbine/README.md +166 -0
- package/examples/strategies/turbine/recipe-runners.yaml +123 -0
- package/examples/strategies/turbine/recipe-volume.yaml +129 -0
- package/examples/strategies/turbine/scanners/scan.py +283 -0
- package/examples/strategies/turbine/scanners/scoring.py +190 -0
- package/package.json +1 -1
- package/dist/scanners/external/liquidation-watchdog/README.md +0 -107
- package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +0 -48
- package/dist/scanners/external/liquidation-watchdog/producer.mjs +0 -336
- package/dist/scanners/external/momentum/README.md +0 -148
- package/dist/scanners/external/momentum/momentum.yaml +0 -116
- package/dist/scanners/external/momentum/producer.mjs +0 -251
- package/dist/scanners/external/shared/ingest.mjs +0 -112
- package/examples/README.md +0 -89
- package/examples/scanners-consumer-quickstart.mjs +0 -134
- package/examples/scanners-emerging-movers-v4.mjs +0 -287
- package/examples/scanners-liquidation-watchdog.mjs +0 -341
- package/examples/scanners-live-mcp.mjs +0 -208
- package/examples/scanners-multi-strategy.mjs +0 -165
- package/examples/scanners-position-tracker-e2e.mjs +0 -225
- package/examples/scanners-scheduled-events.mjs +0 -96
- package/examples/scanners-senpi-provider-client.mjs +0 -182
- package/examples/scanners-single-strategy.mjs +0 -198
- package/examples/strategies/dsl-showcase.yaml +0 -68
- package/examples/strategies/dsl.yaml +0 -51
- package/examples/strategies/fox-minimal-loosened.yaml +0 -240
- package/examples/strategies/fox-minimal.yaml +0 -242
- package/examples/strategies/minimal.yaml +0 -46
- package/examples/strategies/momentum/how-it-works.md +0 -48
- package/examples/strategies/momentum/momentum-guarded.yaml +0 -161
- package/examples/strategies/risk-showcase.yaml +0 -93
- package/examples/strategies/viper.yaml +0 -67
- package/examples/strategies/wolf.yaml +0 -72
- package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +0 -123
|
@@ -20,7 +20,7 @@ class ScanEntrypointError(Exception):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ScanSignatureError(Exception):
|
|
23
|
-
"""scan's call shape is incompatible with
|
|
23
|
+
"""scan's call shape is incompatible with scan(inputs, ctx)."""
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def add_path_to_sys_path(path: str) -> None:
|
|
@@ -32,16 +32,22 @@ def add_path_to_sys_path(path: str) -> None:
|
|
|
32
32
|
def import_entrypoint(path: str, entrypoint: str):
|
|
33
33
|
"""Put `path` on sys.path, then import `entrypoint` by module name.
|
|
34
34
|
|
|
35
|
+
The recipe expresses `entrypoint` as a FILENAME (e.g. `scan.py`, the shape the
|
|
36
|
+
B1 launcher writes verbatim into the launch config and the §4a entrypoint FS
|
|
37
|
+
check validates). importlib needs a MODULE name, so a trailing `.py` is
|
|
38
|
+
stripped here — the only place the filename<->module-name bridge lives.
|
|
39
|
+
|
|
35
40
|
Raises ScanEntrypointError on any import/syntax/missing-sibling failure,
|
|
36
41
|
chaining the underlying exception.
|
|
37
42
|
"""
|
|
38
43
|
add_path_to_sys_path(path)
|
|
44
|
+
module_name = entrypoint[:-3] if entrypoint.endswith(".py") else entrypoint
|
|
39
45
|
try:
|
|
40
46
|
# Force re-import when the module has been previously imported so that
|
|
41
47
|
# tests can write fresh modules to tmp_path without cache collisions.
|
|
42
|
-
if
|
|
43
|
-
del sys.modules[
|
|
44
|
-
return importlib.import_module(
|
|
48
|
+
if module_name in sys.modules:
|
|
49
|
+
del sys.modules[module_name]
|
|
50
|
+
return importlib.import_module(module_name)
|
|
45
51
|
except Exception as exc:
|
|
46
52
|
raise ScanEntrypointError(
|
|
47
53
|
f"Failed to import entrypoint '{entrypoint}' from '{path}': {exc}"
|
|
@@ -67,19 +73,25 @@ def locate_scan(module) -> Callable:
|
|
|
67
73
|
|
|
68
74
|
|
|
69
75
|
def verify_scan_signature(scan: Callable) -> None:
|
|
70
|
-
"""Verify scan's call shape is compatible with scan(
|
|
76
|
+
"""Verify scan's call shape is compatible with the v2 contract scan(inputs, ctx).
|
|
77
|
+
|
|
78
|
+
v2 calls scan with EXACTLY two positionals (inputs, ctx). A signature must be
|
|
79
|
+
callable that way.
|
|
71
80
|
|
|
72
81
|
Accepted shapes:
|
|
73
|
-
scan(
|
|
74
|
-
scan(
|
|
75
|
-
scan(
|
|
76
|
-
scan(params, state, *args) — variadic absorbs client
|
|
82
|
+
scan(inputs, ctx) — 2 required (the canonical shape)
|
|
83
|
+
scan(inputs, ctx, extra=None) — 2 required + defaulted extra
|
|
84
|
+
scan(inputs, ctx, *args) — variadic absorbs nothing extra here
|
|
77
85
|
scan(*args, **kwargs) — fully variadic, accepts anything
|
|
78
86
|
|
|
79
87
|
Rejected shapes:
|
|
80
88
|
scan() — 0 required
|
|
81
|
-
scan(
|
|
82
|
-
scan(
|
|
89
|
+
scan(inputs) — 1 required
|
|
90
|
+
scan(params, state, client) — 3 required (the OLD v1 client shape)
|
|
91
|
+
scan(a, b, c, d) — >=4 required
|
|
92
|
+
|
|
93
|
+
The contract is now EXACTLY two positionals: a 3rd REQUIRED positional cannot
|
|
94
|
+
be satisfied by scan(inputs, ctx) and is rejected.
|
|
83
95
|
|
|
84
96
|
Raises ScanSignatureError naming the found arity on mismatch.
|
|
85
97
|
"""
|
|
@@ -94,57 +106,39 @@ def verify_scan_signature(scan: Callable) -> None:
|
|
|
94
106
|
has_var_positional = any(
|
|
95
107
|
p.kind == inspect.Parameter.VAR_POSITIONAL for p in params
|
|
96
108
|
)
|
|
97
|
-
has_var_keyword = any(
|
|
98
|
-
p.kind == inspect.Parameter.VAR_KEYWORD for p in params
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
# A *args/**kwargs-only signature accepts any positional call.
|
|
102
|
-
if has_var_positional or has_var_keyword:
|
|
103
|
-
# Check that it actually accepts at least 2 positional args (i.e., no
|
|
104
|
-
# required positional params ahead of *args that block the call).
|
|
105
|
-
required_before_var = [
|
|
106
|
-
p for p in params
|
|
107
|
-
if p.kind in (
|
|
108
|
-
inspect.Parameter.POSITIONAL_ONLY,
|
|
109
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
110
|
-
)
|
|
111
|
-
and p.default is inspect.Parameter.empty
|
|
112
|
-
]
|
|
113
|
-
# If there are >=4 required positionals even WITH *args, reject.
|
|
114
|
-
if len(required_before_var) >= 4:
|
|
115
|
-
raise ScanSignatureError(
|
|
116
|
-
f"scan requires {len(required_before_var)} fixed positional params; "
|
|
117
|
-
f"expected 2 or 3 (found arity: {len(required_before_var)})."
|
|
118
|
-
)
|
|
119
|
-
# Otherwise, variadic accepts our call.
|
|
120
|
-
return
|
|
121
109
|
|
|
122
|
-
#
|
|
123
|
-
|
|
110
|
+
# Required (non-defaulted) fixed positional params.
|
|
111
|
+
required_fixed = [
|
|
124
112
|
p for p in params
|
|
125
113
|
if p.kind in (
|
|
126
114
|
inspect.Parameter.POSITIONAL_ONLY,
|
|
127
115
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
128
116
|
)
|
|
117
|
+
and p.default is inspect.Parameter.empty
|
|
129
118
|
]
|
|
130
|
-
required_fixed = [
|
|
131
|
-
p for p in fixed_positional
|
|
132
|
-
if p.default is inspect.Parameter.empty
|
|
133
|
-
]
|
|
134
|
-
|
|
135
119
|
n_required = len(required_fixed)
|
|
136
|
-
|
|
120
|
+
|
|
121
|
+
# A *args (or *args/**kwargs) signature accepts the 2-positional call as long
|
|
122
|
+
# as it has no MORE than 2 required fixed positionals ahead of *args.
|
|
123
|
+
if has_var_positional:
|
|
124
|
+
if n_required > 2:
|
|
125
|
+
raise ScanSignatureError(
|
|
126
|
+
f"scan requires {n_required} fixed positional params; the v2 "
|
|
127
|
+
f"contract calls scan(inputs, ctx) — exactly 2. Found arity: {n_required}."
|
|
128
|
+
)
|
|
129
|
+
return
|
|
137
130
|
|
|
138
131
|
if n_required < 2:
|
|
139
132
|
raise ScanSignatureError(
|
|
140
|
-
f"scan has {n_required} required positional param(s); "
|
|
141
|
-
f"
|
|
133
|
+
f"scan has {n_required} required positional param(s); the v2 contract "
|
|
134
|
+
f"requires exactly 2 (inputs, ctx). Found arity: {n_required}."
|
|
142
135
|
)
|
|
143
136
|
|
|
144
|
-
if n_required
|
|
137
|
+
if n_required > 2:
|
|
145
138
|
raise ScanSignatureError(
|
|
146
|
-
f"scan has {n_required} required positional params; "
|
|
147
|
-
f"
|
|
139
|
+
f"scan has {n_required} required positional params; the v2 contract "
|
|
140
|
+
f"calls scan(inputs, ctx) — exactly 2. A 3rd required positional "
|
|
141
|
+
f"(the old v1 client shape) is no longer accepted. Found arity: {n_required}."
|
|
148
142
|
)
|
|
149
143
|
|
|
150
144
|
|
|
@@ -90,8 +90,19 @@ class Journal:
|
|
|
90
90
|
# ------------------------------------------------------------------
|
|
91
91
|
|
|
92
92
|
def record_produced(self, signal: dict, *, now_ms: int) -> str:
|
|
93
|
-
"""Write a 'produced' line and enqueue the signal. Returns entry id.
|
|
94
|
-
|
|
93
|
+
"""Write a 'produced' line and enqueue the signal. Returns entry id.
|
|
94
|
+
|
|
95
|
+
The entry id IS the wire `signal_id`: the journal stamps `signal_id` onto
|
|
96
|
+
the persisted/queued signal so the POSTed signal carries the dedup key, and
|
|
97
|
+
a replay re-uses the SAME id (no fresh id minted on restart). If the signal
|
|
98
|
+
already carries a `signal_id` (the delivery sink threaded the eid in via the
|
|
99
|
+
envelope), that id is re-used as the entry id; otherwise one is minted.
|
|
100
|
+
"""
|
|
101
|
+
existing = signal.get("signal_id")
|
|
102
|
+
entry_id = existing if isinstance(existing, str) and existing else uuid.uuid4().hex
|
|
103
|
+
# Stamp the id onto the wire signal so the persisted produced line and the
|
|
104
|
+
# POSTed body both carry signal_id == entry id.
|
|
105
|
+
signal["signal_id"] = entry_id
|
|
95
106
|
asset = signal.get("asset", "")
|
|
96
107
|
valid_until = signal.get("valid_until", 0)
|
|
97
108
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Runtime dependencies for the scaffold harness (src/scanners/external/scaffold/).
|
|
2
|
+
#
|
|
3
|
+
# The scaffold is shipped to the scanner box and run via `python3 -m scaffold`.
|
|
4
|
+
# `mcp` (the official Model Context Protocol Python SDK) backs ctx.senpi_mcp:
|
|
5
|
+
# `streamablehttp_client` + a single persistent `ClientSession` (see senpi_mcp.py).
|
|
6
|
+
# It is LAZY-IMPORTED at first use, so the package still imports where `mcp` is
|
|
7
|
+
# absent (e.g. unit tests inject a fake SDK) — install this on the box that runs
|
|
8
|
+
# scanners that call ctx.senpi_mcp.
|
|
9
|
+
#
|
|
10
|
+
# `mcp[cli]` is NOT needed — only the client transport + ClientSession surface.
|
|
11
|
+
mcp>=1.9.0
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
"""Scaffold core loop.
|
|
1
|
+
"""Scaffold core loop (v2).
|
|
2
2
|
|
|
3
3
|
run_scaffold(...) loads the author's entrypoint, finds and validates scan(),
|
|
4
|
-
then drives the interval loop: time-box per tick, graceful
|
|
5
|
-
parent-death detection via ppid, state
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
builds the ScanContext, then drives the interval loop: time-box per tick, graceful
|
|
5
|
+
stop on SIGTERM/SIGINT, parent-death detection via ppid, transactional state via
|
|
6
|
+
ctx.state, candidates to sink.
|
|
7
|
+
|
|
8
|
+
`path` is a scaffold concern only — it is put on sys.path before import and never
|
|
9
|
+
passed to scan(). scan() is called as scan(inputs, ctx) — 2-arg, sync, returning a
|
|
10
|
+
plain list[dict]. State mutates ONLY via ctx.state (commit on a clean tick, rollback
|
|
11
|
+
on error/timeout/malformed/failed-persist — transactional discard wins on failure).
|
|
9
12
|
"""
|
|
10
13
|
|
|
11
14
|
import inspect
|
|
@@ -14,6 +17,7 @@ import time
|
|
|
14
17
|
import threading
|
|
15
18
|
from typing import Any, Callable, Optional
|
|
16
19
|
|
|
20
|
+
from .ctx import ScanContext
|
|
17
21
|
from .entrypoint_loader import (
|
|
18
22
|
ScanEntrypointError,
|
|
19
23
|
ScanSignatureError,
|
|
@@ -29,40 +33,44 @@ from .loop_primitives import (
|
|
|
29
33
|
_interruptible_sleep,
|
|
30
34
|
log_event,
|
|
31
35
|
)
|
|
32
|
-
from .
|
|
36
|
+
from .state_manager import StateManager
|
|
33
37
|
|
|
34
38
|
|
|
35
|
-
def
|
|
36
|
-
"""
|
|
39
|
+
def _build_senpi_mcp() -> Any:
|
|
40
|
+
"""Construct the MCP client when creds are present; else return None.
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
Creds come from the launch env (SENPI_API_KEY / SENPI_MCP_URL), injected by
|
|
43
|
+
the launcher regardless of how the operator supplied them. With neither set,
|
|
44
|
+
return None so the no-creds path does not crash — the client is loud at FIRST
|
|
45
|
+
USE (see scaffold.senpi_mcp). Reference the class through the module attribute
|
|
46
|
+
so a test stub patched onto scaffold.senpi_mcp is honored.
|
|
41
47
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
]
|
|
60
|
-
# If only 2 fixed positional params (no *args), call 2-arg form.
|
|
61
|
-
if not has_var_positional and len(fixed_positional) == 2:
|
|
62
|
-
return scan(params, state)
|
|
48
|
+
api_key = os.environ.get("SENPI_API_KEY") or None
|
|
49
|
+
mcp_url = os.environ.get("SENPI_MCP_URL") or None
|
|
50
|
+
if not api_key and not mcp_url:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
from . import senpi_mcp as senpi_mcp_mod
|
|
54
|
+
|
|
55
|
+
# Read-only-by-default producer MCP boundary: the set of MUTATION tools
|
|
56
|
+
# producers are permitted to call is owned SOLELY by the scaffold-source
|
|
57
|
+
# PRODUCER_WRITE_ALLOWLIST constant (EMPTY by default → all mutations
|
|
58
|
+
# blocked). It is edited by the scaffold maintainer in senpi_mcp.py — it is
|
|
59
|
+
# NOT operator/env-configurable and NOT reachable by authored producer code.
|
|
60
|
+
return senpi_mcp_mod.SenpiMcpClient(
|
|
61
|
+
api_key=api_key,
|
|
62
|
+
mcp_url=mcp_url,
|
|
63
|
+
write_allowlist=senpi_mcp_mod.PRODUCER_WRITE_ALLOWLIST,
|
|
64
|
+
)
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
|
|
67
|
+
def _call_scan(scan: Callable, inputs: dict, ctx: Any):
|
|
68
|
+
"""Call scan(inputs, ctx) — the v2 2-arg contract.
|
|
69
|
+
|
|
70
|
+
There is no `client` and no 3-arg path. scan returns a plain list[dict];
|
|
71
|
+
validation of that return happens in the tick loop.
|
|
72
|
+
"""
|
|
73
|
+
return scan(inputs, ctx)
|
|
66
74
|
|
|
67
75
|
|
|
68
76
|
def run_scaffold(
|
|
@@ -71,13 +79,17 @@ def run_scaffold(
|
|
|
71
79
|
*,
|
|
72
80
|
interval: float,
|
|
73
81
|
timeout_seconds: Optional[float] = None,
|
|
74
|
-
|
|
82
|
+
inputs: Optional[dict] = None,
|
|
75
83
|
state_path: Optional[str] = None,
|
|
84
|
+
scanner_name: str,
|
|
85
|
+
wallet: str,
|
|
86
|
+
default_signal_validity_seconds: int,
|
|
87
|
+
state_history_max_count: int = 0,
|
|
76
88
|
sink: Optional[Callable] = None,
|
|
77
89
|
max_ticks: Optional[int] = None,
|
|
78
90
|
install_signal_handlers: bool = True,
|
|
79
91
|
) -> int:
|
|
80
|
-
"""Load the author's entrypoint and run its scan() on an interval loop.
|
|
92
|
+
"""Load the author's entrypoint and run its scan() on an interval loop (v2).
|
|
81
93
|
|
|
82
94
|
Args:
|
|
83
95
|
path: Directory added to sys.path before import. Never passed to scan().
|
|
@@ -86,9 +98,15 @@ def run_scaffold(
|
|
|
86
98
|
timeout_seconds:
|
|
87
99
|
Per-tick wall-clock budget. Defaults to `interval`. Must be > 0.
|
|
88
100
|
Enforced via SIGALRM on platforms that support it.
|
|
89
|
-
|
|
90
|
-
state_path: Path to the JSON state file.
|
|
91
|
-
|
|
101
|
+
inputs: Tunables dict passed as scan()'s first argument. Default {}.
|
|
102
|
+
state_path: Path to the JSON state file. None means no persistence.
|
|
103
|
+
scanner_name: This scanner's recipe name (read-only ctx metadata).
|
|
104
|
+
wallet: Strategy wallet (read-only ctx metadata).
|
|
105
|
+
default_signal_validity_seconds:
|
|
106
|
+
Fallback signal TTL (seconds); threaded to the envelope path.
|
|
107
|
+
state_history_max_count:
|
|
108
|
+
Bound on the persisted state series. 0/unset = history disabled
|
|
109
|
+
(ctx.state.append raises).
|
|
92
110
|
sink: Callable(candidates) invoked after each clean tick. Default no-op.
|
|
93
111
|
max_ticks: Stop after this many ticks (for tests). None = unbounded.
|
|
94
112
|
install_signal_handlers:
|
|
@@ -112,14 +130,41 @@ def run_scaffold(
|
|
|
112
130
|
if timeout_seconds <= 0:
|
|
113
131
|
raise ValueError(f"timeout_seconds must be > 0; got {timeout_seconds!r}")
|
|
114
132
|
|
|
115
|
-
if
|
|
116
|
-
|
|
133
|
+
if inputs is None:
|
|
134
|
+
inputs = {}
|
|
117
135
|
|
|
118
136
|
# Import and locate scan — any failure raises before tick 1.
|
|
119
137
|
module = import_entrypoint(path, entrypoint)
|
|
120
138
|
scan = locate_scan(module)
|
|
121
139
|
verify_scan_signature(scan)
|
|
122
140
|
|
|
141
|
+
# ---------- ctx + state setup ----------
|
|
142
|
+
|
|
143
|
+
# The StateManager (B2) is the persisted series. It reads any prior state from
|
|
144
|
+
# disk on construction. state_history_max_count 0/unset => history disabled
|
|
145
|
+
# (append raises). It is passed into ctx AS-IS (never wrapped).
|
|
146
|
+
state = StateManager(state_path, state_history_max_count) if state_path is not None \
|
|
147
|
+
else StateManager(None, state_history_max_count)
|
|
148
|
+
|
|
149
|
+
# ctx.interval_seconds is the integer-seconds cadence (design §2.1). The loop's
|
|
150
|
+
# `interval` may be a sub-second float in tests; expose the int seconds when it
|
|
151
|
+
# is whole, else the float as-is.
|
|
152
|
+
interval_seconds = int(interval) if float(interval).is_integer() else interval
|
|
153
|
+
|
|
154
|
+
# B5: fill ctx.senpi_mcp when MCP creds are present (resolved at launch from
|
|
155
|
+
# SENPI_API_KEY / SENPI_MCP_URL). With no creds the slot stays None and launch
|
|
156
|
+
# must NOT crash — the client is loud at FIRST USE, not at construction. Import
|
|
157
|
+
# is deferred so a stub patched onto scaffold.senpi_mcp in tests is honored.
|
|
158
|
+
senpi_mcp = _build_senpi_mcp()
|
|
159
|
+
|
|
160
|
+
ctx = ScanContext(
|
|
161
|
+
state=state,
|
|
162
|
+
wallet=wallet,
|
|
163
|
+
scanner_name=scanner_name,
|
|
164
|
+
interval_seconds=interval_seconds,
|
|
165
|
+
senpi_mcp=senpi_mcp,
|
|
166
|
+
)
|
|
167
|
+
|
|
123
168
|
# ---------- Lifecycle setup ----------
|
|
124
169
|
|
|
125
170
|
stop_event = threading.Event()
|
|
@@ -128,24 +173,17 @@ def run_scaffold(
|
|
|
128
173
|
|
|
129
174
|
start_ppid = os.getppid()
|
|
130
175
|
|
|
131
|
-
# Read initial state — corrupt/missing -> None (logged inside read_state).
|
|
132
|
-
state = read_state(state_path) if state_path is not None else None
|
|
133
|
-
|
|
134
176
|
log_event(
|
|
135
177
|
"scaffold_started",
|
|
136
178
|
path=path,
|
|
137
179
|
entrypoint=entrypoint,
|
|
138
180
|
interval=interval,
|
|
139
181
|
timeout_seconds=timeout_seconds,
|
|
182
|
+
scanner_name=scanner_name,
|
|
140
183
|
max_ticks=max_ticks,
|
|
141
184
|
)
|
|
142
185
|
|
|
143
|
-
client = None # placeholder; will be the real data handle in a later unit
|
|
144
186
|
tick_count = 0
|
|
145
|
-
# True once a state write has failed and we have escalated to /errors;
|
|
146
|
-
# cleared when a write next succeeds. Throttles the escalation to one
|
|
147
|
-
# post per failure run (a persistent failure would otherwise post every tick).
|
|
148
|
-
state_persist_degraded = False
|
|
149
187
|
|
|
150
188
|
try:
|
|
151
189
|
while not stop_event.is_set():
|
|
@@ -159,40 +197,28 @@ def run_scaffold(
|
|
|
159
197
|
tick_started_at = time.time()
|
|
160
198
|
tick_status = "ok"
|
|
161
199
|
tick_error = None
|
|
162
|
-
candidates = []
|
|
163
|
-
new_state = state # default: keep current state on error
|
|
200
|
+
candidates: list = []
|
|
164
201
|
|
|
165
202
|
armed = False
|
|
166
203
|
try:
|
|
167
204
|
armed = _arm_tick_alarm(timeout_seconds)
|
|
168
205
|
try:
|
|
169
|
-
result = _call_scan(scan,
|
|
206
|
+
result = _call_scan(scan, inputs, ctx)
|
|
170
207
|
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
or len(result) != 2
|
|
175
|
-
):
|
|
208
|
+
# v2 return contract: a PLAIN list[dict]. Anything else is
|
|
209
|
+
# malformed (a tuple — the old v1 shape — included).
|
|
210
|
+
if not isinstance(result, list):
|
|
176
211
|
tick_status = "malformed"
|
|
177
212
|
log_event(
|
|
178
213
|
"scaffold_tick_malformed",
|
|
179
214
|
tick=tick_count,
|
|
180
|
-
reason=
|
|
215
|
+
reason=(
|
|
216
|
+
f"scan() returned {type(result).__name__!r}; expected a "
|
|
217
|
+
f"plain list[dict] (the v1 (candidates, state) tuple is gone)"
|
|
218
|
+
),
|
|
181
219
|
)
|
|
182
220
|
else:
|
|
183
|
-
|
|
184
|
-
# Validate that candidates is iterable.
|
|
185
|
-
try:
|
|
186
|
-
candidates = list(raw_candidates)
|
|
187
|
-
except TypeError:
|
|
188
|
-
tick_status = "malformed"
|
|
189
|
-
log_event(
|
|
190
|
-
"scaffold_tick_malformed",
|
|
191
|
-
tick=tick_count,
|
|
192
|
-
reason=f"candidates is not iterable (got {type(raw_candidates).__name__!r})",
|
|
193
|
-
)
|
|
194
|
-
else:
|
|
195
|
-
new_state = returned_state
|
|
221
|
+
candidates = result
|
|
196
222
|
|
|
197
223
|
except _TickTimeout as exc:
|
|
198
224
|
tick_status = "timeout"
|
|
@@ -223,48 +249,40 @@ def run_scaffold(
|
|
|
223
249
|
|
|
224
250
|
duration_ms = int((time.time() - tick_started_at) * 1000)
|
|
225
251
|
|
|
226
|
-
# --- Clean tick:
|
|
252
|
+
# --- Clean tick: commit ctx.state, deliver candidates ---
|
|
227
253
|
if tick_status == "ok":
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
# (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
tick=tick_count,
|
|
261
|
-
error_type=type(perr).__name__,
|
|
262
|
-
error=str(perr),
|
|
263
|
-
)
|
|
264
|
-
state = new_state
|
|
265
|
-
|
|
266
|
-
# Hand candidates to the delivery sink.
|
|
267
|
-
if sink is not None:
|
|
254
|
+
# Transactional commit. A failed persist RAISES (StateManager
|
|
255
|
+
# enforces it) and does NOT advance the committed snapshot — so
|
|
256
|
+
# the next tick recomputes from the un-advanced state. On a
|
|
257
|
+
# commit failure we roll back and treat the tick as failed
|
|
258
|
+
# (discard wins on failed-persist; no advance-on-persist-failure).
|
|
259
|
+
committed = True
|
|
260
|
+
try:
|
|
261
|
+
ctx.state.commit()
|
|
262
|
+
except Exception as commit_exc: # noqa: BLE001
|
|
263
|
+
committed = False
|
|
264
|
+
ctx.state.rollback()
|
|
265
|
+
log_event(
|
|
266
|
+
"scaffold_state_persist_failed",
|
|
267
|
+
tick=tick_count,
|
|
268
|
+
error_type=type(commit_exc).__name__,
|
|
269
|
+
error=str(commit_exc),
|
|
270
|
+
)
|
|
271
|
+
if sink is not None and hasattr(sink, "post_error"):
|
|
272
|
+
_post_error_safe(
|
|
273
|
+
sink,
|
|
274
|
+
{
|
|
275
|
+
"type": "state_persist_failed",
|
|
276
|
+
"tick": tick_count,
|
|
277
|
+
"message": (
|
|
278
|
+
"scanner state could not be persisted; "
|
|
279
|
+
"tick discarded, state not advanced"
|
|
280
|
+
),
|
|
281
|
+
},
|
|
282
|
+
tick_count,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if committed and sink is not None:
|
|
268
286
|
try:
|
|
269
287
|
sink(candidates)
|
|
270
288
|
except Exception as sink_exc: # noqa: BLE001
|
|
@@ -275,18 +293,14 @@ def run_scaffold(
|
|
|
275
293
|
error=str(sink_exc),
|
|
276
294
|
)
|
|
277
295
|
else:
|
|
278
|
-
# Non-ok tick (error / timeout / malformed):
|
|
279
|
-
#
|
|
296
|
+
# Non-ok tick (error / timeout / malformed): roll back the
|
|
297
|
+
# working buffer (discard the tick's mutations — state not
|
|
298
|
+
# advanced) and report to /errors via the sink, when supported.
|
|
299
|
+
ctx.state.rollback()
|
|
280
300
|
if sink is not None and hasattr(sink, "post_error"):
|
|
281
|
-
|
|
282
|
-
sink
|
|
283
|
-
|
|
284
|
-
log_event(
|
|
285
|
-
"scaffold_post_error_failed",
|
|
286
|
-
tick=tick_count,
|
|
287
|
-
error_type=type(perr).__name__,
|
|
288
|
-
error=str(perr),
|
|
289
|
-
)
|
|
301
|
+
_post_error_safe(
|
|
302
|
+
sink, tick_error or {"type": tick_status}, tick_count
|
|
303
|
+
)
|
|
290
304
|
|
|
291
305
|
log_event(
|
|
292
306
|
"scaffold_tick_finished",
|
|
@@ -297,11 +311,15 @@ def run_scaffold(
|
|
|
297
311
|
)
|
|
298
312
|
|
|
299
313
|
except _TickTimeout as late_exc:
|
|
300
|
-
# A SIGALRM queued by the OS
|
|
301
|
-
#
|
|
302
|
-
#
|
|
303
|
-
# log the late arrival, and let the loop continue.
|
|
314
|
+
# A SIGALRM queued by the OS, delivered after the inner try/except
|
|
315
|
+
# exited (e.g. during commit, sink, or log_event). Absorb it: the
|
|
316
|
+
# tick already did its work; disarm, roll back any uncommitted
|
|
317
|
+
# mutations, log the late arrival, and let the loop continue.
|
|
304
318
|
_disarm_tick_alarm()
|
|
319
|
+
try:
|
|
320
|
+
ctx.state.rollback()
|
|
321
|
+
except Exception: # noqa: BLE001
|
|
322
|
+
pass
|
|
305
323
|
log_event(
|
|
306
324
|
"scaffold_tick_timeout_late",
|
|
307
325
|
tick=tick_count,
|
|
@@ -328,3 +346,16 @@ def run_scaffold(
|
|
|
328
346
|
log_event("scaffold_stopping", tick_count=tick_count)
|
|
329
347
|
|
|
330
348
|
return tick_count
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _post_error_safe(sink: Any, error: dict, tick_count: int) -> None:
|
|
352
|
+
"""Call sink.post_error(error), swallowing/logging any failure."""
|
|
353
|
+
try:
|
|
354
|
+
sink.post_error(error)
|
|
355
|
+
except Exception as perr: # noqa: BLE001
|
|
356
|
+
log_event(
|
|
357
|
+
"scaffold_post_error_failed",
|
|
358
|
+
tick=tick_count,
|
|
359
|
+
error_type=type(perr).__name__,
|
|
360
|
+
error=str(perr),
|
|
361
|
+
)
|