@senpi/runtime 3.0.22 → 3.0.24

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 (137) hide show
  1. package/README.md +7 -4
  2. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  3. package/dist/actions/close-position/close-position.action.js +8 -6
  4. package/dist/actions/close-position/close-position.action.js.map +1 -1
  5. package/dist/actions/decision-error-notification.d.ts +14 -0
  6. package/dist/actions/decision-error-notification.d.ts.map +1 -0
  7. package/dist/actions/decision-error-notification.js +36 -0
  8. package/dist/actions/decision-error-notification.js.map +1 -0
  9. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  10. package/dist/actions/open-position/open-position.action.js +8 -6
  11. package/dist/actions/open-position/open-position.action.js.map +1 -1
  12. package/dist/actions/types.d.ts +2 -0
  13. package/dist/actions/types.d.ts.map +1 -1
  14. package/dist/cli/guide-commands.d.ts.map +1 -1
  15. package/dist/cli/guide-commands.js +5 -7
  16. package/dist/cli/guide-commands.js.map +1 -1
  17. package/dist/cli/senpi-cli-config.d.ts +0 -17
  18. package/dist/cli/senpi-cli-config.d.ts.map +1 -1
  19. package/dist/cli/senpi-cli-config.js +0 -30
  20. package/dist/cli/senpi-cli-config.js.map +1 -1
  21. package/dist/cli/senpi-config-commands.d.ts +1 -1
  22. package/dist/cli/senpi-config-commands.d.ts.map +1 -1
  23. package/dist/cli/senpi-config-commands.js +1 -16
  24. package/dist/cli/senpi-config-commands.js.map +1 -1
  25. package/dist/constants/dsl/index.d.ts +1 -5
  26. package/dist/constants/dsl/index.d.ts.map +1 -1
  27. package/dist/constants/dsl/index.js +1 -5
  28. package/dist/constants/dsl/index.js.map +1 -1
  29. package/dist/dsl/engine-client/event-mapping.d.ts +1 -1
  30. package/dist/dsl/engine-client/event-mapping.d.ts.map +1 -1
  31. package/dist/dsl/events/build-closed-event.d.ts +1 -1
  32. package/dist/dsl/events/build-closed-event.js +1 -1
  33. package/dist/dsl/events/handlers.js +1 -1
  34. package/dist/dsl/monitor/process-one-position.js +3 -3
  35. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  36. package/dist/dsl/notifications/format-utils.d.ts +1 -11
  37. package/dist/dsl/notifications/format-utils.d.ts.map +1 -1
  38. package/dist/dsl/notifications/format-utils.js +1 -48
  39. package/dist/dsl/notifications/format-utils.js.map +1 -1
  40. package/dist/dsl/notifications/formatter.d.ts +9 -25
  41. package/dist/dsl/notifications/formatter.d.ts.map +1 -1
  42. package/dist/dsl/notifications/formatter.js +20 -105
  43. package/dist/dsl/notifications/formatter.js.map +1 -1
  44. package/dist/dsl/notifications/index.d.ts +2 -8
  45. package/dist/dsl/notifications/index.d.ts.map +1 -1
  46. package/dist/dsl/notifications/index.js +60 -57
  47. package/dist/dsl/notifications/index.js.map +1 -1
  48. package/dist/dsl/plugin/index.d.ts.map +1 -1
  49. package/dist/dsl/plugin/index.js +1 -27
  50. package/dist/dsl/plugin/index.js.map +1 -1
  51. package/dist/dsl/plugin/options.d.ts +3 -2
  52. package/dist/dsl/plugin/options.d.ts.map +1 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +16 -123
  55. package/dist/index.js.map +1 -1
  56. package/dist/prompt/session-context-seed-hook.d.ts +11 -0
  57. package/dist/prompt/session-context-seed-hook.d.ts.map +1 -0
  58. package/dist/prompt/session-context-seed-hook.js +99 -0
  59. package/dist/prompt/session-context-seed-hook.js.map +1 -0
  60. package/dist/runtime/auto-update/coordinator.d.ts +19 -7
  61. package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
  62. package/dist/runtime/auto-update/coordinator.js +122 -22
  63. package/dist/runtime/auto-update/coordinator.js.map +1 -1
  64. package/dist/runtime/create-action-context.d.ts.map +1 -1
  65. package/dist/runtime/create-action-context.js +1 -0
  66. package/dist/runtime/create-action-context.js.map +1 -1
  67. package/dist/runtime/create-runtime-context.d.ts +1 -0
  68. package/dist/runtime/create-runtime-context.d.ts.map +1 -1
  69. package/dist/runtime/create-runtime-context.js +1 -0
  70. package/dist/runtime/create-runtime-context.js.map +1 -1
  71. package/dist/runtime/decision-errors.d.ts +41 -53
  72. package/dist/runtime/decision-errors.d.ts.map +1 -1
  73. package/dist/runtime/decision-errors.js +48 -111
  74. package/dist/runtime/decision-errors.js.map +1 -1
  75. package/dist/runtime/notifications.d.ts +21 -14
  76. package/dist/runtime/notifications.d.ts.map +1 -1
  77. package/dist/runtime/notifications.js +192 -59
  78. package/dist/runtime/notifications.js.map +1 -1
  79. package/dist/runtime/runtime.d.ts +16 -2
  80. package/dist/runtime/runtime.d.ts.map +1 -1
  81. package/dist/runtime/runtime.js +106 -26
  82. package/dist/runtime/runtime.js.map +1 -1
  83. package/dist/runtime/strategy-lifecycle.d.ts +5 -5
  84. package/dist/runtime/strategy-lifecycle.d.ts.map +1 -1
  85. package/dist/runtime/strategy-lifecycle.js +96 -28
  86. package/dist/runtime/strategy-lifecycle.js.map +1 -1
  87. package/dist/runtime/supervisor/long-running-supervisor.js +3 -3
  88. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  89. package/dist/runtime-api/lifecycle.d.ts +1 -1
  90. package/dist/runtime-api/lifecycle.js +1 -1
  91. package/dist/scripts/run-dsl-standalone-live.d.ts +7 -6
  92. package/dist/scripts/run-dsl-standalone-live.d.ts.map +1 -1
  93. package/dist/scripts/run-dsl-standalone-live.js +25 -21
  94. package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
  95. package/dist/skills-manager/coordinator.d.ts +2 -2
  96. package/dist/skills-manager/coordinator.js +2 -2
  97. package/dist/test-setup.d.ts +2 -0
  98. package/dist/test-setup.d.ts.map +1 -0
  99. package/dist/test-setup.js +23 -0
  100. package/dist/test-setup.js.map +1 -0
  101. package/dist/types/dsl/index.d.ts +1 -1
  102. package/dist/types/notification.d.ts +95 -1
  103. package/dist/types/notification.d.ts.map +1 -1
  104. package/dist/types/notification.js +76 -1
  105. package/dist/types/notification.js.map +1 -1
  106. package/dist/types/runtime.d.ts +6 -0
  107. package/dist/types/runtime.d.ts.map +1 -1
  108. package/dist/types/session-context.d.ts +19 -0
  109. package/dist/types/session-context.d.ts.map +1 -0
  110. package/dist/types/session-context.js +20 -0
  111. package/dist/types/session-context.js.map +1 -0
  112. package/dist/utils/event-catalog.d.ts +73 -0
  113. package/dist/utils/event-catalog.d.ts.map +1 -1
  114. package/dist/utils/event-catalog.js +22 -0
  115. package/dist/utils/event-catalog.js.map +1 -1
  116. package/dist/utils/logger.d.ts +2 -18
  117. package/dist/utils/logger.d.ts.map +1 -1
  118. package/dist/utils/logger.js +2 -40
  119. package/dist/utils/logger.js.map +1 -1
  120. package/openclaw.plugin.json +1 -1
  121. package/package.json +1 -1
  122. package/dist/actions/notifications/formatter.d.ts +0 -39
  123. package/dist/actions/notifications/formatter.d.ts.map +0 -1
  124. package/dist/actions/notifications/formatter.js +0 -68
  125. package/dist/actions/notifications/formatter.js.map +0 -1
  126. package/dist/actions/notifications/index.d.ts +0 -37
  127. package/dist/actions/notifications/index.d.ts.map +0 -1
  128. package/dist/actions/notifications/index.js +0 -89
  129. package/dist/actions/notifications/index.js.map +0 -1
  130. package/dist/dsl/notifications/close-resolver.d.ts +0 -92
  131. package/dist/dsl/notifications/close-resolver.d.ts.map +0 -1
  132. package/dist/dsl/notifications/close-resolver.js +0 -309
  133. package/dist/dsl/notifications/close-resolver.js.map +0 -1
  134. package/dist/dsl/notifications/types.d.ts +0 -27
  135. package/dist/dsl/notifications/types.d.ts.map +0 -1
  136. package/dist/dsl/notifications/types.js +0 -5
  137. package/dist/dsl/notifications/types.js.map +0 -1
