@cogcoin/client 1.0.0 → 1.0.2

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 (56) hide show
  1. package/README.md +2 -1
  2. package/dist/bitcoind/indexer-daemon.d.ts +3 -0
  3. package/dist/bitcoind/indexer-daemon.js +58 -8
  4. package/dist/bitcoind/retryable-rpc.js +3 -0
  5. package/dist/bitcoind/service.d.ts +1 -0
  6. package/dist/bitcoind/service.js +31 -9
  7. package/dist/cli/commands/mining-admin.js +9 -0
  8. package/dist/cli/commands/mining-runtime.js +114 -12
  9. package/dist/cli/commands/sync.js +1 -91
  10. package/dist/cli/commands/update.d.ts +2 -0
  11. package/dist/cli/commands/update.js +101 -0
  12. package/dist/cli/context.js +33 -1
  13. package/dist/cli/mining-format.js +28 -0
  14. package/dist/cli/mining-json.js +6 -0
  15. package/dist/cli/output.js +50 -2
  16. package/dist/cli/parse.d.ts +1 -1
  17. package/dist/cli/parse.js +5 -0
  18. package/dist/cli/prompt.js +109 -0
  19. package/dist/cli/read-json.d.ts +13 -0
  20. package/dist/cli/read-json.js +17 -0
  21. package/dist/cli/runner.js +4 -0
  22. package/dist/cli/sync-progress.d.ts +6 -0
  23. package/dist/cli/sync-progress.js +91 -0
  24. package/dist/cli/types.d.ts +8 -2
  25. package/dist/cli/update-notifier.js +7 -222
  26. package/dist/cli/update-service.d.ts +44 -0
  27. package/dist/cli/update-service.js +218 -0
  28. package/dist/cli/wallet-format.js +3 -0
  29. package/dist/client/initialization.js +5 -0
  30. package/dist/wallet/lifecycle.d.ts +10 -0
  31. package/dist/wallet/lifecycle.js +6 -0
  32. package/dist/wallet/mining/config.js +13 -3
  33. package/dist/wallet/mining/control.d.ts +2 -1
  34. package/dist/wallet/mining/control.js +143 -19
  35. package/dist/wallet/mining/index.d.ts +2 -2
  36. package/dist/wallet/mining/index.js +1 -1
  37. package/dist/wallet/mining/provider-model.d.ts +30 -0
  38. package/dist/wallet/mining/provider-model.js +134 -0
  39. package/dist/wallet/mining/runner.d.ts +105 -3
  40. package/dist/wallet/mining/runner.js +490 -88
  41. package/dist/wallet/mining/runtime-artifacts.js +1 -0
  42. package/dist/wallet/mining/sentences.d.ts +2 -2
  43. package/dist/wallet/mining/sentences.js +25 -2
  44. package/dist/wallet/mining/types.d.ts +9 -1
  45. package/dist/wallet/mining/visualizer.js +28 -5
  46. package/dist/wallet/read/context.js +3 -0
  47. package/dist/wallet/reset.js +1 -0
  48. package/dist/wallet/tx/anchor.js +1 -0
  49. package/dist/wallet/tx/bitcoin-transfer.js +1 -0
  50. package/dist/wallet/tx/cog.js +3 -0
  51. package/dist/wallet/tx/domain-admin.js +1 -0
  52. package/dist/wallet/tx/domain-market.js +3 -0
  53. package/dist/wallet/tx/field.js +1 -0
  54. package/dist/wallet/tx/register.js +1 -0
  55. package/dist/wallet/tx/reputation.js +1 -0
  56. package/package.json +3 -2
@@ -1,5 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
+ import { rm } from "node:fs/promises";
4
+ import { join } from "node:path";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import { getBalance, getBlockWinners, lookupDomain, lookupDomainById, } from "@cogcoin/indexer/queries";
5
7
  import { assaySentences, deriveBlendSeed, displayToInternalBlockhash, getWords, settleBlock, } from "@cogcoin/scoring";
@@ -9,8 +11,8 @@ import { attachOrStartManagedBitcoindService } from "../../bitcoind/service.js";
9
11
  import { createRpcClient } from "../../bitcoind/node.js";
10
12
  import { COG_OPCODES, COG_PREFIX } from "../cogop/constants.js";
11
13
  import { extractOpReturnPayloadFromScriptHex } from "../tx/register.js";
12
- import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
13
- import { acquireFileLock } from "../fs/lock.js";
14
+ import { assertFixedInputPrefixMatches, buildWalletMutationTransaction, isInsufficientFundsError, outpointKey as walletMutationOutpointKey, isAlreadyAcceptedError, isBroadcastUnknownError, reconcilePersistentPolicyLocks, resolveWalletMutationFeeSelection, saveWalletStatePreservingUnlock, } from "../tx/common.js";
15
+ import { FileLockBusyError, acquireFileLock, clearOrphanedFileLock, readLockMetadata, } from "../fs/lock.js";
14
16
  import { isMineableWalletDomain, openWalletReadContext, } from "../read/index.js";
15
17
  import { resolveWalletRuntimePathsForTesting } from "../runtime.js";
16
18
  import { createDefaultWalletSecretProvider, } from "../state/provider.js";
@@ -19,13 +21,16 @@ import { appendMiningEvent, loadMiningRuntimeStatus, saveMiningRuntimeStatus, }
19
21
  import { loadClientConfig } from "./config.js";
20
22
  import { MINING_LOOP_INTERVAL_MS, MINING_NETWORK_SETTLE_WINDOW_MS, MINING_PROVIDER_BACKOFF_BASE_MS, MINING_PROVIDER_BACKOFF_MAX_MS, MINING_SHUTDOWN_GRACE_MS, MINING_STATUS_HEARTBEAT_INTERVAL_MS, MINING_SUSPEND_GAP_THRESHOLD_MS, MINING_TIP_SETTLE_WINDOW_MS, MINING_WORKER_API_VERSION, } from "./constants.js";
21
23
  import { inspectMiningControlPlane, setupBuiltInMining } from "./control.js";
22
- import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
24
+ import { isMiningGenerationAbortRequested, markMiningGenerationActive, markMiningGenerationInactive, readMiningGenerationActivity, readMiningPreemptionRequest, requestMiningGenerationPreemption, } from "./coordination.js";
23
25
  import { clearMiningPublishState, miningPublishIsInMempool, miningPublishMayStillExist, normalizeMiningPublishState, normalizeMiningStateRecord, } from "./state.js";
