ennodia 0.1.1 → 0.2.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.
Files changed (46) hide show
  1. package/CONTRIBUTING.md +60 -36
  2. package/README.md +74 -58
  3. package/bench/README.md +7 -5
  4. package/bench/bug-recall/run.ts +12 -4
  5. package/bin/ennodia +23 -2
  6. package/docs/assets/agents/mcp.svg +1 -1
  7. package/docs/assets/orchestration-pipeline.svg +4 -3
  8. package/docs/comparisons/agent-frameworks.md +15 -10
  9. package/docs/comparisons/autogen.md +20 -15
  10. package/docs/comparisons/chathub.md +15 -14
  11. package/docs/comparisons/index.md +30 -22
  12. package/docs/comparisons/langgraph.md +19 -12
  13. package/docs/comparisons/mixture-of-agents.md +20 -17
  14. package/docs/comparisons/model-merging.md +7 -5
  15. package/docs/comparisons/openrouter.md +30 -22
  16. package/docs/concepts/compositional-audits.md +16 -13
  17. package/docs/concepts/data-governance.md +31 -15
  18. package/docs/concepts/how-ennodia-works.md +48 -22
  19. package/docs/concepts/interfaces-and-core.md +19 -6
  20. package/docs/concepts/second-opinions.md +20 -20
  21. package/docs/getting-started.md +55 -27
  22. package/docs/guides/agent-skills.md +16 -13
  23. package/docs/guides/budgets-and-limits.md +30 -20
  24. package/docs/guides/running-better-audits.md +50 -44
  25. package/docs/index.md +60 -37
  26. package/docs/install.md +41 -32
  27. package/docs/reference/benchmarks.md +6 -4
  28. package/docs/reference/controlled-english.md +130 -0
  29. package/docs/reference/ennodia-io.md +28 -22
  30. package/docs/reference/mcp-tools.md +244 -87
  31. package/docs/reference/supported-harnesses.md +9 -9
  32. package/package.json +7 -5
  33. package/src/advisor.ts +685 -0
  34. package/src/budget.ts +25 -6
  35. package/src/cli.ts +1 -1
  36. package/src/compare.ts +248 -52
  37. package/src/compositional.ts +18 -0
  38. package/src/core.ts +547 -16
  39. package/src/harnesses.ts +33 -2
  40. package/src/index.ts +38 -0
  41. package/src/plan-advice.ts +601 -0
  42. package/src/runs.ts +74 -9
  43. package/src/server.ts +312 -46
  44. package/src/skills.ts +131 -11
  45. package/src/tasks.ts +192 -22
  46. package/src/version.ts +1 -1
