devharness 0.9.3 → 0.9.4

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/build/block-events.d.ts +42 -0
  2. package/build/block-events.d.ts.map +1 -0
  3. package/build/block-events.js +67 -0
  4. package/build/block-events.js.map +1 -0
  5. package/build/block-events.test.d.ts +2 -0
  6. package/build/block-events.test.d.ts.map +1 -0
  7. package/build/block-events.test.js +131 -0
  8. package/build/block-events.test.js.map +1 -0
  9. package/build/index.js +33 -2
  10. package/build/index.js.map +1 -1
  11. package/build/server-block-cleanup.test.d.ts +2 -0
  12. package/build/server-block-cleanup.test.d.ts.map +1 -0
  13. package/build/server-block-cleanup.test.js +143 -0
  14. package/build/server-block-cleanup.test.js.map +1 -0
  15. package/build/server-manager.d.ts +8 -0
  16. package/build/server-manager.d.ts.map +1 -1
  17. package/build/server-manager.js +49 -0
  18. package/build/server-manager.js.map +1 -1
  19. package/build/startup-gate.d.ts +29 -0
  20. package/build/startup-gate.d.ts.map +1 -0
  21. package/build/startup-gate.js +49 -0
  22. package/build/startup-gate.js.map +1 -0
  23. package/build/startup-gate.test.d.ts +2 -0
  24. package/build/startup-gate.test.d.ts.map +1 -0
  25. package/build/startup-gate.test.js +73 -0
  26. package/build/startup-gate.test.js.map +1 -0
  27. package/build/tool-response.d.ts +3 -0
  28. package/build/tool-response.d.ts.map +1 -1
  29. package/build/tool-response.js +46 -1
  30. package/build/tool-response.js.map +1 -1
  31. package/build/tools/assert-tools.d.ts +3 -3
  32. package/build/tools/breakpoint-tools.d.ts +3 -3
  33. package/build/tools/config-tools.d.ts +3 -3
  34. package/build/tools/console-tools.d.ts +9 -9
  35. package/build/tools/content-tools.d.ts +3 -3
  36. package/build/tools/input-tools.d.ts +9 -9
  37. package/build/tools/issues-tools.d.ts +6 -6
  38. package/build/tools/page-tools.d.ts +3 -3
  39. package/build/tools/replay-tools.d.ts +9 -9
  40. package/build/tools/request-tools.d.ts +3 -3
  41. package/build/tools/server-tools.d.ts +9 -9
  42. package/build/tools/storage-tools.d.ts +3 -3
  43. package/build/tools/wait-tools.d.ts +6 -6
  44. package/docs/instructions.md +23 -0
  45. package/package.json +1 -1
  46. package/plugin/skills/devharness/SKILL.md +18 -1
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Block event stream - one JSON line per new guard block, appended to
3
+ * .devharness/logs/blocks.jsonl.
4
+ *
5
+ * Guards only surface on the *next* tool call, so a session doing unrelated
6
+ * work never learns its dev server died until it happens to call a devharness
7
+ * tool again. This file is the push side of that: a Claude Code Monitor can
8
+ * `tail -f` it and get notified the moment a block appears.
9
+ *
10
+ * Deduplicated by key - a block that keeps firing on every subsequent tool call
11
+ * writes one line, not one per call. The active set clears once a call gets
12
+ * through all guards.
13
+ */
14
+ export type BlockGuard = 'port' | 'breakpoint' | 'pendingStartup' | 'bug' | 'duplicateSession';
15
+ /**
16
+ * Identity and summary of a block, attached to the BlockingResponse that
17
+ * carries it so index.ts can log it without re-deriving the reason.
18
+ */
19
+ export interface BlockEventInfo {
20
+ guard: BlockGuard;
21
+ /** Dedupe identity - same key while the same block persists. */
22
+ key: string;
23
+ /** One-line summary of what is blocked. */
24
+ detail: string;
25
+ /** The call that clears it. */
26
+ resolve: string;
27
+ }
28
+ /**
29
+ * Append a block event if this block is new. No-op for a block already logged.
30
+ */
31
+ export declare function recordBlockEvent(info: BlockEventInfo | undefined, tool: string): Promise<void>;
32
+ /**
33
+ * Forget every active block. Called when a tool call clears all guards, so a
34
+ * block that recurs later gets logged again.
35
+ */
36
+ export declare function clearBlockEvents(): void;
37
+ /**
38
+ * Test/debug helper - which blocks are currently deduped out.
39
+ */
40
+ export declare function getActiveBlockKeys(): string[];
41
+ export declare function getBlockEventFilePath(): string;
42
+ //# sourceMappingURL=block-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-events.d.ts","sourceRoot":"","sources":["../src/block-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,YAAY,GACZ,gBAAgB,GAChB,KAAK,GACL,kBAAkB,CAAC;AAEvB;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,gEAAgE;IAChE,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;CACjB;AAUD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,cAAc,GAAG,SAAS,EAChC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAE7C;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Block event stream - one JSON line per new guard block, appended to
3
+ * .devharness/logs/blocks.jsonl.
4
+ *
5
+ * Guards only surface on the *next* tool call, so a session doing unrelated
6
+ * work never learns its dev server died until it happens to call a devharness
7
+ * tool again. This file is the push side of that: a Claude Code Monitor can
8
+ * `tail -f` it and get notified the moment a block appears.
9
+ *
10
+ * Deduplicated by key - a block that keeps firing on every subsequent tool call
11
+ * writes one line, not one per call. The active set clears once a call gets
12
+ * through all guards.
13
+ */
14
+ import { promises as fs } from 'fs';
15
+ import { join } from 'path';
16
+ import { getOutputPath } from './helpers/paths.js';
17
+ // Keys currently blocking. Written once per key, cleared when a tool call
18
+ // passes every guard.
19
+ const activeKeys = new Set();
20
+ function getBlockFile() {
21
+ return join(getOutputPath('logs'), 'blocks.jsonl');
22
+ }
23
+ /**
24
+ * Append a block event if this block is new. No-op for a block already logged.
25
+ */
26
+ export async function recordBlockEvent(info, tool) {
27
+ if (!info)
28
+ return;
29
+ const dedupeKey = `${info.guard}:${info.key}`;
30
+ if (activeKeys.has(dedupeKey)) {
31
+ return;
32
+ }
33
+ activeKeys.add(dedupeKey);
34
+ const line = JSON.stringify({
35
+ ts: new Date().toISOString(),
36
+ guard: info.guard,
37
+ tool,
38
+ detail: info.detail,
39
+ resolve: info.resolve,
40
+ });
41
+ try {
42
+ const dir = getOutputPath('logs');
43
+ await fs.mkdir(dir, { recursive: true });
44
+ await fs.appendFile(getBlockFile(), line + '\n');
45
+ }
46
+ catch (error) {
47
+ // Never let the event stream break the block itself
48
+ console.error(`[BlockEvents] Failed to write block event: ${error}`);
49
+ }
50
+ }
51
+ /**
52
+ * Forget every active block. Called when a tool call clears all guards, so a
53
+ * block that recurs later gets logged again.
54
+ */
55
+ export function clearBlockEvents() {
56
+ activeKeys.clear();
57
+ }
58
+ /**
59
+ * Test/debug helper - which blocks are currently deduped out.
60
+ */
61
+ export function getActiveBlockKeys() {
62
+ return [...activeKeys];
63
+ }
64
+ export function getBlockEventFilePath() {
65
+ return getBlockFile();
66
+ }
67
+ //# sourceMappingURL=block-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-events.js","sourceRoot":"","sources":["../src/block-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAuBnD,0EAA0E;AAC1E,sBAAsB;AACtB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;AAErC,SAAS,YAAY;IACnB,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC,CAAC;AACrD,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAgC,EAChC,IAAY;IAEZ,IAAI,CAAC,IAAI;QAAE,OAAO;IAElB,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAC9C,IAAI,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO;IACT,CAAC;IACD,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAE1B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;QAC1B,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAC5B,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QAClC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,MAAM,EAAE,CAAC,UAAU,CAAC,YAAY,EAAE,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,oDAAoD;QACpD,OAAO,CAAC,KAAK,CAAC,8CAA8C,KAAK,EAAE,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=block-events.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-events.test.d.ts","sourceRoot":"","sources":["../src/block-events.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * blocks.jsonl is what a Claude Code Monitor tails, so a notification storm is
3
+ * the failure mode that matters: guards re-fire on every subsequent tool call,
4
+ * and a naive append would emit one line per blocked call instead of one per
5
+ * block. These cover the dedupe, the clear-on-unblocked reset, and that each
6
+ * guard actually attaches the info the line is built from.
7
+ */
8
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9
+ import { mkdtempSync, rmSync, existsSync, readFileSync } from 'fs';
10
+ import { tmpdir } from 'os';
11
+ import { join } from 'path';
12
+ import { initializePaths } from './helpers/paths.js';
13
+ import { recordBlockEvent, clearBlockEvents, getBlockEventFilePath, } from './block-events.js';
14
+ import { checkPortFailures, checkPendingStartups, checkDuplicateSession, } from './tool-response.js';
15
+ let workDir;
16
+ let originalCwd;
17
+ let originalGlobalDir;
18
+ const portBlock = {
19
+ guard: 'port',
20
+ key: '3000',
21
+ detail: 'Monitored port(s) down: 3000 (web)',
22
+ resolve: `server({ action: 'acknowledgePort', port: 3000 })`,
23
+ };
24
+ function lines() {
25
+ const path = getBlockEventFilePath();
26
+ if (!existsSync(path))
27
+ return [];
28
+ return readFileSync(path, 'utf-8')
29
+ .split('\n')
30
+ .filter(l => l.trim())
31
+ .map(l => JSON.parse(l));
32
+ }
33
+ beforeEach(() => {
34
+ originalCwd = process.cwd();
35
+ workDir = mkdtempSync(join(tmpdir(), 'cdp-block-events-test-'));
36
+ process.chdir(workDir);
37
+ originalGlobalDir = process.env.CDP_TOOLS_DIR;
38
+ process.env.CDP_TOOLS_DIR = join(workDir, 'global');
39
+ initializePaths();
40
+ clearBlockEvents();
41
+ });
42
+ afterEach(() => {
43
+ clearBlockEvents();
44
+ process.chdir(originalCwd);
45
+ if (originalGlobalDir === undefined)
46
+ delete process.env.CDP_TOOLS_DIR;
47
+ else
48
+ process.env.CDP_TOOLS_DIR = originalGlobalDir;
49
+ initializePaths();
50
+ rmSync(workDir, { recursive: true, force: true });
51
+ });
52
+ describe('block event stream', () => {
53
+ it('writes one line per block, not per blocked call', async () => {
54
+ await recordBlockEvent(portBlock, 'navigate');
55
+ await recordBlockEvent(portBlock, 'screenshot');
56
+ await recordBlockEvent(portBlock, 'content');
57
+ expect(lines()).toHaveLength(1);
58
+ expect(lines()[0]).toMatchObject({
59
+ guard: 'port',
60
+ tool: 'navigate',
61
+ detail: 'Monitored port(s) down: 3000 (web)',
62
+ });
63
+ });
64
+ it('logs the same block again after a call clears every guard', async () => {
65
+ await recordBlockEvent(portBlock, 'navigate');
66
+ clearBlockEvents();
67
+ await recordBlockEvent(portBlock, 'navigate');
68
+ expect(lines()).toHaveLength(2);
69
+ });
70
+ it('treats a different block as a new event while the first is still active', async () => {
71
+ await recordBlockEvent(portBlock, 'navigate');
72
+ await recordBlockEvent({ ...portBlock, key: '3000,5173', detail: 'Monitored port(s) down: 3000, 5173' }, 'navigate');
73
+ expect(lines()).toHaveLength(2);
74
+ });
75
+ it('separates guards that happen to share a key', async () => {
76
+ await recordBlockEvent({ ...portBlock, guard: 'port', key: 'web' }, 'navigate');
77
+ await recordBlockEvent({ ...portBlock, guard: 'pendingStartup', key: 'web' }, 'navigate');
78
+ expect(lines().map(l => l.guard)).toEqual(['port', 'pendingStartup']);
79
+ });
80
+ it('does nothing when a guard supplies no block info', async () => {
81
+ await recordBlockEvent(undefined, 'navigate');
82
+ expect(lines()).toHaveLength(0);
83
+ });
84
+ });
85
+ describe('guards attach block info', () => {
86
+ it('port failure', () => {
87
+ const failed = [
88
+ { port: 3000, level: 'block', failedAt: new Date('2026-01-01T00:00:00Z'), description: 'web' },
89
+ ];
90
+ const result = checkPortFailures(failed, 'navigate');
91
+ expect(result.blocked).toBe(true);
92
+ if (!result.blocked)
93
+ return;
94
+ expect(result.block?.guard).toBe('port');
95
+ expect(result.block?.key).toBe('3000');
96
+ expect(result.block?.resolve).toContain('acknowledgePort');
97
+ });
98
+ it('pending startup failure', () => {
99
+ const failures = [
100
+ { serverId: 'web', reason: 'died', startedAt: new Date('2026-01-01T00:00:00Z') },
101
+ ];
102
+ const result = checkPendingStartups(failures, 'navigate');
103
+ expect(result.blocked).toBe(true);
104
+ if (!result.blocked)
105
+ return;
106
+ expect(result.block?.guard).toBe('pendingStartup');
107
+ expect(result.block?.detail).toContain('died before port detected');
108
+ expect(result.block?.resolve).toContain('acknowledgeStartup');
109
+ });
110
+ it('duplicate session, from both sides', () => {
111
+ const base = {
112
+ sessionId: 'abc-123',
113
+ shortId: 'abc',
114
+ allPids: [10, 20],
115
+ allPpids: [100, 200],
116
+ };
117
+ const original = checkDuplicateSession({ ...base, currentPid: 10, currentPpid: 100 }, 'navigate');
118
+ expect(original.blocked).toBe(true);
119
+ if (original.blocked) {
120
+ expect(original.block?.key).toBe('original:abc-123');
121
+ expect(original.block?.resolve).toBe('kill 200');
122
+ }
123
+ const duplicate = checkDuplicateSession({ ...base, currentPid: 20, currentPpid: 200 }, 'navigate');
124
+ expect(duplicate.blocked).toBe(true);
125
+ if (duplicate.blocked) {
126
+ expect(duplicate.block?.key).toBe('duplicate:abc-123');
127
+ expect(duplicate.block?.resolve).toContain('--fork-session');
128
+ }
129
+ });
130
+ });
131
+ //# sourceMappingURL=block-events.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-events.test.js","sourceRoot":"","sources":["../src/block-events.test.ts"],"names":[],"mappings":"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"}
package/build/index.js CHANGED
@@ -55,6 +55,8 @@ import { ServerManager, detectAutoRestartCommand } from './server-manager.js';
55
55
  import { configManager } from './config.js';