@@ -1,92 +1,225 @@
1
+ import { toPayload, truncateForPush, PUSH_TITLE_MAX, PUSH_BODY_MAX } from "../types/notification.js";
2
+ import { decodeSenpiUserId } from "../telemetry/otel-sdk.js";
1
3
  import { createLogger } from "../utils/logger.js";
2
4
  import { classifyDecisionError } from "./decision-errors.js";
5
+ import { randomUUID } from "node:crypto";
3
6
  const logger = createLogger("[senpi-core][notifications]");
4
7
  /**
5
- * Map a raw LLM-decision error string to the user-facing Telegram message.
8
+ * Every notification the runtime emits is a strategy/trading alert, so the ingest
9
+ * envelope's top-level category is fixed. Per-alert differentiation (limit trip,
10
+ * decision error, scanner alert, ...) travels in `actionMetadata.notificationSubtype`.
11
+ */
12
+ const NOTIFICATION_CATEGORY = "PERPS";
13
+ const NOTIFICATION_SOURCE = "senpi-trading-runtime";
14
+ /**
15
+ * Bounded per-request timeout for the ingest POST. The service is called both
16
+ * fire-and-forget and awaited inline by producers on the serial signal path, so
17
+ * this must never hang indefinitely: one retry on a retryable failure, no more.
18
+ * Worst-case latency is therefore ~2x this value, never unbounded.
19
+ */
20
+ const DEFAULT_TIMEOUT_MS = 5000;
21
+ /** Only one retry attempt on a retryable failure — see {@link classifyOutcome}. */
22
+ const MAX_ATTEMPTS = 2;
23
+ /** Suppress repeat WARN logs for the same failure reason within this window. */
24
+ const WARN_REPEAT_WINDOW_MS = 30_000;
25
+ /**
26
+ * Map a raw LLM-decision error string to a user-facing notification message.
6
27
  *
7
- * Thin wrapper over {@link classifyDecisionError} — all message wording and the
28
+ * Thin wrapper over {@link classifyDecisionError} — the two-bucket copy and the
8
29
  * failure-mode lookup live in `decision-errors.ts` (the single source of truth
9
30
  * shared with the retry logic in `openclaw-llm-decision.ts`). The runtime catches
10
31
  * errors from the LLM-decision call and passes `err.message` here.
11
32
  *
12
33
  * @param rawMessage - Error message thrown by the decision client (may embed an
13
34
  * OpenClaw gateway message).
14
- * @param ctx - Optional plugin-side context (action name, configured model) for
15
- * placeholders the gateway error string cannot supply.
16
35
  */
