@senpi/runtime 3.0.127 → 3.0.129

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.
Files changed (267) hide show
  1. package/README.md +4 -12
  2. package/dist/actions/action-events.d.ts +7 -21
  3. package/dist/actions/action-events.d.ts.map +1 -1
  4. package/dist/actions/action-events.js +26 -54
  5. package/dist/actions/action-events.js.map +1 -1
  6. package/dist/actions/action-runtime-module.d.ts.map +1 -1
  7. package/dist/actions/action-runtime-module.js +7 -38
  8. package/dist/actions/action-runtime-module.js.map +1 -1
  9. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  10. package/dist/actions/close-position/close-position.action.js +15 -122
  11. package/dist/actions/close-position/close-position.action.js.map +1 -1
  12. package/dist/actions/decision-engine.d.ts.map +1 -1
  13. package/dist/actions/decision-engine.js +2 -15
  14. package/dist/actions/decision-engine.js.map +1 -1
  15. package/dist/actions/event-derivation.d.ts +2 -71
  16. package/dist/actions/event-derivation.d.ts.map +1 -1
  17. package/dist/actions/event-derivation.js +8 -251
  18. package/dist/actions/event-derivation.js.map +1 -1
  19. package/dist/actions/open-position/open-position.action.d.ts +2 -17
  20. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  21. package/dist/actions/open-position/open-position.action.js +47 -162
  22. package/dist/actions/open-position/open-position.action.js.map +1 -1
  23. package/dist/actions/position-tracker/position-tracker.action.d.ts +1 -2
  24. package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
  25. package/dist/actions/position-tracker/position-tracker.action.js +17 -62
  26. package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
  27. package/dist/actions/store/types.d.ts +2 -9
  28. package/dist/actions/store/types.d.ts.map +1 -1
  29. package/dist/actions/store/types.js.map +1 -1
  30. package/dist/actions/types.d.ts +0 -13
  31. package/dist/actions/types.d.ts.map +1 -1
  32. package/dist/actions/types.js +0 -13
  33. package/dist/actions/types.js.map +1 -1
  34. package/dist/cli/guide-commands.d.ts.map +1 -1
  35. package/dist/cli/guide-commands.js +6 -7
  36. package/dist/cli/guide-commands.js.map +1 -1
  37. package/dist/cli/senpi-commands.d.ts.map +1 -1
  38. package/dist/cli/senpi-commands.js +11 -4
  39. package/dist/cli/senpi-commands.js.map +1 -1
  40. package/dist/dsl/engine-client/event-mapping.d.ts.map +1 -1
  41. package/dist/dsl/engine-client/event-mapping.js +0 -14
  42. package/dist/dsl/engine-client/event-mapping.js.map +1 -1
  43. package/dist/dsl/events/build-closed-event.d.ts +1 -16
  44. package/dist/dsl/events/build-closed-event.d.ts.map +1 -1
  45. package/dist/dsl/events/build-closed-event.js +0 -6
  46. package/dist/dsl/events/build-closed-event.js.map +1 -1
  47. package/dist/dsl/events/handlers.d.ts +0 -7
  48. package/dist/dsl/events/handlers.d.ts.map +1 -1
  49. package/dist/dsl/events/handlers.js +7 -37
  50. package/dist/dsl/events/handlers.js.map +1 -1
  51. package/dist/dsl/monitor/create-monitor.d.ts +2 -6
  52. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  53. package/dist/dsl/monitor/create-monitor.js +9 -15
  54. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  55. package/dist/dsl/monitor/dsl-failure-events.d.ts +0 -2
  56. package/dist/dsl/monitor/dsl-failure-events.d.ts.map +1 -1
  57. package/dist/dsl/monitor/dsl-failure-events.js +1 -2
  58. package/dist/dsl/monitor/dsl-failure-events.js.map +1 -1
  59. package/dist/dsl/monitor/process-one-position.d.ts +0 -1
  60. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  61. package/dist/dsl/monitor/process-one-position.js +25 -59
  62. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  63. package/dist/dsl/plugin/index.d.ts +2 -8
  64. package/dist/dsl/plugin/index.d.ts.map +1 -1
  65. package/dist/dsl/plugin/index.js +6 -8
  66. package/dist/dsl/plugin/index.js.map +1 -1
  67. package/dist/health/types.d.ts +0 -13
  68. package/dist/health/types.d.ts.map +1 -1
  69. package/dist/health/types.js +0 -12
  70. package/dist/health/types.js.map +1 -1
  71. package/dist/index.d.ts +4 -27
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +42 -254
  74. package/dist/index.js.map +1 -1
  75. package/dist/runtime/action-registry.d.ts.map +1 -1
  76. package/dist/runtime/action-registry.js +3 -4
  77. package/dist/runtime/action-registry.js.map +1 -1
  78. package/dist/runtime/auto-update/coordinator.d.ts +6 -13
  79. package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
  80. package/dist/runtime/auto-update/coordinator.js +18 -86
  81. package/dist/runtime/auto-update/coordinator.js.map +1 -1
  82. package/dist/runtime/auto-update/run-update-command.d.ts.map +1 -1
  83. package/dist/runtime/auto-update/run-update-command.js +3 -4
  84. package/dist/runtime/auto-update/run-update-command.js.map +1 -1
  85. package/dist/runtime/auto-update/types.d.ts +5 -11
  86. package/dist/runtime/auto-update/types.d.ts.map +1 -1
  87. package/dist/runtime/auto-update/types.js +0 -11
  88. package/dist/runtime/auto-update/types.js.map +1 -1
  89. package/dist/runtime/domain-events.d.ts.map +1 -1
  90. package/dist/runtime/domain-events.js +0 -7
  91. package/dist/runtime/domain-events.js.map +1 -1
  92. package/dist/runtime/lifecycle-events.d.ts +1 -9
  93. package/dist/runtime/lifecycle-events.d.ts.map +1 -1
  94. package/dist/runtime/lifecycle-events.js +2 -11
  95. package/dist/runtime/lifecycle-events.js.map +1 -1
  96. package/dist/runtime/reconcile-run.d.ts +3 -13
  97. package/dist/runtime/reconcile-run.d.ts.map +1 -1
  98. package/dist/runtime/reconcile-run.js +3 -4
  99. package/dist/runtime/reconcile-run.js.map +1 -1
  100. package/dist/runtime/reconcile-state.d.ts +0 -8
  101. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  102. package/dist/runtime/reconcile-state.js +1 -4
  103. package/dist/runtime/reconcile-state.js.map +1 -1
  104. package/dist/runtime/run.d.ts +0 -1
  105. package/dist/runtime/run.d.ts.map +1 -1
  106. package/dist/runtime/run.js.map +1 -1
  107. package/dist/runtime/runtime-schema.js +3 -3
  108. package/dist/runtime/runtime-schema.js.map +1 -1
  109. package/dist/runtime/runtime.d.ts +0 -1
  110. package/dist/runtime/runtime.d.ts.map +1 -1
  111. package/dist/runtime/runtime.js +3 -27
  112. package/dist/runtime/runtime.js.map +1 -1
  113. package/dist/runtime/supervisor/long-running-supervisor.d.ts +0 -13
  114. package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
  115. package/dist/runtime/supervisor/long-running-supervisor.js +0 -1
  116. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  117. package/dist/runtime/validate-dsl-position-tracking.d.ts.map +1 -1
  118. package/dist/runtime/validate-dsl-position-tracking.js +1 -2
  119. package/dist/runtime/validate-dsl-position-tracking.js.map +1 -1
  120. package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
  121. package/dist/runtime-api/handlers/ingest-signals.js +0 -1
  122. package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
  123. package/dist/runtime-api/handlers/types.d.ts +0 -5
  124. package/dist/runtime-api/handlers/types.d.ts.map +1 -1
  125. package/dist/runtime-api/intake/intake-events.d.ts +1 -1
  126. package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
  127. package/dist/runtime-api/intake/intake-events.js +3 -9
  128. package/dist/runtime-api/intake/intake-events.js.map +1 -1
  129. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  130. package/dist/runtime-api/intake/intake-mount.js +19 -4
  131. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  132. package/dist/runtime-api/intake/process-error-post.d.ts +1 -6
  133. package/dist/runtime-api/intake/process-error-post.d.ts.map +1 -1
  134. package/dist/runtime-api/intake/process-error-post.js +1 -5
  135. package/dist/runtime-api/intake/process-error-post.js.map +1 -1
  136. package/dist/runtime-api/intake/process-signal-post.d.ts +1 -7
  137. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  138. package/dist/runtime-api/intake/process-signal-post.js +0 -5
  139. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  140. package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
  141. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  142. package/dist/runtime-api/intake/scanner-launcher.js +0 -15
  143. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  144. package/dist/runtime-api/intake/types.d.ts +0 -55
  145. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  146. package/dist/runtime-api/routes/signals.d.ts +0 -2
  147. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  148. package/dist/runtime-api/routes/signals.js +1 -5
  149. package/dist/runtime-api/routes/signals.js.map +1 -1
  150. package/dist/runtime-api/routes/signals.schema.d.ts +0 -43
  151. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  152. package/dist/runtime-api/routes/signals.schema.js +0 -43
  153. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  154. package/dist/runtime-api/server.d.ts +1 -7
  155. package/dist/runtime-api/server.d.ts.map +1 -1
  156. package/dist/runtime-api/server.js +0 -1
  157. package/dist/runtime-api/server.js.map +1 -1
  158. package/dist/scanners/engine/engine.d.ts +1 -22
  159. package/dist/scanners/engine/engine.d.ts.map +1 -1
  160. package/dist/scanners/engine/engine.js +11 -78
  161. package/dist/scanners/engine/engine.js.map +1 -1
  162. package/dist/scanners/engine/lifecycle.d.ts +1 -7
  163. package/dist/scanners/engine/lifecycle.d.ts.map +1 -1
  164. package/dist/scanners/events.d.ts +0 -4
  165. package/dist/scanners/events.d.ts.map +1 -1
  166. package/dist/scanners/events.js +0 -2
  167. package/dist/scanners/events.js.map +1 -1
  168. package/dist/scanners/external/scaffold/delivery.py +25 -194
  169. package/dist/scanners/external/scaffold/scaffold.py +31 -172
  170. package/dist/scanners/external/scaffold/senpi_mcp.py +8 -150
  171. package/dist/scanners/external-scanner-receiver.d.ts +0 -17
  172. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  173. package/dist/scanners/external-scanner-receiver.js +0 -8
  174. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  175. package/dist/scanners/runtime-module.d.ts +0 -1
  176. package/dist/scanners/runtime-module.d.ts.map +1 -1
  177. package/dist/scanners/runtime-module.js +9 -66
  178. package/dist/scanners/runtime-module.js.map +1 -1
  179. package/dist/telemetry/attributes.d.ts +0 -20
  180. package/dist/telemetry/attributes.d.ts.map +1 -1
  181. package/dist/telemetry/attributes.js +0 -24
  182. package/dist/telemetry/attributes.js.map +1 -1
  183. package/dist/telemetry/event-builders.d.ts +6 -81
  184. package/dist/telemetry/event-builders.d.ts.map +1 -1
  185. package/dist/telemetry/event-builders.js +17 -141
  186. package/dist/telemetry/event-builders.js.map +1 -1
  187. package/dist/telemetry/event-store.d.ts +0 -6
  188. package/dist/telemetry/event-store.d.ts.map +1 -1
  189. package/dist/telemetry/event-store.js +8 -12
  190. package/dist/telemetry/event-store.js.map +1 -1
  191. package/dist/telemetry/metrics.d.ts +2 -14
  192. package/dist/telemetry/metrics.d.ts.map +1 -1
  193. package/dist/telemetry/metrics.js +4 -19
  194. package/dist/telemetry/metrics.js.map +1 -1
  195. package/dist/telemetry/spans/redact-pii.d.ts.map +1 -1
  196. package/dist/telemetry/spans/redact-pii.js +2 -18
  197. package/dist/telemetry/spans/redact-pii.js.map +1 -1
  198. package/dist/telemetry/tracing.d.ts +0 -9
  199. package/dist/telemetry/tracing.d.ts.map +1 -1
  200. package/dist/telemetry/tracing.js +0 -36
  201. package/dist/telemetry/tracing.js.map +1 -1
  202. package/dist/types/dsl/index.d.ts +0 -11
  203. package/dist/types/dsl/index.d.ts.map +1 -1
  204. package/dist/types/scanner.d.ts +0 -15
  205. package/dist/types/scanner.d.ts.map +1 -1
  206. package/dist/utils/event-catalog.d.ts +0 -166
  207. package/dist/utils/event-catalog.d.ts.map +1 -1
  208. package/dist/utils/event-catalog.js +0 -18
  209. package/dist/utils/event-catalog.js.map +1 -1
  210. package/dist/utils/logger.d.ts +1 -4
  211. package/dist/utils/logger.d.ts.map +1 -1
  212. package/dist/utils/logger.js.map +1 -1
  213. package/dist/validate/recipe-checks.d.ts.map +1 -1
  214. package/dist/validate/recipe-checks.js +2 -2
  215. package/dist/validate/recipe-checks.js.map +1 -1
  216. package/openclaw.plugin.json +4 -8
  217. package/package.json +1 -1
  218. package/dist/actions/skipped-signal-results.d.ts +0 -25
  219. package/dist/actions/skipped-signal-results.d.ts.map +0 -1
  220. package/dist/actions/skipped-signal-results.js +0 -71
  221. package/dist/actions/skipped-signal-results.js.map +0 -1
  222. package/dist/runtime-api/intake/ingest-request.d.ts +0 -17
  223. package/dist/runtime-api/intake/ingest-request.d.ts.map +0 -1
  224. package/dist/runtime-api/intake/ingest-request.js +0 -34
  225. package/dist/runtime-api/intake/ingest-request.js.map +0 -1
  226. package/dist/runtime-api/intake/scaffold-tick-router.d.ts +0 -39
  227. package/dist/runtime-api/intake/scaffold-tick-router.d.ts.map +0 -1
  228. package/dist/runtime-api/intake/scaffold-tick-router.js +0 -122
  229. package/dist/runtime-api/intake/scaffold-tick-router.js.map +0 -1
  230. package/dist/scanners/external/scaffold/tick_facts.py +0 -130
  231. package/dist/scanners/external/scaffold/ulid.py +0 -58
  232. package/dist/telemetry/live-telemetry.d.ts +0 -238
  233. package/dist/telemetry/live-telemetry.d.ts.map +0 -1
  234. package/dist/telemetry/live-telemetry.js +0 -853
  235. package/dist/telemetry/live-telemetry.js.map +0 -1
  236. package/dist/telemetry/scanner-stats.d.ts +0 -143
  237. package/dist/telemetry/scanner-stats.d.ts.map +0 -1
  238. package/dist/telemetry/scanner-stats.js +0 -426
  239. package/dist/telemetry/scanner-stats.js.map +0 -1
  240. package/dist/telemetry/scanner-tick-feed.d.ts +0 -87
  241. package/dist/telemetry/scanner-tick-feed.d.ts.map +0 -1
  242. package/dist/telemetry/scanner-tick-feed.js +0 -48
  243. package/dist/telemetry/scanner-tick-feed.js.map +0 -1
  244. package/dist/telemetry/strategy-topology.d.ts +0 -99
  245. package/dist/telemetry/strategy-topology.d.ts.map +0 -1
  246. package/dist/telemetry/strategy-topology.js +0 -93
  247. package/dist/telemetry/strategy-topology.js.map +0 -1
  248. package/dist/telemetry/telemetry-publisher.d.ts +0 -63
  249. package/dist/telemetry/telemetry-publisher.d.ts.map +0 -1
  250. package/dist/telemetry/telemetry-publisher.js +0 -229
  251. package/dist/telemetry/telemetry-publisher.js.map +0 -1
  252. package/dist/telemetry/tick-frame.d.ts +0 -212
  253. package/dist/telemetry/tick-frame.d.ts.map +0 -1
  254. package/dist/telemetry/tick-frame.js +0 -421
  255. package/dist/telemetry/tick-frame.js.map +0 -1
  256. package/dist/telemetry/timeline-buffer.d.ts +0 -95
  257. package/dist/telemetry/timeline-buffer.d.ts.map +0 -1
  258. package/dist/telemetry/timeline-buffer.js +0 -237
  259. package/dist/telemetry/timeline-buffer.js.map +0 -1
  260. package/dist/utils/telemetry-required-log.d.ts +0 -60
  261. package/dist/utils/telemetry-required-log.d.ts.map +0 -1
  262. package/dist/utils/telemetry-required-log.js +0 -105
  263. package/dist/utils/telemetry-required-log.js.map +0 -1
  264. package/dist/utils/ulid.d.ts +0 -9
  265. package/dist/utils/ulid.d.ts.map +0 -1
  266. package/dist/utils/ulid.js +0 -43
  267. 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, tick_id=None, tick=None)