@@ -0,0 +1,601 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ buildPlanAdvisorPrompt,
7
+ compileAdvisorExecutionPlan,
8
+ digestAdvisorExecutionPlan,
9
+ parseAdvisorPlanProposal,
10
+ type AdvisorExecutionPlan,
11
+ type AdvisorInventorySnapshot,
12
+ type AdvisorPlanIssue,
13
+ type AdvisorPlanProposal,
14
+ } from "./advisor";
15
+ import {
16
+ assertBudgetWithinLimits,
17
+ checkBudgetLimits,
18
+ estimateTaskBatchBudget,
19
+ type BudgetCheck,
20
+ type BudgetLimits,
21
+ } from "./budget";
22
+ import type { HarnessAdapter, HarnessDiscovery } from "./harnesses";
23
+ import { errorMessage, preview, tailItems } from "./internal";
24
+ import { TaskManager, type TaskView } from "./tasks";
25
+
26
+ export type PlanAdviceStatus =
27
+ | "advising"
28
+ | "ready"
29
+ | "consumed"
30
+ | "invalid"
31
+ | "failed"
32
+ | "cancelled";
33
+
34
+ export type PlanAdviceEvent = {
35
+ at: string;
36
+ type:
37
+ | "started"
38
+ | "advisor-succeeded"
39
+ | "plan-ready"
40
+ | "execution-authorized"
41
+ | "plan-invalid"
42
+ | "cleanup-warning"
43
+ | "failed"
44
+ | "cancelled";
45
+ message?: string;
46
+ };
47
+
48
+ export type PlanAdviceIssue = {
49
+ code: AdvisorPlanIssue["code"] | "execution-budget-exceeded" | "advisor-failed";
50
+ path: Array<string | number>;
51
+ message: string;
52
+ };
53
+
54
+ export type PlanAdviceView = {
55
+ id: string;
56
+ status: PlanAdviceStatus;
57
+ promptPreview: string;
58
+ advisorTaskId: string;
59
+ advisorHarnessId: string;
60
+ advisorModel?: string;
61
+ inventorySnapshotId: string;
62
+ createdAt: string;
63
+ updatedAt: string;
64
+ endedAt?: string;
65
+ elapsedMs: number;
66
+ remainingMs: number | null;
67
+ etaConfidence: "timeout-budget" | "unknown" | "complete";
68
+ proposal?: AdvisorPlanProposal;
69
+ plan?: AdvisorExecutionPlan;
70
+ planDigest?: string;
71
+ consumedAt?: string;
72
+ issues: PlanAdviceIssue[];
73
+ advisorBudget: BudgetCheck;
74
+ executionBudget?: BudgetCheck;
75
+ events: PlanAdviceEvent[];
76
+ };
77
+
78
+ export type PlanAdviceViewOptions = {
79
+ includeProposal?: boolean;
80
+ includePlan?: boolean;
81
+ includeEvents?: boolean;
82
+ maxEvents?: number;
83
+ };
84
+
85
+ export type PlanAdviceManagerOptions = {
86
+ maxAdvice?: number;
87
+ };
88
+
89
+ export type PlanAdviceManagerStartInput = {
90
+ prompt: string;
91
+ inventory: AdvisorInventorySnapshot;
92
+ inventoryCwd?: string;
93
+ advisor: {
94
+ adapter: HarnessAdapter;
95
+ discovery: HarnessDiscovery;
96
+ };
97
+ advisorModel?: string;
98
+ timeoutMs?: number;
99
+ budget?: BudgetLimits;
100
+ };
101
+
102
+ export type PlanAdviceRevalidationContext = {
103
+ prompt: string;
104
+ inventoryCwd?: string;
105
+ inventory: AdvisorInventorySnapshot;
106
+ };
107
+
108
+ type InternalPlanAdvice = {
109
+ id: string;
110
+ status: PlanAdviceStatus;
111
+ prompt: string;
112
+ inventoryCwd?: string;
113
+ inventory: AdvisorInventorySnapshot;
114
+ advisorTaskId: string;
115
+ advisorScratchCwd: string;
116
+ advisorHarnessId: string;
117
+ advisorModel?: string;
118
+ advisorTimeoutMs: number;
119
+ budgetLimits?: BudgetLimits;
120
+ advisorBudget: BudgetCheck;
121
+ executionBudget?: BudgetCheck;
122
+ proposal?: AdvisorPlanProposal;
123
+ plan?: AdvisorExecutionPlan;
124
+ planDigest?: string;
125
+ consumedAtMs?: number;
126
+ issues: PlanAdviceIssue[];
127
+ createdAtMs: number;
128
+ updatedAtMs: number;
129
+ endedAtMs?: number;
130
+ events: PlanAdviceEvent[];
131
+ settled?: Promise<void>;
132
+ };
133
+
134
+ const DEFAULT_MAX_ADVICE = 100;
135
+ const DEFAULT_ADVISOR_TIMEOUT_MS = 5 * 60 * 1_000;
136
+ const DEFAULT_MAX_EVENTS = 100;
137
+ const DEFAULT_SHUTDOWN_DEADLINE_MS = 5_000;
138
+
139
+ /**
140
+ * Owns the one-task Plan Advisor lifecycle. It can compile and freeze a plan,
141
+ * but it has no worker execution capability; execution remains a separate
142
+ * EnnodiaCore operation guarded by the returned digest.
143
+ */
144
+ export class PlanAdviceManager {
145
+ private readonly advice = new Map<string, InternalPlanAdvice>();
146
+ private readonly maxAdvice: number;
147
+ private shuttingDown = false;
148
+ private shutdownPromise?: Promise<void>;
149
+
150
+ constructor(
151
+ private readonly taskManager: TaskManager,
152
+ options: PlanAdviceManagerOptions = {},
153
+ ) {
154
+ this.maxAdvice = Math.max(1, options.maxAdvice ?? DEFAULT_MAX_ADVICE);
155
+ }
156
+
157
+ start(input: PlanAdviceManagerStartInput): PlanAdviceView {
158
+ if (this.shuttingDown) {
159
+ throw new Error("PlanAdviceManager is shutting down.");
160
+ }
161
+
162
+ const advisorPrompt = buildPlanAdvisorPrompt(input.prompt, input.inventory);
163
+ const advisorBudget = checkBudgetLimits(
164
+ estimateTaskBatchBudget({
165
+ tasks: [{
166
+ prompt: advisorPrompt,
167
+ harnessId: input.advisor.adapter.id,
168
+ }],
169
+ comparePlanned: false,
170
+ }),
171
+ input.budget,
172
+ );
173
+ assertBudgetWithinLimits(advisorBudget);
174
+
175
+ // The Advisor receives the closed inventory snapshot, not the target
176
+ // workspace. A fresh empty cwd also avoids accidentally inheriting the
177
+ // MCP server process's project directory.
178
+ const advisorScratchCwd = mkdtempSync(
179
+ join(tmpdir(), "ennodia-plan-advisor-"),
180
+ );
181
+ let advisorTask: TaskView;
182
+ try {
183
+ advisorTask = this.taskManager.start(
184
+ forceAdapterCwd(input.advisor.adapter, advisorScratchCwd),
185
+ input.advisor.discovery,
186
+ {
187
+ prompt: advisorPrompt,
188
+ cwd: advisorScratchCwd,
189
+ model: input.advisorModel,
190
+ timeoutMs: input.timeoutMs,
191
+ },
192
+ ).task;
193
+ } catch (error) {
194
+ rmSync(advisorScratchCwd, { recursive: true, force: true });
195
+ throw error;
196
+ }
197
+ const now = Date.now();
198
+ const record: InternalPlanAdvice = {
199
+ id: randomUUID(),
200
+ status: "advising",
201
+ prompt: input.prompt,
202
+ inventoryCwd: input.inventoryCwd,
203
+ inventory: structuredClone(input.inventory),
204
+ advisorTaskId: advisorTask.id,
205
+ advisorScratchCwd,
206
+ advisorHarnessId: input.advisor.adapter.id,
207
+ advisorModel: input.advisorModel,
208
+ advisorTimeoutMs: advisorTask.timeoutMs ??
209
+ input.timeoutMs ?? DEFAULT_ADVISOR_TIMEOUT_MS,
210
+ budgetLimits: input.budget ? { ...input.budget } : undefined,
211
+ advisorBudget,
212
+ issues: [],
213
+ createdAtMs: now,
214
+ updatedAtMs: now,
215
+ events: [],
216
+ };
217
+
218
+ this.advice.set(record.id, record);
219
+ this.prune(record.id);
220
+ this.pushEvent(record, {
221
+ type: "started",
222
+ message: `Plan Advisor task started through ${advisorTask.harnessName}.`,
223
+ });
224
+ record.settled = this.run(record);
225
+ void record.settled;
226
+
227
+ return this.toView(record);
228
+ }
229
+
230
+ listViews(options: PlanAdviceViewOptions = {}): PlanAdviceView[] {
231
+ return [...this.advice.values()]
232
+ .sort((left, right) => right.createdAtMs - left.createdAtMs)
233
+ .map((record) => this.toView(record, options));
234
+ }
235
+
236
+ get(
237
+ id: string,
238
+ options: PlanAdviceViewOptions = {},
239
+ ): PlanAdviceView | undefined {
240
+ const record = this.advice.get(id);
241
+ return record ? this.toView(record, options) : undefined;
242
+ }
243
+
244
+ async waitForTerminal(
245
+ id: string,
246
+ timeoutMs?: number,
247
+ options: PlanAdviceViewOptions = {},
248
+ ): Promise<PlanAdviceView | undefined> {
249
+ const record = this.advice.get(id);
250
+ if (!record) {
251
+ return undefined;
252
+ }
253
+
254
+ if (!isTerminal(record.status) && record.settled) {
255
+ await settleWithDeadline(record.settled, timeoutMs);
256
+ }
257
+
258
+ return this.toView(record, options);
259
+ }
260
+
261
+ cancel(id: string): PlanAdviceView {
262
+ const record = this.require(id);
263
+ if (isTerminal(record.status)) {
264
+ return this.toView(record);
265
+ }
266
+
267
+ record.status = "cancelled";
268
+ record.endedAtMs = Date.now();
269
+ this.taskManager.cancel(record.advisorTaskId);
270
+ this.pushEvent(record, {
271
+ type: "cancelled",
272
+ message: "Plan advice cancelled.",
273
+ });
274
+ return this.toView(record);
275
+ }
276
+
277
+ getRevalidationContext(id: string): PlanAdviceRevalidationContext {
278
+ const record = this.require(id);
279
+ return {
280
+ prompt: record.prompt,
281
+ inventoryCwd: record.inventoryCwd,
282
+ inventory: structuredClone(record.inventory),
283
+ };
284
+ }
285
+
286
+ authorizeExecution(
287
+ id: string,
288
+ expectedPlanDigest: string,
289
+ currentInventory: AdvisorInventorySnapshot,
290
+ validate?: (plan: AdvisorExecutionPlan) => void,
291
+ ): AdvisorExecutionPlan {
292
+ const record = this.require(id);
293
+ if (record.status === "consumed") {
294
+ throw new Error(`Plan advice has already been executed: ${id}.`);
295
+ }
296
+ if (record.status !== "ready" || !record.proposal || !record.planDigest) {
297
+ throw new Error(`Plan advice is not ready for execution: ${id}.`);
298
+ }
299
+ if (expectedPlanDigest !== record.planDigest) {
300
+ throw new Error("Advised plan digest mismatch; no tasks were started.");
301
+ }
302
+ if (currentInventory.snapshotId !== record.inventory.snapshotId) {
303
+ throw new Error(
304
+ "Advisor inventory changed after the plan was proposed; request new advice before executing.",
305
+ );
306
+ }
307
+
308
+ const compiled = compileAdvisorExecutionPlan(
309
+ record.proposal,
310
+ currentInventory,
311
+ );
312
+ if (!compiled.compiled) {
313
+ throw new Error(
314
+ `Advised plan no longer validates: ${
315
+ compiled.issues.map((issue) => issue.message).join(" ")
316
+ }`,
317
+ );
318
+ }
319
+ const currentDigest = digestAdvisorExecutionPlan(compiled.plan);
320
+ if (currentDigest !== record.planDigest) {
321
+ throw new Error("Advised plan changed after approval; no tasks were started.");
322
+ }
323
+
324
+ // Keep this final state change after every caller-supplied validation. The
325
+ // callback is synchronous, so no second authorization can interleave.
326
+ validate?.(structuredClone(compiled.plan));
327
+ record.status = "consumed";
328
+ record.consumedAtMs = Date.now();
329
+ record.endedAtMs = record.consumedAtMs;
330
+ this.pushEvent(record, {
331
+ type: "execution-authorized",
332
+ message: "Validated plan authorized for one execution.",
333
+ });
334
+
335
+ return structuredClone(compiled.plan);
336
+ }
337
+
338
+ async shutdown(options: { deadlineMs?: number } = {}): Promise<void> {
339
+ this.shuttingDown = true;
340
+ this.shutdownPromise ??= this.performShutdown(
341
+ options.deadlineMs ?? DEFAULT_SHUTDOWN_DEADLINE_MS,
342
+ );
343
+ await this.shutdownPromise;
344
+ }
345
+
346
+ private async run(record: InternalPlanAdvice): Promise<void> {
347
+ try {
348
+ const task = await this.taskManager.waitForTerminal(record.advisorTaskId);
349
+ if (record.status === "cancelled") {
350
+ return;
351
+ }
352
+ if (!task || task.status !== "succeeded") {
353
+ this.fail(
354
+ record,
355
+ "advisor-failed",
356
+ taskFailureMessage(task),
357
+ );
358
+ return;
359
+ }
360
+ this.pushEvent(record, {
361
+ type: "advisor-succeeded",
362
+ message: "Plan Advisor task succeeded; validating its response.",
363
+ });
364
+
365
+ const raw = (task.finalMessage ?? task.stdout).trim();
366
+ const parsed = parseAdvisorPlanProposal(raw);
367
+ if (!parsed.success) {
368
+ this.invalidate(record, parsed.issues);
369
+ return;
370
+ }
371
+ record.proposal = parsed.proposal;
372
+
373
+ const compiled = compileAdvisorExecutionPlan(
374
+ parsed.proposal,
375
+ record.inventory,
376
+ );
377
+ if (!compiled.compiled) {
378
+ this.invalidate(record, compiled.issues);
379
+ return;
380
+ }
381
+
382
+ const executionBudget = checkBudgetLimits(
383
+ estimateTaskBatchBudget({
384
+ tasks: compiled.plan.slices.map((slice) => ({
385
+ prompt: slice.prompt,
386
+ harnessId: slice.harnessId,
387
+ })),
388
+ comparePlanned: false,
389
+ }),
390
+ record.budgetLimits,
391
+ );
392
+ record.executionBudget = executionBudget;
393
+ if (executionBudget.exceeded) {
394
+ this.invalidate(
395
+ record,
396
+ executionBudget.issues.map((message) => ({
397
+ code: "execution-budget-exceeded" as const,
398
+ path: ["plan", "budget"],
399
+ message,
400
+ })),
401
+ );
402
+ return;
403
+ }
404
+
405
+ record.plan = compiled.plan;
406
+ record.planDigest = digestAdvisorExecutionPlan(compiled.plan);
407
+ record.status = "ready";
408
+ record.endedAtMs = Date.now();
409
+ this.pushEvent(record, {
410
+ type: "plan-ready",
411
+ message: `Validated and froze ${compiled.plan.counts.sliceCount} plan slice(s).`,
412
+ });
413
+ } catch (error) {
414
+ if (record.status !== "cancelled") {
415
+ this.fail(record, "advisor-failed", errorMessage(error));
416
+ }
417
+ } finally {
418
+ try {
419
+ rmSync(record.advisorScratchCwd, { recursive: true, force: true });
420
+ } catch (error) {
421
+ this.pushEvent(record, {
422
+ type: "cleanup-warning",
423
+ message: `Could not remove Plan Advisor scratch directory: ${
424
+ errorMessage(error)
425
+ }`,
426
+ });
427
+ }
428
+ record.updatedAtMs = Date.now();
429
+ this.prune(record.id);
430
+ }
431
+ }
432
+
433
+ private invalidate(
434
+ record: InternalPlanAdvice,
435
+ issues: PlanAdviceIssue[] | AdvisorPlanIssue[],
436
+ ): void {
437
+ record.status = "invalid";
438
+ record.issues = issues.map((issue) => ({ ...issue }));
439
+ record.endedAtMs = Date.now();
440
+ this.pushEvent(record, {
441
+ type: "plan-invalid",
442
+ message: `Plan Advisor response was rejected with ${issues.length} issue(s).`,
443
+ });
444
+ }
445
+
446
+ private fail(
447
+ record: InternalPlanAdvice,
448
+ code: "advisor-failed",
449
+ message: string,
450
+ ): void {
451
+ record.status = "failed";
452
+ record.issues = [{ code, path: ["advisorTask"], message }];
453
+ record.endedAtMs = Date.now();
454
+ this.pushEvent(record, { type: "failed", message });
455
+ }
456
+
457
+ private require(id: string): InternalPlanAdvice {
458
+ const record = this.advice.get(id);
459
+ if (!record) {
460
+ throw new Error(`Unknown plan advice: ${id}`);
461
+ }
462
+ return record;
463
+ }
464
+
465
+ private toView(
466
+ record: InternalPlanAdvice,
467
+ options: PlanAdviceViewOptions = {},
468
+ ): PlanAdviceView {
469
+ const now = Date.now();
470
+ const end = record.endedAtMs ?? now;
471
+ const elapsedMs = Math.max(0, end - record.createdAtMs);
472
+ const running = record.status === "advising";
473
+ const maxEvents = options.maxEvents ?? DEFAULT_MAX_EVENTS;
474
+
475
+ return {
476
+ id: record.id,
477
+ status: record.status,
478
+ promptPreview: preview(record.prompt),
479
+ advisorTaskId: record.advisorTaskId,
480
+ advisorHarnessId: record.advisorHarnessId,
481
+ advisorModel: record.advisorModel,
482
+ inventorySnapshotId: record.inventory.snapshotId,
483
+ createdAt: new Date(record.createdAtMs).toISOString(),
484
+ updatedAt: new Date(record.updatedAtMs).toISOString(),
485
+ endedAt: record.endedAtMs
486
+ ? new Date(record.endedAtMs).toISOString()
487
+ : undefined,
488
+ elapsedMs,
489
+ remainingMs: running
490
+ ? Math.max(0, record.advisorTimeoutMs - elapsedMs)
491
+ : 0,
492
+ etaConfidence: running ? "timeout-budget" : "complete",
493
+ proposal: (options.includeProposal ?? true) && record.proposal
494
+ ? structuredClone(record.proposal)
495
+ : undefined,
496
+ plan: (options.includePlan ?? true) && record.plan
497
+ ? structuredClone(record.plan)
498
+ : undefined,
499
+ planDigest: record.planDigest,
500
+ consumedAt: record.consumedAtMs
501
+ ? new Date(record.consumedAtMs).toISOString()
502
+ : undefined,
503
+ issues: record.issues.map((issue) => ({ ...issue, path: [...issue.path] })),
504
+ advisorBudget: structuredClone(record.advisorBudget),
505
+ executionBudget: record.executionBudget
506
+ ? structuredClone(record.executionBudget)
507
+ : undefined,
508
+ events: options.includeEvents ?? true
509
+ ? tailItems(record.events, maxEvents).map((event) => ({ ...event }))
510
+ : [],
511
+ };
512
+ }
513
+
514
+ private pushEvent(
515
+ record: InternalPlanAdvice,
516
+ event: Omit<PlanAdviceEvent, "at">,
517
+ ): void {
518
+ record.events.push({ at: new Date().toISOString(), ...event });
519
+ record.updatedAtMs = Date.now();
520
+ }
521
+
522
+ private prune(protectedId?: string): void {
523
+ const overflow = this.advice.size - this.maxAdvice;
524
+ if (overflow <= 0) {
525
+ return;
526
+ }
527
+
528
+ const removable = [...this.advice.values()]
529
+ .filter((record) =>
530
+ record.id !== protectedId && isTerminal(record.status)
531
+ )
532
+ .sort((left, right) => left.createdAtMs - right.createdAtMs);
533
+ for (const record of removable.slice(0, overflow)) {
534
+ this.advice.delete(record.id);
535
+ }
536
+ }
537
+
538
+ private async performShutdown(deadlineMs: number): Promise<void> {
539
+ const pending: Promise<void>[] = [];
540
+ for (const record of this.advice.values()) {
541
+ if (isTerminal(record.status)) {
542
+ continue;
543
+ }
544
+ this.cancel(record.id);
545
+ if (record.settled) {
546
+ pending.push(settleWithDeadline(record.settled, deadlineMs));
547
+ }
548
+ }
549
+ await Promise.allSettled(pending);
550
+ }
551
+ }
552
+
553
+ function isTerminal(status: PlanAdviceStatus): boolean {
554
+ return status !== "advising";
555
+ }
556
+
557
+ function taskFailureMessage(task: TaskView | undefined): string {
558
+ if (!task) {
559
+ return "Plan Advisor task disappeared before its result was collected.";
560
+ }
561
+ const detail = task.stderr.trim();
562
+ return detail
563
+ ? `Plan Advisor task ${task.status}: ${detail}`
564
+ : `Plan Advisor task ${task.status}.`;
565
+ }
566
+
567
+ function forceAdapterCwd(
568
+ adapter: HarnessAdapter,
569
+ cwd: string,
570
+ ): HarnessAdapter {
571
+ const buildCommand = adapter.buildCommand;
572
+ if (!buildCommand) {
573
+ return adapter;
574
+ }
575
+
576
+ return {
577
+ ...adapter,
578
+ buildCommand: (commandPath, input) => ({
579
+ ...buildCommand.call(adapter, commandPath, input),
580
+ cwd,
581
+ }),
582
+ };
583
+ }
584
+
585
+ async function settleWithDeadline(
586
+ settled: Promise<void>,
587
+ timeoutMs?: number,
588
+ ): Promise<void> {
589
+ if (timeoutMs === undefined) {
590
+ await settled;
591
+ return;
592
+ }
593
+
594
+ await new Promise<void>((resolve) => {
595
+ const timer = setTimeout(resolve, timeoutMs);
596
+ void settled.finally(() => {
597
+ clearTimeout(timer);
598
+ resolve();
599
+ });
600
+ });
601
+ }