iflow-mcp-deeflect-smart-spawn 0.1.0

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.
@@ -0,0 +1,418 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { RunCreateInput, PlannedRun, PlannedNode } from "../types.ts";
3
+ import { SmartSpawnClient } from "../smart-spawn-client.ts";
4
+
5
+ function makeNodeId(prefix: string): string {
6
+ return `${prefix}-${randomUUID().slice(0, 8)}`;
7
+ }
8
+
9
+ function fallbackModel(): string {
10
+ return "openai/gpt-4o-mini";
11
+ }
12
+
13
+ function fallbackPremiumModel(): string {
14
+ return "anthropic/claude-sonnet-4";
15
+ }
16
+
17
+ function fallbackCollectiveModels(count: number): string[] {
18
+ const base = [
19
+ "openai/gpt-4o-mini",
20
+ "anthropic/claude-sonnet-4",
21
+ "google/gemini-2.5-pro",
22
+ "openai/gpt-4o",
23
+ "meta-llama/llama-3.3-70b-instruct",
24
+ ];
25
+ return base.slice(0, Math.max(1, Math.min(count, base.length)));
26
+ }
27
+
28
+ function splitTaskFallback(task: string): string[] {
29
+ const numbered = task
30
+ .split(/\r?\n/)
31
+ .map((line) => line.trim())
32
+ .filter((line) => /^\d+[.)]\s+/.test(line))
33
+ .map((line) => line.replace(/^\d+[.)]\s+/, "").trim())
34
+ .filter(Boolean);
35
+ if (numbered.length > 0) return numbered;
36
+
37
+ const bullet = task
38
+ .split(/\r?\n/)
39
+ .map((line) => line.trim())
40
+ .filter((line) => /^[-*]\s+/.test(line))
41
+ .map((line) => line.replace(/^[-*]\s+/, "").trim())
42
+ .filter(Boolean);
43
+ if (bullet.length > 0) return bullet;
44
+
45
+ const byAnd = task
46
+ .split(/\s+(?:and then|then|and)\s+/i)
47
+ .map((part) => part.trim())
48
+ .filter((part) => part.length > 8);
49
+ if (byAnd.length > 1) return byAnd.slice(0, 6);
50
+
51
+ return [task.trim()];
52
+ }
53
+
54
+ export async function buildRunPlan(
55
+ input: RunCreateInput,
56
+ smartSpawn: SmartSpawnClient
57
+ ): Promise<PlannedRun> {
58
+ switch (input.mode) {
59
+ case "single":
60
+ return buildSinglePlan(input, smartSpawn);
61
+ case "collective":
62
+ return buildCollectivePlan(input, smartSpawn);
63
+ case "cascade":
64
+ return buildCascadePlan(input, smartSpawn);
65
+ case "plan":
66
+ return buildSequentialPlan(input, smartSpawn);
67
+ case "swarm":
68
+ return buildSwarmPlan(input, smartSpawn);
69
+ default:
70
+ return buildSinglePlan(input, smartSpawn);
71
+ }
72
+ }
73
+
74
+ export async function buildSinglePlan(
75
+ input: RunCreateInput,
76
+ smartSpawn?: SmartSpawnClient
77
+ ): Promise<PlannedRun> {
78
+ let planningSource: "api" | "fallback" = smartSpawn ? "api" : "fallback";
79
+ let picked = { modelId: fallbackModel(), reason: "Fallback single model" };
80
+ if (smartSpawn) {
81
+ try {
82
+ picked = await smartSpawn.pick({
83
+ task: input.task,
84
+ budget: input.budget,
85
+ context: input.context,
86
+ });
87
+ } catch {
88
+ planningSource = "fallback";
89
+ picked = {
90
+ modelId: fallbackModel(),
91
+ reason: "Fallback single model (Smart Spawn API unavailable)",
92
+ };
93
+ }
94
+ }
95
+
96
+ let prompt = input.task;
97
+ if (smartSpawn) {
98
+ try {
99
+ prompt = await smartSpawn.composeRole(input.task, input.role);
100
+ } catch {
101
+ prompt = input.task;
102
+ }
103
+ }
104
+
105
+ const node: PlannedNode = {
106
+ id: makeNodeId("node"),
107
+ kind: "task",
108
+ wave: 0,
109
+ dependsOn: [],
110
+ task: input.task,
111
+ model: picked.modelId,
112
+ prompt,
113
+ meta: { reason: picked.reason, mode: "single", planningSource },
114
+ };
115
+
116
+ return {
117
+ plannerSummary: `single plan with model ${picked.modelId}`,
118
+ nodes: [node],
119
+ };
120
+ }
121
+
122
+ async function buildCollectivePlan(
123
+ input: RunCreateInput,
124
+ smartSpawn: SmartSpawnClient
125
+ ): Promise<PlannedRun> {
126
+ const count = Math.max(2, Math.min(input.collectiveCount ?? 3, 5));
127
+ let picks: Array<{ modelId: string; reason: string }> = [];
128
+ let planningSource: "api" | "fallback" = "api";
129
+ try {
130
+ picks = await smartSpawn.recommend({
131
+ task: input.task,
132
+ budget: input.budget,
133
+ count,
134
+ context: input.context,
135
+ });
136
+ } catch {
137
+ planningSource = "fallback";
138
+ picks = fallbackCollectiveModels(count).map((modelId) => ({
139
+ modelId,
140
+ reason: "Fallback recommendation (Smart Spawn API unavailable)",
141
+ }));
142
+ }
143
+
144
+ let prompt = input.task;
145
+ try {
146
+ prompt = await smartSpawn.composeRole(input.task, input.role);
147
+ } catch {
148
+ prompt = input.task;
149
+ }
150
+ const taskNodes: PlannedNode[] = picks.map((p, idx) => ({
151
+ id: makeNodeId(`collective-${idx + 1}`),
152
+ kind: "task",
153
+ wave: 0,
154
+ dependsOn: [],
155
+ task: input.task,
156
+ model: p.modelId,
157
+ prompt,
158
+ meta: { reason: p.reason, mode: "collective", planningSource },
159
+ }));
160
+
161
+ if (taskNodes.length === 0) {
162
+ return buildSinglePlan(input, smartSpawn);
163
+ }
164
+
165
+ const mergeNode: PlannedNode = {
166
+ id: "merged",
167
+ kind: "merge",
168
+ wave: 1,
169
+ dependsOn: taskNodes.map((n) => n.id),
170
+ task: input.task,
171
+ model: input.merge?.model ?? taskNodes[0]?.model ?? fallbackModel(),
172
+ prompt: "",
173
+ meta: {
174
+ mode: "collective",
175
+ mergeStyle: input.merge?.style ?? "detailed",
176
+ planningSource,
177
+ },
178
+ };
179
+
180
+ return {
181
+ plannerSummary: `collective plan with ${taskNodes.length} worker nodes`,
182
+ nodes: [...taskNodes, mergeNode],
183
+ };
184
+ }
185
+
186
+ async function buildCascadePlan(
187
+ input: RunCreateInput,
188
+ smartSpawn: SmartSpawnClient
189
+ ): Promise<PlannedRun> {
190
+ let planningSource: "api" | "fallback" = "api";
191
+ let cheap: { modelId: string; reason: string };
192
+ let premium: { modelId: string; reason: string };
193
+ try {
194
+ cheap = await smartSpawn.pick({
195
+ task: input.task,
196
+ budget: "low",
197
+ context: input.context,
198
+ });
199
+
200
+ premium = await smartSpawn.pick({
201
+ task: input.task,
202
+ budget: input.budget === "high" ? "high" : "medium",
203
+ context: input.context,
204
+ exclude: [cheap.modelId],
205
+ });
206
+ } catch {
207
+ planningSource = "fallback";
208
+ cheap = {
209
+ modelId: fallbackModel(),
210
+ reason: "Fallback cheap model (Smart Spawn API unavailable)",
211
+ };
212
+ premium = {
213
+ modelId: fallbackPremiumModel(),
214
+ reason: "Fallback premium model (Smart Spawn API unavailable)",
215
+ };
216
+ }
217
+
218
+ let prompt = input.task;
219
+ try {
220
+ prompt = await smartSpawn.composeRole(input.task, input.role);
221
+ } catch {
222
+ prompt = input.task;
223
+ }
224
+ const cheapNode: PlannedNode = {
225
+ id: makeNodeId("cascade-cheap"),
226
+ kind: "task",
227
+ wave: 0,
228
+ dependsOn: [],
229
+ task: input.task,
230
+ model: cheap.modelId,
231
+ prompt,
232
+ meta: { mode: "cascade", tier: "cheap", reason: cheap.reason, planningSource },
233
+ };
234
+
235
+ const premiumNode: PlannedNode = {
236
+ id: makeNodeId("cascade-premium"),
237
+ kind: "task",
238
+ wave: 1,
239
+ dependsOn: [cheapNode.id],
240
+ task: input.task,
241
+ model: premium.modelId,
242
+ prompt,
243
+ meta: { mode: "cascade", tier: "premium", reason: premium.reason, conditional: true, planningSource },
244
+ };
245
+
246
+ const mergeNode: PlannedNode = {
247
+ id: "merged",
248
+ kind: "merge",
249
+ wave: 2,
250
+ dependsOn: [cheapNode.id, premiumNode.id],
251
+ task: input.task,
252
+ model: input.merge?.model ?? premium.modelId,
253
+ prompt: "",
254
+ meta: { mode: "cascade", mergeStyle: input.merge?.style ?? "decision", planningSource },
255
+ };
256
+
257
+ return {
258
+ plannerSummary: "cascade plan with cheap and premium fallback",
259
+ nodes: [cheapNode, premiumNode, mergeNode],
260
+ };
261
+ }
262
+
263
+ async function buildSequentialPlan(
264
+ input: RunCreateInput,
265
+ smartSpawn: SmartSpawnClient
266
+ ): Promise<PlannedRun> {
267
+ let planningSource: "api" | "fallback" = "api";
268
+ let steps: Array<{ id: string; task: string; modelId: string; wave: number; dependsOn: string[]; reason: string }> = [];
269
+ try {
270
+ const result = await smartSpawn.decompose({
271
+ task: input.task,
272
+ budget: input.budget,
273
+ context: input.context,
274
+ });
275
+ if (result.decomposed && result.steps.length > 0) {
276
+ steps = result.steps;
277
+ }
278
+ } catch {
279
+ // fall through to fallback steps
280
+ }
281
+
282
+ if (steps.length === 0) {
283
+ planningSource = "fallback";
284
+ const split = splitTaskFallback(input.task);
285
+ if (split.length <= 1) {
286
+ return buildSinglePlan(input, smartSpawn);
287
+ }
288
+ steps = split.map((task, idx) => ({
289
+ id: `step-${idx + 1}`,
290
+ task,
291
+ modelId: idx === split.length - 1 ? fallbackPremiumModel() : fallbackModel(),
292
+ wave: idx,
293
+ dependsOn: idx === 0 ? [] : [`step-${idx}`],
294
+ reason: "Fallback decomposition (Smart Spawn API unavailable)",
295
+ }));
296
+ }
297
+
298
+ const nodes: PlannedNode[] = [];
299
+ for (const step of steps) {
300
+ let prompt = step.task;
301
+ try {
302
+ prompt = await smartSpawn.composeRole(step.task, input.role);
303
+ } catch {
304
+ prompt = step.task;
305
+ }
306
+ nodes.push({
307
+ id: step.id,
308
+ kind: "task",
309
+ wave: step.wave,
310
+ dependsOn: step.dependsOn,
311
+ task: step.task,
312
+ model: step.modelId,
313
+ prompt,
314
+ meta: { mode: "plan", reason: step.reason, planningSource },
315
+ });
316
+ }
317
+
318
+ const mergeNode: PlannedNode = {
319
+ id: "merged",
320
+ kind: "merge",
321
+ wave: Math.max(...nodes.map((n) => n.wave)) + 1,
322
+ dependsOn: nodes.map((n) => n.id),
323
+ task: input.task,
324
+ model: input.merge?.model ?? nodes[nodes.length - 1]?.model ?? fallbackModel(),
325
+ prompt: "",
326
+ meta: { mode: "plan", mergeStyle: input.merge?.style ?? "detailed", planningSource },
327
+ };
328
+
329
+ return {
330
+ plannerSummary: `plan mode with ${nodes.length} sequential nodes`,
331
+ nodes: [...nodes, mergeNode],
332
+ };
333
+ }
334
+
335
+ async function buildSwarmPlan(
336
+ input: RunCreateInput,
337
+ smartSpawn: SmartSpawnClient
338
+ ): Promise<PlannedRun> {
339
+ let planningSource: "api" | "fallback" = "api";
340
+ let tasks: Array<{ id: string; task: string; modelId: string; wave: number; dependsOn: string[]; reason: string }> = [];
341
+ try {
342
+ const result = await smartSpawn.swarm({
343
+ task: input.task,
344
+ budget: input.budget,
345
+ context: input.context,
346
+ maxParallel: 5,
347
+ });
348
+ if (result.decomposed && result.tasks.length > 0) {
349
+ tasks = result.tasks;
350
+ }
351
+ } catch {
352
+ // fall through to fallback tasks
353
+ }
354
+
355
+ if (tasks.length === 0) {
356
+ planningSource = "fallback";
357
+ const split = splitTaskFallback(input.task);
358
+ if (split.length <= 1) {
359
+ return buildSinglePlan(input, smartSpawn);
360
+ }
361
+ if (split.length === 2) {
362
+ tasks = split.map((task, idx) => ({
363
+ id: `swarm-${idx + 1}`,
364
+ task,
365
+ modelId: fallbackModel(),
366
+ wave: idx,
367
+ dependsOn: idx === 0 ? [] : ["swarm-1"],
368
+ reason: "Fallback swarm decomposition (Smart Spawn API unavailable)",
369
+ }));
370
+ } else {
371
+ const lastIdx = split.length - 1;
372
+ tasks = split.map((task, idx) => ({
373
+ id: `swarm-${idx + 1}`,
374
+ task,
375
+ modelId: idx === lastIdx ? fallbackPremiumModel() : fallbackModel(),
376
+ wave: idx === lastIdx ? 1 : 0,
377
+ dependsOn: idx === lastIdx ? split.slice(0, lastIdx).map((_, i) => `swarm-${i + 1}`) : [],
378
+ reason: "Fallback swarm decomposition (Smart Spawn API unavailable)",
379
+ }));
380
+ }
381
+ }
382
+
383
+ const nodes: PlannedNode[] = [];
384
+ for (const t of tasks) {
385
+ let prompt = t.task;
386
+ try {
387
+ prompt = await smartSpawn.composeRole(t.task, input.role);
388
+ } catch {
389
+ prompt = t.task;
390
+ }
391
+ nodes.push({
392
+ id: t.id,
393
+ kind: "task",
394
+ wave: t.wave,
395
+ dependsOn: t.dependsOn,
396
+ task: t.task,
397
+ model: t.modelId,
398
+ prompt,
399
+ meta: { mode: "swarm", reason: t.reason, planningSource },
400
+ });
401
+ }
402
+
403
+ const mergeNode: PlannedNode = {
404
+ id: "merged",
405
+ kind: "merge",
406
+ wave: Math.max(...nodes.map((n) => n.wave)) + 1,
407
+ dependsOn: nodes.map((n) => n.id),
408
+ task: input.task,
409
+ model: input.merge?.model ?? nodes[0]?.model ?? fallbackModel(),
410
+ prompt: "",
411
+ meta: { mode: "swarm", mergeStyle: input.merge?.style ?? "detailed", planningSource },
412
+ };
413
+
414
+ return {
415
+ plannerSummary: `swarm mode with ${nodes.length} nodes`,
416
+ nodes: [...nodes, mergeNode],
417
+ };
418
+ }
@@ -0,0 +1,223 @@
1
+ import type { McpConfig } from "../config.ts";
2
+ import { McpStore } from "../db.ts";
3
+ import { OpenRouterClient } from "../openrouter-client.ts";
4
+ import { SmartSpawnClient } from "../smart-spawn-client.ts";
5
+ import { ArtifactStorage } from "../storage.ts";
6
+ import type { RunCreateInput, RunProgress, RunRecord, RunStatus } from "../types.ts";
7
+ import { buildRunPlan } from "./planner.ts";
8
+ import { RunExecutor } from "./executor.ts";
9
+
10
+ function parseJson<T>(raw: string): T {
11
+ return JSON.parse(raw) as T;
12
+ }
13
+
14
+ function formatPercent(value: number): number {
15
+ return Math.max(0, Math.min(100, Number(value.toFixed(2))));
16
+ }
17
+
18
+ export class RuntimeQueue {
19
+ private interval: Timer | null = null;
20
+ private processing = new Set<string>();
21
+ private readonly executor: RunExecutor;
22
+
23
+ constructor(
24
+ private readonly config: McpConfig,
25
+ private readonly store: McpStore,
26
+ private readonly storage: ArtifactStorage,
27
+ private readonly smartSpawn: SmartSpawnClient,
28
+ private readonly openRouter: OpenRouterClient
29
+ ) {
30
+ this.executor = new RunExecutor(config, store, storage, openRouter);
31
+ }
32
+
33
+ async start(): Promise<void> {
34
+ await this.storage.ensure();
35
+ this.interval = setInterval(() => {
36
+ void this.tick();
37
+ }, this.config.pollIntervalMs);
38
+ await this.tick();
39
+ }
40
+
41
+ stop(): void {
42
+ if (this.interval) clearInterval(this.interval);
43
+ this.interval = null;
44
+ }
45
+
46
+ async createRun(input: RunCreateInput): Promise<RunRecord> {
47
+ const run = this.store.createRun(input);
48
+ this.store.addEvent(run.id, "info", "Run created");
49
+ await this.tick();
50
+ return run;
51
+ }
52
+
53
+ getRun(runId: string): RunRecord | null {
54
+ return this.store.getRun(runId);
55
+ }
56
+
57
+ listRuns(status?: RunStatus, limit?: number): RunRecord[] {
58
+ return this.store.listRuns(status, limit ?? 20);
59
+ }
60
+
61
+ cancelRun(runId: string): RunRecord | null {
62
+ const run = this.store.getRun(runId);
63
+ if (!run) return null;
64
+ if (run.status === "completed" || run.status === "failed") return run;
65
+ this.store.updateRunStatus(runId, "canceled");
66
+ this.store.addEvent(runId, "warn", "Run canceled by user");
67
+ return this.store.getRun(runId);
68
+ }
69
+
70
+ getProgress(runId: string): RunProgress {
71
+ const nodes = this.store.listNodes(runId);
72
+ const totalNodes = nodes.length;
73
+ const doneNodes = nodes.filter((n) => n.status === "completed" || n.status === "skipped").length;
74
+ const runningNodes = nodes.filter((n) => n.status === "running").length;
75
+ const failedNodes = nodes.filter((n) => n.status === "failed").length;
76
+ const percent = totalNodes > 0 ? formatPercent((doneNodes / totalNodes) * 100) : 0;
77
+ return { totalNodes, doneNodes, runningNodes, failedNodes, percent };
78
+ }
79
+
80
+ getLastEvent(runId: string): string | null {
81
+ const events = this.store.listRecentEvents(runId, 1);
82
+ return events.length > 0 ? (events[0]?.message ?? null) : null;
83
+ }
84
+
85
+ async getResult(runId: string, includeRaw = false): Promise<{
86
+ status: string;
87
+ mergedOutput: string | null;
88
+ summary: string;
89
+ artifacts: Array<{ nodeId: string; path: string; type: string; model: string; status: string }>;
90
+ cost: { promptTokens: number; completionTokens: number; usdEstimate: number };
91
+ rawOutputs?: Array<{ nodeId: string; output: string }>;
92
+ } | null> {
93
+ const run = this.store.getRun(runId);
94
+ if (!run) return null;
95
+
96
+ const nodes = this.store.listNodes(runId);
97
+ const artifacts = this.store.listArtifacts(runId);
98
+ const mergedArtifact = artifacts.find((a) => a.nodeId === "merged");
99
+ const mergedOutput = mergedArtifact ? await this.storage.readArtifact(mergedArtifact.path) : null;
100
+ const cost = this.store.getRunCost(runId);
101
+
102
+ const artifactRows = artifacts.map((a) => {
103
+ const node = nodes.find((n) => n.id === a.nodeId || (a.nodeId === "merged" && n.kind === "merge"));
104
+ return {
105
+ nodeId: a.nodeId,
106
+ path: a.path,
107
+ type: a.type,
108
+ model: node?.model ?? "",
109
+ status: node?.status ?? run.status,
110
+ };
111
+ });
112
+
113
+ const rawOutputs = [];
114
+ if (includeRaw) {
115
+ for (const artifact of artifacts.filter((a) => a.type === "raw")) {
116
+ const content = await this.storage.readArtifact(artifact.path);
117
+ rawOutputs.push({ nodeId: artifact.nodeId, output: content.slice(0, 12000) });
118
+ }
119
+ }
120
+
121
+ return {
122
+ status: run.status,
123
+ mergedOutput,
124
+ summary: `${run.mode} run with ${nodes.length} nodes`,
125
+ artifacts: artifactRows,
126
+ cost,
127
+ ...(includeRaw ? { rawOutputs } : {}),
128
+ };
129
+ }
130
+
131
+ async getArtifact(runId: string, nodeId: string): Promise<{
132
+ type: string;
133
+ content: string;
134
+ metadata: { bytes: number; sha256: string; createdAt: string; path: string };
135
+ } | null> {
136
+ const artifact = this.store.getArtifact(runId, nodeId);
137
+ if (!artifact) return null;
138
+ const content = await this.storage.readArtifact(artifact.path);
139
+ return {
140
+ type: artifact.type,
141
+ content,
142
+ metadata: {
143
+ bytes: artifact.bytes,
144
+ sha256: artifact.sha256,
145
+ createdAt: artifact.createdAt,
146
+ path: artifact.path,
147
+ },
148
+ };
149
+ }
150
+
151
+ async tick(): Promise<void> {
152
+ if (this.processing.size >= this.config.maxParallelRuns) return;
153
+ const openSlots = this.config.maxParallelRuns - this.processing.size;
154
+ const active = this.store.listActiveRuns(openSlots * 2);
155
+ for (const run of active) {
156
+ if (this.processing.size >= this.config.maxParallelRuns) break;
157
+ if (this.processing.has(run.id)) continue;
158
+ this.processing.add(run.id);
159
+ void this.processRun(run).finally(() => {
160
+ this.processing.delete(run.id);
161
+ });
162
+ }
163
+ }
164
+
165
+ private async processRun(run: RunRecord): Promise<void> {
166
+ const latest = this.store.getRun(run.id);
167
+ if (!latest || latest.status === "canceled" || latest.status === "completed" || latest.status === "failed") {
168
+ return;
169
+ }
170
+
171
+ let nodes = this.store.listNodes(run.id);
172
+ if (nodes.length === 0) {
173
+ const input = parseJson<RunCreateInput>(latest.paramsJson);
174
+ const plan = await buildRunPlan(input, this.smartSpawn);
175
+ this.store.createNodes(run.id, plan.nodes);
176
+ const planFile = await this.storage.writeArtifact(run.id, "plan", "plan", JSON.stringify(plan, null, 2), "json");
177
+ this.store.createArtifact({
178
+ runId: run.id,
179
+ nodeId: "plan",
180
+ type: "plan",
181
+ path: planFile.relativePath,
182
+ bytes: planFile.bytes,
183
+ sha256: planFile.sha256,
184
+ createdAt: new Date().toISOString(),
185
+ });
186
+ this.store.addEvent(run.id, "info", plan.plannerSummary);
187
+ nodes = this.store.listNodes(run.id);
188
+ if (nodes.length === 0) {
189
+ this.store.updateRunStatus(run.id, "failed", "Planner returned no nodes");
190
+ return;
191
+ }
192
+ }
193
+
194
+ await this.executor.processRun(run);
195
+ }
196
+
197
+ async health(): Promise<{
198
+ openrouterConfigured: boolean;
199
+ smartSpawnApiReachable: boolean;
200
+ dbWritable: boolean;
201
+ artifactStorageWritable: boolean;
202
+ workerAlive: boolean;
203
+ }> {
204
+ const smart = await this.smartSpawn.health();
205
+ const dbWritable = this.store.pingWritable();
206
+
207
+ let artifactStorageWritable = false;
208
+ try {
209
+ await this.storage.ensure();
210
+ artifactStorageWritable = true;
211
+ } catch {
212
+ artifactStorageWritable = false;
213
+ }
214
+
215
+ return {
216
+ openrouterConfigured: Boolean(this.config.openRouterApiKey),
217
+ smartSpawnApiReachable: smart.reachable,
218
+ dbWritable,
219
+ artifactStorageWritable,
220
+ workerAlive: this.interval !== null,
221
+ };
222
+ }
223
+ }