@senpi/runtime 3.0.0-dev.openclaw-upgrade-mai.20260623210452 → 3.0.0-dev.runtime-phase-2.20260623210537
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 +9 -10
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +0 -3
- 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 +2 -6
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +56 -43
- 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 +108 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +0 -2
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +0 -2
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/engine-client/config-mapping.d.ts +0 -7
- package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/config-mapping.js +2 -16
- package/dist/dsl/engine-client/config-mapping.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +2 -18
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +213 -302
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +0 -15
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +9 -44
- 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 +1 -24
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +1 -16
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +4 -35
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/dsl/plugin/options.d.ts +0 -18
- package/dist/dsl/plugin/options.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +3 -4
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +5 -14
- 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 +1 -12
- 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 +8 -7
- 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 +3 -12
- package/dist/runtime/map-strategies.js.map +1 -1
- package/dist/runtime/reconcile-state.d.ts +0 -44
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +44 -80
- 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 +12 -17
- package/dist/runtime/risk-guard.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +995 -2855
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +40 -133
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +0 -26
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +5 -233
- 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 +12 -3
- 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/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +36 -28
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +0 -0
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts +4 -18
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +3 -24
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +0 -6
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +0 -5
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/scanners/external/liquidation-watchdog/README.md +107 -0
- package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +48 -0
- package/dist/scanners/external/liquidation-watchdog/producer.mjs +336 -0
- package/dist/scanners/external/momentum/README.md +148 -0
- package/dist/scanners/external/momentum/momentum.yaml +116 -0
- package/dist/scanners/external/momentum/producer.mjs +251 -0
- package/dist/scanners/external/scaffold/__main__.py +24 -65
- package/dist/scanners/external/scaffold/delivery.py +22 -56
- package/dist/scanners/external/scaffold/entrypoint_loader.py +48 -42
- package/dist/scanners/external/scaffold/journal.py +2 -13
- package/dist/scanners/external/scaffold/scaffold.py +128 -159
- package/dist/scanners/external/scaffold/signal_envelope.py +34 -177
- package/dist/scanners/external/shared/ingest.mjs +112 -0
- package/dist/scanners/external-scanner-receiver.d.ts +0 -6
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +1 -14
- 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 +3 -5
- package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
- package/dist/senpi/client.d.ts +0 -12
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +2 -31
- package/dist/senpi/client.js.map +1 -1
- package/dist/state/state-manager.d.ts +0 -17
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +0 -28
- package/dist/state/state-manager.js.map +1 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/strategy/strategy-state.js +1 -7
- package/dist/strategy/strategy-state.js.map +1 -1
- package/dist/tracking/constants.d.ts.map +1 -1
- package/dist/tracking/constants.js +0 -2
- 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 +0 -7
- package/dist/types/scanner.d.ts.map +1 -1
- package/examples/README.md +89 -0
- package/examples/scanners-consumer-quickstart.mjs +134 -0
- package/examples/scanners-emerging-movers-v4.mjs +287 -0
- package/examples/scanners-liquidation-watchdog.mjs +341 -0
- package/examples/scanners-live-mcp.mjs +208 -0
- package/examples/scanners-multi-strategy.mjs +165 -0
- package/examples/scanners-position-tracker-e2e.mjs +225 -0
- package/examples/scanners-scheduled-events.mjs +96 -0
- package/examples/scanners-senpi-provider-client.mjs +182 -0
- package/examples/scanners-single-strategy.mjs +198 -0
- package/examples/strategies/README.md +12 -17
- package/examples/strategies/dsl-showcase.yaml +68 -0
- package/examples/strategies/dsl.yaml +51 -0
- package/examples/strategies/fox-minimal-loosened.yaml +240 -0
- package/examples/strategies/fox-minimal.yaml +242 -0
- package/examples/strategies/minimal.yaml +46 -0
- package/examples/strategies/momentum/how-it-works.md +48 -0
- package/examples/strategies/momentum/momentum-guarded.yaml +161 -0
- package/examples/strategies/risk-showcase.yaml +93 -0
- package/examples/strategies/viper.yaml +67 -0
- package/examples/strategies/wolf.yaml +72 -0
- package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +123 -0
- package/package.json +1 -1
- package/dist/runtime/reconcile-decide.d.ts +0 -73
- package/dist/runtime/reconcile-decide.d.ts.map +0 -1
- package/dist/runtime/reconcile-decide.js +0 -166
- package/dist/runtime/reconcile-decide.js.map +0 -1
- package/dist/runtime/reconcile-run.d.ts +0 -62
- package/dist/runtime/reconcile-run.d.ts.map +0 -1
- package/dist/runtime/reconcile-run.js +0 -173
- package/dist/runtime/reconcile-run.js.map +0 -1
- package/dist/scanners/external/scaffold/ctx.py +0 -78
- package/dist/scanners/external/scaffold/requirements.txt +0 -11
- package/dist/scanners/external/scaffold/senpi_mcp.py +0 -250
- package/dist/scanners/external/scaffold/state_manager.py +0 -175
- package/dist/utils/keyed-mutex.d.ts +0 -37
- package/dist/utils/keyed-mutex.d.ts.map +0 -1
- package/dist/utils/keyed-mutex.js +0 -77
- package/dist/utils/keyed-mutex.js.map +0 -1
- package/examples/strategies/iguana/README.md +0 -135
- package/examples/strategies/iguana/recipe.yaml +0 -137
- package/examples/strategies/iguana/scanners/scan.py +0 -243
- package/examples/strategies/iguana/scanners/scoring.py +0 -152
- package/examples/strategies/raptor/README.md +0 -156
- package/examples/strategies/raptor/recipe.yaml +0 -154
- package/examples/strategies/raptor/scanners/scan.py +0 -349
- package/examples/strategies/raptor/scanners/scoring.py +0 -452
- package/examples/strategies/spider/README.md +0 -97
- package/examples/strategies/spider/scalp/recipe.yaml +0 -129
- package/examples/strategies/spider/scalp/scanners/scan.py +0 -219
- package/examples/strategies/spider/scalp/scanners/scoring.py +0 -179
- package/examples/strategies/spider/swing/recipe.yaml +0 -141
- package/examples/strategies/spider/swing/scanners/scan.py +0 -334
- package/examples/strategies/spider/swing/scanners/scoring.py +0 -184
- package/examples/strategies/turbine/README.md +0 -167
- package/examples/strategies/turbine/recipe-runners.yaml +0 -123
- package/examples/strategies/turbine/recipe-volume.yaml +0 -129
- package/examples/strategies/turbine/scanners/scan.py +0 -283
- package/examples/strategies/turbine/scanners/scoring.py +0 -190
|
@@ -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 scaffold(params, state[, client])."""
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def add_path_to_sys_path(path: str) -> None:
|
|
@@ -32,22 +32,16 @@ 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
|
-
|
|
40
35
|
Raises ScanEntrypointError on any import/syntax/missing-sibling failure,
|
|
41
36
|
chaining the underlying exception.
|
|
42
37
|
"""
|
|
43
38
|
add_path_to_sys_path(path)
|
|
44
|
-
module_name = entrypoint[:-3] if entrypoint.endswith(".py") else entrypoint
|
|
45
39
|
try:
|
|
46
40
|
# Force re-import when the module has been previously imported so that
|
|
47
41
|
# tests can write fresh modules to tmp_path without cache collisions.
|
|
48
|
-
if
|
|
49
|
-
del sys.modules[
|
|
50
|
-
return importlib.import_module(
|
|
42
|
+
if entrypoint in sys.modules:
|
|
43
|
+
del sys.modules[entrypoint]
|
|
44
|
+
return importlib.import_module(entrypoint)
|
|
51
45
|
except Exception as exc:
|
|
52
46
|
raise ScanEntrypointError(
|
|
53
47
|
f"Failed to import entrypoint '{entrypoint}' from '{path}': {exc}"
|
|
@@ -73,25 +67,19 @@ def locate_scan(module) -> Callable:
|
|
|
73
67
|
|
|
74
68
|
|
|
75
69
|
def verify_scan_signature(scan: Callable) -> None:
|
|
76
|
-
"""Verify scan's call shape is compatible with
|
|
77
|
-
|
|
78
|
-
v2 calls scan with EXACTLY two positionals (inputs, ctx). A signature must be
|
|
79
|
-
callable that way.
|
|
70
|
+
"""Verify scan's call shape is compatible with scan(params, state[, client]).
|
|
80
71
|
|
|
81
72
|
Accepted shapes:
|
|
82
|
-
scan(
|
|
83
|
-
scan(
|
|
84
|
-
scan(
|
|
73
|
+
scan(params, state) — 2 required
|
|
74
|
+
scan(params, state, client) — 3 required
|
|
75
|
+
scan(params, state, client=None) — 2 required, 1 optional
|
|
76
|
+
scan(params, state, *args) — variadic absorbs client
|
|
85
77
|
scan(*args, **kwargs) — fully variadic, accepts anything
|
|
86
78
|
|
|
87
79
|
Rejected shapes:
|
|
88
80
|
scan() — 0 required
|
|
89
|
-
scan(
|
|
90
|
-
scan(
|
|
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.
|
|
81
|
+
scan(params) — 1 required
|
|
82
|
+
scan(a, b, c, d) — >=4 fixed required
|
|
95
83
|
|
|
96
84
|
Raises ScanSignatureError naming the found arity on mismatch.
|
|
97
85
|
"""
|
|
@@ -106,39 +94,57 @@ def verify_scan_signature(scan: Callable) -> None:
|
|
|
106
94
|
has_var_positional = any(
|
|
107
95
|
p.kind == inspect.Parameter.VAR_POSITIONAL for p in params
|
|
108
96
|
)
|
|
97
|
+
has_var_keyword = any(
|
|
98
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in params
|
|
99
|
+
)
|
|
109
100
|
|
|
110
|
-
#
|
|
111
|
-
|
|
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
|
+
|
|
122
|
+
# Count fixed positional params (POSITIONAL_ONLY or POSITIONAL_OR_KEYWORD).
|
|
123
|
+
fixed_positional = [
|
|
112
124
|
p for p in params
|
|
113
125
|
if p.kind in (
|
|
114
126
|
inspect.Parameter.POSITIONAL_ONLY,
|
|
115
127
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
116
128
|
)
|
|
117
|
-
and p.default is inspect.Parameter.empty
|
|
118
129
|
]
|
|
119
|
-
|
|
130
|
+
required_fixed = [
|
|
131
|
+
p for p in fixed_positional
|
|
132
|
+
if p.default is inspect.Parameter.empty
|
|
133
|
+
]
|
|
120
134
|
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
135
|
+
n_required = len(required_fixed)
|
|
136
|
+
n_fixed = len(fixed_positional)
|
|
130
137
|
|
|
131
138
|
if n_required < 2:
|
|
132
139
|
raise ScanSignatureError(
|
|
133
|
-
f"scan has {n_required} required positional param(s);
|
|
134
|
-
f"
|
|
140
|
+
f"scan has {n_required} required positional param(s); "
|
|
141
|
+
f"expected at least 2 (params, state). Found arity: {n_required}."
|
|
135
142
|
)
|
|
136
143
|
|
|
137
|
-
if n_required
|
|
144
|
+
if n_required >= 4:
|
|
138
145
|
raise ScanSignatureError(
|
|
139
|
-
f"scan has {n_required} required positional params;
|
|
140
|
-
f"
|
|
141
|
-
f"(the old v1 client shape) is no longer accepted. Found arity: {n_required}."
|
|
146
|
+
f"scan has {n_required} required positional params; "
|
|
147
|
+
f"expected at most 3 (params, state, client). Found arity: {n_required}."
|
|
142
148
|
)
|
|
143
149
|
|
|
144
150
|
|
|
@@ -90,19 +90,8 @@ 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
|
-
|
|
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
|
|
93
|
+
"""Write a 'produced' line and enqueue the signal. Returns entry id."""
|
|
94
|
+
entry_id = uuid.uuid4().hex
|
|
106
95
|
asset = signal.get("asset", "")
|
|
107
96
|
valid_until = signal.get("valid_until", 0)
|
|
108
97
|
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
"""Scaffold core loop
|
|
1
|
+
"""Scaffold core loop.
|
|
2
2
|
|
|
3
3
|
run_scaffold(...) loads the author's entrypoint, finds and validates scan(),
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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).
|
|
4
|
+
then drives the interval loop: time-box per tick, graceful stop on SIGTERM/SIGINT,
|
|
5
|
+
parent-death detection via ppid, state in/out, candidates to sink.
|
|
6
|
+
|
|
7
|
+
`path` is a scaffold concern only — it is put on sys.path before import and
|
|
8
|
+
never passed to scan(). scan() is called as scan(params, state, client=None).
|
|
12
9
|
"""
|
|
13
10
|
|
|
14
11
|
import inspect
|
|
@@ -17,7 +14,6 @@ import time
|
|
|
17
14
|
import threading
|
|
18
15
|
from typing import Any, Callable, Optional
|
|
19
16
|
|
|
20
|
-
from .ctx import ScanContext
|
|
21
17
|
from .entrypoint_loader import (
|
|
22
18
|
ScanEntrypointError,
|
|
23
19
|
ScanSignatureError,
|
|
@@ -33,44 +29,40 @@ from .loop_primitives import (
|
|
|
33
29
|
_interruptible_sleep,
|
|
34
30
|
log_event,
|
|
35
31
|
)
|
|
36
|
-
from .
|
|
32
|
+
from .scaffold_state import read_state, write_state
|
|
37
33
|
|
|
38
34
|
|
|
39
|
-
def
|
|
40
|
-
"""
|
|
35
|
+
def _call_scan(scan: Callable, params: dict, state: Any, client: Any) -> tuple:
|
|
36
|
+
"""Call scan with the right arity.
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
38
|
+
Tries scan(params, state, client) first; if that raises TypeError because
|
|
39
|
+
scan only accepts 2 positional args, falls back to scan(params, state).
|
|
40
|
+
This lets 2-arg and 3-arg (and variadic) scan functions all work uniformly.
|
|
47
41
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
42
|
+
sig = None
|
|
43
|
+
try:
|
|
44
|
+
sig = inspect.signature(scan)
|
|
45
|
+
except (ValueError, TypeError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
if sig is not None:
|
|
49
|
+
params_list = list(sig.parameters.values())
|
|
50
|
+
has_var_positional = any(
|
|
51
|
+
p.kind == inspect.Parameter.VAR_POSITIONAL for p in params_list
|
|
52
|
+
)
|
|
53
|
+
fixed_positional = [
|
|
54
|
+
p for p in params_list
|
|
55
|
+
if p.kind in (
|
|
56
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
57
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
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)
|
|
69
63
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"""
|
|
73
|
-
return scan(inputs, ctx)
|
|
64
|
+
# Default: 3-arg call (variadic or 3-param).
|
|
65
|
+
return scan(params, state, client)
|
|
74
66
|
|
|
75
67
|
|
|
76
68
|
def run_scaffold(
|
|
@@ -79,17 +71,13 @@ def run_scaffold(
|
|
|
79
71
|
*,
|
|
80
72
|
interval: float,
|
|
81
73
|
timeout_seconds: Optional[float] = None,
|
|
82
|
-
|
|
74
|
+
params: Optional[dict] = None,
|
|
83
75
|
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,
|
|
88
76
|
sink: Optional[Callable] = None,
|
|
89
77
|
max_ticks: Optional[int] = None,
|
|
90
78
|
install_signal_handlers: bool = True,
|
|
91
79
|
) -> int:
|
|
92
|
-
"""Load the author's entrypoint and run its scan() on an interval loop
|
|
80
|
+
"""Load the author's entrypoint and run its scan() on an interval loop.
|
|
93
81
|
|
|
94
82
|
Args:
|
|
95
83
|
path: Directory added to sys.path before import. Never passed to scan().
|
|
@@ -98,15 +86,9 @@ def run_scaffold(
|
|
|
98
86
|
timeout_seconds:
|
|
99
87
|
Per-tick wall-clock budget. Defaults to `interval`. Must be > 0.
|
|
100
88
|
Enforced via SIGALRM on platforms that support it.
|
|
101
|
-
|
|
102
|
-
state_path: Path to the JSON state file.
|
|
103
|
-
|
|
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).
|
|
89
|
+
params: Tunables dict passed as scan()'s first argument. Default {}.
|
|
90
|
+
state_path: Path to the JSON state file. Reads on start, written after
|
|
91
|
+
each clean tick. None means no persistence.
|
|
110
92
|
sink: Callable(candidates) invoked after each clean tick. Default no-op.
|
|
111
93
|
max_ticks: Stop after this many ticks (for tests). None = unbounded.
|
|
112
94
|
install_signal_handlers:
|
|
@@ -130,41 +112,14 @@ def run_scaffold(
|
|
|
130
112
|
if timeout_seconds <= 0:
|
|
131
113
|
raise ValueError(f"timeout_seconds must be > 0; got {timeout_seconds!r}")
|
|
132
114
|
|
|
133
|
-
if
|
|
134
|
-
|
|
115
|
+
if params is None:
|
|
116
|
+
params = {}
|
|
135
117
|
|
|
136
118
|
# Import and locate scan — any failure raises before tick 1.
|
|
137
119
|
module = import_entrypoint(path, entrypoint)
|
|
138
120
|
scan = locate_scan(module)
|
|
139
121
|
verify_scan_signature(scan)
|
|
140
122
|
|
|
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
|
-
|
|
168
123
|
# ---------- Lifecycle setup ----------
|
|
169
124
|
|
|
170
125
|
stop_event = threading.Event()
|
|
@@ -173,17 +128,24 @@ def run_scaffold(
|
|
|
173
128
|
|
|
174
129
|
start_ppid = os.getppid()
|
|
175
130
|
|
|
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
|
+
|
|
176
134
|
log_event(
|
|
177
135
|
"scaffold_started",
|
|
178
136
|
path=path,
|
|
179
137
|
entrypoint=entrypoint,
|
|
180
138
|
interval=interval,
|
|
181
139
|
timeout_seconds=timeout_seconds,
|
|
182
|
-
scanner_name=scanner_name,
|
|
183
140
|
max_ticks=max_ticks,
|
|
184
141
|
)
|
|
185
142
|
|
|
143
|
+
client = None # placeholder; will be the real data handle in a later unit
|
|
186
144
|
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
|
|
187
149
|
|
|
188
150
|
try:
|
|
189
151
|
while not stop_event.is_set():
|
|
@@ -197,28 +159,40 @@ def run_scaffold(
|
|
|
197
159
|
tick_started_at = time.time()
|
|
198
160
|
tick_status = "ok"
|
|
199
161
|
tick_error = None
|
|
200
|
-
candidates
|
|
162
|
+
candidates = []
|
|
163
|
+
new_state = state # default: keep current state on error
|
|
201
164
|
|
|
202
165
|
armed = False
|
|
203
166
|
try:
|
|
204
167
|
armed = _arm_tick_alarm(timeout_seconds)
|
|
205
168
|
try:
|
|
206
|
-
result = _call_scan(scan,
|
|
169
|
+
result = _call_scan(scan, params, state, client)
|
|
207
170
|
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
|
|
171
|
+
# Validate the return value: must be a 2-tuple.
|
|
172
|
+
if (
|
|
173
|
+
not isinstance(result, tuple)
|
|
174
|
+
or len(result) != 2
|
|
175
|
+
):
|
|
211
176
|
tick_status = "malformed"
|
|
212
177
|
log_event(
|
|
213
178
|
"scaffold_tick_malformed",
|
|
214
179
|
tick=tick_count,
|
|
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
|
-
),
|
|
180
|
+
reason=f"scan() returned {type(result).__name__!r}, expected (candidates, new_state) tuple",
|
|
219
181
|
)
|
|
220
182
|
else:
|
|
221
|
-
|
|
183
|
+
raw_candidates, returned_state = result
|
|
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
|
|
222
196
|
|
|
223
197
|
except _TickTimeout as exc:
|
|
224
198
|
tick_status = "timeout"
|
|
@@ -249,40 +223,48 @@ def run_scaffold(
|
|
|
249
223
|
|
|
250
224
|
duration_ms = int((time.time() - tick_started_at) * 1000)
|
|
251
225
|
|
|
252
|
-
# --- Clean tick:
|
|
226
|
+
# --- Clean tick: persist (best-effort), advance state, deliver ---
|
|
253
227
|
if tick_status == "ok":
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
#
|
|
258
|
-
# (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
sink,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
228
|
+
# Disk persistence is best-effort: advance the in-memory state
|
|
229
|
+
# regardless of the write result, so the next tick never
|
|
230
|
+
# recomputes from stale state (which could re-deliver the same
|
|
231
|
+
# signal). But a persist failure is almost always persistent
|
|
232
|
+
# (unserialisable state, read-only dir, full disk), and the
|
|
233
|
+
# un-persisted ticks are LOST on the next restart — silently
|
|
234
|
+
# degrading any scanner that decides on accumulated history.
|
|
235
|
+
# So escalate it like a tick error: post to /errors once on the
|
|
236
|
+
# first failure (throttled while it keeps failing) and clear
|
|
237
|
+
# when a write next succeeds.
|
|
238
|
+
if state_path is not None:
|
|
239
|
+
if write_state(state_path, new_state):
|
|
240
|
+
if state_persist_degraded:
|
|
241
|
+
state_persist_degraded = False
|
|
242
|
+
log_event("scaffold_state_persist_recovered", tick=tick_count)
|
|
243
|
+
else:
|
|
244
|
+
log_event("scaffold_state_persist_failed", tick=tick_count)
|
|
245
|
+
if not state_persist_degraded:
|
|
246
|
+
state_persist_degraded = True
|
|
247
|
+
if sink is not None and hasattr(sink, "post_error"):
|
|
248
|
+
try:
|
|
249
|
+
sink.post_error({
|
|
250
|
+
"type": "state_persist_failed",
|
|
251
|
+
"tick": tick_count,
|
|
252
|
+
"message": (
|
|
253
|
+
"scanner state could not be persisted; "
|
|
254
|
+
"accumulated history will be lost on restart"
|
|
255
|
+
),
|
|
256
|
+
})
|
|
257
|
+
except Exception as perr: # noqa: BLE001
|
|
258
|
+
log_event(
|
|
259
|
+
"scaffold_post_error_failed",
|
|
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:
|
|
286
268
|
try:
|
|
287
269
|
sink(candidates)
|
|
288
270
|
except Exception as sink_exc: # noqa: BLE001
|
|
@@ -293,14 +275,18 @@ def run_scaffold(
|
|
|
293
275
|
error=str(sink_exc),
|
|
294
276
|
)
|
|
295
277
|
else:
|
|
296
|
-
# Non-ok tick (error / timeout / malformed):
|
|
297
|
-
#
|
|
298
|
-
# advanced) and report to /errors via the sink, when supported.
|
|
299
|
-
ctx.state.rollback()
|
|
278
|
+
# Non-ok tick (error / timeout / malformed): report it to the
|
|
279
|
+
# runtime's errors endpoint via the sink, when supported.
|
|
300
280
|
if sink is not None and hasattr(sink, "post_error"):
|
|
301
|
-
|
|
302
|
-
sink
|
|
303
|
-
|
|
281
|
+
try:
|
|
282
|
+
sink.post_error(tick_error or {"type": tick_status})
|
|
283
|
+
except Exception as perr: # noqa: BLE001
|
|
284
|
+
log_event(
|
|
285
|
+
"scaffold_post_error_failed",
|
|
286
|
+
tick=tick_count,
|
|
287
|
+
error_type=type(perr).__name__,
|
|
288
|
+
error=str(perr),
|
|
289
|
+
)
|
|
304
290
|
|
|
305
291
|
log_event(
|
|
306
292
|
"scaffold_tick_finished",
|
|
@@ -311,15 +297,11 @@ def run_scaffold(
|
|
|
311
297
|
)
|
|
312
298
|
|
|
313
299
|
except _TickTimeout as late_exc:
|
|
314
|
-
# A SIGALRM queued by the OS
|
|
315
|
-
# exited (e.g. during
|
|
316
|
-
# tick already did its work; disarm
|
|
317
|
-
#
|
|
300
|
+
# A SIGALRM queued by the OS was delivered after the inner
|
|
301
|
+
# try/except exited (e.g. during write_state, sink, or log_event).
|
|
302
|
+
# Absorb it: the tick already did its work; disarm just in case,
|
|
303
|
+
# log the late arrival, and let the loop continue.
|
|
318
304
|
_disarm_tick_alarm()
|
|
319
|
-
try:
|
|
320
|
-
ctx.state.rollback()
|
|
321
|
-
except Exception: # noqa: BLE001
|
|
322
|
-
pass
|
|
323
305
|
log_event(
|
|
324
306
|
"scaffold_tick_timeout_late",
|
|
325
307
|
tick=tick_count,
|
|
@@ -346,16 +328,3 @@ def run_scaffold(
|
|
|
346
328
|
log_event("scaffold_stopping", tick_count=tick_count)
|
|
347
329
|
|
|
348
330
|
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
|
-
)
|