15
- `tick` is the scaffold's per-tick observability facts; the sink stamps the journal's
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), stamping the
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, tick: dict | None = None) -> None
28
- POSTs {scanner_id, error} to <intake_url>/errors. Fire-and-forget. A failed tick's
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
- # The producing tick's id, read off the journalled signal — the tick that
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
- # Merged into one dict before expanding: a reject record that happens to carry
159
- # its own `tick_id` would otherwise be a duplicate kwarg — a TypeError raised
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, tick_id: str | None) -> list[tuple]:
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 _is_shape_reject(status: int, parsed: Any) -> bool:
215
- """True when the intake refused the whole POST because it did not know the shape."""
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], pending_signals: list) -> bool:
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
- if produced_by is None:
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, tick_id=None, tick=None) -> None:
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, tick_id)
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==[]) — which is what makes this the channel
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, tick_body)
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. The signals are the ones the
415
- # journal holds — with their tick ids — so a log line still names the producing tick
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
- Fire-and-forget, with one exception: an intake that refuses the body because it
423
- does not know `tick` is retried once without it and latched, so a failed tick's
424
- error record is never lost to a field that only telemetry wanted.
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
- sent_tick_facts = tick_body is not None and not intake_predates_tick_facts[0]
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), **origin)
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), **origin)
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. Each tick mints one id, handed to the sink with the
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, tick_id=None) invoked after each clean tick. A sink
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", "tick_id": tick_id, "message": str(exc)}
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
- # Stamped here, not in the classifier: every error record posted to
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
- persist_error = {
420
- "type": "state_persist_failed",
421
- "tick": tick_count,
422
- "tick_id": tick_id,
423
- "message": (
424
- "scanner state could not be persisted; "
425
- "tick discarded, state not advanced"
426
- ),
427
- }
428
- commit_outcome = "committed" if committed else "persist_failed"
429
- else:
430
- # Non-ok tick (error / timeout / malformed): roll back the
431
- # working buffer (discard the tick's mutations — state not
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
- kwargs = {}
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
- elif sink_can_post_error:
497
- _post_error_safe(
498
- sink,
499
- tick_error or {"type": tick_status, "tick_id": tick_id},
500
- tick_count,
501
- tick_id,
502
- facts_for_error,
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: Any,
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
- if tick is None:
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
  )