@senpi/runtime 3.0.93 → 3.0.94
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.
|
@@ -60,6 +60,11 @@ def build_delivery_sink(
|
|
|
60
60
|
|
|
61
61
|
journal = Journal(journal_path)
|
|
62
62
|
replayed = [False] # mutable cell; replay runs once on first invocation
|
|
63
|
+
# Mutable cell; the id of the tick currently inside _sink, so the reject/warn
|
|
64
|
+
# callbacks — which are built once, here — can name it. _sink sets it on entry,
|
|
65
|
+
# before anything can read it, and both callbacks run only from inside _sink.
|
|
66
|
+
# None when the caller passed no id, or one the wire would refuse.
|
|
67
|
+
current_tick_id: list[str | None] = [None]
|
|
63
68
|
# Distinct dropped-field shapes already warned about (rate-limit: a scanner that
|
|
64
69
|
# sets a stray key every tick warns ONCE per shape, not every tick).
|
|
65
70
|
warned_dropped_shapes: set[frozenset] = set()
|
|
@@ -68,6 +73,22 @@ def build_delivery_sink(
|
|
|
68
73
|
# Internal helpers
|
|
69
74
|
# ------------------------------------------------------------------
|
|
70
75
|
|
|
76
|
+
def _tick_field(tick_id: Any) -> dict:
|
|
77
|
+
"""`tick_id=...` as log kwargs, or nothing at all when no tick is nameable.
|
|
78
|
+
|
|
79
|
+
An absent key is the honest answer; a null or a blank would join to nothing
|
|
80
|
+
while looking like an id.
|
|
81
|
+
"""
|
|
82
|
+
return {"tick_id": tick_id} if isinstance(tick_id, str) and tick_id else {}
|
|
83
|
+
|
|
84
|
+
def _signal_tick_id(signal: Any) -> Any:
|
|
85
|
+
"""The producing tick's id off a stored signal, or None.
|
|
86
|
+
|
|
87
|
+
A journal line is arbitrary text on disk: `signal` may be a null, a string or
|
|
88
|
+
a list, so this never assumes a mapping.
|
|
89
|
+
"""
|
|
90
|
+
return signal.get("tick_id") if isinstance(signal, dict) else None
|
|
91
|
+
|
|
71
92
|
def _now_ms() -> int:
|
|
72
93
|
return int(clock() * 1000)
|
|
73
94
|
|
|
@@ -90,7 +111,14 @@ def build_delivery_sink(
|
|
|
90
111
|
to_drop.append(entry)
|
|
91
112
|
for entry in to_drop:
|
|
92
113
|
journal.record_outcome(entry.id, "dropped_expired")
|
|
93
|
-
|
|
114
|
+
# The producing tick's id, read off the journalled signal — the tick that
|
|
115
|
+
# noticed the expiry did not cause it.
|
|
116
|
+
log_event(
|
|
117
|
+
"delivery_dropped_expired",
|
|
118
|
+
entry_id=entry.id,
|
|
119
|
+
asset=entry.asset,
|
|
120
|
+
**_tick_field(_signal_tick_id(entry.signal)),
|
|
121
|
+
)
|
|
94
122
|
return to_send
|
|
95
123
|
|
|
96
124
|
def _usable_tick_id(tick_id: Any) -> str | None:
|
|
@@ -102,12 +130,12 @@ def build_delivery_sink(
|
|
|
102
130
|
"""
|
|
103
131
|
if tick_id is None or (isinstance(tick_id, str) and tick_id):
|
|
104
132
|
return tick_id
|
|
105
|
-
log_event("delivery_tick_id_unusable", scanner_id=scanner_id,
|
|
133
|
+
log_event("delivery_tick_id_unusable", scanner_id=scanner_id, rejected_tick_id=repr(tick_id)[:80])
|
|
106
134
|
return None
|
|
107
135
|
|
|
108
136
|
def _on_reject(reason: dict) -> None:
|
|
109
137
|
"""Loud rejection: log AND POST to /errors (design §4 — never a silent drop)."""
|
|
110
|
-
log_event("delivery_candidate_invalid", **reason)
|
|
138
|
+
log_event("delivery_candidate_invalid", **_tick_field(current_tick_id[0]), **reason)
|
|
111
139
|
_post_error({"type": "candidate_rejected", **reason})
|
|
112
140
|
|
|
113
141
|
def _on_warn(info: dict) -> None:
|
|
@@ -123,6 +151,7 @@ def build_delivery_sink(
|
|
|
123
151
|
scanner_id=scanner_id,
|
|
124
152
|
dropped=info.get("dropped"),
|
|
125
153
|
asset=info.get("asset"),
|
|
154
|
+
**_tick_field(current_tick_id[0]),
|
|
126
155
|
)
|
|
127
156
|
|
|
128
157
|
def _wrap_candidates(candidates, produced_at_ms: int, tick_id: str | None) -> list[tuple]:
|
|
@@ -171,7 +200,7 @@ def build_delivery_sink(
|
|
|
171
200
|
url = intake_url.rstrip("/") + SIGNALS_PATH
|
|
172
201
|
return post_json(url, body, timeout)
|
|
173
202
|
|
|
174
|
-
def _interpret_response(status: int, parsed: Any, pending_eids: list[str],
|
|
203
|
+
def _interpret_response(status: int, parsed: Any, pending_eids: list[str], pending_signals: list) -> bool:
|
|
175
204
|
"""Interpret POST response; journal outcomes. Returns True if successful envelope."""
|
|
176
205
|
if status != 200 or parsed is None:
|
|
177
206
|
if isinstance(parsed, dict):
|
|
@@ -211,6 +240,10 @@ def build_delivery_sink(
|
|
|
211
240
|
# signals), and unknown/missing signal_id (ignored safely, never applied by
|
|
212
241
|
# position). An unmatched entry stays pending and is retried.
|
|
213
242
|
pending = set(pending_eids)
|
|
243
|
+
# eid -> the tick that produced it, for the per-entry lines below.
|
|
244
|
+
produced_by = {
|
|
245
|
+
eid: _signal_tick_id(sig) for eid, sig in zip(pending_eids, pending_signals)
|
|
246
|
+
}
|
|
214
247
|
for sig_status in per_signal:
|
|
215
248
|
if not isinstance(sig_status, dict):
|
|
216
249
|
continue
|
|
@@ -228,7 +261,12 @@ def build_delivery_sink(
|
|
|
228
261
|
journal.record_outcome(eid, st)
|
|
229
262
|
else:
|
|
230
263
|
# Unknown status — treat as undelivered (no outcome written)
|
|
231
|
-
log_event(
|
|
264
|
+
log_event(
|
|
265
|
+
"delivery_unknown_status",
|
|
266
|
+
entry_id=eid,
|
|
267
|
+
status=st,
|
|
268
|
+
**_tick_field(produced_by.get(eid)),
|
|
269
|
+
)
|
|
232
270
|
# Entries with no matching status returned stay pending (retry next tick).
|
|
233
271
|
|
|
234
272
|
return True
|
|
@@ -239,6 +277,7 @@ def build_delivery_sink(
|
|
|
239
277
|
|
|
240
278
|
def _sink(candidates, tick_id=None) -> None:
|
|
241
279
|
tick_id = _usable_tick_id(tick_id)
|
|
280
|
+
current_tick_id[0] = tick_id
|
|
242
281
|
|
|
243
282
|
# Replay journal on very first invocation
|
|
244
283
|
if not replayed[0]:
|
|
@@ -284,18 +323,21 @@ def build_delivery_sink(
|
|
|
284
323
|
# 4. Interpret response
|
|
285
324
|
# The response per-signal list corresponds only to all_signals that have
|
|
286
325
|
# produced entries (all_eids). Empty no-ops: no eids.
|
|
287
|
-
_interpret_response(status, parsed, all_eids,
|
|
326
|
+
_interpret_response(status, parsed, all_eids, all_signals)
|
|
288
327
|
|
|
289
328
|
def _post_error(error: dict) -> None:
|
|
290
329
|
"""POST an error dict. Fire-and-forget."""
|
|
291
330
|
body = {"scanner_id": scanner_id, "error": error}
|
|
292
331
|
url = intake_url.rstrip("/") + ERRORS_PATH
|
|
332
|
+
# The error belongs to exactly one tick and already names it; a body that
|
|
333
|
+
# names none leaves the key off rather than borrowing the current one.
|
|
334
|
+
origin = _tick_field(error.get("tick_id")) if isinstance(error, dict) else {}
|
|
293
335
|
try:
|
|
294
336
|
post_json(url, body, timeout)
|
|
295
337
|
except TransportError as exc:
|
|
296
|
-
log_event("delivery_error_post_failed", error=str(exc))
|
|
338
|
+
log_event("delivery_error_post_failed", error=str(exc), **origin)
|
|
297
339
|
except Exception as exc: # noqa: BLE001
|
|
298
|
-
log_event("delivery_error_post_failed", error=str(exc))
|
|
340
|
+
log_event("delivery_error_post_failed", error=str(exc), **origin)
|
|
299
341
|
|
|
300
342
|
_sink.post_error = _post_error # type: ignore[attr-defined]
|
|
301
343
|
return _sink
|
|
@@ -261,6 +261,7 @@ def run_scaffold(
|
|
|
261
261
|
log_event(
|
|
262
262
|
"scaffold_tick_malformed",
|
|
263
263
|
tick=tick_count,
|
|
264
|
+
tick_id=tick_id,
|
|
264
265
|
reason=malformed_reason,
|
|
265
266
|
)
|
|
266
267
|
else:
|
|
@@ -273,6 +274,7 @@ def run_scaffold(
|
|
|
273
274
|
log_event(
|
|
274
275
|
"scaffold_tick_timeout",
|
|
275
276
|
tick=tick_count,
|
|
277
|
+
tick_id=tick_id,
|
|
276
278
|
reason=str(exc),
|
|
277
279
|
)
|
|
278
280
|
except Exception as exc: # noqa: BLE001 — author bug must not crash scaffold
|
|
@@ -287,6 +289,7 @@ def run_scaffold(
|
|
|
287
289
|
log_event(
|
|
288
290
|
"scaffold_tick_error",
|
|
289
291
|
tick=tick_count,
|
|
292
|
+
tick_id=tick_id,
|
|
290
293
|
error_type=type(exc).__name__,
|
|
291
294
|
error=str(exc),
|
|
292
295
|
)
|
|
@@ -312,6 +315,7 @@ def run_scaffold(
|
|
|
312
315
|
log_event(
|
|
313
316
|
"scaffold_state_persist_failed",
|
|
314
317
|
tick=tick_count,
|
|
318
|
+
tick_id=tick_id,
|
|
315
319
|
error_type=type(commit_exc).__name__,
|
|
316
320
|
error=str(commit_exc),
|
|
317
321
|
)
|
|
@@ -328,6 +332,7 @@ def run_scaffold(
|
|
|
328
332
|
),
|
|
329
333
|
},
|
|
330
334
|
tick_count,
|
|
335
|
+
tick_id,
|
|
331
336
|
)
|
|
332
337
|
|
|
333
338
|
if committed and sink is not None:
|
|
@@ -340,6 +345,7 @@ def run_scaffold(
|
|
|
340
345
|
log_event(
|
|
341
346
|
"scaffold_sink_error",
|
|
342
347
|
tick=tick_count,
|
|
348
|
+
tick_id=tick_id,
|
|
343
349
|
error_type=type(sink_exc).__name__,
|
|
344
350
|
error=str(sink_exc),
|
|
345
351
|
)
|
|
@@ -353,11 +359,13 @@ def run_scaffold(
|
|
|
353
359
|
sink,
|
|
354
360
|
tick_error or {"type": tick_status, "tick_id": tick_id},
|
|
355
361
|
tick_count,
|
|
362
|
+
tick_id,
|
|
356
363
|
)
|
|
357
364
|
|
|
358
365
|
log_event(
|
|
359
366
|
"scaffold_tick_finished",
|
|
360
367
|
tick=tick_count,
|
|
368
|
+
tick_id=tick_id,
|
|
361
369
|
status=tick_status,
|
|
362
370
|
duration_ms=duration_ms,
|
|
363
371
|
candidate_count=len(candidates),
|
|
@@ -376,6 +384,7 @@ def run_scaffold(
|
|
|
376
384
|
log_event(
|
|
377
385
|
"scaffold_tick_timeout_late",
|
|
378
386
|
tick=tick_count,
|
|
387
|
+
tick_id=tick_id,
|
|
379
388
|
reason=str(late_exc),
|
|
380
389
|
)
|
|
381
390
|
|
|
@@ -401,7 +410,7 @@ def run_scaffold(
|
|
|
401
410
|
return tick_count
|
|
402
411
|
|
|
403
412
|
|
|
404
|
-
def _post_error_safe(sink: Any, error: dict, tick_count: int) -> None:
|
|
413
|
+
def _post_error_safe(sink: Any, error: dict, tick_count: int, tick_id: str) -> None:
|
|
405
414
|
"""Call sink.post_error(error), swallowing/logging any failure."""
|
|
406
415
|
try:
|
|
407
416
|
sink.post_error(error)
|
|
@@ -409,6 +418,7 @@ def _post_error_safe(sink: Any, error: dict, tick_count: int) -> None:
|
|
|
409
418
|
log_event(
|
|
410
419
|
"scaffold_post_error_failed",
|
|
411
420
|
tick=tick_count,
|
|
421
|
+
tick_id=tick_id,
|
|
412
422
|
error_type=type(perr).__name__,
|
|
413
423
|
error=str(perr),
|
|
414
424
|
)
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "runtime",
|
|
3
3
|
"name": "Senpi Runtime",
|
|
4
4
|
"description": "Senpi runtime plugin for OpenClaw. Orchestrates scanners, LLM-based trade decisions, and a DSL trailing stop-loss exit engine.",
|
|
5
|
-
"version": "3.0.
|
|
5
|
+
"version": "3.0.94",
|
|
6
6
|
"commandAliases": [
|
|
7
7
|
{
|
|
8
8
|
"name": "senpi"
|