@gajae-code/coding-agent 0.12.19 → 0.12.21

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 (100) hide show
  1. package/CHANGELOG.md +27 -9
  2. package/dist/types/commands/gc.d.ts +4 -0
  3. package/dist/types/commands/sdk.d.ts +66 -0
  4. package/dist/types/config/settings-schema.d.ts +34 -0
  5. package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
  6. package/dist/types/modes/acp/acp-agent.d.ts +33 -1
  7. package/dist/types/modes/components/bash-execution.d.ts +7 -0
  8. package/dist/types/modes/components/eval-execution.d.ts +7 -0
  9. package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
  10. package/dist/types/sdk/acp/adapter.d.ts +9 -17
  11. package/dist/types/sdk/acp/index.d.ts +1 -1
  12. package/dist/types/sdk/broker/broker.d.ts +93 -1
  13. package/dist/types/sdk/broker/ensure.d.ts +17 -0
  14. package/dist/types/sdk/broker/index.d.ts +1 -0
  15. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
  16. package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
  17. package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
  18. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  19. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
  20. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
  21. package/dist/types/sdk/bus/index.d.ts +20 -9
  22. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
  23. package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
  24. package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
  25. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  26. package/dist/types/sdk/lifecycle-session.d.ts +13 -2
  27. package/dist/types/sdk/prompt-failure.d.ts +13 -0
  28. package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
  29. package/dist/types/sdk/session-reconnect.d.ts +20 -0
  30. package/dist/types/sdk/startup-capability.d.ts +1 -1
  31. package/dist/types/session/agent-session.d.ts +14 -4
  32. package/dist/types/session/blob-store.d.ts +32 -0
  33. package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
  34. package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
  35. package/dist/types/session/session-manager.d.ts +302 -27
  36. package/dist/types/session/session-storage.d.ts +170 -0
  37. package/dist/types/task/executor.d.ts +2 -1
  38. package/dist/types/tools/bash.d.ts +1 -0
  39. package/dist/types/tools/todo-contract.d.ts +10 -1
  40. package/dist/types/tui/output-block.d.ts +6 -0
  41. package/package.json +8 -8
  42. package/src/cli/args.ts +75 -34
  43. package/src/commands/acp.ts +7 -1
  44. package/src/commands/gc.ts +31 -2
  45. package/src/commands/sdk.ts +141 -7
  46. package/src/config/settings-schema.ts +43 -0
  47. package/src/coordinator-mcp/server.ts +6 -1
  48. package/src/export/html/index.ts +83 -39
  49. package/src/gjc-runtime/gc-runtime.ts +1283 -4
  50. package/src/internal-urls/docs-index.generated.ts +1 -1
  51. package/src/internal-urls/local-protocol.ts +14 -0
  52. package/src/main.ts +40 -9
  53. package/src/modes/acp/acp-agent.ts +564 -37
  54. package/src/modes/acp/terminal-auth.ts +2 -2
  55. package/src/modes/components/bash-execution.ts +14 -0
  56. package/src/modes/components/eval-execution.ts +14 -0
  57. package/src/modes/controllers/command-controller.ts +63 -8
  58. package/src/modes/utils/ui-helpers.ts +81 -11
  59. package/src/sdk/acp/adapter.ts +46 -45
  60. package/src/sdk/acp/index.ts +1 -1
  61. package/src/sdk/broker/broker.ts +392 -11
  62. package/src/sdk/broker/ensure.ts +131 -61
  63. package/src/sdk/broker/index.ts +1 -0
  64. package/src/sdk/broker/lifecycle-ledger.ts +59 -5
  65. package/src/sdk/broker/lifecycle.ts +278 -63
  66. package/src/sdk/broker/startup-budget.ts +103 -0
  67. package/src/sdk/bus/chat-daemon-control.ts +9 -3
  68. package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
  69. package/src/sdk/bus/conversation-store.ts +1 -1
  70. package/src/sdk/bus/discord-daemon.ts +2 -1
  71. package/src/sdk/bus/discord-live-provider.ts +34 -6
  72. package/src/sdk/bus/index.ts +118 -20
  73. package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
  74. package/src/sdk/bus/prompt-reconciliation.ts +4 -9
  75. package/src/sdk/bus/slack-daemon.ts +91 -18
  76. package/src/sdk/cli/session-cli.ts +8 -1
  77. package/src/sdk/host/query/handlers.ts +1 -1
  78. package/src/sdk/host/query/revision-store.ts +48 -12
  79. package/src/sdk/host/session-runtime.test.ts +50 -0
  80. package/src/sdk/host/session-runtime.ts +12 -2
  81. package/src/sdk/lifecycle-session.ts +26 -3
  82. package/src/sdk/mcp/server.ts +6 -1
  83. package/src/sdk/prompt-failure.ts +42 -0
  84. package/src/sdk/prompt-watchdog.ts +162 -0
  85. package/src/sdk/session-reconnect.ts +43 -0
  86. package/src/sdk/startup-capability.ts +13 -4
  87. package/src/session/agent-session.ts +541 -226
  88. package/src/session/blob-store.ts +89 -0
  89. package/src/session/internal/managed-session-scope.ts +36 -26
  90. package/src/session/internal/managed-session-storage.ts +303 -32
  91. package/src/session/internal/session-memory-sidecar.ts +2160 -0
  92. package/src/session/session-manager.ts +7057 -399
  93. package/src/session/session-storage.ts +1134 -11
  94. package/src/task/executor.ts +17 -4
  95. package/src/tools/bash.ts +109 -7
  96. package/src/tools/descriptors.test.ts +51 -0
  97. package/src/tools/find.ts +55 -26
  98. package/src/tools/todo-contract.ts +82 -13
  99. package/src/tools/todo-write.ts +2 -5
  100. package/src/tui/output-block.ts +11 -1
