@senpi/runtime 3.0.127 → 3.0.128
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/actions/action-events.d.ts +7 -21
- package/dist/actions/action-events.d.ts.map +1 -1
- package/dist/actions/action-events.js +26 -54
- package/dist/actions/action-events.js.map +1 -1
- package/dist/actions/action-runtime-module.d.ts.map +1 -1
- package/dist/actions/action-runtime-module.js +7 -38
- package/dist/actions/action-runtime-module.js.map +1 -1
- package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
- package/dist/actions/close-position/close-position.action.js +15 -122
- package/dist/actions/close-position/close-position.action.js.map +1 -1
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +2 -15
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/event-derivation.d.ts +2 -71
- package/dist/actions/event-derivation.d.ts.map +1 -1
- package/dist/actions/event-derivation.js +8 -251
- package/dist/actions/event-derivation.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts +2 -17
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +47 -162
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.d.ts +1 -2
- package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
- package/dist/actions/position-tracker/position-tracker.action.js +17 -62
- package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
- package/dist/actions/store/types.d.ts +2 -9
- package/dist/actions/store/types.d.ts.map +1 -1
- package/dist/actions/store/types.js.map +1 -1
- package/dist/actions/types.d.ts +0 -13
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +0 -13
- package/dist/actions/types.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +0 -3
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/dsl/engine-client/event-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/event-mapping.js +0 -14
- package/dist/dsl/engine-client/event-mapping.js.map +1 -1
- package/dist/dsl/events/build-closed-event.d.ts +1 -16
- package/dist/dsl/events/build-closed-event.d.ts.map +1 -1
- package/dist/dsl/events/build-closed-event.js +0 -6
- package/dist/dsl/events/build-closed-event.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +0 -7
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +7 -37
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +2 -6
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +9 -15
- package/dist/dsl/monitor/create-monitor.js.map +1 -1
- package/dist/dsl/monitor/dsl-failure-events.d.ts +0 -2
- package/dist/dsl/monitor/dsl-failure-events.d.ts.map +1 -1
- package/dist/dsl/monitor/dsl-failure-events.js +1 -2
- package/dist/dsl/monitor/dsl-failure-events.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts +0 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +25 -59
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +2 -8
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +6 -8
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/health/types.d.ts +0 -13
- package/dist/health/types.d.ts.map +1 -1
- package/dist/health/types.js +0 -12
- package/dist/health/types.js.map +1 -1
- package/dist/index.d.ts +4 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -254
- package/dist/index.js.map +1 -1
- package/dist/runtime/action-registry.d.ts.map +1 -1
- package/dist/runtime/action-registry.js +3 -4
- package/dist/runtime/action-registry.js.map +1 -1
- package/dist/runtime/domain-events.d.ts.map +1 -1
- package/dist/runtime/domain-events.js +0 -7
- package/dist/runtime/domain-events.js.map +1 -1
- package/dist/runtime/lifecycle-events.d.ts +1 -9
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js +2 -11
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/reconcile-run.d.ts +3 -13
- package/dist/runtime/reconcile-run.d.ts.map +1 -1
- package/dist/runtime/reconcile-run.js +3 -4
- package/dist/runtime/reconcile-run.js.map +1 -1
- package/dist/runtime/reconcile-state.d.ts +0 -8
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +1 -4
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/run.d.ts +0 -1
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runtime-schema.js +3 -3
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +0 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +3 -27
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.d.ts +0 -13
- package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js +0 -1
- package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
- package/dist/runtime/validate-dsl-position-tracking.d.ts.map +1 -1
- package/dist/runtime/validate-dsl-position-tracking.js +1 -2
- package/dist/runtime/validate-dsl-position-tracking.js.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.js +0 -1
- package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
- package/dist/runtime-api/handlers/types.d.ts +0 -5
- package/dist/runtime-api/handlers/types.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-events.d.ts +1 -1
- package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-events.js +3 -9
- package/dist/runtime-api/intake/intake-events.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +19 -4
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/process-error-post.d.ts +1 -6
- package/dist/runtime-api/intake/process-error-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-error-post.js +1 -5
- package/dist/runtime-api/intake/process-error-post.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts +1 -7
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +0 -5
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +0 -15
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +0 -55
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.d.ts +0 -2
- package/dist/runtime-api/routes/signals.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.js +1 -5
- package/dist/runtime-api/routes/signals.js.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +0 -43
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +0 -43
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/runtime-api/server.d.ts +1 -7
- package/dist/runtime-api/server.d.ts.map +1 -1
- package/dist/runtime-api/server.js +0 -1
- package/dist/runtime-api/server.js.map +1 -1
- package/dist/scanners/engine/engine.d.ts +1 -22
- package/dist/scanners/engine/engine.d.ts.map +1 -1
- package/dist/scanners/engine/engine.js +11 -78
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/engine/lifecycle.d.ts +1 -7
- package/dist/scanners/engine/lifecycle.d.ts.map +1 -1
- package/dist/scanners/events.d.ts +0 -4
- package/dist/scanners/events.d.ts.map +1 -1
- package/dist/scanners/events.js +0 -2
- package/dist/scanners/events.js.map +1 -1
- package/dist/scanners/external/scaffold/delivery.py +25 -194
- package/dist/scanners/external/scaffold/scaffold.py +31 -172
- package/dist/scanners/external/scaffold/senpi_mcp.py +8 -150
- package/dist/scanners/external-scanner-receiver.d.ts +0 -17
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +0 -8
- package/dist/scanners/external-scanner-receiver.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts +0 -1
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +9 -66
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/telemetry/attributes.d.ts +0 -20
- package/dist/telemetry/attributes.d.ts.map +1 -1
- package/dist/telemetry/attributes.js +0 -24
- package/dist/telemetry/attributes.js.map +1 -1
- package/dist/telemetry/event-builders.d.ts +6 -81
- package/dist/telemetry/event-builders.d.ts.map +1 -1
- package/dist/telemetry/event-builders.js +17 -141
- package/dist/telemetry/event-builders.js.map +1 -1
- package/dist/telemetry/event-store.d.ts +0 -6
- package/dist/telemetry/event-store.d.ts.map +1 -1
- package/dist/telemetry/event-store.js +8 -12
- package/dist/telemetry/event-store.js.map +1 -1
- package/dist/telemetry/metrics.d.ts +2 -14
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +4 -19
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/spans/redact-pii.d.ts.map +1 -1
- package/dist/telemetry/spans/redact-pii.js +2 -18
- package/dist/telemetry/spans/redact-pii.js.map +1 -1
- package/dist/telemetry/tracing.d.ts +0 -9
- package/dist/telemetry/tracing.d.ts.map +1 -1
- package/dist/telemetry/tracing.js +0 -36
- package/dist/telemetry/tracing.js.map +1 -1
- package/dist/types/dsl/index.d.ts +0 -11
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/types/scanner.d.ts +0 -15
- package/dist/types/scanner.d.ts.map +1 -1
- package/dist/utils/event-catalog.d.ts +0 -166
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +0 -18
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -4
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/validate/recipe-checks.d.ts.map +1 -1
- package/dist/validate/recipe-checks.js +2 -2
- package/dist/validate/recipe-checks.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/actions/skipped-signal-results.d.ts +0 -25
- package/dist/actions/skipped-signal-results.d.ts.map +0 -1
- package/dist/actions/skipped-signal-results.js +0 -71
- package/dist/actions/skipped-signal-results.js.map +0 -1
- package/dist/runtime-api/intake/ingest-request.d.ts +0 -17
- package/dist/runtime-api/intake/ingest-request.d.ts.map +0 -1
- package/dist/runtime-api/intake/ingest-request.js +0 -34
- package/dist/runtime-api/intake/ingest-request.js.map +0 -1
- package/dist/runtime-api/intake/scaffold-tick-router.d.ts +0 -39
- package/dist/runtime-api/intake/scaffold-tick-router.d.ts.map +0 -1
- package/dist/runtime-api/intake/scaffold-tick-router.js +0 -122
- package/dist/runtime-api/intake/scaffold-tick-router.js.map +0 -1
- package/dist/scanners/external/scaffold/tick_facts.py +0 -130
- package/dist/scanners/external/scaffold/ulid.py +0 -58
- package/dist/telemetry/live-telemetry.d.ts +0 -238
- package/dist/telemetry/live-telemetry.d.ts.map +0 -1
- package/dist/telemetry/live-telemetry.js +0 -853
- package/dist/telemetry/live-telemetry.js.map +0 -1
- package/dist/telemetry/scanner-stats.d.ts +0 -143
- package/dist/telemetry/scanner-stats.d.ts.map +0 -1
- package/dist/telemetry/scanner-stats.js +0 -426
- package/dist/telemetry/scanner-stats.js.map +0 -1
- package/dist/telemetry/scanner-tick-feed.d.ts +0 -87
- package/dist/telemetry/scanner-tick-feed.d.ts.map +0 -1
- package/dist/telemetry/scanner-tick-feed.js +0 -48
- package/dist/telemetry/scanner-tick-feed.js.map +0 -1
- package/dist/telemetry/strategy-topology.d.ts +0 -99
- package/dist/telemetry/strategy-topology.d.ts.map +0 -1
- package/dist/telemetry/strategy-topology.js +0 -93
- package/dist/telemetry/strategy-topology.js.map +0 -1
- package/dist/telemetry/telemetry-publisher.d.ts +0 -63
- package/dist/telemetry/telemetry-publisher.d.ts.map +0 -1
- package/dist/telemetry/telemetry-publisher.js +0 -229
- package/dist/telemetry/telemetry-publisher.js.map +0 -1
- package/dist/telemetry/tick-frame.d.ts +0 -212
- package/dist/telemetry/tick-frame.d.ts.map +0 -1
- package/dist/telemetry/tick-frame.js +0 -421
- package/dist/telemetry/tick-frame.js.map +0 -1
- package/dist/telemetry/timeline-buffer.d.ts +0 -95
- package/dist/telemetry/timeline-buffer.d.ts.map +0 -1
- package/dist/telemetry/timeline-buffer.js +0 -237
- package/dist/telemetry/timeline-buffer.js.map +0 -1
- package/dist/utils/telemetry-required-log.d.ts +0 -60
- package/dist/utils/telemetry-required-log.d.ts.map +0 -1
- package/dist/utils/telemetry-required-log.js +0 -105
- package/dist/utils/telemetry-required-log.js.map +0 -1
- package/dist/utils/ulid.d.ts +0 -9
- package/dist/utils/ulid.d.ts.map +0 -1
- package/dist/utils/ulid.js +0 -43
- package/dist/utils/ulid.js.map +0 -1
|
@@ -11,23 +11,18 @@ build_delivery_sink(
|
|
|
11
11
|
clock: Callable[[], float] = time.time,
|
|
12
12
|
) -> sink
|
|
13
13
|
|
|
14
|
-
The returned sink is a callable: sink(candidates
|
|
15
|
-
|
|
16
|
-
pending count onto them (it owns the journal) and rides them on the POST it already
|
|
17
|
-
makes. On each call it:
|
|
14
|
+
The returned sink is a callable: sink(candidates)
|
|
15
|
+
On each call it:
|
|
18
16
|
1. Replays journal on first call (if file-backed), re-queuing valid undelivered entries
|
|
19
17
|
2. Flushes still-valid undelivered entries from prior ticks (retry)
|
|
20
|
-
3. Wraps this tick's candidates into wire signals (skips invalid ones)
|
|
21
|
-
producing tick's id on each
|
|
18
|
+
3. Wraps this tick's candidates into wire signals (skips invalid ones)
|
|
22
19
|
4. Drops locally superseded / expired entries
|
|
23
20
|
5. POSTs {scanner_id, signals:[...]} to <intake_url>/signals
|
|
24
21
|
6. Interprets the response: accepted/expired/superseded/rejected = delivered;
|
|
25
22
|
transport failure or non-success envelope leaves entry queued for retry
|
|
26
23
|
|
|
27
|
-
sink.post_error(error: dict
|
|
28
|
-
POSTs {scanner_id, error} to <intake_url>/errors. Fire-and-forget.
|
|
29
|
-
facts ride here for the same reason they ride /signals on a clean one: this is the
|
|
30
|
-
POST that tick already makes.
|
|
24
|
+
sink.post_error(error: dict) -> None
|
|
25
|
+
POSTs {scanner_id, error} to <intake_url>/errors. Fire-and-forget.
|
|
31
26
|
|
|
32
27
|
Module constants:
|
|
33
28
|
SIGNALS_PATH = "/signals"
|
|
@@ -64,47 +59,14 @@ def build_delivery_sink(
|
|
|
64
59
|
|
|
65
60
|
journal = Journal(journal_path)
|
|
66
61
|
replayed = [False] # mutable cell; replay runs once on first invocation
|
|
67
|
-
# Mutable cell; the id of the tick currently inside _sink, so the reject/warn
|
|
68
|
-
# callbacks — which are built once, here — can name it. _sink sets it on entry,
|
|
69
|
-
# before anything can read it, and both callbacks run only from inside _sink.
|
|
70
|
-
# None when the caller passed no id, or one the wire would refuse.
|
|
71
|
-
current_tick_id: list[str | None] = [None]
|
|
72
62
|
# Distinct dropped-field shapes already warned about (rate-limit: a scanner that
|
|
73
63
|
# sets a stray key every tick warns ONCE per shape, not every tick).
|
|
74
64
|
warned_dropped_shapes: set[frozenset] = set()
|
|
75
|
-
# Distinct unusable tick-id shapes already warned about, same rate-limit reason: a loop
|
|
76
|
-
# handing the sink the same bad id every tick warns ONCE, not on every tick forever.
|
|
77
|
-
warned_unusable_tick_ids: set[str] = set()
|
|
78
|
-
# Set once per process, the first time the intake refuses a batch for carrying tick_id.
|
|
79
|
-
# From then on the field is left off the wire and the "this intake predates the field" line
|
|
80
|
-
# is not written again. Never cleared: an intake that refuses the field once would refuse it
|
|
81
|
-
# again, so re-arming would buy back the guaranteed-400 round-trip on every later tick.
|
|
82
|
-
intake_predates_tick_id = [False]
|
|
83
|
-
# The same latch for the body-level `tick` facts. Separate from the one above because
|
|
84
|
-
# they are separate fields on separate levels of the body: an intake may know one and
|
|
85
|
-
# not the other, and stripping both on either refusal would throw away what still works.
|
|
86
|
-
intake_predates_tick_facts = [False]
|
|
87
65
|
|
|
88
66
|
# ------------------------------------------------------------------
|
|
89
67
|
# Internal helpers
|
|
90
68
|
# ------------------------------------------------------------------
|
|
91
69
|
|
|
92
|
-
def _tick_field(tick_id: Any) -> dict:
|
|
93
|
-
"""`tick_id=...` as log kwargs, or nothing at all when no tick is nameable.
|
|
94
|
-
|
|
95
|
-
An absent key is the honest answer; a null or a blank would join to nothing
|
|
96
|
-
while looking like an id.
|
|
97
|
-
"""
|
|
98
|
-
return {"tick_id": tick_id} if isinstance(tick_id, str) and tick_id else {}
|
|
99
|
-
|
|
100
|
-
def _signal_tick_id(signal: Any) -> Any:
|
|
101
|
-
"""The producing tick's id off a stored signal, or None.
|
|
102
|
-
|
|
103
|
-
A journal line is arbitrary text on disk: `signal` may be a null, a string or
|
|
104
|
-
a list, so this never assumes a mapping.
|
|
105
|
-
"""
|
|
106
|
-
return signal.get("tick_id") if isinstance(signal, dict) else None
|
|
107
|
-
|
|
108
70
|
def _now_ms() -> int:
|
|
109
71
|
return int(clock() * 1000)
|
|
110
72
|
|
|
@@ -127,40 +89,13 @@ def build_delivery_sink(
|
|
|
127
89
|
to_drop.append(entry)
|
|
128
90
|
for entry in to_drop:
|
|
129
91
|
journal.record_outcome(entry.id, "dropped_expired")
|
|
130
|
-
|
|
131
|
-
# noticed the expiry did not cause it.
|
|
132
|
-
log_event(
|
|
133
|
-
"delivery_dropped_expired",
|
|
134
|
-
entry_id=entry.id,
|
|
135
|
-
asset=entry.asset,
|
|
136
|
-
**_tick_field(_signal_tick_id(entry.signal)),
|
|
137
|
-
)
|
|
92
|
+
log_event("delivery_dropped_expired", entry_id=entry.id, asset=entry.asset)
|
|
138
93
|
return to_send
|
|
139
94
|
|
|
140
|
-
def _usable_tick_id(tick_id: Any) -> str | None:
|
|
141
|
-
"""The tick id to stamp, or None to leave the field off the wire entirely.
|
|
142
|
-
|
|
143
|
-
The intake requires a non-empty string and rejects the WHOLE post on one bad field, so an
|
|
144
|
-
unusable id is dropped here rather than taking the tick's signals down with it. No id at
|
|
145
|
-
all is the old call shape, not a fault, and is not logged.
|
|
146
|
-
"""
|
|
147
|
-
if tick_id is None or (isinstance(tick_id, str) and tick_id):
|
|
148
|
-
return tick_id
|
|
149
|
-
shape = repr(tick_id)[:80]
|
|
150
|
-
if shape in warned_unusable_tick_ids:
|
|
151
|
-
return None
|
|
152
|
-
warned_unusable_tick_ids.add(shape)
|
|
153
|
-
log_event("delivery_tick_id_unusable", scanner_id=scanner_id, rejected_tick_id=shape)
|
|
154
|
-
return None
|
|
155
|
-
|
|
156
95
|
def _on_reject(reason: dict) -> None:
|
|
157
96
|
"""Loud rejection: log AND POST to /errors (design §4 — never a silent drop)."""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
# here would take down the whole tick's delivery. The scaffold's id goes last,
|
|
161
|
-
# so the tick that actually ran the candidate is the one named.
|
|
162
|
-
log_event("delivery_candidate_invalid", **{**reason, **_tick_field(current_tick_id[0])})
|
|
163
|
-
_post_error({"type": "candidate_rejected", **reason, **_tick_field(current_tick_id[0])})
|
|
97
|
+
log_event("delivery_candidate_invalid", **reason)
|
|
98
|
+
_post_error({"type": "candidate_rejected", **reason})
|
|
164
99
|
|
|
165
100
|
def _on_warn(info: dict) -> None:
|
|
166
101
|
"""Loud-but-non-fatal: a valid signal still emitted, but author top-level
|
|
@@ -175,10 +110,9 @@ def build_delivery_sink(
|
|
|
175
110
|
scanner_id=scanner_id,
|
|
176
111
|
dropped=info.get("dropped"),
|
|
177
112
|
asset=info.get("asset"),
|
|
178
|
-
**_tick_field(current_tick_id[0]),
|
|
179
113
|
)
|
|
180
114
|
|
|
181
|
-
def _wrap_candidates(candidates, produced_at_ms: int
|
|
115
|
+
def _wrap_candidates(candidates, produced_at_ms: int) -> list[tuple]:
|
|
182
116
|
"""Wrap candidates into (signal, entry_id) pairs, skipping invalid ones.
|
|
183
117
|
|
|
184
118
|
Schema-rejected candidates are NOT silently dropped: candidate_to_signal
|
|
@@ -201,51 +135,13 @@ def build_delivery_sink(
|
|
|
201
135
|
)
|
|
202
136
|
if sig is None:
|
|
203
137
|
continue
|
|
204
|
-
# Stamped before the journal writes the entry, so a retry — later in this process or
|
|
205
|
-
# after a restart — re-posts the id of the tick that PRODUCED the signal. Skipped
|
|
206
|
-
# once the intake has proved it predates the field: a stamped signal is refused and
|
|
207
|
-
# re-posted without it, and stamping again would buy that same 400 every tick.
|
|
208
|
-
if tick_id is not None and not intake_predates_tick_id[0]:
|
|
209
|
-
sig["tick_id"] = tick_id
|
|
210
138
|
entry_id = journal.record_produced(sig, now_ms=produced_at_ms)
|
|
211
139
|
result.append((sig, entry_id))
|
|
212
140
|
return result
|
|
213
141
|
|
|
214
|
-
def
|
|
215
|
-
"""
|
|
216
|
-
if status != 400 or not isinstance(parsed, dict):
|
|
217
|
-
return False
|
|
218
|
-
err = parsed.get("error")
|
|
219
|
-
return isinstance(err, dict) and err.get("code") == "INVALID_SHAPE"
|
|
220
|
-
|
|
221
|
-
def _without_tick_id(signals: list[dict]) -> list[dict]:
|
|
222
|
-
"""Copies of `signals` with `tick_id` removed.
|
|
223
|
-
|
|
224
|
-
Copies, never edits: the journal keeps each entry's producing tick, so once the intake
|
|
225
|
-
is upgraded a later retry of the same entry names it again.
|
|
226
|
-
"""
|
|
227
|
-
return [
|
|
228
|
-
{k: v for k, v in sig.items() if k != "tick_id"} if isinstance(sig, dict) else sig
|
|
229
|
-
for sig in signals
|
|
230
|
-
]
|
|
231
|
-
|
|
232
|
-
def _with_pending(tick: dict | None) -> dict | None:
|
|
233
|
-
"""The tick facts plus the journal's BACKLOG, which only this closure knows.
|
|
234
|
-
|
|
235
|
-
Counted before this tick's own candidates are journalled, so the number is
|
|
236
|
-
signals from EARLIER ticks that the intake never acknowledged — a scanner
|
|
237
|
-
producing work that is not arriving. Counting this tick's own signals too
|
|
238
|
-
would put a 1 on every healthy signal-bearing tick and say nothing.
|
|
239
|
-
"""
|
|
240
|
-
if tick is None:
|
|
241
|
-
return None
|
|
242
|
-
return {**tick, "journal_pending": len(journal.pending())}
|
|
243
|
-
|
|
244
|
-
def _post_signals(signals: list[dict], tick: dict | None = None) -> tuple[int, Any]:
|
|
245
|
-
"""POST the signals list to the intake, with this tick's facts when it has any."""
|
|
142
|
+
def _post_signals(signals: list[dict]) -> tuple[int, Any]:
|
|
143
|
+
"""POST the signals list to the intake."""
|
|
246
144
|
body = {"scanner_id": scanner_id, "signals": signals}
|
|
247
|
-
if tick is not None and not intake_predates_tick_facts[0]:
|
|
248
|
-
body["tick"] = tick
|
|
249
145
|
# Log the exact wire shape so a 400 can be matched to the field that broke.
|
|
250
146
|
if signals:
|
|
251
147
|
log_event(
|
|
@@ -258,7 +154,7 @@ def build_delivery_sink(
|
|
|
258
154
|
url = intake_url.rstrip("/") + SIGNALS_PATH
|
|
259
155
|
return post_json(url, body, timeout)
|
|
260
156
|
|
|
261
|
-
def _interpret_response(status: int, parsed: Any, pending_eids: list[str],
|
|
157
|
+
def _interpret_response(status: int, parsed: Any, pending_eids: list[str], pending_entries: list) -> bool:
|
|
262
158
|
"""Interpret POST response; journal outcomes. Returns True if successful envelope."""
|
|
263
159
|
if status != 200 or parsed is None:
|
|
264
160
|
if isinstance(parsed, dict):
|
|
@@ -298,10 +194,6 @@ def build_delivery_sink(
|
|
|
298
194
|
# signals), and unknown/missing signal_id (ignored safely, never applied by
|
|
299
195
|
# position). An unmatched entry stays pending and is retried.
|
|
300
196
|
pending = set(pending_eids)
|
|
301
|
-
# eid -> the tick that produced it, for the per-entry lines below. Built only if one of
|
|
302
|
-
# those lines is actually reached: every POST pays for it otherwise, and the unknown-status
|
|
303
|
-
# branch that reads it is the rare one.
|
|
304
|
-
produced_by: dict | None = None
|
|
305
197
|
for sig_status in per_signal:
|
|
306
198
|
if not isinstance(sig_status, dict):
|
|
307
199
|
continue
|
|
@@ -319,16 +211,7 @@ def build_delivery_sink(
|
|
|
319
211
|
journal.record_outcome(eid, st)
|
|
320
212
|
else:
|
|
321
213
|
# Unknown status — treat as undelivered (no outcome written)
|
|
322
|
-
|
|
323
|
-
produced_by = {
|
|
324
|
-
e: _signal_tick_id(sig) for e, sig in zip(pending_eids, pending_signals)
|
|
325
|
-
}
|
|
326
|
-
log_event(
|
|
327
|
-
"delivery_unknown_status",
|
|
328
|
-
entry_id=eid,
|
|
329
|
-
status=st,
|
|
330
|
-
**_tick_field(produced_by.get(eid)),
|
|
331
|
-
)
|
|
214
|
+
log_event("delivery_unknown_status", entry_id=eid, status=st)
|
|
332
215
|
# Entries with no matching status returned stay pending (retry next tick).
|
|
333
216
|
|
|
334
217
|
return True
|
|
@@ -337,10 +220,7 @@ def build_delivery_sink(
|
|
|
337
220
|
# The sink callable
|
|
338
221
|
# ------------------------------------------------------------------
|
|
339
222
|
|
|
340
|
-
def _sink(candidates
|
|
341
|
-
tick_id = _usable_tick_id(tick_id)
|
|
342
|
-
current_tick_id[0] = tick_id
|
|
343
|
-
|
|
223
|
+
def _sink(candidates) -> None:
|
|
344
224
|
# Replay journal on very first invocation
|
|
345
225
|
if not replayed[0]:
|
|
346
226
|
replayed[0] = True
|
|
@@ -352,12 +232,8 @@ def build_delivery_sink(
|
|
|
352
232
|
# 1. Flush prior pending (retry candidates from older ticks)
|
|
353
233
|
prior_entries = _flush_prior_pending(now_ms)
|
|
354
234
|
|
|
355
|
-
# Read here and not below: the backlog is what EARLIER ticks left behind, and
|
|
356
|
-
# step 2 is about to journal this tick's own candidates on top of it.
|
|
357
|
-
tick_body = _with_pending(tick)
|
|
358
|
-
|
|
359
235
|
# 2. Wrap new candidates into signals
|
|
360
|
-
new_signal_pairs = _wrap_candidates(candidates, produced_at_ms
|
|
236
|
+
new_signal_pairs = _wrap_candidates(candidates, produced_at_ms)
|
|
361
237
|
|
|
362
238
|
# Build the ordered signal list and corresponding entry-id list:
|
|
363
239
|
# prior entries first (retry), then new ones.
|
|
@@ -377,75 +253,30 @@ def build_delivery_sink(
|
|
|
377
253
|
all_signals.append(sig)
|
|
378
254
|
all_eids.append(eid)
|
|
379
255
|
|
|
380
|
-
# 3. POST (always, even when signals==[])
|
|
381
|
-
# the tick facts ride: a quiet scanner still posts, every tick.
|
|
256
|
+
# 3. POST (always, even when signals==[])
|
|
382
257
|
try:
|
|
383
|
-
status, parsed = _post_signals(all_signals
|
|
258
|
+
status, parsed = _post_signals(all_signals)
|
|
384
259
|
except TransportError as exc:
|
|
385
260
|
log_event("delivery_transport_error", error=str(exc))
|
|
386
261
|
# All signals remain undelivered -> retry on next tick
|
|
387
262
|
# Empty-list no-ops: nothing to retry (no produced lines were written)
|
|
388
263
|
return
|
|
389
264
|
|
|
390
|
-
# 3b. Forward-compat guard for an intake rolled back to before tick_id. Such an intake
|
|
391
|
-
# validates with additionalProperties:false, so it 400s the WHOLE batch on the unknown
|
|
392
|
-
# field — and because the journal re-posts the stored signal untouched, that batch is
|
|
393
|
-
# re-sent every tick forever, taking every fresh signal batched with it down too. Re-post
|
|
394
|
-
# the same batch without the field so one unknown key cannot strand real signals, and
|
|
395
|
-
# latch the discovery so later ticks leave the field off and stop paying the refused POST.
|
|
396
|
-
# Entries already journalled with an id still reach here and are still stripped per POST.
|
|
397
|
-
sent_tick_id = any(isinstance(sig, dict) and "tick_id" in sig for sig in all_signals)
|
|
398
|
-
sent_tick_facts = tick_body is not None and not intake_predates_tick_facts[0]
|
|
399
|
-
if _is_shape_reject(status, parsed) and (sent_tick_id or sent_tick_facts):
|
|
400
|
-
if sent_tick_id and not intake_predates_tick_id[0]:
|
|
401
|
-
intake_predates_tick_id[0] = True
|
|
402
|
-
log_event("delivery_intake_predates_tick_id", scanner_id=scanner_id)
|
|
403
|
-
if sent_tick_facts:
|
|
404
|
-
intake_predates_tick_facts[0] = True
|
|
405
|
-
log_event("delivery_intake_predates_tick_facts", scanner_id=scanner_id)
|
|
406
|
-
try:
|
|
407
|
-
status, parsed = _post_signals(_without_tick_id(all_signals))
|
|
408
|
-
except TransportError as exc:
|
|
409
|
-
log_event("delivery_transport_error", error=str(exc))
|
|
410
|
-
return
|
|
411
|
-
|
|
412
265
|
# 4. Interpret response
|
|
413
266
|
# The response per-signal list corresponds only to all_signals that have
|
|
414
|
-
# produced entries (all_eids). Empty no-ops: no eids.
|
|
415
|
-
|
|
416
|
-
# even when the retry above had to send them without it.
|
|
417
|
-
_interpret_response(status, parsed, all_eids, all_signals)
|
|
418
|
-
|
|
419
|
-
def _post_error(error: dict, tick: dict | None = None) -> None:
|
|
420
|
-
"""POST an error dict, with the failed tick's facts when the caller has them.
|
|
267
|
+
# produced entries (all_eids). Empty no-ops: no eids.
|
|
268
|
+
_interpret_response(status, parsed, all_eids, [])
|
|
421
269
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
"""
|
|
426
|
-
tick_body = _with_pending(tick)
|
|
270
|
+
def _post_error(error: dict) -> None:
|
|
271
|
+
"""POST an error dict. Fire-and-forget."""
|
|
272
|
+
body = {"scanner_id": scanner_id, "error": error}
|
|
427
273
|
url = intake_url.rstrip("/") + ERRORS_PATH
|
|
428
|
-
# The error belongs to exactly one tick and already names it; a body that
|
|
429
|
-
# names none leaves the key off rather than borrowing the current one.
|
|
430
|
-
origin = _tick_field(error.get("tick_id")) if isinstance(error, dict) else {}
|
|
431
|
-
|
|
432
|
-
def _body(with_tick: bool) -> dict:
|
|
433
|
-
body = {"scanner_id": scanner_id, "error": error}
|
|
434
|
-
if with_tick and tick_body is not None and not intake_predates_tick_facts[0]:
|
|
435
|
-
body["tick"] = tick_body
|
|
436
|
-
return body
|
|
437
|
-
|
|
438
274
|
try:
|
|
439
|
-
|
|
440
|
-
status, parsed = post_json(url, _body(True), timeout)
|
|
441
|
-
if sent_tick_facts and _is_shape_reject(status, parsed):
|
|
442
|
-
intake_predates_tick_facts[0] = True
|
|
443
|
-
log_event("delivery_intake_predates_tick_facts", scanner_id=scanner_id)
|
|
444
|
-
post_json(url, _body(False), timeout)
|
|
275
|
+
post_json(url, body, timeout)
|
|
445
276
|
except TransportError as exc:
|
|
446
|
-
log_event("delivery_error_post_failed", error=str(exc)
|
|
277
|
+
log_event("delivery_error_post_failed", error=str(exc))
|
|
447
278
|
except Exception as exc: # noqa: BLE001
|
|
448
|
-
log_event("delivery_error_post_failed", error=str(exc)
|
|
279
|
+
log_event("delivery_error_post_failed", error=str(exc))
|
|
449
280
|
|
|
450
281
|
_sink.post_error = _post_error # type: ignore[attr-defined]
|
|
451
282
|
return _sink
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
run_scaffold(...) loads the author's entrypoint, finds and validates scan(),
|
|
4
4
|
builds the ScanContext, then drives the interval loop: time-box per tick, graceful
|
|
5
5
|
stop on SIGTERM/SIGINT, parent-death detection via ppid, transactional state via
|
|
6
|
-
ctx.state, candidates to sink.
|
|
7
|
-
tick's candidates and carried on every error the tick reports.
|
|
6
|
+
ctx.state, candidates to sink.
|
|
8
7
|
|
|
9
8
|
`path` is a scaffold concern only — it is put on sys.path before import and never
|
|
10
9
|
passed to scan(). scan() is called as scan(inputs, ctx) — 2-arg, sync, returning a
|
|
@@ -43,8 +42,6 @@ from .loop_primitives import (
|
|
|
43
42
|
log_event,
|
|
44
43
|
)
|
|
45
44
|
from .state_manager import StateManager
|
|
46
|
-
from .tick_facts import build_tick_facts, count_changed, project_state_row
|
|
47
|
-
from .ulid import new_ulid
|
|
48
45
|
|
|
49
46
|
|
|
50
47
|
def _read_call_timeout_seconds(senpi_mcp_mod: Any) -> float:
|
|
@@ -106,28 +103,6 @@ def _build_senpi_mcp() -> Any:
|
|
|
106
103
|
)
|
|
107
104
|
|
|
108
105
|
|
|
109
|
-
def _accepts_kwarg(fn: Any, name: str) -> bool:
|
|
110
|
-
"""Whether `fn` takes `name` as a keyword argument.
|
|
111
|
-
|
|
112
|
-
Read from the signature, once, rather than calling with the keyword and retrying on
|
|
113
|
-
TypeError: a sink that raises TypeError from its own body would then be called twice and
|
|
114
|
-
deliver the same candidates twice. A callable whose signature cannot be read keeps the
|
|
115
|
-
narrower call it has today.
|
|
116
|
-
"""
|
|
117
|
-
try:
|
|
118
|
-
params = list(inspect.signature(fn).parameters.values())
|
|
119
|
-
except (TypeError, ValueError):
|
|
120
|
-
return False
|
|
121
|
-
return any(
|
|
122
|
-
p.kind == inspect.Parameter.VAR_KEYWORD
|
|
123
|
-
or (
|
|
124
|
-
p.name == name
|
|
125
|
-
and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
126
|
-
)
|
|
127
|
-
for p in params
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
106
|
def _call_scan(scan: Callable, inputs: dict, ctx: Any):
|
|
132
107
|
"""Call scan(inputs, ctx) — the v2 2-arg contract.
|
|
133
108
|
|
|
@@ -173,8 +148,7 @@ def run_scaffold(
|
|
|
173
148
|
state_history_max_count:
|
|
174
149
|
Bound on the persisted state series. 0/unset = history disabled
|
|
175
150
|
(ctx.state.append raises).
|
|
176
|
-
sink: Callable(candidates
|
|
177
|
-
that takes candidates only is called with candidates only. Default no-op.
|
|
151
|
+
sink: Callable(candidates) invoked after each clean tick. Default no-op.
|
|
178
152
|
max_ticks: Stop after this many ticks. None = unbounded. Used by tests and by a
|
|
179
153
|
validation run, which forces exactly one tick.
|
|
180
154
|
senpi_mcp: Data client to hand to scan(). Default: built from the launch environment.
|
|
@@ -261,24 +235,6 @@ def run_scaffold(
|
|
|
261
235
|
# The MCP boundary's per-tick failure tally, when the client exposes one. Duck-typed:
|
|
262
236
|
# an injected stub without it (a validation run) simply never reclassifies a tick.
|
|
263
237
|
mcp_tally = getattr(senpi_mcp, "tick_errors", None)
|
|
264
|
-
sink_takes_tick_id = sink is not None and _accepts_kwarg(sink, "tick_id")
|
|
265
|
-
# The per-tick observability facts ride the POST the sink already makes. A sink that
|
|
266
|
-
# predates them is called exactly as before, so an older sink is not broken by them.
|
|
267
|
-
sink_takes_tick = sink is not None and _accepts_kwarg(sink, "tick")
|
|
268
|
-
post_error_takes_tick = (
|
|
269
|
-
sink is not None
|
|
270
|
-
and hasattr(sink, "post_error")
|
|
271
|
-
and _accepts_kwarg(sink.post_error, "tick")
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
# Consecutive ticks that produced no candidate. 47 quiet ticks is information; the word
|
|
275
|
-
# "quiet" repeated 47 times is not, so the streak is counted here and sent as a number.
|
|
276
|
-
quiet_ticks = 0
|
|
277
|
-
# The previous tick's start, for the cadence lag. None on the first tick of a process:
|
|
278
|
-
# there is no gap to measure yet, and a 0 would claim the scanner was exactly on time.
|
|
279
|
-
prev_tick_started_at: Optional[float] = None
|
|
280
|
-
# The last projection reported, so `state_changed` says what MOVED rather than what exists.
|
|
281
|
-
prev_state_projection: Optional[dict] = None
|
|
282
238
|
|
|
283
239
|
try:
|
|
284
240
|
while not stop_event.is_set():
|
|
@@ -289,18 +245,7 @@ def run_scaffold(
|
|
|
289
245
|
break
|
|
290
246
|
|
|
291
247
|
tick_count += 1
|
|
292
|
-
# One id per tick, minted before any of the tick's work so every candidate and every
|
|
293
|
-
# error it reports names the same tick.
|
|
294
|
-
tick_id = new_ulid()
|
|
295
248
|
tick_started_at = time.time()
|
|
296
|
-
# Actual gap between tick starts, minus the cadence the scanner is configured for.
|
|
297
|
-
# A positive number is a scanner falling behind its own interval.
|
|
298
|
-
lag_ms = (
|
|
299
|
-
None
|
|
300
|
-
if prev_tick_started_at is None
|
|
301
|
-
else int(round((tick_started_at - prev_tick_started_at - interval) * 1000))
|
|
302
|
-
)
|
|
303
|
-
prev_tick_started_at = tick_started_at
|
|
304
249
|
tick_status = "ok"
|
|
305
250
|
tick_error = None
|
|
306
251
|
candidates: list = []
|
|
@@ -324,15 +269,10 @@ def run_scaffold(
|
|
|
324
269
|
)
|
|
325
270
|
# Carried on the error record, not only in this log line: the sink is what
|
|
326
271
|
# a caller reads, and "malformed" without the reason is not actionable.
|
|
327
|
-
tick_error = {
|
|
328
|
-
"type": "malformed",
|
|
329
|
-
"tick_id": tick_id,
|
|
330
|
-
"message": malformed_reason,
|
|
331
|
-
}
|
|
272
|
+
tick_error = {"type": "malformed", "message": malformed_reason}
|
|
332
273
|
log_event(
|
|
333
274
|
"scaffold_tick_malformed",
|
|
334
275
|
tick=tick_count,
|
|
335
|
-
tick_id=tick_id,
|
|
336
276
|
reason=malformed_reason,
|
|
337
277
|
)
|
|
338
278
|
else:
|
|
@@ -341,11 +281,10 @@ def run_scaffold(
|
|
|
341
281
|
except _TickTimeout as exc:
|
|
342
282
|
tick_status = "timeout"
|
|
343
283
|
candidates = []
|
|
344
|
-
tick_error = {"type": "timeout", "
|
|
284
|
+
tick_error = {"type": "timeout", "message": str(exc)}
|
|
345
285
|
log_event(
|
|
346
286
|
"scaffold_tick_timeout",
|
|
347
287
|
tick=tick_count,
|
|
348
|
-
tick_id=tick_id,
|
|
349
288
|
reason=str(exc),
|
|
350
289
|
)
|
|
351
290
|
except Exception as exc: # noqa: BLE001 — author bug must not crash scaffold
|
|
@@ -353,14 +292,12 @@ def run_scaffold(
|
|
|
353
292
|
candidates = []
|
|
354
293
|
tick_error = {
|
|
355
294
|
"type": "error",
|
|
356
|
-
"tick_id": tick_id,
|
|
357
295
|
"error_type": type(exc).__name__,
|
|
358
296
|
"message": str(exc),
|
|
359
297
|
}
|
|
360
298
|
log_event(
|
|
361
299
|
"scaffold_tick_error",
|
|
362
300
|
tick=tick_count,
|
|
363
|
-
tick_id=tick_id,
|
|
364
301
|
error_type=type(exc).__name__,
|
|
365
302
|
error=str(exc),
|
|
366
303
|
)
|
|
@@ -380,23 +317,15 @@ def run_scaffold(
|
|
|
380
317
|
mcp_error_count, mcp_failure = _classify_mcp_errors(mcp_tally, candidates)
|
|
381
318
|
if tick_status == "ok" and mcp_failure is not None:
|
|
382
319
|
tick_status = "mcp_error"
|
|
383
|
-
|
|
384
|
-
# `/errors` names the tick it came from, and the classifier is tick-agnostic.
|
|
385
|
-
tick_error = {**mcp_failure, "tick_id": tick_id}
|
|
320
|
+
tick_error = mcp_failure
|
|
386
321
|
log_event(
|
|
387
322
|
"scaffold_tick_mcp_error",
|
|
388
323
|
tick=tick_count,
|
|
389
|
-
tick_id=tick_id,
|
|
390
324
|
error_type=mcp_failure["error_type"],
|
|
391
325
|
error_count=mcp_error_count,
|
|
392
326
|
)
|
|
393
327
|
|
|
394
|
-
# The quiet streak counts ticks, not words: a tick that produced a
|
|
395
|
-
# candidate ends the streak whatever its status.
|
|
396
|
-
quiet_ticks = 0 if candidates else quiet_ticks + 1
|
|
397
|
-
|
|
398
328
|
# --- Clean tick: commit ctx.state, deliver candidates ---
|
|
399
|
-
persist_error = None
|
|
400
329
|
if tick_status == "ok":
|
|
401
330
|
# Transactional commit. A failed persist RAISES (StateManager
|
|
402
331
|
# enforces it) and does NOT advance the committed snapshot — so
|
|
@@ -412,100 +341,46 @@ def run_scaffold(
|
|
|
412
341
|
log_event(
|
|
413
342
|
"scaffold_state_persist_failed",
|
|
414
343
|
tick=tick_count,
|
|
415
|
-
tick_id=tick_id,
|
|
416
344
|
error_type=type(commit_exc).__name__,
|
|
417
345
|
error=str(commit_exc),
|
|
418
346
|
)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
# advanced) and report to /errors via the sink, when supported.
|
|
433
|
-
ctx.state.rollback()
|
|
434
|
-
committed = False
|
|
435
|
-
commit_outcome = "rolled_back"
|
|
436
|
-
|
|
437
|
-
# Read AFTER the commit/rollback above, so this is always the last
|
|
438
|
-
# COMMITTED row — stale by one tick on a failed tick, which is exactly
|
|
439
|
-
# when the last state the scanner agreed on is worth seeing.
|
|
440
|
-
state_row = ctx.state.last()
|
|
441
|
-
state_projection = project_state_row(state_row)
|
|
442
|
-
state_fields = len(state_row) if isinstance(state_row, dict) else None
|
|
443
|
-
state_changed = (
|
|
444
|
-
None
|
|
445
|
-
if state_fields is None
|
|
446
|
-
else count_changed(state_projection, prev_state_projection)
|
|
447
|
-
)
|
|
448
|
-
prev_state_projection = state_projection if state_fields is not None else None
|
|
449
|
-
|
|
450
|
-
tick_facts = build_tick_facts(
|
|
451
|
-
tick_id=tick_id,
|
|
452
|
-
status=tick_status,
|
|
453
|
-
candidate_count=len(candidates),
|
|
454
|
-
duration_ms=duration_ms,
|
|
455
|
-
commit=commit_outcome,
|
|
456
|
-
quiet_ticks=quiet_ticks,
|
|
457
|
-
lag_ms=lag_ms,
|
|
458
|
-
mcp_snapshot=mcp_tally.snapshot() if mcp_tally is not None else None,
|
|
459
|
-
call_snapshot=(
|
|
460
|
-
mcp_tally.call_snapshot()
|
|
461
|
-
if mcp_tally is not None and hasattr(mcp_tally, "call_snapshot")
|
|
462
|
-
else None
|
|
463
|
-
),
|
|
464
|
-
state_row=state_row,
|
|
465
|
-
state_fields=state_fields,
|
|
466
|
-
state_changed=state_changed,
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
# Exactly one POST carries this tick's facts: /signals on a clean tick,
|
|
470
|
-
# /errors on any tick that failed. Both already happen every tick, so the
|
|
471
|
-
# facts add no request of their own.
|
|
472
|
-
sink_can_post_error = sink is not None and hasattr(sink, "post_error")
|
|
473
|
-
facts_for_error = tick_facts if post_error_takes_tick else None
|
|
474
|
-
if tick_status == "ok":
|
|
475
|
-
if persist_error is not None and sink_can_post_error:
|
|
476
|
-
_post_error_safe(
|
|
477
|
-
sink, persist_error, tick_count, tick_id, facts_for_error
|
|
478
|
-
)
|
|
347
|
+
if sink is not None and hasattr(sink, "post_error"):
|
|
348
|
+
_post_error_safe(
|
|
349
|
+
sink,
|
|
350
|
+
{
|
|
351
|
+
"type": "state_persist_failed",
|
|
352
|
+
"tick": tick_count,
|
|
353
|
+
"message": (
|
|
354
|
+
"scanner state could not be persisted; "
|
|
355
|
+
"tick discarded, state not advanced"
|
|
356
|
+
),
|
|
357
|
+
},
|
|
358
|
+
tick_count,
|
|
359
|
+
)
|
|
479
360
|
|
|
480
361
|
if committed and sink is not None:
|
|
481
362
|
try:
|
|
482
|
-
|
|
483
|
-
if sink_takes_tick_id:
|
|
484
|
-
kwargs["tick_id"] = tick_id
|
|
485
|
-
if sink_takes_tick:
|
|
486
|
-
kwargs["tick"] = tick_facts
|
|
487
|
-
sink(candidates, **kwargs)
|
|
363
|
+
sink(candidates)
|
|
488
364
|
except Exception as sink_exc: # noqa: BLE001
|
|
489
365
|
log_event(
|
|
490
366
|
"scaffold_sink_error",
|
|
491
367
|
tick=tick_count,
|
|
492
|
-
tick_id=tick_id,
|
|
493
368
|
error_type=type(sink_exc).__name__,
|
|
494
369
|
error=str(sink_exc),
|
|
495
370
|
)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
371
|
+
else:
|
|
372
|
+
# Non-ok tick (error / timeout / malformed): roll back the
|
|
373
|
+
# working buffer (discard the tick's mutations — state not
|
|
374
|
+
# advanced) and report to /errors via the sink, when supported.
|
|
375
|
+
ctx.state.rollback()
|
|
376
|
+
if sink is not None and hasattr(sink, "post_error"):
|
|
377
|
+
_post_error_safe(
|
|
378
|
+
sink, tick_error or {"type": tick_status}, tick_count
|
|
379
|
+
)
|
|
504
380
|
|
|
505
381
|
log_event(
|
|
506
382
|
"scaffold_tick_finished",
|
|
507
383
|
tick=tick_count,
|
|
508
|
-
tick_id=tick_id,
|
|
509
384
|
status=tick_status,
|
|
510
385
|
duration_ms=duration_ms,
|
|
511
386
|
candidate_count=len(candidates),
|
|
@@ -527,7 +402,6 @@ def run_scaffold(
|
|
|
527
402
|
log_event(
|
|
528
403
|
"scaffold_tick_timeout_late",
|
|
529
404
|
tick=tick_count,
|
|
530
|
-
tick_id=tick_id,
|
|
531
405
|
reason=str(late_exc),
|
|
532
406
|
)
|
|
533
407
|
|
|
@@ -580,29 +454,14 @@ def _classify_mcp_errors(mcp_tally: Any, candidates: list) -> tuple[int, Optiona
|
|
|
580
454
|
}
|
|
581
455
|
|
|
582
456
|
|
|
583
|
-
def _post_error_safe(
|
|
584
|
-
sink
|
|
585
|
-
error: dict,
|
|
586
|
-
tick_count: int,
|
|
587
|
-
tick_id: str,
|
|
588
|
-
tick: Optional[dict] = None,
|
|
589
|
-
) -> None:
|
|
590
|
-
"""Call sink.post_error(error), swallowing/logging any failure.
|
|
591
|
-
|
|
592
|
-
`tick` is passed only when the sink's signature accepts it — the caller checks
|
|
593
|
-
once at launch, so a sink that predates the per-tick facts keeps its one-argument
|
|
594
|
-
call rather than being handed a keyword it would raise on.
|
|
595
|
-
"""
|
|
457
|
+
def _post_error_safe(sink: Any, error: dict, tick_count: int) -> None:
|
|
458
|
+
"""Call sink.post_error(error), swallowing/logging any failure."""
|
|
596
459
|
try:
|
|
597
|
-
|
|
598
|
-
sink.post_error(error)
|
|
599
|
-
else:
|
|
600
|
-
sink.post_error(error, tick=tick)
|
|
460
|
+
sink.post_error(error)
|
|
601
461
|
except Exception as perr: # noqa: BLE001
|
|
602
462
|
log_event(
|
|
603
463
|
"scaffold_post_error_failed",
|
|
604
464
|
tick=tick_count,
|
|
605
|
-
tick_id=tick_id,
|
|
606
465
|
error_type=type(perr).__name__,
|
|
607
466
|
error=str(perr),
|
|
608
467
|
)
|