@openfinclaw/findoo-alpha-plugin 2026.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts ADDED
@@ -0,0 +1,115 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { A2AClient } from "./src/a2a-client.js";
3
+ import { resolveConfig } from "./src/config.js";
4
+ import { extractSummary, PendingTaskTracker } from "./src/pending-task-tracker.js";
5
+ import { registerTools } from "./src/register-tools.js";
6
+
7
+ const findooPlugin = {
8
+ id: "findoo-alpha-plugin",
9
+ name: "Findoo Alpha",
10
+ description:
11
+ "Bridge to LangGraph strategy-agent via A2A protocol — " +
12
+ "37 professional financial analysis skills covering A-shares, US/HK equities, crypto, macro, and risk.",
13
+ kind: "financial" as const,
14
+
15
+ register(api: OpenClawPluginApi) {
16
+ const config = resolveConfig(api);
17
+ const log = api.logger;
18
+
19
+ // ── License Gate: no key → skip all registration ──
20
+ if (!config.apiKey) {
21
+ log.warn(
22
+ "Findoo: license key not configured — plugin inactive. " +
23
+ "Set FINDOO_API_KEY env var or configure in Control UI → Plugins → Findoo.",
24
+ );
25
+ return;
26
+ }
27
+
28
+ log.info(`findoo-alpha: connecting to ${config.strategyAgentUrl}`);
29
+ log.info(`findoo-alpha: assistant ${config.strategyAssistantId}`);
30
+
31
+ // Shared A2AClient instance (used by both tools and tracker)
32
+ const a2a = new A2AClient(config.strategyAgentUrl, config.strategyAssistantId);
33
+
34
+ // Verify connectivity at startup (non-blocking)
35
+ fetch(`${config.strategyAgentUrl}/ok`, {
36
+ signal: AbortSignal.timeout(5_000),
37
+ })
38
+ .then((r) => {
39
+ if (r.ok) {
40
+ log.info("findoo-alpha: strategy-agent is reachable ✓");
41
+ } else {
42
+ log.warn(`findoo-alpha: strategy-agent returned ${r.status}`);
43
+ }
44
+ })
45
+ .catch((err) => {
46
+ log.warn(
47
+ `findoo-alpha: strategy-agent unreachable (${err instanceof Error ? err.message : err}). Tools will retry on use.`,
48
+ );
49
+ });
50
+
51
+ // ── enqueueSystemEvent bridge (heartbeat push) ──
52
+ type RuntimeServices = {
53
+ system?: {
54
+ enqueueSystemEvent?: (
55
+ text: string,
56
+ options: { sessionKey: string; contextKey?: string },
57
+ ) => void;
58
+ };
59
+ };
60
+
61
+ const runtime = api.runtime as unknown as RuntimeServices | undefined;
62
+ const enqueueSystemEvent = runtime?.system?.enqueueSystemEvent;
63
+
64
+ // ── PendingTaskTracker (background stream consumer) ──
65
+ let tracker: PendingTaskTracker | undefined;
66
+
67
+ if (enqueueSystemEvent) {
68
+ tracker = new PendingTaskTracker({
69
+ a2aClient: a2a,
70
+ timeoutMs: config.taskTimeoutMs,
71
+ log: (level, msg) => {
72
+ if (level === "warn" || level === "error") log.warn(msg);
73
+ else log.info(msg);
74
+ },
75
+
76
+ onTaskCompleted(task, result) {
77
+ const summary = extractSummary(result);
78
+ enqueueSystemEvent(`[findoo] 深度分析完成 — "${task.query.slice(0, 40)}"\n\n${summary}`, {
79
+ sessionKey: "main",
80
+ contextKey: "findoo-analysis",
81
+ });
82
+ },
83
+
84
+ onTaskFailed(task, error) {
85
+ enqueueSystemEvent(`[findoo] 分析任务失败 — "${task.query.slice(0, 40)}": ${error}`, {
86
+ sessionKey: "main",
87
+ contextKey: "findoo-analysis",
88
+ });
89
+ },
90
+ });
91
+
92
+ log.info(
93
+ `findoo-alpha: task tracker ready (timeout=${config.taskTimeoutMs}ms, stream-based)`,
94
+ );
95
+ } else {
96
+ log.info(
97
+ "findoo-alpha: enqueueSystemEvent not available, tracker disabled (async results won't be pushed)",
98
+ );
99
+ }
100
+
101
+ // Register A2A-bridged tools
102
+ registerTools(api, config, a2a, tracker);
103
+
104
+ // Expose service for cross-plugin consumption
105
+ api.runtime?.services?.set("fin-strategy-agent", {
106
+ id: "fin-strategy-agent",
107
+ getConfig: () => ({
108
+ url: config.strategyAgentUrl,
109
+ assistantId: config.strategyAssistantId,
110
+ }),
111
+ });
112
+ },
113
+ };
114
+
115
+ export default findooPlugin;
@@ -0,0 +1,42 @@
1
+ {
2
+ "id": "findoo-alpha-plugin",
3
+ "name": "Findoo Alpha",
4
+ "description": "Alpha discovery bridge to LangGraph strategy-agent via A2A protocol — financial analysis, strategy design, risk assessment, portfolio management.",
5
+ "kind": "financial",
6
+ "version": "2026.3.12",
7
+ "skills": ["./skills"],
8
+ "configSchema": {
9
+ "type": "object",
10
+ "properties": {
11
+ "apiKey": {
12
+ "type": "string",
13
+ "description": "Findoo license key (required). Also accepted via FINDOO_API_KEY env var."
14
+ },
15
+ "strategyAgentUrl": {
16
+ "type": "string",
17
+ "description": "Strategy Agent service URL (LangGraph A2A). Default: http://43.128.100.43:5085"
18
+ },
19
+ "strategyAssistantId": {
20
+ "type": "string",
21
+ "description": "LangGraph assistant UUID to route A2A calls to."
22
+ }
23
+ }
24
+ },
25
+ "uiHints": {
26
+ "apiKey": {
27
+ "label": "License Key",
28
+ "help": "Findoo 分析服务授权密钥",
29
+ "placeholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
30
+ },
31
+ "strategyAgentUrl": {
32
+ "label": "Strategy Agent URL",
33
+ "help": "LangGraph A2A 策略分析服务地址",
34
+ "placeholder": "http://43.128.100.43:5085"
35
+ },
36
+ "strategyAssistantId": {
37
+ "label": "Assistant ID",
38
+ "help": "LangGraph assistant UUID",
39
+ "placeholder": "d2310a07-b552-453c-a8bb-7b9b86de6b23"
40
+ }
41
+ }
42
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@openfinclaw/findoo-alpha-plugin",
3
+ "version": "2026.3.12",
4
+ "description": "Strategy Agent bridge — A2A protocol integration for LangGraph strategy-agent (37 financial analysis skills)",
5
+ "keywords": [
6
+ "a2a",
7
+ "agent",
8
+ "finance",
9
+ "langgraph",
10
+ "strategy"
11
+ ],
12
+ "homepage": "https://github.com/cryptoSUN2049/openFinclaw#readme",
13
+ "type": "module",
14
+ "main": "./index.ts",
15
+ "dependencies": {
16
+ "@sinclair/typebox": "0.34.48"
17
+ },
18
+ "devDependencies": {
19
+ "openfinclaw": "workspace:*"
20
+ },
21
+ "peerDependencies": {
22
+ "openclaw": ">=2026.1.0"
23
+ },
24
+ "openclaw": {
25
+ "extensions": [
26
+ "./index.ts"
27
+ ],
28
+ "install": {
29
+ "npmSpec": "@openfinclaw/findoo-alpha-plugin"
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,53 @@
1
+ # Findoo — 专业金融分析 Agent
2
+
3
+ Findoo 是远程部署的深度金融分析 Agent,具备多步推理、交叉验证、策略回测和报告生成能力。通过 `fin_analyze` 工具调用。
4
+
5
+ ## 路由规则(最重要)
6
+
7
+ **默认规则:所有金融问题优先使用 `fin_analyze`。**
8
+
9
+ 只有在以下情况下才绕过 Findoo、直接使用 `fin_stock` / `fin_crypto` / `fin_macro` / `fin_index`:
10
+
11
+ - 用户明确只需要一个数据点(报价、点位、汇率)
12
+ - 回答只需要一句话、一个数字
13
+ - 示例:"茅台现在多少钱"、"BTC 价格"、"沪深300点位"、"美元兑人民币汇率"
14
+
15
+ **以下情况必须使用 `fin_analyze`(即使看起来"不复杂"):**
16
+
17
+ | 触发信号 | 示例 |
18
+ | -------------------------------- | ------------------------------ |
19
+ | 包含"分析/评估/诊断/研究" | "帮我分析茅台"、"评估持仓风险" |
20
+ | 包含"怎么样/怎么看/如何" | "NVDA 怎么样"、"港股怎么看" |
21
+ | 包含"能不能/适不适合/值不值得" | "现在能加仓吗"、"值不值得买" |
22
+ | 包含"对比/比较" | "茅台和五粮液比"、"SPY vs QQQ" |
23
+ | 包含"为什么/原因" | "为什么跌这么多"、"涨的原因" |
24
+ | 包含"策略/回测/配置" | "设计轮动策略"、"回测均线" |
25
+ | 包含"筛选/推荐/选" | "选几只高股息股"、"推荐 ETF" |
26
+ | 包含"报告/周报/总结" | "出一份研究报告"、"本周总结" |
27
+ | 包含"龙虎榜/游资/北向/南向/资金" | "今天龙虎榜"、"游资在买什么" |
28
+ | 包含"周期/阶段/趋势/拐点" | "BTC什么周期"、"见底了吗" |
29
+ | 包含"风险/套利/对冲" | "可转债套利"、"资金费率" |
30
+ | 涉及多个标的或市场 | "A股和港股怎么选"、"中美利差" |
31
+ | 需要解读、判断或建议 | 任何非纯数据查询 |
32
+
33
+ ## Agent 能力概览
34
+
35
+ 37 个专业 skill:
36
+
37
+ - **A股** (14 skills) — 个股X-ray五维诊断、游资追踪(雷达/跟单/画像三层)、龙虎榜、概念周期、可转债、打新、北向资金、估值定投、财报季、因子筛选
38
+ - **美股** (5 skills) — DCF估值+期权、ETF对比、GICS板块轮动、财报季IV、股息策略
39
+ - **港股** (5 skills) — HSI估值脉搏、南向资金alpha、中概互联、高息策略
40
+ - **加密** (5 skills) — BTC周期、DeFi收益、资金费率套利、山寨季、稳定币流向
41
+ - **跨资产** (8 skills) — 宏观周期、风险监控、因子筛选、衍生品、ETF/基金、通用数据查询
42
+ - **策略回测** — 量化策略设计、历史回测、参数优化、研究报告生成
43
+
44
+ ## 多轮对话
45
+
46
+ 支持 `thread_id` 保持上下文,适用于渐进式深入分析:
47
+
48
+ ```
49
+ fin_analyze(query="分析茅台")
50
+ fin_analyze(query="和五粮液对比", thread_id="...")
51
+ fin_analyze(query="回测均线策略", thread_id="...")
52
+ fin_analyze(query="生成报告", thread_id="...")
53
+ ```
@@ -0,0 +1,287 @@
1
+ /**
2
+ * A2A (Agent-to-Agent) Protocol Client
3
+ *
4
+ * Implements Google A2A standard (JSON-RPC 2.0) for communicating
5
+ * with LangGraph strategy-agent.
6
+ *
7
+ * Protocol: POST /a2a/{assistant_id}
8
+ * Methods: message/send, message/stream, tasks/get
9
+ */
10
+
11
+ export type A2ATextPart = { kind: "text"; text: string };
12
+ export type A2ADataPart = { kind: "data"; data: Record<string, unknown> };
13
+ export type A2APart = A2ATextPart | A2ADataPart;
14
+
15
+ export type A2AMessage = {
16
+ role: "user" | "assistant";
17
+ parts: A2APart[];
18
+ messageId: string;
19
+ };
20
+
21
+ export type A2ARequest = {
22
+ jsonrpc: "2.0";
23
+ id: string;
24
+ method: "message/send" | "message/stream" | "tasks/get";
25
+ params: {
26
+ message?: A2AMessage;
27
+ thread?: { threadId: string };
28
+ id?: string;
29
+ contextId?: string;
30
+ };
31
+ };
32
+
33
+ export type A2AResponse = {
34
+ jsonrpc: "2.0";
35
+ id: string;
36
+ result?: Record<string, unknown>;
37
+ error?: { code: number; message: string };
38
+ };
39
+
40
+ /** A single parsed SSE event from message/stream */
41
+ export type A2AStreamEvent = {
42
+ kind: "task" | "status-update" | "artifact-update" | "error" | "unknown";
43
+ status?: { state: string; message?: Record<string, unknown> };
44
+ final: boolean;
45
+ raw: Record<string, unknown>;
46
+ };
47
+
48
+ export class A2AClient {
49
+ constructor(
50
+ private baseUrl: string,
51
+ private assistantId: string,
52
+ ) {}
53
+
54
+ /**
55
+ * Send a message to the strategy agent via A2A protocol.
56
+ */
57
+ async sendMessage(
58
+ text: string,
59
+ options?: {
60
+ data?: Record<string, unknown>;
61
+ threadId?: string;
62
+ timeoutMs?: number;
63
+ },
64
+ ): Promise<A2AResponse> {
65
+ const parts: A2APart[] = [{ kind: "text", text }];
66
+ if (options?.data) {
67
+ parts.push({ kind: "data", data: options.data });
68
+ }
69
+
70
+ const body: A2ARequest = {
71
+ jsonrpc: "2.0",
72
+ id: `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
73
+ method: "message/send",
74
+ params: {
75
+ message: {
76
+ role: "user",
77
+ parts,
78
+ messageId: `msg-${Date.now()}`,
79
+ },
80
+ ...(options?.threadId ? { thread: { threadId: options.threadId } } : {}),
81
+ },
82
+ };
83
+
84
+ const resp = await fetch(`${this.baseUrl}/a2a/${this.assistantId}`, {
85
+ method: "POST",
86
+ headers: {
87
+ "Content-Type": "application/json",
88
+ Accept: "application/json",
89
+ },
90
+ body: JSON.stringify(body),
91
+ signal: options?.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined,
92
+ });
93
+
94
+ if (!resp.ok) {
95
+ throw new Error(`A2A request failed: ${resp.status} ${resp.statusText}`);
96
+ }
97
+
98
+ return (await resp.json()) as A2AResponse;
99
+ }
100
+
101
+ /**
102
+ * Send a message via A2A SSE stream (message/stream).
103
+ * Yields parsed events; the final event has `final: true`.
104
+ */
105
+ async *sendMessageStream(
106
+ text: string,
107
+ options?: {
108
+ data?: Record<string, unknown>;
109
+ threadId?: string;
110
+ timeoutMs?: number;
111
+ },
112
+ ): AsyncGenerator<A2AStreamEvent> {
113
+ const parts: A2APart[] = [{ kind: "text", text }];
114
+ if (options?.data) {
115
+ parts.push({ kind: "data", data: options.data });
116
+ }
117
+
118
+ const body: A2ARequest = {
119
+ jsonrpc: "2.0",
120
+ id: `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
121
+ method: "message/stream",
122
+ params: {
123
+ message: {
124
+ role: "user",
125
+ parts,
126
+ messageId: `msg-${Date.now()}`,
127
+ },
128
+ ...(options?.threadId ? { thread: { threadId: options.threadId } } : {}),
129
+ },
130
+ };
131
+
132
+ const resp = await fetch(`${this.baseUrl}/a2a/${this.assistantId}`, {
133
+ method: "POST",
134
+ headers: {
135
+ "Content-Type": "application/json",
136
+ Accept: "text/event-stream",
137
+ },
138
+ body: JSON.stringify(body),
139
+ signal: options?.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined,
140
+ });
141
+
142
+ if (!resp.ok) {
143
+ throw new Error(`A2A stream request failed: ${resp.status} ${resp.statusText}`);
144
+ }
145
+
146
+ if (!resp.body) {
147
+ throw new Error("A2A stream response has no body");
148
+ }
149
+
150
+ // Parse SSE line by line from the ReadableStream
151
+ const reader = resp.body.getReader();
152
+ const decoder = new TextDecoder();
153
+ let buffer = "";
154
+
155
+ try {
156
+ while (true) {
157
+ const { done, value } = await reader.read();
158
+ if (done) break;
159
+
160
+ buffer += decoder.decode(value, { stream: true });
161
+ const lines = buffer.split("\n");
162
+ // Keep the last partial line in buffer
163
+ buffer = lines.pop() ?? "";
164
+
165
+ let currentData = "";
166
+
167
+ for (const line of lines) {
168
+ // Heartbeat comment — ignore
169
+ if (line.startsWith(":")) continue;
170
+ // Empty line = end of event block
171
+ if (line.trim() === "") {
172
+ if (currentData) {
173
+ const event = A2AClient.parseStreamData(currentData);
174
+ currentData = "";
175
+ yield event;
176
+ if (event.final) return;
177
+ }
178
+ continue;
179
+ }
180
+ if (line.startsWith("data: ")) {
181
+ currentData += line.slice(6);
182
+ }
183
+ // "event:" lines are informational; we derive kind from the data payload
184
+ }
185
+ }
186
+
187
+ // Flush remaining data in buffer
188
+ if (buffer.trim().startsWith("data: ")) {
189
+ const event = A2AClient.parseStreamData(buffer.trim().slice(6));
190
+ yield event;
191
+ }
192
+ } finally {
193
+ reader.releaseLock();
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Consume the stream until final event, return the completed result
199
+ * as an A2AResponse (same shape as sendMessage for drop-in replacement).
200
+ */
201
+ async collectStreamResult(
202
+ text: string,
203
+ options?: {
204
+ data?: Record<string, unknown>;
205
+ threadId?: string;
206
+ timeoutMs?: number;
207
+ },
208
+ ): Promise<A2AResponse> {
209
+ let lastEvent: A2AStreamEvent | undefined;
210
+
211
+ for await (const event of this.sendMessageStream(text, options)) {
212
+ lastEvent = event;
213
+ if (event.kind === "error") {
214
+ const errMsg =
215
+ (event.raw as Record<string, unknown>)?.error ??
216
+ event.status?.message ??
217
+ "Unknown stream error";
218
+ return {
219
+ jsonrpc: "2.0",
220
+ id: "",
221
+ error: {
222
+ code: -1,
223
+ message: typeof errMsg === "string" ? errMsg : JSON.stringify(errMsg),
224
+ },
225
+ };
226
+ }
227
+ }
228
+
229
+ if (!lastEvent) {
230
+ return {
231
+ jsonrpc: "2.0",
232
+ id: "",
233
+ error: { code: -1, message: "No events received from stream" },
234
+ };
235
+ }
236
+
237
+ // Extract result from the final event's status.message or raw payload
238
+ const result = lastEvent.status?.message ?? lastEvent.raw;
239
+ return { jsonrpc: "2.0", id: "", result: result as Record<string, unknown> };
240
+ }
241
+
242
+ /** Parse a JSON data payload from an SSE event into an A2AStreamEvent */
243
+ private static parseStreamData(data: string): A2AStreamEvent {
244
+ try {
245
+ const parsed = JSON.parse(data) as { result?: Record<string, unknown>; error?: unknown };
246
+
247
+ if (parsed.error) {
248
+ return { kind: "error", final: true, raw: parsed as Record<string, unknown> };
249
+ }
250
+
251
+ const result = parsed.result ?? {};
252
+ const kind = (result.kind as string) ?? "unknown";
253
+ const status = result.status as
254
+ | { state: string; message?: Record<string, unknown> }
255
+ | undefined;
256
+ const final = (result.final as boolean) ?? false;
257
+
258
+ return { kind: kind as A2AStreamEvent["kind"], status, final, raw: result };
259
+ } catch {
260
+ return { kind: "unknown", final: false, raw: { unparsed: data } };
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Get task status/result by task ID.
266
+ */
267
+ async getTask(taskId: string, contextId?: string): Promise<A2AResponse> {
268
+ const body: A2ARequest = {
269
+ jsonrpc: "2.0",
270
+ id: `req-${Date.now()}`,
271
+ method: "tasks/get",
272
+ params: { id: taskId, ...(contextId ? { contextId } : {}) },
273
+ };
274
+
275
+ const resp = await fetch(`${this.baseUrl}/a2a/${this.assistantId}`, {
276
+ method: "POST",
277
+ headers: {
278
+ "Content-Type": "application/json",
279
+ Accept: "application/json",
280
+ },
281
+ body: JSON.stringify(body),
282
+ signal: AbortSignal.timeout(10_000),
283
+ });
284
+
285
+ return (await resp.json()) as A2AResponse;
286
+ }
287
+ }
package/src/config.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+
3
+ export type PluginConfig = {
4
+ apiKey: string;
5
+ strategyAgentUrl: string;
6
+ strategyAssistantId: string;
7
+ requestTimeoutMs: number;
8
+ pollIntervalMs: number;
9
+ taskTimeoutMs: number;
10
+ };
11
+
12
+ function readEnv(keys: string[]): string | undefined {
13
+ for (const key of keys) {
14
+ const value = process.env[key]?.trim();
15
+ if (value) return value;
16
+ }
17
+ return undefined;
18
+ }
19
+
20
+ const DEFAULT_STRATEGY_AGENT_URL = "http://43.128.100.43:5085";
21
+ const DEFAULT_ASSISTANT_ID = "d2310a07-b552-453c-a8bb-7b9b86de6b23";
22
+
23
+ export function resolveConfig(api: OpenClawPluginApi): PluginConfig {
24
+ const raw = api.pluginConfig as Record<string, unknown> | undefined;
25
+
26
+ const strategyAgentUrl =
27
+ (typeof raw?.strategyAgentUrl === "string" ? raw.strategyAgentUrl : undefined) ??
28
+ readEnv(["STRATEGY_AGENT_URL", "OPENFINCLAW_STRATEGY_AGENT_URL"]) ??
29
+ DEFAULT_STRATEGY_AGENT_URL;
30
+
31
+ const strategyAssistantId =
32
+ (typeof raw?.strategyAssistantId === "string" ? raw.strategyAssistantId : undefined) ??
33
+ readEnv(["STRATEGY_ASSISTANT_ID", "OPENFINCLAW_STRATEGY_ASSISTANT_ID"]) ??
34
+ DEFAULT_ASSISTANT_ID;
35
+
36
+ const timeoutRaw = raw?.requestTimeoutMs ?? readEnv(["OPENFINCLAW_STRATEGY_TIMEOUT_MS"]);
37
+ const timeout = Number(timeoutRaw);
38
+
39
+ const pollRaw = raw?.pollIntervalMs ?? readEnv(["OPENFINCLAW_FINDOO_POLL_INTERVAL_MS"]);
40
+ const pollInterval = Number(pollRaw);
41
+
42
+ const taskTimeoutRaw = raw?.taskTimeoutMs ?? readEnv(["OPENFINCLAW_FINDOO_TASK_TIMEOUT_MS"]);
43
+ const taskTimeout = Number(taskTimeoutRaw);
44
+
45
+ const apiKey =
46
+ (typeof raw?.apiKey === "string" ? raw.apiKey : undefined) ??
47
+ readEnv(["FINDOO_API_KEY", "OPENFINCLAW_FINDOO_API_KEY"]) ??
48
+ "";
49
+
50
+ return {
51
+ apiKey,
52
+ strategyAgentUrl: strategyAgentUrl.replace(/\/+$/, ""),
53
+ strategyAssistantId,
54
+ requestTimeoutMs: Number.isFinite(timeout) && timeout >= 5000 ? Math.floor(timeout) : 120_000,
55
+ pollIntervalMs:
56
+ Number.isFinite(pollInterval) && pollInterval >= 5000 ? Math.floor(pollInterval) : 15_000,
57
+ taskTimeoutMs:
58
+ Number.isFinite(taskTimeout) && taskTimeout >= 60_000 ? Math.floor(taskTimeout) : 1_200_000,
59
+ };
60
+ }