@@ -1,4 +1,8 @@
1
1
  import { type ChildProcess, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import type { FileHandle } from "node:fs/promises";
4
+ import * as fs from "node:fs/promises";
5
+ import path from "node:path";
2
6
  import { type BrokerDiscovery, brokerProcessIncarnation, readBrokerDiscovery } from "./discovery";
3
7
  import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
4
8
  export interface EnsureBrokerSettings {
@@ -18,6 +22,57 @@ const FIXTURE_DISCOVERY_TIMEOUT_MS = 30_000;
18
22
  // owned-process teardown convention (SIGTERM -> grace -> SIGKILL -> hard cap).
19
23
  const REAP_GRACEFUL_MS = 2_000;
20
24
  const REAP_SIGKILL_CAP_MS = 2_000;
25
+
26
+ /**
27
+ * Tail of the detached broker's stderr folded into a discovery failure.
28
+ *
29
+ * The broker used to spawn with `stdio: "ignore"`, so a broker that exited
30
+ * cleanly told the caller nothing beyond `code=0` (#3963). Its stderr goes to a
31
+ * file instead of a pipe because the child is detached and outlives this
32
+ * process: a pipe would break under it the moment the parent exits.
33
+ */
34
+ export const BROKER_SPAWN_LOG_TAIL_BYTES = 4_096;
35
+
36
+ export interface BrokerSpawnLog {
37
+ path: string;
38
+ handle: FileHandle;
39
+ }
40
+
41
+ function brokerSpawnLogPath(agentDir: string): string {
42
+ return path.join(agentDir, "sdk", `broker-spawn.${randomUUID()}.log`);
43
+ }
44
+
45
+ /** Opens an isolated, bounded-lifetime diagnostic sink for one broker spawn. */
46
+ export async function openBrokerSpawnLog(agentDir: string): Promise<BrokerSpawnLog | undefined> {
47
+ try {
48
+ await fs.mkdir(path.join(agentDir, "sdk"), { recursive: true, mode: 0o700 });
49
+ const spawnLogPath = brokerSpawnLogPath(agentDir);
50
+ return { path: spawnLogPath, handle: await fs.open(spawnLogPath, "w", 0o600) };
51
+ } catch {
52
+ // Diagnostics are never allowed to block a broker spawn.
53
+ return undefined;
54
+ }
55
+ }
56
+
57
+ export async function readBrokerSpawnLogTail(spawnLogPath: string): Promise<string> {
58
+ try {
59
+ const file = Bun.file(spawnLogPath);
60
+ const size = file.size;
61
+ if (!Number.isFinite(size) || size <= 0) return "";
62
+ const tail = size > BROKER_SPAWN_LOG_TAIL_BYTES ? file.slice(size - BROKER_SPAWN_LOG_TAIL_BYTES) : file;
63
+ return (await tail.text()).trim();
64
+ } catch {
65
+ return "";
66
+ }
67
+ }
68
+
69
+ async function removeBrokerSpawnLog(spawnLogPath: string): Promise<void> {
70
+ try {
71
+ await fs.unlink(spawnLogPath);
72
+ } catch {
73
+ // Diagnostics are best-effort and must not affect broker ownership.
74
+ }
75
+ }
21
76
  export interface FixtureBrokerLease {
22
77
  /** Backward-compatible fixture cleanup alias for exact child termination. */
23
78
  close(): Promise<void>;
@@ -245,74 +300,89 @@ async function ensureBrokerOnce(settings: EnsureBrokerSettings, initiator: Ensur
245
300
  }
246
301
 
247
302
  const command = resolveSdkInternalSpawnCommand("broker-internal");
248
- const child = spawn(command.file, [...command.args, "--agent-dir", settings.agentDir], {
249
- detached: true,
250
- stdio: "ignore",
251
- env: brokerSpawnEnvironment(command, settings.env),
252
- ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
253
- });
254
- child.unref();
255
- let spawnError: Error | undefined;
256
- child.once("error", error => {
257
- spawnError = error;
258
- });
259
- const owner = registerBrokerOwner(settings.agentDir, child);
260
- const discoveryTimeoutMs = initiator === "fixture-lease" ? FIXTURE_DISCOVERY_TIMEOUT_MS : DISCOVERY_TIMEOUT_MS;
261
- const deadline = Date.now() + discoveryTimeoutMs;
262
- let discoveryError: unknown;
263
- while (Date.now() < deadline) {
264
- if (spawnError || child.exitCode !== null || child.signalCode !== null) break;
265
- try {
266
- const discovered = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
267
- if (discovered) {
268
- if (owner.markReady(discovered)) {
269
- return initiator === "fixture-lease"
270
- ? { kind: "local-started-fixture", discovery: discovered, owner, child }
271
- : { kind: "local-started-discovery", discovery: discovered };
303
+ const spawnLog = await openBrokerSpawnLog(settings.agentDir);
304
+ try {
305
+ const child = spawn(command.file, [...command.args, "--agent-dir", settings.agentDir], {
306
+ detached: true,
307
+ stdio: ["ignore", "ignore", spawnLog ? spawnLog.handle.fd : "ignore"],
308
+ env: brokerSpawnEnvironment(command, settings.env),
309
+ ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
310
+ });
311
+ // The child holds its own duplicate of the descriptor; this one is done.
312
+ await spawnLog?.handle.close();
313
+ child.unref();
314
+ let spawnError: Error | undefined;
315
+ child.once("error", error => {
316
+ spawnError = error;
317
+ });
318
+ const owner = registerBrokerOwner(settings.agentDir, child);
319
+ const discoveryTimeoutMs = initiator === "fixture-lease" ? FIXTURE_DISCOVERY_TIMEOUT_MS : DISCOVERY_TIMEOUT_MS;
320
+ const deadline = Date.now() + discoveryTimeoutMs;
321
+ let discoveryError: unknown;
322
+ while (Date.now() < deadline) {
323
+ if (spawnError || child.exitCode !== null || child.signalCode !== null) break;
324
+ try {
325
+ const discovered = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
326
+ if (discovered) {
327
+ if (owner.markReady(discovered)) {
328
+ return initiator === "fixture-lease"
329
+ ? { kind: "local-started-fixture", discovery: discovered, owner, child }
330
+ : { kind: "local-started-discovery", discovery: discovered };
331
+ }
332
+ await owner.stop();
333
+ return { kind: "external-discovery", discovery: discovered };
272
334
  }
273
- await owner.stop();
274
- return { kind: "external-discovery", discovery: discovered };
335
+ } catch (error) {
336
+ discoveryError = error;
275
337
  }
276
- } catch (error) {
277
- discoveryError = error;
338
+ await sleep(50);
278
339
  }
279
- await sleep(50);
280
- }
281
- const exitedBeforeDiscovery = child.exitCode !== null || child.signalCode !== null;
282
- if (exitedBeforeDiscovery && child.exitCode === 0) {
283
- // A clean exit means another broker won the ownership lock (two ACP
284
- // processes racing a cold broker state, e.g. a provider probe and an
285
- // agent launch). The winner may publish its discovery right after our
286
- // last poll; reuse it instead of failing the caller. Transient discovery
287
- // read failures fall through to the common cleanup + failure path below.
288
- try {
289
- for (let retry = 0; retry < 20; retry++) {
290
- const winner = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
291
- if (winner) {
292
- await owner.stop();
293
- return { kind: "external-discovery", discovery: winner };
340
+ const exitedBeforeDiscovery = child.exitCode !== null || child.signalCode !== null;
341
+ if (exitedBeforeDiscovery && child.exitCode === 0) {
342
+ // A clean exit means another broker won the ownership lock (two ACP
343
+ // processes racing a cold broker state, e.g. a provider probe and an
344
+ // agent launch). The winner may publish its discovery right after our
345
+ // last poll; reuse it instead of failing the caller. Transient discovery
346
+ // read failures fall through to the common cleanup + failure path below.
347
+ try {
348
+ for (let retry = 0; retry < 20; retry++) {
349
+ const winner = await readBrokerDiscovery(settings.agentDir, settings.heartbeatTtlMs);
350
+ if (winner) {
351
+ await owner.stop();
352
+ return { kind: "external-discovery", discovery: winner };
353
+ }
354
+ await sleep(50);
294
355
  }
295
- await sleep(50);
356
+ } catch {
357
+ // fall through to cleanup + failure
296
358
  }
297
- } catch {
298
- // fall through to cleanup + failure
299
359
  }
360
+ const spawnLogTail = exitedBeforeDiscovery && spawnLog ? await readBrokerSpawnLogTail(spawnLog.path) : "";
361
+ const failure = spawnError
362
+ ? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
363
+ : exitedBeforeDiscovery
364
+ ? new Error(
365
+ `Detached SDK broker exited before discovery (code=${child.exitCode}, signal=${child.signalCode}).${
366
+ spawnLogTail
367
+ ? ` Broker stderr: ${spawnLogTail}`
368
+ : " The broker wrote no stderr; see the gjc log for its lock-contention diagnostic."
369
+ }`,
370
+ )
371
+ : discoveryError
372
+ ? discoveryError
373
+ : new Error("Timed out waiting for detached SDK broker discovery.");
374
+ try {
375
+ await owner.stop();
376
+ } catch (cleanupError) {
377
+ throw new AggregateError(
378
+ [failure, cleanupError],
379
+ "SDK broker discovery and spawned broker cleanup both failed.",
380
+ );
381
+ }
382
+ throw failure;
383
+ } finally {
384
+ if (spawnLog) await removeBrokerSpawnLog(spawnLog.path);
300
385
  }
301
- const failure = spawnError
302
- ? new Error(`Failed to spawn detached SDK broker: ${spawnError.message}`)
303
- : exitedBeforeDiscovery
304
- ? new Error(
305
- `Detached SDK broker exited before discovery (code=${child.exitCode}, signal=${child.signalCode}).`,
306
- )
307
- : discoveryError
308
- ? discoveryError
309
- : new Error("Timed out waiting for detached SDK broker discovery.");
310
- try {
311
- await owner.stop();
312
- } catch (cleanupError) {
313
- throw new AggregateError([failure, cleanupError], "SDK broker discovery and spawned broker cleanup both failed.");
314
- }
315
- throw failure;
316
386
  }
317
387
 
318
388
  function startEnsure(settings: EnsureBrokerSettings, initiator: EnsureInitiator): EnsureInFlight {
@@ -5,4 +5,5 @@ export * from "./identity";
5
5
  export * from "./lifecycle";
6
6
  export * from "./lifecycle-ledger";
7
7
  export * from "./session-index";
8
+ export * from "./startup-budget";
8
9
  export * from "./transport";
@@ -99,6 +99,11 @@ export interface LifecycleLedgerLimits {
99
99
  maxBytes?: number;
100
100
  maxLineBytes?: number;
101
101
  maxRows?: number;
102
+ /**
103
+ * Cap for the `.corrupt` quarantine sidecar. Reaching it rotates one
104
+ * generation aside instead of appending forever.
105
+ */
106
+ maxCorruptBytes?: number;
102
107
  }
103
108
  function canonicalCleanupSessionId(value: unknown): value is string {
104
109
  return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
@@ -108,7 +113,11 @@ const DEFAULT_LIFECYCLE_LEDGER_LIMITS: Required<LifecycleLedgerLimits> = {
108
113
  maxBytes: 64 * 1024 * 1024,
109
114
  maxLineBytes: 8 * 1024 * 1024,
110
115
  maxRows: 10_000,
116
+ maxCorruptBytes: 8 * 1024 * 1024,
111
117
  };
118
+
119
+ /** Appended when a quarantined row is clipped to the sidecar cap. */
120
+ const LIFECYCLE_QUARANTINE_TRUNCATION_MARKER = "\n[gjc: quarantined row truncated at the corrupt-ledger cap]";
112
121
  const MAX_LIFECYCLE_LEDGER_JSON_DEPTH = 64;
113
122
  const MAX_LIFECYCLE_LEDGER_JSON_FIELDS = 1024;
114
123
 
@@ -251,6 +260,7 @@ export class LifecycleLedger {
251
260
  maxBytes: limits.maxBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxBytes,
252
261
  maxLineBytes: limits.maxLineBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxLineBytes,
253
262
  maxRows: limits.maxRows ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxRows,
263
+ maxCorruptBytes: limits.maxCorruptBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxCorruptBytes,
254
264
  };
255
265
  }
256
266
  async open(): Promise<this> {
@@ -353,12 +363,16 @@ export class LifecycleLedger {
353
363
  return this;
354
364
  }
355
365
  /**
356
- * Reads terminal proof for one request without recovering or changing the ledger.
366
+ * Reads the durable terminal record for one request without recovering or changing the ledger.
357
367
  *
358
368
  * This intentionally accepts an unrelated unterminated final write: concurrent
359
369
  * appenders may have started a different row after this request's synced terminal
360
370
  * row. Complete rows are still decoded and validated strictly, and any incomplete
361
371
  * tail that identifies this request withholds proof.
372
+ *
373
+ * `terminal_uncertain` is a durable terminal record too: an operation that concluded
374
+ * uncertainly persists that conclusion, and refusing to read it back would report a
375
+ * synced row as unpersisted and replace its true reason with a generic one.
362
376
  */
363
377
  async readTerminal(identity: string, requestHash: string): Promise<LifecycleLedgerEntry | undefined> {
364
378
  const source = await this.#readBoundedSourceReadOnly();
@@ -403,12 +417,20 @@ export class LifecycleLedger {
403
417
  const identityMarker = Buffer.from(`"identity":${JSON.stringify(identity)}`);
404
418
  if (tail.includes(identityMarker)) return undefined;
405
419
  }
406
- return latest && terminal(latest.state) ? latest : undefined;
420
+ return latest && final(latest.state) ? latest : undefined;
407
421
  }
408
422
 
423
+ /**
424
+ * A recovery pass appends `terminal_uncertain` for every row it finds mid-flight, so a
425
+ * long-running operation can have that marker interleaved before its own terminal row
426
+ * lands. The marker records unresolved work, not a proven outcome: the owner that ran
427
+ * the effects stays authoritative and may still resolve it. A proven terminal row is
428
+ * immutable and admits no successor.
429
+ */
409
430
  #isValidHistoryContinuation(previous: LifecycleLedgerEntry | undefined, next: LifecycleLedgerEntry): boolean {
410
431
  if (!previous) return next.state === "accepted";
411
- if (previous.requestHash !== next.requestHash || final(previous.state)) return false;
432
+ if (previous.requestHash !== next.requestHash || terminal(previous.state)) return false;
433
+ if (previous.state === "terminal_uncertain") return final(next.state);
412
434
  if (previous.state === "accepted") return true;
413
435
  return next.state === "effect_started" || next.state === "awaiting_ready" || final(next.state);
414
436
  }
@@ -533,11 +555,43 @@ export class LifecycleLedger {
533
555
  await h.close();
534
556
  }
535
557
  }
558
+ /**
559
+ * Rotate the quarantine sidecar aside once it would exceed its cap.
560
+ *
561
+ * Quarantine is diagnostic, not durable state: an unreadable ledger
562
+ * re-quarantines its whole contents on every open, so a plain append grows
563
+ * the sidecar without bound (a 126 MB `.corrupt` next to a 6 MB live ledger,
564
+ * #3963). One retained generation keeps the newest evidence while bounding
565
+ * the pair at twice the cap.
566
+ */
567
+ async #rotateCorruptWhenFull(incomingBytes: number): Promise<void> {
568
+ let size: number;
569
+ try {
570
+ const stat = await fs.lstat(this.#corruptFile);
571
+ // A non-regular quarantine path is never rotated; the append guard rejects it.
572
+ if (!stat.isFile()) return;
573
+ size = stat.size;
574
+ } catch (error) {
575
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
576
+ throw error;
577
+ }
578
+ if (size + incomingBytes <= this.#limits.maxCorruptBytes) return;
579
+ await fs.rename(this.#corruptFile, `${this.#corruptFile}.1`);
580
+ }
581
+
536
582
  async #quarantine(line: string | Uint8Array): Promise<void> {
583
+ const payload =
584
+ typeof line === "string" ? Buffer.from(line) : Buffer.from(line.buffer, line.byteOffset, line.byteLength);
585
+ // A single quarantined row can be as large as the whole ledger, so the
586
+ // write itself is capped too; otherwise one oversized row would blow past
587
+ // the rotation bound it just triggered.
588
+ const truncated = payload.length > this.#limits.maxCorruptBytes;
589
+ const retained = truncated ? payload.subarray(0, this.#limits.maxCorruptBytes) : payload;
590
+ await this.#rotateCorruptWhenFull(retained.length + 1);
537
591
  const h = await this.#openAppendRegular(this.#corruptFile);
538
592
  try {
539
- await h.writeFile(line);
540
- await h.writeFile("\n");
593
+ await h.writeFile(retained);
594
+ await h.writeFile(truncated ? `${LIFECYCLE_QUARANTINE_TRUNCATION_MARKER}\n` : "\n");
541
595
  await h.sync();
542
596
  } finally {
543
597
  await h.close();