17
- export function formatDecisionErrorNotification(rawMessage, ctx) {
18
- return classifyDecisionError(rawMessage, ctx).userMessage;
36
+ export function formatDecisionErrorNotification(rawMessage) {
37
+ return classifyDecisionError(rawMessage).userMessage;
38
+ }
39
+ /** Strip anything shaped like a bearer credential before it can reach a log line. */
40
+ function redactSecrets(text) {
41
+ return text.replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]");
42
+ }
43
+ function describeError(err) {
44
+ const raw = err instanceof Error ? err.message : String(err);
45
+ return redactSecrets(raw);
46
+ }
47
+ /** Classify an HTTP response status into a delivery outcome. 503 and any 5xx are
48
+ * transient (server-side produce failure) and get one retry; every other non-2xx
49
+ * (400 malformed envelope, 401 bad/expired token, 403 recipient mismatch, or any
50
+ * other 4xx) is a caller-side condition a retry cannot fix, so it drops immediately. */
51
+ function classifyStatus(status) {
52
+ if (status >= 200 && status < 300)
53
+ return "ok";
54
+ if (status === 503 || status >= 500)
55
+ return "retry";
56
+ return "drop";
57
+ }
58
+ const lastWarnedAt = new Map();
59
+ /** Log a WARN, but suppress repeats of the same reason within {@link WARN_REPEAT_WINDOW_MS}
60
+ * so a hot failure path (e.g. a sustained outage) doesn't flood the log. */
61
+ function warnRateLimited(key, message, attributes) {
62
+ const now = Date.now();
63
+ const last = lastWarnedAt.get(key);
64
+ if (last !== undefined && now - last < WARN_REPEAT_WINDOW_MS)
65
+ return;
66
+ lastWarnedAt.set(key, now);
67
+ logger.warn(message, undefined, attributes);
68
+ }
69
+ async function postOnce(ingestUrl, authToken, envelope, timeoutMs) {
70
+ const controller = new AbortController();
71
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
72
+ try {
73
+ const res = await fetch(ingestUrl, {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ Authorization: `Bearer ${authToken}`,
78
+ },
79
+ body: JSON.stringify(envelope),
80
+ signal: controller.signal,
81
+ });
82
+ if (res.ok)
83
+ return { kind: "ok" };
84
+ const bodyText = await res.text().catch(() => "");
85
+ const kind = classifyStatus(res.status);
86
+ return { kind, status: res.status, reason: redactSecrets(bodyText).slice(0, 300) };
87
+ }
88
+ catch (err) {
89
+ // Network failure, DNS failure, connection refused, or our own timeout abort.
90
+ return { kind: "retry", reason: describeError(err) };
91
+ }
92
+ finally {
93
+ clearTimeout(timer);
94
+ }
95
+ }
96
+ /** POST the envelope with a single bounded retry on a transient failure (5xx / network / timeout).
97
+ * Never throws — every outcome resolves, so a delivery failure can never surface as a rejected
98
+ * promise into the caller's (often serial, trading-critical) code path. */
99
+ async function deliver(ingestUrl, authToken, envelope, timeoutMs) {
100
+ let outcome = await postOnce(ingestUrl, authToken, envelope, timeoutMs);
101
+ for (let attempt = 2; outcome.kind === "retry" && attempt <= MAX_ATTEMPTS; attempt++) {
102
+ outcome = await postOnce(ingestUrl, authToken, envelope, timeoutMs);
103
+ }
104
+ if (outcome.kind === "ok")
105
+ return;
106
+ const notificationId = envelope.notificationId;
107
+ const subtype = envelope.actionMetadata?.notificationSubtype;
108
+ const rateLimitKey = `${outcome.status ?? "network"}:${typeof subtype === "string" ? subtype : "unknown"}`;
109
+ warnRateLimited(rateLimitKey, `Notification delivery dropped notificationId=${String(notificationId)} status=${outcome.status ?? "n/a"}: ${outcome.reason}`, { notificationId, status: outcome.status });
19
110
  }