56
56
  import { ToolError } from './tool-error.js';
57
57
  import { checkPortFailures, checkBreakpointPause, checkBugBlocking, checkPendingStartups, checkDuplicateSession, prependToResponse, appendToResponse, buildStatusSuffix } from './tool-response.js';
58
+ import { recordBlockEvent, clearBlockEvents } from './block-events.js';
59
+ import { createStartupGate } from './startup-gate.js';
58
60
  import { createSuccessResponse, createErrorResponse } from './messages.js';
59
61
  import { setChromeLauncher } from './error-helpers.js';
60
62
  import { createServer } from 'net';
@@ -83,6 +85,11 @@ const SERVER_VERSION = (() => {
83
85
  return '0.0.0';
84
86
  }
85
87
  })();
88
+ /** Tool calls wait on this until serverManager.initialize() has restored state. */
89
+ const startupGate = createStartupGate({
90
+ timeoutMs: 30_000,
91
+ onTimeout: (ms) => console.error(`[devharness] Startup recovery still running after ${ms}ms - serving tools anyway`),
92
+ });
86
93
  /**
87
94
  * Which build is actually answering, so a session can tell whether the code it
88
95
  * is calling is the code it just compiled.
@@ -1356,6 +1363,15 @@ function registerToolHandlers(server) {
1356
1363
  isError: true
1357
1364
  };
1358
1365
  }
1366
+ // The transport starts serving before serverManager.initialize() has
1367
+ // restored state, so a call landing in that window sees an empty world: a
1368
+ // dead server's guard silently passes, and `acknowledgeStartup` in there
1369
+ // acknowledges nothing and reverts as soon as recovery lands. Wait for it.
1370
+ // `config` is exempt: it reads no server state, and `config restart` is the
1371
+ // escape hatch you want available precisely when recovery is what's stuck.
1372
+ if (toolName !== 'config') {
1373
+ await startupGate.wait();
1374
+ }
1359
1375
  // Check for tool dependency conflicts (blocks ALL tools except config)
1360
1376
  if (toolName !== 'config' && configManager.hasDependencyConflicts()) {
1361
1377
  const conflicts = configManager.getDependencyConflicts();
@@ -1399,31 +1415,38 @@ Edit ${configPath} to resolve, then restart the MCP server.`,
1399
1415
  const failedPorts = portMonitor.getFailedPorts();
1400
1416
  const portCheck = checkPortFailures(failedPorts, toolName);
1401
1417
  if (portCheck.blocked) {
1418
+ await recordBlockEvent(portCheck.block, toolName);
1402
1419
  return portCheck.response;
1403
1420
  }
1404
1421
  // Check for breakpoint pauses (block tools until acknowledged or resumed)
1405
1422
  const allConnections = connectionManager.getAllConnections();
1406
1423
  const breakpointCheck = checkBreakpointPause(allConnections, toolName, (port) => serverManager.getPendingRestartByInspectorPort(port), validation.data?.action);
1407
1424
  if (breakpointCheck.blocked) {
1425
+ await recordBlockEvent(breakpointCheck.block, toolName);
1408
1426
  return breakpointCheck.response;
1409
1427
  }
1410
1428
  // Check for pending startup failures (block tools until acknowledged)
1411
1429
  const pendingStartupFailures = serverManager.getPendingStartupFailures();
1412
1430
  const pendingStartupCheck = checkPendingStartups(pendingStartupFailures, toolName);
1413
1431
  if (pendingStartupCheck.blocked) {
1432
+ await recordBlockEvent(pendingStartupCheck.block, toolName);
1414
1433
  return pendingStartupCheck.response;
1415
1434
  }
1416
1435
  // Check for blocking bugs from recordings
1417
1436
  const bugCheck = await checkBugBlocking(toolName, validation.data);
1418
1437
  if (bugCheck.blocked) {
1438
+ await recordBlockEvent(bugCheck.block, toolName);
1419
1439
  return bugCheck.response;
1420
1440
  }
1421
1441
  // Check for duplicate session (multiple MCPs for same Claude session)
1422
1442
  const duplicateInfo = getDuplicateSessionInfo();
1423
1443
  const duplicateCheck = checkDuplicateSession(duplicateInfo, toolName);
1424
1444
  if (duplicateCheck.blocked) {
1445
+ await recordBlockEvent(duplicateCheck.block, toolName);
1425
1446
  return duplicateCheck.response;
1426
1447
  }
1448
+ // Every guard passed - a block that recurs later is a new event
1449
+ clearBlockEvents();
1427
1450
  // Pass validated data to handler
1428
1451
  try {
1429
1452
  const result = await tool.handler(validation.data, extra?.signal);
@@ -1815,8 +1838,16 @@ async function main() {
1815
1838
  // session that has not registered yet is invisible to that question.
1816
1839
  serverClaims.collectDeadSessions();
1817
1840
  await serverClaims.registerSession(process.cwd());
1818
- // Initialize server manager - recover running servers and start auto-run servers
1819
- const serverInitResult = await serverManager.initialize();
1841
+ // Initialize server manager - recover running servers and start auto-run servers.
1842
+ // Tool calls queue behind this (see waitForStartupRecovery); release the gate
1843
+ // even if recovery throws, or every later call would wait out the timeout.
1844
+ let serverInitResult;
1845
+ try {
1846
+ serverInitResult = await serverManager.initialize();
1847
+ }
1848
+ finally {
1849
+ startupGate.markComplete();
1850
+ }
1820
1851
  if (serverInitResult.recovered.length > 0) {
1821
1852
  console.error(`[devharness] Recovered ${serverInitResult.recovered.length} running server(s): ${serverInitResult.recovered.join(', ')}`);
1822
1853
  }