24
26
  import { createMiningSentenceRequestLimits } from "./sentence-protocol.js";
25
27
  import { generateMiningSentences, MiningProviderRequestError } from "./sentences.js";
26
28
  import { createEmptyMiningFollowVisualizerState, MiningFollowVisualizer, } from "./visualizer.js";
27
29
  const BEST_BLOCK_POLL_INTERVAL_MS = 500;
28
30
  const BACKGROUND_START_TIMEOUT_MS = 15_000;
31
+ function resolveSnapshotOverride(override, fallback) {
32
+ return override === undefined ? fallback : override;
33
+ }
29
34
  class MiningSuspendDetectedError extends Error {
30
35
  detectedAtUnixMs;
31
36
  constructor(detectedAtUnixMs) {
@@ -88,6 +93,239 @@ async function isProcessAlive(pid) {
88
93
  return true;
89
94
  }
90
95
  }
96
+ function normalizeMiningPid(value) {
97
+ return typeof value === "number" && Number.isInteger(value) && value > 0
98
+ ? value
99
+ : null;
100
+ }
101
+ function resolveMiningGenerationRequestPath(paths) {
102
+ return join(paths.miningRoot, "generation-request.json");
103
+ }
104
+ function resolveMiningGenerationActivityPath(paths) {
105
+ return join(paths.miningRoot, "generation-activity.json");
106
+ }
107
+ function createTakeoverStoppedMiningNote(livePublishInMempool) {
108
+ return livePublishInMempool
109
+ ? "Mining runtime replaced. The last mining transaction may still confirm from mempool."
110
+ : "Mining runtime replaced.";
111
+ }
112
+ function createStoppedMiningRuntimeSnapshotForTakeover(options) {
113
+ const note = createTakeoverStoppedMiningNote(options.snapshot?.livePublishInMempool);
114
+ if (options.snapshot !== null) {
115
+ return {
116
+ ...options.snapshot,
117
+ updatedAtUnixMs: options.nowUnixMs,
118
+ runMode: "stopped",
119
+ backgroundWorkerPid: null,
120
+ backgroundWorkerRunId: null,
121
+ backgroundWorkerHeartbeatAtUnixMs: null,
122
+ backgroundWorkerHealth: null,
123
+ currentPhase: "idle",
124
+ note,
125
+ };
126
+ }
127
+ return {
128
+ schemaVersion: 1,
129
+ walletRootId: options.walletRootId,
130
+ workerApiVersion: null,
131
+ workerBinaryVersion: null,
132
+ workerBuildId: null,
133
+ updatedAtUnixMs: options.nowUnixMs,
134
+ runMode: "stopped",
135
+ backgroundWorkerPid: null,
136
+ backgroundWorkerRunId: null,
137
+ backgroundWorkerHeartbeatAtUnixMs: null,
138
+ backgroundWorkerHealth: null,
139
+ indexerDaemonState: null,
140
+ indexerDaemonInstanceId: null,
141
+ indexerSnapshotSeq: null,
142
+ indexerSnapshotOpenedAtUnixMs: null,
143
+ indexerTruthSource: undefined,
144
+ indexerHeartbeatAtUnixMs: null,
145
+ coreBestHeight: null,
146
+ coreBestHash: null,
147
+ indexerTipHeight: null,
148
+ indexerTipHash: null,
149
+ indexerReorgDepth: null,
150
+ indexerTipAligned: null,
151
+ corePublishState: null,
152
+ providerState: null,
153
+ lastSuspendDetectedAtUnixMs: null,
154
+ reconnectSettledUntilUnixMs: null,
155
+ tipSettledUntilUnixMs: null,
156
+ miningState: "idle",
157
+ currentPhase: "idle",
158
+ currentPublishState: "none",
159
+ targetBlockHeight: null,
160
+ referencedBlockHashDisplay: null,
161
+ currentDomainId: null,
162
+ currentDomainName: null,
163
+ currentSentenceDisplay: null,
164
+ currentCanonicalBlend: null,
165
+ currentTxid: null,
166
+ currentWtxid: null,
167
+ livePublishInMempool: null,
168
+ currentFeeRateSatVb: null,
169
+ currentAbsoluteFeeSats: null,
170
+ currentBlockFeeSpentSats: "0",
171
+ sessionFeeSpentSats: "0",
172
+ lifetimeFeeSpentSats: "0",
173
+ sameDomainCompetitorSuppressed: null,
174
+ higherRankedCompetitorDomainCount: null,
175
+ dedupedCompetitorDomainCount: null,
176
+ competitivenessGateIndeterminate: null,
177
+ mempoolSequenceCacheStatus: null,
178
+ currentPublishDecision: null,
179
+ lastMempoolSequence: null,
180
+ lastCompetitivenessGateAtUnixMs: null,
181
+ pauseReason: null,
182
+ providerConfigured: false,
183
+ providerKind: null,
184
+ bitcoindHealth: "unavailable",
185
+ bitcoindServiceState: null,
186
+ bitcoindReplicaStatus: null,
187
+ nodeHealth: "unavailable",
188
+ indexerHealth: "unavailable",
189
+ tipsAligned: null,
190
+ lastEventAtUnixMs: null,
191
+ lastError: null,
192
+ note,
193
+ };
194
+ }
195
+ async function waitForMiningProcessExit(pid, timeoutMs, sleepImpl = sleep) {
196
+ const deadline = Date.now() + timeoutMs;
197
+ while (Date.now() < deadline) {
198
+ if (!await isProcessAlive(pid)) {
199
+ return true;
200
+ }
201
+ await sleepImpl(Math.min(250, Math.max(timeoutMs, 1)));
202
+ }
203
+ return !await isProcessAlive(pid);
204
+ }
205
+ async function terminateMiningRuntimePid(options) {
206
+ if (!await isProcessAlive(options.pid)) {
207
+ return false;
208
+ }
209
+ try {
210
+ process.kill(options.pid, "SIGTERM");
211
+ }
212
+ catch (error) {
213
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
214
+ throw error;
215
+ }
216
+ }
217
+ if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.sleepImpl)) {
218
+ return true;
219
+ }
220
+ try {
221
+ process.kill(options.pid, "SIGKILL");
222
+ }
223
+ catch (error) {
224
+ if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
225
+ throw error;
226
+ }
227
+ }
228
+ if (await waitForMiningProcessExit(options.pid, options.shutdownGraceMs, options.sleepImpl)) {
229
+ return true;
230
+ }
231
+ throw new Error("mining_process_stop_timeout");
232
+ }
233
+ async function takeOverMiningRuntime(options) {
234
+ const snapshot = await loadMiningRuntimeStatus(options.paths.miningStatusPath).catch(() => null);
235
+ const controlLockMetadata = options.controlLockMetadata ?? (options.clearControlLockFile === true
236
+ ? await readLockMetadata(options.paths.miningControlLockPath).catch(() => null)
237
+ : null);
238
+ const generationActivity = await readMiningGenerationActivity(options.paths).catch(() => null);
239
+ const shutdownGraceMs = options.shutdownGraceMs ?? MINING_SHUTDOWN_GRACE_MS;
240
+ const requestPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
241
+ const controlLockPid = normalizeMiningPid(controlLockMetadata?.processId);
242
+ const backgroundWorkerPid = normalizeMiningPid(snapshot?.backgroundWorkerPid);
243
+ const generationOwnerPid = normalizeMiningPid(generationActivity?.generationOwnerPid);
244
+ const terminatedPids = [];
245
+ const discoveredPids = new Set();
246
+ for (const pid of [controlLockPid, backgroundWorkerPid, generationOwnerPid]) {
247
+ if (pid === null
248
+ || pid === process.pid
249
+ || discoveredPids.has(pid)
250
+ || !await isProcessAlive(pid)) {
251
+ continue;
252
+ }
253
+ discoveredPids.add(pid);
254
+ }
255
+ const shouldPreemptGeneration = discoveredPids.size > 0 && (generationActivity?.generationActive === true
256
+ || snapshot?.currentPhase === "generating"
257
+ || snapshot?.currentPhase === "scoring");
258
+ const preemption = shouldPreemptGeneration
259
+ ? await requestPreemption({
260
+ paths: options.paths,
261
+ reason: options.reason,
262
+ timeoutMs: Math.min(shutdownGraceMs, 15_000),
263
+ }).catch(() => null)
264
+ : null;
265
+ try {
266
+ for (const pid of discoveredPids) {
267
+ if (await terminateMiningRuntimePid({
268
+ pid,
269
+ shutdownGraceMs,
270
+ sleepImpl: options.sleepImpl,
271
+ })) {
272
+ terminatedPids.push(pid);
273
+ }
274
+ }
275
+ }
276
+ finally {
277
+ await preemption?.release().catch(() => undefined);
278
+ }
279
+ const controlLockCleared = options.clearControlLockFile === true
280
+ ? await clearOrphanedFileLock(options.paths.miningControlLockPath, isProcessAlive).catch(() => false)
281
+ : false;
282
+ await rm(resolveMiningGenerationRequestPath(options.paths), { force: true }).catch(() => undefined);
283
+ await rm(resolveMiningGenerationActivityPath(options.paths), { force: true }).catch(() => undefined);
284
+ const walletRootId = snapshot?.walletRootId
285
+ ?? (typeof controlLockMetadata?.walletRootId === "string" ? controlLockMetadata.walletRootId : null);
286
+ if (snapshot !== null || walletRootId !== null || terminatedPids.length > 0 || controlLockCleared) {
287
+ await saveMiningRuntimeStatus(options.paths.miningStatusPath, createStoppedMiningRuntimeSnapshotForTakeover({
288
+ snapshot,
289
+ walletRootId,
290
+ nowUnixMs: Date.now(),
291
+ }));
292
+ }
293
+ return {
294
+ controlLockCleared,
295
+ replaced: terminatedPids.length > 0,
296
+ snapshot,
297
+ terminatedPids,
298
+ };
299
+ }
300
+ async function acquireMiningStartControlLock(options) {
301
+ while (true) {
302
+ try {
303
+ return await acquireFileLock(options.paths.miningControlLockPath, {
304
+ purpose: options.purpose,
305
+ });
306
+ }
307
+ catch (error) {
308
+ if (!(error instanceof FileLockBusyError)) {
309
+ throw error;
310
+ }
311
+ if (error.existingMetadata?.processId === process.pid) {
312
+ throw error;
313
+ }
314
+ const takeover = await takeOverMiningRuntime({
315
+ paths: options.paths,
316
+ reason: options.takeoverReason,
317
+ clearControlLockFile: true,
318
+ controlLockMetadata: error.existingMetadata,
319
+ requestMiningPreemption: options.requestMiningPreemption,
320
+ shutdownGraceMs: options.shutdownGraceMs,
321
+ sleepImpl: options.sleepImpl,
322
+ });
323
+ if (!takeover.replaced && !takeover.controlLockCleared) {
324
+ throw error;
325
+ }
326
+ }
327
+ }
328
+ }
91
329
  function writeStdout(stream, line) {
92
330
  if (stream === undefined) {
93
331
  return;
@@ -232,7 +470,7 @@ function fallbackSettledWinnerDomainName(domainId) {
232
470
  return `domain-${domainId}`;
233
471
  }
234
472
  function resolveCurrentMinedBlockBoard(options) {
235
- const settledBlockHeight = options.nodeBestHeight ?? options.snapshotTipHeight ?? null;
473
+ const settledBlockHeight = options.snapshotTipHeight ?? null;
236
474
  if (settledBlockHeight === null) {
237
475
  return {
238
476
  settledBlockHeight,
@@ -245,12 +483,6 @@ function resolveCurrentMinedBlockBoard(options) {
245
483
  settledBoardEntries: [],
246
484
  };
247
485
  }
248
- if (options.nodeBestHeight !== null && (options.snapshotTipHeight ?? -1) < options.nodeBestHeight) {
249
- return {
250
- settledBlockHeight,
251
- settledBoardEntries: [],
252
- };
253
- }
254
486
  const settledBoardEntries = (getBlockWinners(options.snapshotState, settledBlockHeight) ?? [])
255
487
  .slice()
256
488
  .sort((left, right) => left.rank - right.rank || left.txIndex - right.txIndex)
@@ -268,15 +500,33 @@ function resolveCurrentMinedBlockBoard(options) {
268
500
  export function resolveSettledBoardForTesting(options) {
269
501
  return resolveCurrentMinedBlockBoard(options);
270
502
  }
271
- function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight, nodeBestHeight) {
503
+ function syncMiningUiSettledBoard(loopState, snapshotState, snapshotTipHeight) {
272
504
  const settledBoard = resolveCurrentMinedBlockBoard({
273
505
  snapshotState,
274
506
  snapshotTipHeight,
275
- nodeBestHeight,
507
+ nodeBestHeight: null,
276
508
  });
277
509
  loopState.ui.settledBlockHeight = settledBoard.settledBlockHeight;
278
510
  loopState.ui.settledBoardEntries = settledBoard.settledBoardEntries;
279
511
  }
512
+ function syncMiningUiForCurrentTip(options) {
513
+ const targetBlockHeight = options.nodeBestHeight === null
514
+ ? null
515
+ : options.nodeBestHeight + 1;
516
+ const tipKey = buildMiningTipKey(options.nodeBestHash, targetBlockHeight);
517
+ if (tipKey !== options.loopState.currentTipKey) {
518
+ options.loopState.currentTipKey = tipKey;
519
+ resetMiningUiForTip(options.loopState, targetBlockHeight);
520
+ if (options.recentWin !== null) {
521
+ options.loopState.ui.recentWin = options.recentWin;
522
+ }
523
+ }
524
+ syncMiningUiSettledBoard(options.loopState, options.snapshotState, options.snapshotTipHeight);
525
+ return {
526
+ targetBlockHeight,
527
+ tipKey,
528
+ };
529
+ }
280
530
  function setMiningUiCandidate(loopState, candidate) {
281
531
  loopState.ui.latestSentence = candidate.sentence;
282
532
  loopState.ui.provisionalRequiredWords = [...candidate.bip39Words];
@@ -348,6 +598,21 @@ function syncMiningVisualizerBalances(loopState, readContext, balanceSats) {
348
598
  : getBalance(readContext.snapshot.state, readContext.localState.state.funding.scriptPubKeyHex);
349
599
  loopState.ui.balanceSats = balanceSats;
350
600
  }
601
+ function createIndexedMiningFollowVisualizerState(readContext) {
602
+ const uiState = createEmptyMiningFollowVisualizerState();
603
+ const localState = readContext.localState;
604
+ const settledBoard = resolveCurrentMinedBlockBoard({
605
+ snapshotState: readContext.snapshot?.state ?? null,
606
+ snapshotTipHeight: readContext.snapshot?.tip?.height ?? readContext.indexer.snapshotTip?.height ?? null,
607
+ nodeBestHeight: readContext.nodeStatus?.nodeBestHeight ?? null,
608
+ });
609
+ uiState.settledBlockHeight = settledBoard.settledBlockHeight;
610
+ uiState.settledBoardEntries = settledBoard.settledBoardEntries;
611
+ if (readContext.snapshot !== null && localState.availability === "ready" && localState.state !== null) {
612
+ uiState.balanceCogtoshi = getBalance(readContext.snapshot.state, localState.state.funding.scriptPubKeyHex);
613
+ }
614
+ return uiState;
615
+ }
351
616
  function syncMiningVisualizerBlockTimes(loopState, blockTimesByHeight) {
352
617
  loopState.ui.visibleBlockTimesByHeight = { ...blockTimesByHeight };
353
618
  }
@@ -642,28 +907,71 @@ async function resolveOverlayAuthorizedMiningDomain(options) {
642
907
  function buildStatusSnapshot(view, overrides = {}) {
643
908
  return {
644
909
  ...view.runtime,
645
- runMode: overrides.runMode ?? view.runtime.runMode,
646
- backgroundWorkerPid: overrides.backgroundWorkerPid ?? view.runtime.backgroundWorkerPid,
647
- backgroundWorkerRunId: overrides.backgroundWorkerRunId ?? view.runtime.backgroundWorkerRunId,
648
- backgroundWorkerHeartbeatAtUnixMs: overrides.backgroundWorkerHeartbeatAtUnixMs ?? view.runtime.backgroundWorkerHeartbeatAtUnixMs,
649
- currentPhase: overrides.currentPhase ?? view.runtime.currentPhase,
650
- lastSuspendDetectedAtUnixMs: overrides.lastSuspendDetectedAtUnixMs ?? view.runtime.lastSuspendDetectedAtUnixMs,
651
- providerState: overrides.providerState ?? view.runtime.providerState,
652
- corePublishState: overrides.corePublishState ?? view.runtime.corePublishState,
653
- currentPublishDecision: overrides.currentPublishDecision ?? view.runtime.currentPublishDecision,
654
- sameDomainCompetitorSuppressed: overrides.sameDomainCompetitorSuppressed ?? view.runtime.sameDomainCompetitorSuppressed,
655
- higherRankedCompetitorDomainCount: overrides.higherRankedCompetitorDomainCount ?? view.runtime.higherRankedCompetitorDomainCount,
656
- dedupedCompetitorDomainCount: overrides.dedupedCompetitorDomainCount ?? view.runtime.dedupedCompetitorDomainCount,
657
- competitivenessGateIndeterminate: overrides.competitivenessGateIndeterminate ?? view.runtime.competitivenessGateIndeterminate,
658
- mempoolSequenceCacheStatus: overrides.mempoolSequenceCacheStatus ?? view.runtime.mempoolSequenceCacheStatus,
659
- lastMempoolSequence: overrides.lastMempoolSequence ?? view.runtime.lastMempoolSequence,
660
- lastCompetitivenessGateAtUnixMs: overrides.lastCompetitivenessGateAtUnixMs ?? view.runtime.lastCompetitivenessGateAtUnixMs,
661
- lastError: overrides.lastError ?? view.runtime.lastError,
662
- note: overrides.note ?? view.runtime.note,
663
- livePublishInMempool: overrides.livePublishInMempool ?? view.runtime.livePublishInMempool,
910
+ runMode: resolveSnapshotOverride(overrides.runMode, view.runtime.runMode),
911
+ backgroundWorkerPid: resolveSnapshotOverride(overrides.backgroundWorkerPid, view.runtime.backgroundWorkerPid),
912
+ backgroundWorkerRunId: resolveSnapshotOverride(overrides.backgroundWorkerRunId, view.runtime.backgroundWorkerRunId),
913
+ backgroundWorkerHeartbeatAtUnixMs: resolveSnapshotOverride(overrides.backgroundWorkerHeartbeatAtUnixMs, view.runtime.backgroundWorkerHeartbeatAtUnixMs),
914
+ currentPhase: resolveSnapshotOverride(overrides.currentPhase, view.runtime.currentPhase),
915
+ currentPublishState: resolveSnapshotOverride(overrides.currentPublishState, view.runtime.currentPublishState),
916
+ targetBlockHeight: resolveSnapshotOverride(overrides.targetBlockHeight, view.runtime.targetBlockHeight),
917
+ referencedBlockHashDisplay: resolveSnapshotOverride(overrides.referencedBlockHashDisplay, view.runtime.referencedBlockHashDisplay),
918
+ currentDomainId: resolveSnapshotOverride(overrides.currentDomainId, view.runtime.currentDomainId),
919
+ currentDomainName: resolveSnapshotOverride(overrides.currentDomainName, view.runtime.currentDomainName),
920
+ currentSentenceDisplay: resolveSnapshotOverride(overrides.currentSentenceDisplay, view.runtime.currentSentenceDisplay),
921
+ currentCanonicalBlend: resolveSnapshotOverride(overrides.currentCanonicalBlend, view.runtime.currentCanonicalBlend),
922
+ currentTxid: resolveSnapshotOverride(overrides.currentTxid, view.runtime.currentTxid),
923
+ currentWtxid: resolveSnapshotOverride(overrides.currentWtxid, view.runtime.currentWtxid),
924
+ currentFeeRateSatVb: resolveSnapshotOverride(overrides.currentFeeRateSatVb, view.runtime.currentFeeRateSatVb),
925
+ currentAbsoluteFeeSats: resolveSnapshotOverride(overrides.currentAbsoluteFeeSats, view.runtime.currentAbsoluteFeeSats),
926
+ currentBlockFeeSpentSats: resolveSnapshotOverride(overrides.currentBlockFeeSpentSats, view.runtime.currentBlockFeeSpentSats),
927
+ lastSuspendDetectedAtUnixMs: resolveSnapshotOverride(overrides.lastSuspendDetectedAtUnixMs, view.runtime.lastSuspendDetectedAtUnixMs),
928
+ providerState: resolveSnapshotOverride(overrides.providerState, view.runtime.providerState),
929
+ corePublishState: resolveSnapshotOverride(overrides.corePublishState, view.runtime.corePublishState),
930
+ currentPublishDecision: resolveSnapshotOverride(overrides.currentPublishDecision, view.runtime.currentPublishDecision),
931
+ sameDomainCompetitorSuppressed: resolveSnapshotOverride(overrides.sameDomainCompetitorSuppressed, view.runtime.sameDomainCompetitorSuppressed),
932
+ higherRankedCompetitorDomainCount: resolveSnapshotOverride(overrides.higherRankedCompetitorDomainCount, view.runtime.higherRankedCompetitorDomainCount),
933
+ dedupedCompetitorDomainCount: resolveSnapshotOverride(overrides.dedupedCompetitorDomainCount, view.runtime.dedupedCompetitorDomainCount),
934
+ competitivenessGateIndeterminate: resolveSnapshotOverride(overrides.competitivenessGateIndeterminate, view.runtime.competitivenessGateIndeterminate),
935
+ mempoolSequenceCacheStatus: resolveSnapshotOverride(overrides.mempoolSequenceCacheStatus, view.runtime.mempoolSequenceCacheStatus),
936
+ lastMempoolSequence: resolveSnapshotOverride(overrides.lastMempoolSequence, view.runtime.lastMempoolSequence),
937
+ lastCompetitivenessGateAtUnixMs: resolveSnapshotOverride(overrides.lastCompetitivenessGateAtUnixMs, view.runtime.lastCompetitivenessGateAtUnixMs),
938
+ lastError: resolveSnapshotOverride(overrides.lastError, view.runtime.lastError),
939
+ note: resolveSnapshotOverride(overrides.note, view.runtime.note),
940
+ livePublishInMempool: resolveSnapshotOverride(overrides.livePublishInMempool, view.runtime.livePublishInMempool),
664
941
  updatedAtUnixMs: Date.now(),
665
942
  };
666
943
  }
944
+ function buildPrePublishStatusOverrides(options) {
945
+ const replacing = options.state.miningState.currentTxid !== null;
946
+ const replacingAcrossTips = replacing && !livePublishTargetsCandidateTip({
947
+ liveState: options.state.miningState,
948
+ candidate: options.candidate,
949
+ });
950
+ return {
951
+ currentPhase: replacing ? "replacing" : "publishing",
952
+ currentPublishDecision: replacing ? "replacing" : "publishing",
953
+ targetBlockHeight: options.candidate.targetBlockHeight,
954
+ referencedBlockHashDisplay: options.candidate.referencedBlockHashDisplay,
955
+ currentDomainId: options.candidate.domainId,
956
+ currentDomainName: options.candidate.domainName,
957
+ currentSentenceDisplay: options.candidate.sentence,
958
+ currentCanonicalBlend: options.candidate.canonicalBlend.toString(),
959
+ note: replacing
960
+ ? "Replacing the live mining transaction for the current tip."
961
+ : "Broadcasting the best mining candidate for the current tip.",
962
+ ...(replacingAcrossTips
963
+ ? {
964
+ currentPublishState: "none",
965
+ currentTxid: null,
966
+ currentWtxid: null,
967
+ livePublishInMempool: false,
968
+ currentFeeRateSatVb: null,
969
+ currentAbsoluteFeeSats: null,
970
+ currentBlockFeeSpentSats: "0",
971
+ }
972
+ : {}),
973
+ };
974
+ }
667
975
  async function refreshAndSaveStatus(options) {
668
976
  const view = await inspectMiningControlPlane({
669
977
  provider: options.provider,
@@ -705,6 +1013,7 @@ async function handleDetectedMiningRuntimeResume(options) {
705
1013
  note: "Mining discarded stale in-flight work after a large local runtime gap and is rechecking health.",
706
1014
  },
707
1015
  visualizer: options.visualizer,
1016
+ visualizerState: createIndexedMiningFollowVisualizerState(readContext),
708
1017
  });
709
1018
  }
710
1019
  finally {
@@ -897,6 +1206,12 @@ function createStaleMiningCandidateWaitingNote() {
897
1206
  function createRetryableMiningPublishWaitingNote() {
898
1207
  return "Selected mining candidate did not reach mempool and will be retried on the current tip with refreshed wallet state.";
899
1208
  }
1209
+ function createInsufficientFundsMiningPublishWaitingNote() {
1210
+ return "Mining is waiting for enough confirmed safe BTC funding that Bitcoin Core can use for the next publish.";
1211
+ }
1212
+ function createInsufficientFundsMiningPublishErrorMessage() {
1213
+ return "Bitcoin Core could not fund the next mining publish with confirmed safe BTC.";
1214
+ }
900
1215
  async function generateCandidatesForDomains(options) {
901
1216
  const bestBlockHash = options.readContext.nodeStatus?.nodeBestHashHex;
902
1217
  if (bestBlockHash === null || bestBlockHash === undefined) {
@@ -1524,6 +1839,7 @@ async function publishCandidateOnce(options) {
1524
1839
  dataDir: options.dataDir,
1525
1840
  chain: "main",
1526
1841
  startHeight: 0,
1842
+ serviceLifetime: "ephemeral",
1527
1843
  walletRootId: options.readContext.localState.state.walletRootId,
1528
1844
  });
1529
1845
  const rpc = options.rpcFactory(service.rpc);
@@ -1801,6 +2117,29 @@ async function publishCandidate(options) {
1801
2117
  candidate: refreshedCandidate,
1802
2118
  };
1803
2119
  }
2120
+ if (isInsufficientFundsError(error)) {
2121
+ const note = createInsufficientFundsMiningPublishWaitingNote();
2122
+ const lastError = createInsufficientFundsMiningPublishErrorMessage();
2123
+ await appendEventFn(options.paths, createEvent("publish-paused-insufficient-funds", "Paused mining publish because Bitcoin Core could not fund the next mining transaction with confirmed safe BTC.", {
2124
+ level: "warn",
2125
+ runId: options.runId,
2126
+ targetBlockHeight: refreshedCandidate.targetBlockHeight,
2127
+ referencedBlockHashDisplay: refreshedCandidate.referencedBlockHashDisplay,
2128
+ domainId: refreshedCandidate.domainId,
2129
+ domainName: refreshedCandidate.domainName,
2130
+ score: refreshedCandidate.canonicalBlend.toString(),
2131
+ reason: "insufficient-funds",
2132
+ }));
2133
+ return {
2134
+ state: readyReadContext.localState.state,
2135
+ txid: null,
2136
+ decision: "publish-paused-insufficient-funds",
2137
+ note,
2138
+ lastError,
2139
+ skipped: true,
2140
+ candidate: null,
2141
+ };
2142
+ }
1804
2143
  throw error;
1805
2144
  }
1806
2145
  }
@@ -1811,7 +2150,7 @@ async function publishCandidate(options) {
1811
2150
  export async function publishCandidateForTesting(options) {
1812
2151
  return await publishCandidate(options);
1813
2152
  }
1814
- async function ensureBuiltInSetupIfNeeded(options) {
2153
+ export async function ensureBuiltInMiningSetupIfNeeded(options) {
1815
2154
  const config = await loadClientConfig({
1816
2155
  path: options.paths.clientConfigPath,
1817
2156
  provider: options.provider,
@@ -1869,6 +2208,7 @@ async function performMiningCycle(options) {
1869
2208
  dataDir: options.dataDir,
1870
2209
  chain: "main",
1871
2210
  startHeight: 0,
2211
+ serviceLifetime: "ephemeral",
1872
2212
  walletRootId: readContext.localState.state.walletRootId,
1873
2213
  });
1874
2214
  checkpointMiningSuspendDetector(options.suspendDetector);
@@ -1911,6 +2251,16 @@ async function performMiningCycle(options) {
1911
2251
  indexedTipHashHex: indexedTip?.blockHashHex ?? null,
1912
2252
  }).catch(() => ({}));
1913
2253
  syncMiningVisualizerBlockTimes(options.loopState, visibleBlockTimes);
2254
+ const { targetBlockHeight, tipKey } = syncMiningUiForCurrentTip({
2255
+ loopState: options.loopState,
2256
+ snapshotState: effectiveReadContext.snapshot?.state ?? null,
2257
+ snapshotTipHeight: effectiveReadContext.snapshot?.tip?.height ?? effectiveReadContext.indexer.snapshotTip?.height ?? null,
2258
+ nodeBestHeight: effectiveReadContext.nodeStatus?.nodeBestHeight ?? null,
2259
+ nodeBestHash: effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null,
2260
+ recentWin: reconciliation.recentWin,
2261
+ });
2262
+ const displaySats = await resolveFundingDisplaySats(effectiveReadContext.localState.state, rpc).catch(() => null);
2263
+ syncMiningVisualizerBalances(options.loopState, effectiveReadContext, displaySats);
1914
2264
  if (effectiveReadContext.localState.state.miningState.state === "repair-required") {
1915
2265
  await refreshAndSaveStatus({
1916
2266
  paths: options.paths,
@@ -2038,18 +2388,21 @@ async function performMiningCycle(options) {
2038
2388
  });
2039
2389
  return;
2040
2390
  }
2041
- const targetBlockHeight = (effectiveReadContext.nodeStatus?.nodeBestHeight ?? 0) + 1;
2042
- const tipKey = buildMiningTipKey(effectiveReadContext.nodeStatus?.nodeBestHashHex ?? null, targetBlockHeight);
2043
- if (tipKey !== options.loopState.currentTipKey) {
2044
- options.loopState.currentTipKey = tipKey;
2045
- resetMiningUiForTip(options.loopState, targetBlockHeight);
2046
- if (reconciliation.recentWin !== null) {
2047
- options.loopState.ui.recentWin = reconciliation.recentWin;
2048
- }
2391
+ if (targetBlockHeight === null) {
2392
+ await refreshAndSaveStatus({
2393
+ paths: options.paths,
2394
+ provider: options.provider,
2395
+ readContext: effectiveReadContext,
2396
+ overrides: {
2397
+ runMode: options.runMode,
2398
+ currentPhase: "waiting-bitcoin-network",
2399
+ note: "Mining is waiting for the local Bitcoin node to become publishable.",
2400
+ },
2401
+ visualizer: options.visualizer,
2402
+ visualizerState: options.loopState.ui,
2403
+ });
2404
+ return;
2049
2405
  }
2050
- syncMiningUiSettledBoard(options.loopState, effectiveReadContext.snapshot?.state ?? null, effectiveReadContext.snapshot?.tip?.height ?? effectiveReadContext.indexer.snapshotTip?.height ?? null, effectiveReadContext.nodeStatus?.nodeBestHeight ?? null);
2051
- const displaySats = await resolveFundingDisplaySats(effectiveReadContext.localState.state, rpc).catch(() => null);
2052
- syncMiningVisualizerBalances(options.loopState, effectiveReadContext, displaySats);
2053
2406
  if (getBlockRewardCogtoshi(targetBlockHeight) === 0n) {
2054
2407
  const nextState = defaultMiningStatePatch(effectiveReadContext.localState.state, {
2055
2408
  state: "paused",
@@ -2393,12 +2746,10 @@ async function performMiningCycle(options) {
2393
2746
  readContext: effectiveReadContext,
2394
2747
  overrides: {
2395
2748
  runMode: options.runMode,
2396
- currentPhase: effectiveReadContext.localState.state.miningState.currentTxid === null
2397
- ? "publishing"
2398
- : "replacing",
2399
- note: effectiveReadContext.localState.state.miningState.currentTxid === null
2400
- ? "Broadcasting the best mining candidate for the current tip."
2401
- : "Replacing the live mining transaction for the current tip.",
2749
+ ...buildPrePublishStatusOverrides({
2750
+ state: effectiveReadContext.localState.state,
2751
+ candidate: selectedCandidate,
2752
+ }),
2402
2753
  },
2403
2754
  visualizer: options.visualizer,
2404
2755
  visualizerState: options.loopState.ui,
@@ -2465,6 +2816,9 @@ async function performMiningCycle(options) {
2465
2816
  clearSelectedCandidate(options.loopState);
2466
2817
  setMiningUiCandidate(options.loopState, selectedCandidate);
2467
2818
  options.loopState.waitingNote = published.note;
2819
+ const lastError = published.decision === "publish-paused-insufficient-funds"
2820
+ ? published.lastError ?? createInsufficientFundsMiningPublishErrorMessage()
2821
+ : undefined;
2468
2822
  await refreshAndSaveStatus({
2469
2823
  paths: options.paths,
2470
2824
  provider: options.provider,
@@ -2486,6 +2840,7 @@ async function performMiningCycle(options) {
2486
2840
  mempoolSequenceCacheStatus: gateSnapshot.mempoolSequenceCacheStatus,
2487
2841
  lastMempoolSequence: gateSnapshot.lastMempoolSequence,
2488
2842
  lastCompetitivenessGateAtUnixMs: Date.now(),
2843
+ lastError,
2489
2844
  note: published.note,
2490
2845
  livePublishInMempool: published.state.miningState.livePublishInMempool,
2491
2846
  },
@@ -2580,6 +2935,7 @@ async function saveStopSnapshot(options) {
2580
2935
  dataDir: options.dataDir,
2581
2936
  chain: "main",
2582
2937
  startHeight: 0,
2938
+ serviceLifetime: "ephemeral",
2583
2939
  walletRootId: localState.state.walletRootId,
2584
2940
  }).catch(() => null);
2585
2941
  if (service !== null) {
@@ -2692,6 +3048,7 @@ async function runMiningLoop(options) {
2692
3048
  dataDir: options.dataDir,
2693
3049
  chain: "main",
2694
3050
  startHeight: 0,
3051
+ serviceLifetime: "ephemeral",
2695
3052
  walletRootId: undefined,
2696
3053
  }).catch(() => null);
2697
3054
  if (service !== null) {
@@ -2723,34 +3080,50 @@ export async function runForegroundMining(options) {
2723
3080
  const openReadContext = options.openReadContext ?? openWalletReadContext;
2724
3081
  const attachService = options.attachService ?? attachOrStartManagedBitcoindService;
2725
3082
  const rpcFactory = options.rpcFactory ?? createRpcClient;
2726
- const controlLock = await acquireFileLock(paths.miningControlLockPath, {
2727
- purpose: "mine-foreground",
2728
- });
3083
+ const requestMiningPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
3084
+ const runMiningLoopImpl = options.runMiningLoopImpl ?? runMiningLoop;
3085
+ const saveStopSnapshotImpl = options.saveStopSnapshotImpl ?? saveStopSnapshot;
2729
3086
  let visualizer = null;
2730
- try {
2731
- const existing = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
2732
- if (existing?.runMode === "background") {
2733
- throw new Error("Background mining is already active. Run `cogcoin mine stop` first.");
2734
- }
2735
- const setupReady = await ensureBuiltInSetupIfNeeded({
3087
+ const setupReady = options.builtInSetupEnsured === true
3088
+ ? true
3089
+ : await ensureBuiltInMiningSetupIfNeeded({
2736
3090
  provider,
2737
3091
  prompter: options.prompter,
2738
3092
  paths,
2739
3093
  });
2740
- if (!setupReady) {
2741
- throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
2742
- }
3094
+ if (!setupReady) {
3095
+ throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
3096
+ }
3097
+ const controlLock = await acquireMiningStartControlLock({
3098
+ paths,
3099
+ purpose: "mine-foreground",
3100
+ takeoverReason: "mine-foreground-replace",
3101
+ requestMiningPreemption,
3102
+ shutdownGraceMs: options.shutdownGraceMs,
3103
+ sleepImpl: options.sleepImpl,
3104
+ });
3105
+ const abortController = new AbortController();
3106
+ const abortListener = () => {
3107
+ abortController.abort();
3108
+ };
3109
+ const handleSigint = () => abortController.abort();
3110
+ const handleSigterm = () => abortController.abort();
3111
+ try {
3112
+ await takeOverMiningRuntime({
3113
+ paths,
3114
+ reason: "mine-foreground-replace",
3115
+ requestMiningPreemption,
3116
+ shutdownGraceMs: options.shutdownGraceMs,
3117
+ sleepImpl: options.sleepImpl,
3118
+ });
2743
3119
  visualizer = new MiningFollowVisualizer({
2744
3120
  progressOutput: options.progressOutput ?? "auto",
2745
3121
  stream: options.stderr,
2746
3122
  });
2747
- const abortController = new AbortController();
2748
- options.signal?.addEventListener("abort", () => {
2749
- abortController.abort();
2750
- }, { once: true });
2751
- process.on("SIGINT", () => abortController.abort());
2752
- process.on("SIGTERM", () => abortController.abort());
2753
- await runMiningLoop({
3123
+ options.signal?.addEventListener("abort", abortListener, { once: true });
3124
+ process.on("SIGINT", handleSigint);
3125
+ process.on("SIGTERM", handleSigterm);
3126
+ await runMiningLoopImpl({
2754
3127
  dataDir: options.dataDir,
2755
3128
  databasePath: options.databasePath,
2756
3129
  provider,
@@ -2766,7 +3139,7 @@ export async function runForegroundMining(options) {
2766
3139
  stdout: options.stdout,
2767
3140
  visualizer,
2768
3141
  });
2769
- await saveStopSnapshot({
3142
+ await saveStopSnapshotImpl({
2770
3143
  dataDir: options.dataDir,
2771
3144
  databasePath: options.databasePath,
2772
3145
  provider,
@@ -2778,6 +3151,9 @@ export async function runForegroundMining(options) {
2778
3151
  });
2779
3152
  }
2780
3153
  finally {
3154
+ options.signal?.removeEventListener("abort", abortListener);
3155
+ process.off("SIGINT", handleSigint);
3156
+ process.off("SIGTERM", handleSigterm);
2781
3157
  visualizer?.close();
2782
3158
  await controlLock.release();
2783
3159
  }
@@ -2785,33 +3161,50 @@ export async function runForegroundMining(options) {
2785
3161
  export async function startBackgroundMining(options) {
2786
3162
  const provider = options.provider ?? createDefaultWalletSecretProvider();
2787
3163
  const paths = options.paths ?? resolveWalletRuntimePathsForTesting();
2788
- const controlLock = await acquireFileLock(paths.miningControlLockPath, {
2789
- purpose: "mine-start",
2790
- });
3164
+ const requestMiningPreemption = options.requestMiningPreemption ?? requestMiningGenerationPreemption;
3165
+ const spawnWorkerProcess = options.spawnWorkerProcess ?? spawn;
3166
+ const waitForBackgroundHealthyImpl = options.waitForBackgroundHealthyImpl ?? waitForBackgroundHealthy;
3167
+ const setupReady = options.builtInSetupEnsured === true
3168
+ ? true
3169
+ : await ensureBuiltInMiningSetupIfNeeded({
3170
+ provider,
3171
+ prompter: options.prompter,
3172
+ paths,
3173
+ });
3174
+ if (!setupReady) {
3175
+ throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
3176
+ }
3177
+ let controlLock;
2791
3178
  try {
2792
- const existing = await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null);
2793
- if (existing?.runMode === "background"
2794
- && existing.backgroundWorkerPid !== null
2795
- && await isProcessAlive(existing.backgroundWorkerPid)) {
3179
+ controlLock = await acquireMiningStartControlLock({
3180
+ paths,
3181
+ purpose: "mine-start",
3182
+ takeoverReason: "mine-start-replace",
3183
+ requestMiningPreemption,
3184
+ shutdownGraceMs: options.shutdownGraceMs,
3185
+ sleepImpl: options.sleepImpl,
3186
+ });
3187
+ }
3188
+ catch (error) {
3189
+ if (error instanceof FileLockBusyError && error.existingMetadata?.processId === process.pid) {
2796
3190
  return {
2797
3191
  started: false,
2798
- snapshot: existing,
3192
+ snapshot: await loadMiningRuntimeStatus(paths.miningStatusPath).catch(() => null),
2799
3193
  };
2800
3194
  }
2801
- if (existing?.runMode === "foreground") {
2802
- throw new Error("Foreground mining is already active. Interrupt that process directly.");
2803
- }
2804
- const setupReady = await ensureBuiltInSetupIfNeeded({
2805
- provider,
2806
- prompter: options.prompter,
3195
+ throw error;
3196
+ }
3197
+ try {
3198
+ await takeOverMiningRuntime({
2807
3199
  paths,
3200
+ reason: "mine-start-replace",
3201
+ requestMiningPreemption,
3202
+ shutdownGraceMs: options.shutdownGraceMs,
3203
+ sleepImpl: options.sleepImpl,
2808
3204
  });
2809
- if (!setupReady) {
2810
- throw new Error("Built-in mining provider is not configured. Run `cogcoin mine setup`.");
2811
- }
2812
3205
  const runId = randomBytes(16).toString("hex");
2813
3206
  const workerMainPath = fileURLToPath(new URL("./worker-main.js", import.meta.url));
2814
- const child = spawn(process.execPath, [
3207
+ const child = spawnWorkerProcess(process.execPath, [
2815
3208
  workerMainPath,
2816
3209
  `--data-dir=${options.dataDir}`,
2817
3210
  `--database-path=${options.databasePath}`,
@@ -2821,7 +3214,7 @@ export async function startBackgroundMining(options) {
2821
3214
  stdio: "ignore",
2822
3215
  });
2823
3216
  child.unref();
2824
- const snapshot = await waitForBackgroundHealthy(paths);
3217
+ const snapshot = await waitForBackgroundHealthyImpl(paths);
2825
3218
  return {
2826
3219
  started: true,
2827
3220
  snapshot,
@@ -2955,12 +3348,21 @@ export async function runBackgroundMiningWorker(options) {
2955
3348
  export async function handleDetectedMiningRuntimeResumeForTesting(options) {
2956
3349
  await handleDetectedMiningRuntimeResume(options);
2957
3350
  }
3351
+ export async function takeOverMiningRuntimeForTesting(options) {
3352
+ return await takeOverMiningRuntime(options);
3353
+ }
2958
3354
  export async function performMiningCycleForTesting(options) {
2959
3355
  await performMiningCycle({
2960
3356
  ...options,
2961
3357
  loopState: options.loopState ?? createMiningLoopState(),
2962
3358
  });
2963
3359
  }
3360
+ export function buildPrePublishStatusOverridesForTesting(options) {
3361
+ return buildPrePublishStatusOverrides(options);
3362
+ }
3363
+ export function buildStatusSnapshotForTesting(view, overrides = {}) {
3364
+ return buildStatusSnapshot(view, overrides);
3365
+ }
2964
3366
  export function shouldKeepCurrentTipLivePublishForTesting(options) {
2965
3367
  return livePublishTargetsCandidateTip(options);
2966
3368
  }