20
111
  /**
21
- * IMPORTANT: Do NOT use logger.error(...) anywhere inside this service.
22
- * NotificationService is itself the Telegram delivery path for error notifications.
23
- * Using logger.error here would trigger the error notifier hook recursively,
24
- * which is blocked by the reentrancy guard (causing the notification to be silently dropped)
25
- * or could cause unexpected feedback loops in future. Use logger.warn for all failure paths.
112
+ * Create the runtime's notification service. Every notification is delivered by
113
+ * POSTing the full structured envelope to the authenticated notification-ingest
114
+ * endpoint (`NOTIFICATIONS_INGEST_URL`) — there is no other delivery path. Missing
115
+ * configuration is a startup error (thrown here, not discovered later as silent
116
+ * notification loss), so callers must construct this before a runtime starts
117
+ * trading, not lazily on first notify.
118
+ *
119
+ * Failure paths inside this service log at warn, not error: a delivery failure is a
120
+ * handled, degraded condition — the caller's notify() contract is fire-and-forget and
121
+ * the next notification retries the transport fresh.
26
122
  */
27
123
  export function createNotificationService(options) {
28
- const chatId = typeof options?.telegram_chat_id === "string" ? options.telegram_chat_id.trim() : undefined;
29
124
  const mcpTransport = options?.mcpTransport;
30
- const gatewayUrl = options?.gateway_url || process.env.OPENCLAW_GATEWAY_URL || "http://127.0.0.1:18789";
31
- const gatewayToken = options?.gateway_token || process.env.OPENCLAW_GATEWAY_TOKEN;
32
- if (chatId) {
33
- logger.info(`Notifications enabled for ${chatId}; routing via OpenClaw /tools/invoke`);
125
+ const ingestUrl = (options?.ingest_url || process.env.NOTIFICATIONS_INGEST_URL)?.trim();
126
+ if (!ingestUrl) {
127
+ throw new Error("NotificationService misconfigured: NOTIFICATIONS_INGEST_URL is not set. " +
128
+ "The runtime has no notification transport — set NOTIFICATIONS_INGEST_URL " +
129
+ "(or pass options.ingest_url) before starting.");
130
+ }
131
+ const authToken = (options?.senpi_auth_token ||
132
+ process.env.SENPI_AUTH_TOKEN ||
133
+ process.env.SENPI_API_KEY)?.trim();
134
+ if (!authToken) {
135
+ throw new Error("NotificationService misconfigured: no Senpi auth token available. " +
136
+ "Set options.senpi_auth_token, or SENPI_AUTH_TOKEN / SENPI_API_KEY in the environment — " +
137
+ "the runtime authenticates to the notification-ingest endpoint with its own Senpi token.");
34
138
  }
139
+ const timeoutMs = options?.ingest_timeout_ms ?? DEFAULT_TIMEOUT_MS;
140
+ // Telegram delivery is gone; a recipe still carrying telegram_chat_id from before
141
+ // the cutover is a silent misconfiguration footgun otherwise — the operator set it
142
+ // expecting delivery and gets nothing. Ignore it, but say so loudly once.
143
+ if (typeof options?.telegram_chat_id === "string" && options.telegram_chat_id.trim().length > 0) {
144
+ logger.warn("telegram_chat_id is set but ignored: Telegram delivery has been removed.");
145
+ }
146
+ // Resolve the recipient once from THIS service's own token — never a process
147
+ // global — so sibling services with different tokens stamp different users.
148
+ const recipientUserId = decodeSenpiUserId(authToken);
149
+ if (recipientUserId === undefined) {
150
+ // A live runtime always holds a valid token, so an undecodable one is an
151
+ // anomaly. WARN only (never logger.error — see the reentrancy note above);
152
+ // delivery still proceeds unstamped.
153
+ logger.warn("Could not resolve recipient senpi_user_id from auth token; delivering notification unstamped.");
154
+ }
155
+ logger.info(`Notifications enabled; routing via POST ${ingestUrl}`);
35
156
  return {
36
- async notify(message) {
157
+ async notify(input) {
158
+ const p = toPayload(input);
159
+ const message = p.message;
37
160
  if (typeof message !== "string" || message.length === 0)
38
161
  return;
162
+ // Precedence: the service's own resolved recipient wins, but an
163
+ // unresolved token must never clobber a value already on the payload.
164
+ const recipient = recipientUserId ?? p.recipientUserId;
39
165
  const preview = message.length > 120 ? message.slice(0, 120) + "…" : message;
40
166
  logger.info("Notification service invoked", { messagePreview: preview });
41
- if (!chatId) {
42
- logger.warn("Notification not sent: no telegram_chat_id in runtime config.");
43
- return;
44
- }
45
- // 1. Prefer MCP transport if host injects it in the future
167
+ // The openability fields (canOpenAgentChat + context) forward verbatim — never inferred,
168
+ // never repaired. notificationId/subtype fall back to the payload's own
169
+ // context when present (the producer already minted one), else this is the
170
+ // first and only place an id is minted for a bare-string/non-openable call.
171
+ const notificationId = p.context?.notificationId ?? randomUUID();
172
+ const subtype = p.subtype ?? p.context?.subtype;
173
+ // notifications-service renders user-visible text ONLY from these three
174
+ // actionMetadata fields (each `|| ""`) — it never displays the top-level
175
+ // `message`. A bare-string / copy-less notify() would otherwise ship a
176
+ // blank notification, so a copy-less payload always gets a synthesized
177
+ // fallback derived from `message` — every family ships non-empty copy.
178
+ const copy = p.copy ?? {
179
+ pushTitle: truncateForPush(message.split("\n")[0], PUSH_TITLE_MAX),
180
+ pushBody: truncateForPush(message.replace(/\n/g, " · "), PUSH_BODY_MAX),
181
+ inApp: message,
182
+ };
183
+ const envelope = {
184
+ notificationId,
185
+ message,
186
+ category: NOTIFICATION_CATEGORY,
187
+ source: NOTIFICATION_SOURCE,
188
+ timestamp: p.context?.createdAt ?? new Date().toISOString(),
189
+ canOpenAgentChat: p.canOpenAgentChat === true,
190
+ actionMetadata: {
191
+ uniqueKeys: [notificationId],
192
+ ...(subtype !== undefined ? { notificationSubtype: subtype } : {}),
193
+ id: notificationId,
194
+ pushNotificationTitle: copy.pushTitle,
195
+ pushNotificationMessage: copy.pushBody,
196
+ inAppNotificationMessage: copy.inApp,
197
+ senpiFormattedMessage: copy.inApp,
198
+ ctas: [],
199
+ category: NOTIFICATION_CATEGORY,
200
+ },
201
+ // Same id drives the ingest endpoint's idempotency precedence, so the two
202
+ // attempts of one delivery (see `deliver`'s bounded retry) collapse to a
203
+ // single ledger row even if the first attempt's response was lost.
204
+ idempotencyKey: notificationId,
205
+ };
206
+ if (p.context !== undefined)
207
+ envelope.context = p.context;
208
+ if (recipient !== undefined)
209
+ envelope.recipientUserId = recipient;
210
+ // Test-only seam: the deployed OpenClaw gateway never injects mcpTransport
211
+ // for notifications, so this branch only ever runs under test. When present
212
+ // it short-circuits before any HTTP call.
46
213
  if (mcpTransport) {
47
214
  try {
48
- await mcpTransport("send_telegram_notification", { target: `telegram:${chatId}`, message });
49
- return;
50
- }
51
- catch (err) {
52
- logger.warn("MCP transport failed; falling back to Gateway HTTP", err);
53
- }
54
- }
55
- // 2. HTTP Gateway Tool Invocation (Matches your LLM Engine)
56
- if (gatewayToken) {
57
- try {
58
- const baseUrl = gatewayUrl.endsWith("/") ? gatewayUrl.slice(0, -1) : gatewayUrl;
59
- const res = await fetch(`${baseUrl}/tools/invoke`, {
60
- method: "POST",
61
- headers: {
62
- "Content-Type": "application/json",
63
- "Authorization": `Bearer ${gatewayToken}`
64
- },
65
- body: JSON.stringify({
66
- tool: "message",
67
- args: {
68
- action: "send",
69
- target: `telegram:${chatId}`,
70
- message: message,
71
- },
72
- })
73
- });
74
- if (!res.ok) {
75
- const errText = await res.text().catch(() => "No response body");
76
- throw new Error(`HTTP ${res.status}: ${errText}`);
77
- }
78
- const body = await res.json();
79
- if (!body.ok) {
80
- throw new Error(`Gateway rejected message tool: ${body.error ?? JSON.stringify(body)}`);
81
- }
82
- return;
215
+ await mcpTransport("notify", envelope);
83
216
  }
84
217
  catch (err) {
85
- logger.warn("Gateway tool invocation failed", err);
218
+ logger.warn("mcpTransport notify failed", undefined, { err: describeError(err) });
86
219
  }
87
220
  return;
88
221
  }
89
- logger.warn("Notification dropped: Missing OPENCLAW_GATEWAY_TOKEN in environment.");
222
+ await deliver(ingestUrl, authToken, envelope, timeoutMs);
90
223
  },
91
224
  };
92
225
  }
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../src/runtime/notifications.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../src/runtime/notifications.ts"],"names":[],"mappings":"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"}
@@ -63,8 +63,10 @@ export declare class Runtime {
63
63
  private cachedStrategyId;
64
64
  /** In-flight resolution promise; ensures concurrent first-burst callers reuse one `listStrategies`. */
65
65
  private strategyIdResolution;
66
- /** Action-layer Telegram subscriber (open submit / close); disposed in {@link Runtime.stop}. */
67
- private actionNotificationSubscriber;
66
+ /** Scanner-alert dedupe: (scannerId, message) key → last-emitted ms. Pruned opportunistically. */
67
+ private scannerAlertLastEmitted;
68
+ /** Suppression-WARN rate limiting for the same keyspace — at most one WARN per key per window. */
69
+ private scannerAlertLastWarned;
68
70
  /** Serializes reconcile vs the trade loop. One shared key "reconcile". */
69
71
  private reconcileMutex;
70
72
  /** Deferred gate: consumer awaits this before touching slot counts / running actions. */
@@ -156,6 +158,18 @@ export declare class Runtime {
156
158
  * states. Best-effort: a failure here must not block runtime start.
157
159
  */
158
160
  private rehydratePositionIdRegistry;
161
+ /**
162
+ * Filter one scan run's alerts down to the ones fresh enough to notify on,
163
+ * per (scannerId, message) — see {@link SCANNER_ALERT_DEDUPE_MS}. A run
164
+ * mixing a brand-new alert with a repeat of one already notified within the
165
+ * window still surfaces the new one; the repeat is silently dropped from
166
+ * the result and, on its first suppression this window, logs one rate-limited
167
+ * WARN. Freshly-emitted keys record `now` so the next occurrence inside the
168
+ * window is recognized as a repeat.
169
+ */
170
+ private freshScannerAlerts;
171
+ /** Opportunistic cleanup of scanner-alert dedupe state — bounds memory for long-running runtimes. */
172
+ private pruneScannerAlertDedupe;
159
173
  /**
160
174
  * Cached wallet → strategy-id lookup. Concurrent callers share one
161
175
  * in-flight `listStrategies` promise; the cached id is reused across
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime/runtime.ts"],"names":[],"mappings":"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"}
@@ -28,7 +28,6 @@ import { initRisk } from "../risk/index.js";
28
28
  import { createHealthStateModule, } from "../health/index.js";
29
29
  import { DslPlugin as DslPluginClass } from "../dsl/plugin/index.js";
30
30
  import { HOOK_EVENT } from "../types/hooks.js";
31
- import { createActionNotificationSubscriber, isActionLifecycleNotificationsEnabled } from "../actions/notifications/index.js";
32
31
  import { MaxLeverageCache } from "../leverage/max-leverage-cache.js";
33
32
  import { buildStateLayout } from "./state-layout.js";
34
33
  import { createLogger } from "../utils/logger.js";
@@ -58,6 +57,23 @@ const RECONCILE_RETRY_INTERVAL_MS = 15_000;
58
57
  const SIGNAL_QUEUE_BACKPRESSURE_THRESHOLD = 10;
59
58
  /** Default liveness-heartbeat window (~15 min) when `health.window_seconds` is unset. */
60
59
  const DEFAULT_LIVENESS_WINDOW_SECONDS = 900;
60
+ /**
61
+ * Scanner-alert dedupe window: scanner intervals run in minutes, so an identical
62
+ * alert re-firing inside this window is noise from the same condition, not a new
63
+ * signal. Long enough to suppress a watchdog repeating every scan interval, short
64
+ * enough that a genuinely persistent condition still re-notifies within the hour.
65
+ */
66
+ const SCANNER_ALERT_DEDUPE_MS = 10 * 60_000;
67
+ /**
68
+ * Dedupe key for one scanner alert: NUL-joined so a scannerId/message never
69
+ * collides across the pair. Numeric runs in the message are collapsed to `#`
70
+ * before keying — watchdog-style alerts embed live floats (prices, margins)
71
+ * that drift every scan tick, and keying on the raw text would make every
72
+ * tick look like a new alert. Key-only: the delivered message is untouched.
73
+ */
74
+ function scannerAlertDedupeKey(scannerId, message) {
75
+ return `${scannerId}\u0000${message.replace(/\d+(?:\.\d+)?/g, "#")}`;
76
+ }
61
77
  export class Runtime {
62
78
  config;
63
79
  options;
@@ -99,8 +115,10 @@ export class Runtime {
99
115
  cachedStrategyId = null;
100
116
  /** In-flight resolution promise; ensures concurrent first-burst callers reuse one `listStrategies`. */
101
117
  strategyIdResolution = null;
102
- /** Action-layer Telegram subscriber (open submit / close); disposed in {@link Runtime.stop}. */
103
- actionNotificationSubscriber = null;
118
+ /** Scanner-alert dedupe: (scannerId, message) key → last-emitted ms. Pruned opportunistically. */
119
+ scannerAlertLastEmitted = new Map();
120
+ /** Suppression-WARN rate limiting for the same keyspace — at most one WARN per key per window. */
121
+ scannerAlertLastWarned = new Map();
104
122
  // ─── reconcile gate + mutex ──────────────────────────────────────────────────
105
123
  /** Serializes reconcile vs the trade loop. One shared key "reconcile". */
106
124
  reconcileMutex = new KeyedMutex();
@@ -251,6 +269,42 @@ export class Runtime {
251
269
  logger.warn("position.id registry rehydrate failed, continuing with empty registry", { err });
252
270
  }
253
271
  }
272
+ /**
273
+ * Filter one scan run's alerts down to the ones fresh enough to notify on,
274
+ * per (scannerId, message) — see {@link SCANNER_ALERT_DEDUPE_MS}. A run
275
+ * mixing a brand-new alert with a repeat of one already notified within the
276
+ * window still surfaces the new one; the repeat is silently dropped from
277
+ * the result and, on its first suppression this window, logs one rate-limited
278
+ * WARN. Freshly-emitted keys record `now` so the next occurrence inside the
279
+ * window is recognized as a repeat.
280
+ */
281
+ freshScannerAlerts(scannerId, alerts, now) {
282
+ return alerts.filter((alert) => {
283
+ const key = scannerAlertDedupeKey(scannerId, alert.message ?? "");
284
+ const lastEmitted = this.scannerAlertLastEmitted.get(key);
285
+ if (lastEmitted !== undefined && now - lastEmitted < SCANNER_ALERT_DEDUPE_MS) {
286
+ const lastWarned = this.scannerAlertLastWarned.get(key);
287
+ if (lastWarned === undefined || now - lastWarned >= SCANNER_ALERT_DEDUPE_MS) {
288
+ this.scannerAlertLastWarned.set(key, now);
289
+ logger.warn("scanner alert suppressed (dedupe)", { scannerId, window_ms: SCANNER_ALERT_DEDUPE_MS });
290
+ }
291
+ return false;
292
+ }
293
+ this.scannerAlertLastEmitted.set(key, now);
294
+ return true;
295
+ });
296
+ }
297
+ /** Opportunistic cleanup of scanner-alert dedupe state — bounds memory for long-running runtimes. */
298
+ pruneScannerAlertDedupe(now) {
299
+ for (const [key, ts] of this.scannerAlertLastEmitted) {
300
+ if (now - ts >= SCANNER_ALERT_DEDUPE_MS)
301
+ this.scannerAlertLastEmitted.delete(key);
302
+ }
303
+ for (const [key, ts] of this.scannerAlertLastWarned) {
304
+ if (now - ts >= SCANNER_ALERT_DEDUPE_MS)
305
+ this.scannerAlertLastWarned.delete(key);
306
+ }
307
+ }
254
308
  /**
255
309
  * Cached wallet → strategy-id lookup. Concurrent callers share one
256
310
  * in-flight `listStrategies` promise; the cached id is reused across
@@ -344,6 +398,9 @@ export class Runtime {
344
398
  this.senpi = senpi;
345
399
  this.strategyConfig = resolveStrategyConfig(this.config);
346
400
  await this.rehydratePositionIdRegistry();
401
+ // Human strategy label for every runtime notification family: the operator's
402
+ // `group` when set, else the recipe `name` (schema-required, always resolves).
403
+ const notificationLabel = this.config.group ?? this.config.name;
347
404
  // The OTLP exporter env is the telemetry master switch: the OTel SDK (stood up at plugin
348
405
  // register()) and the domain-event log both gate on it.
349
406
  const telemetryEnabled = isOtlpEndpointConfigured();
@@ -379,6 +436,7 @@ export class Runtime {
379
436
  const notifications = createNotificationService({
380
437
  ...this.config.notifications,
381
438
  mcpTransport: this.options.mcpTransport,
439
+ senpi_auth_token: this.options.apiKey ?? process.env.SENPI_AUTH_TOKEN ?? process.env.SENPI_API_KEY,
382
440
  });
383
441
  const riskBlock = this.config.risk;
384
442
  const strategyAddress = this.strategyConfig.address;
@@ -402,7 +460,7 @@ export class Runtime {
402
460
  strategies: new Map([[strategyAddress, this.strategyConfig]]),
403
461
  // Human strategy label for alerts; risk-guard falls back to the
404
462
  // truncated address when an address has no name here.
405
- strategyNames: { [strategyAddress]: this.config.name },
463
+ strategyNames: { [strategyAddress]: notificationLabel },
406
464
  auditLog,
407
465
  lifecycleTracker: lifecycle,
408
466
  })
@@ -436,6 +494,7 @@ export class Runtime {
436
494
  scannerModule: this.scannerModule,
437
495
  maxLeverageCache,
438
496
  positionIdRegistry: this.positionIdRegistry,
497
+ notificationLabel,
439
498
  });
440
499
  this.actionRuntimeModule = new ActionRuntimeModule({ stateDir: layout.actionRoot });
441
500
  const { runActionsForSignal } = await createActions(this.config, {
@@ -470,7 +529,7 @@ export class Runtime {
470
529
  const notificationsConfig = this.config.notifications;
471
530
  if (hasDsl) {
472
531
  const dslNotificationsDisabled = notificationsConfig?.dsl_lifecycle === false;
473
- const dslNotifySlUpdates = notificationsConfig?.dsl_notify_sl_updates === true;
532
+ const dslNotifySlUpdates = notificationsConfig?.dsl_notify_sl_updates !== false;
474
533
  this.dslPlugin = await DslPluginClass.create({
475
534
  config: exitConfig,
476
535
  bus: this.bus,
@@ -480,7 +539,7 @@ export class Runtime {
480
539
  strategyStates: strategyStatesMap,
481
540
  notifications: dslNotificationsDisabled ? undefined : notifications,
482
541
  notifySlUpdates: dslNotifySlUpdates,
483
- strategyName: this.config.name,
542
+ strategyName: notificationLabel,
484
543
  // Monitor runs the unified periodic reconcile (adopt/resize/gone), serialized
485
544
  // against the trade loop on the same reconcileMutex key, and can resolve the boot gate.
486
545
  reconcileSerialize: (fn) => this.reconcileMutex.withKeyLock("reconcile", fn),
@@ -499,14 +558,6 @@ export class Runtime {
499
558
  void this.runBootReconcileLoop(strategyStatesMap).catch((err) => {
500
559
  logger.error("[runtime][reconcile] boot reconcile loop crashed", { err });
501
560
  });
502
- if (isActionLifecycleNotificationsEnabled(notificationsConfig)) {
503
- this.actionNotificationSubscriber = createActionNotificationSubscriber(this.bus, notifications, {
504
- strategyName: this.config.name,
505
- });
506
- }
507
- else {
508
- this.actionNotificationSubscriber = null;
509
- }
510
561
  initRisk(this.bus);
511
562
  this.healthStateModule = createHealthStateModule({
512
563
  runtimeName: this.config.name,
@@ -556,12 +607,49 @@ export class Runtime {
556
607
  // disturbs the scan path.
557
608
  emitEvent(logger, () => buildScannerSummaryEvent(scannerRunPayload, Date.now()));
558
609
  recordScannerRun({ scannerId: event.result.scannerId, outcome: event.result.status });
559
- // Forward scanner summary alerts (e.g. watchdog) to notification service
610
+ // Forward scanner summary alerts (e.g. watchdog) to notification service, deduped
611
+ // per (scannerId, message) within SCANNER_ALERT_DEDUPE_MS — see freshScannerAlerts.
560
612
  const alerts = event.result.summary?.alerts;
561
613
  if (Array.isArray(alerts) && alerts.length > 0) {
562
- const header = `[${event.result.scannerId}] ${alerts.length} alert(s):`;
563
- const lines = alerts.map((a) => ` ${a.level ?? "ALERT"}: ${a.message ?? ""}`);
564
- notifications.notify(`${header}\n${lines.join("\n")}`).catch(() => { });
614
+ const scannerId = event.result.scannerId;
615
+ const now = Date.now();
616
+ const freshAlerts = this.freshScannerAlerts(scannerId, alerts, now);
617
+ this.pruneScannerAlertDedupe(now);
618
+ if (freshAlerts.length > 0) {
619
+ // Push disabled until per-type notification opt-in exists; re-enable then.
620
+ // Push and in-app share this notify() payload, so both are disabled together.
621
+ //
622
+ // const copy: NotificationCopy = {
623
+ // pushTitle: "🔔 Market alert",
624
+ // pushBody: freshAlerts[0]?.message ?? "",
625
+ // inApp:
626
+ // `🔔 ${freshAlerts.length} market alert(s)<br>` +
627
+ // freshAlerts
628
+ // .map((a: { level?: string; message?: string }) => `${a.level ?? "ALERT"}: ${a.message ?? ""}`)
629
+ // .join("<br>"),
630
+ // };
631
+ // // Address-less by design: the scanner watches the market, not one
632
+ // // strategy, so the strategy wallet in scope must not leak into the seed.
633
+ // // The agent answers from the runtime's event log, so the alert opens a
634
+ // // chat by category alone.
635
+ // const createdAt = new Date().toISOString();
636
+ // const payload = applyMessagePrefix(
637
+ // {
638
+ // message: copy.inApp,
639
+ // canOpenAgentChat: true,
640
+ // subtype: "MARKET_SCANNER",
641
+ // copy,
642
+ // context: {
643
+ // notificationId: randomUUID(),
644
+ // subtype: "MARKET_SCANNER",
645
+ // createdAt,
646
+ // contextText: copy.inApp,
647
+ // },
648
+ // },
649
+ // labelPrefix(notificationLabel),
650
+ // );
651
+ // notifications.notify(payload).catch(() => {});
652
+ }
565
653
  }
566
654
  };
567
655
  this.scannerModule.on("scanner:run:complete", this.onScannerRunComplete);
@@ -608,10 +696,6 @@ export class Runtime {
608
696
  this.scannerModule.off("scanner:run:complete", this.onScannerRunComplete);
609
697
  this.onScannerRunComplete = null;
610
698
  }
611
- if (this.actionNotificationSubscriber != null) {
612
- this.actionNotificationSubscriber.dispose();
613
- this.actionNotificationSubscriber = null;
614
- }
615
699
  if (this.dslPlugin) {
616
700
  this.dslPlugin.stop();
617
701
  this.dslPlugin = null;
@@ -978,10 +1062,6 @@ export class Runtime {
978
1062
  this.scannerModule.off("scanner:run:complete", this.onScannerRunComplete);
979
1063
  this.onScannerRunComplete = null;
980
1064
  }
981
- if (this.actionNotificationSubscriber != null) {
982
- this.actionNotificationSubscriber.dispose();
983
- this.actionNotificationSubscriber = null;
984
- }
985
1065
  await this.scannerModule.stop();
986
1066
  if (this.dslPlugin) {
987
1067
  this.dslPlugin.stop();