@cogcoin/client 1.1.14 → 1.1.15

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.1.14` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
3
+ `@cogcoin/client@1.1.15` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
4
4
 
5
5
  Use Node 22 or newer.
6
6
 
@@ -1,5 +1,6 @@
1
1
  import type { ManagedIndexerDaemonProbeResult } from "../managed-runtime/types.js";
2
2
  import type { ManagedIndexerDaemonObservedStatus } from "../types.js";
3
+ import { resolveManagedServicePaths } from "../service-paths.js";
3
4
  import type { CoherentIndexerSnapshotLease, IndexerDaemonClient, IndexerDaemonStopResult, ManagedIndexerDaemonServiceLifetime } from "./types.js";
4
5
  export type { IndexerDaemonCompatibility } from "../managed-runtime/types.js";
5
6
  export declare const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
@@ -8,6 +9,11 @@ export declare function probeIndexerDaemon(options: {
8
9
  dataDir: string;
9
10
  walletRootId?: string;
10
11
  }): Promise<IndexerDaemonProbeResult>;
12
+ export declare function probeIndexerDaemonForStart(options: {
13
+ dataDir: string;
14
+ walletRootId: string;
15
+ paths?: ReturnType<typeof resolveManagedServicePaths>;
16
+ }): Promise<IndexerDaemonProbeResult>;
11
17
  export declare function readSnapshotWithRetry(daemon: IndexerDaemonClient, expectedWalletRootId: string): Promise<CoherentIndexerSnapshotLease>;
12
18
  export declare function readObservedIndexerDaemonStatus(options: {
13
19
  dataDir: string;
@@ -7,7 +7,8 @@ import { attachOrStartManagedIndexerRuntime } from "../managed-runtime/indexer-r
7
7
  import { readJsonFileIfPresent } from "../managed-runtime/status.js";
8
8
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
9
9
  import { createIndexerDaemonClient, probeIndexerDaemonAtSocket, } from "./client.js";
10
- import { DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS, sleep, stopIndexerDaemonService as stopIndexerDaemonServiceInternal, stopIndexerDaemonServiceWithLockHeld, waitForIndexerDaemon, } from "./process.js";
10
+ import { DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS, clearIndexerDaemonRuntimeArtifacts, isIndexerDaemonProcessAlive, sleep, stopIndexerDaemonService as stopIndexerDaemonServiceInternal, stopIndexerDaemonServiceWithLockHeld, waitForIndexerDaemon, } from "./process.js";
11
+ import { openIndexerDaemonStartupLog, recordIndexerDaemonStartupFailure, waitForIndexerDaemonStartup, } from "./startup.js";
11
12
  export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
12
13
  const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
13
14
  export async function probeIndexerDaemon(options) {
@@ -15,6 +16,25 @@ export async function probeIndexerDaemon(options) {
15
16
  const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
16
17
  return probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
17
18
  }
19
+ export async function probeIndexerDaemonForStart(options) {
20
+ const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, options.walletRootId);
21
+ const probe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, options.walletRootId);
22
+ if (probe.compatibility === "compatible") {
23
+ return probe;
24
+ }
25
+ const status = await readJsonFileIfPresent(paths.indexerDaemonStatusPath).catch(() => null);
26
+ const processId = status?.processId ?? null;
27
+ if (processId !== null && !await isIndexerDaemonProcessAlive(processId)) {
28
+ await clearIndexerDaemonRuntimeArtifacts(paths);
29
+ return {
30
+ compatibility: "unreachable",
31
+ status: null,
32
+ client: null,
33
+ error: null,
34
+ };
35
+ }
36
+ return probe;
37
+ }
18
38
  export async function readSnapshotWithRetry(daemon, expectedWalletRootId) {
19
39
  let lastError = null;
20
40
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -69,14 +89,16 @@ export async function attachOrStartIndexerDaemon(options) {
69
89
  const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
70
90
  const startDaemon = async () => {
71
91
  await mkdir(paths.indexerServiceRoot, { recursive: true });
92
+ const startupLog = await openIndexerDaemonStartupLog(paths);
72
93
  const daemonEntryPath = fileURLToPath(new URL("../indexer-daemon-main.js", import.meta.url));
94
+ const stdio = ["ignore", startupLog.fd, startupLog.fd];
73
95
  const spawnOptions = serviceLifetime === "ephemeral"
74
96
  ? {
75
- stdio: "ignore",
97
+ stdio,
76
98
  }
77
99
  : {
78
100
  detached: true,
79
- stdio: "ignore",
101
+ stdio,
80
102
  };
81
103
  const child = spawn(process.execPath, [
82
104
  daemonEntryPath,
@@ -86,11 +108,16 @@ export async function attachOrStartIndexerDaemon(options) {
86
108
  ], {
87
109
  ...spawnOptions,
88
110
  });
111
+ await startupLog.close().catch(() => undefined);
89
112
  if (serviceLifetime !== "ephemeral") {
90
113
  child.unref();
91
114
  }
92
115
  try {
93
- await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
116
+ await waitForIndexerDaemonStartup({
117
+ child,
118
+ logPath: startupLog.logPath,
119
+ waitForReady: () => waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs),
120
+ });
94
121
  }
95
122
  catch (error) {
96
123
  if (child.pid !== undefined) {
@@ -101,6 +128,13 @@ export async function attachOrStartIndexerDaemon(options) {
101
128
  // ignore shutdown failures while unwinding startup errors
102
129
  }
103
130
  }
131
+ await recordIndexerDaemonStartupFailure({
132
+ paths,
133
+ walletRootId,
134
+ binaryVersion: expectedBinaryVersion ?? "0.0.0",
135
+ lastError: error instanceof Error ? error.message : String(error),
136
+ processId: child.pid ?? null,
137
+ }).catch(() => undefined);
104
138
  throw error;
105
139
  }
106
140
  return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
@@ -122,7 +156,11 @@ export async function attachOrStartIndexerDaemon(options) {
122
156
  expectedBinaryVersion,
123
157
  }, {
124
158
  getPaths: (runtimeOptions) => resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
125
- probeDaemon: async (runtimeOptions, runtimePaths) => probeIndexerDaemonAtSocket(runtimePaths.indexerDaemonSocketPath, runtimeOptions.walletRootId),
159
+ probeDaemon: async (runtimeOptions, runtimePaths) => probeIndexerDaemonForStart({
160
+ dataDir: runtimeOptions.dataDir,
161
+ walletRootId: runtimeOptions.walletRootId,
162
+ paths: runtimePaths,
163
+ }),
126
164
  requestBackgroundFollow,
127
165
  closeClient: async (client) => {
128
166
  await client.close();
@@ -0,0 +1,7 @@
1
+ import { loadBetterSqlite3 } from "../../sqlite/better-sqlite3.js";
2
+ export declare const SQLITE_NATIVE_MODULE_UNAVAILABLE = "sqlite_native_module_unavailable";
3
+ type LoadBetterSqlite3 = typeof loadBetterSqlite3;
4
+ export declare function isSqliteNativeModuleLoadFailure(error: unknown): boolean;
5
+ export declare function createSqliteNativeModuleUnavailableError(error: unknown): Error;
6
+ export declare function assertIndexerDaemonNativeDependencies(loadBetterSqlite3Impl?: LoadBetterSqlite3): Promise<void>;
7
+ export {};
@@ -0,0 +1,27 @@
1
+ import { loadBetterSqlite3 } from "../../sqlite/better-sqlite3.js";
2
+ export const SQLITE_NATIVE_MODULE_UNAVAILABLE = "sqlite_native_module_unavailable";
3
+ export function isSqliteNativeModuleLoadFailure(error) {
4
+ if (!(error instanceof Error)) {
5
+ return false;
6
+ }
7
+ const message = error.message;
8
+ return message.includes("NODE_MODULE_VERSION")
9
+ || message.includes("better_sqlite3.node")
10
+ || message.includes("Cannot find module 'better-sqlite3'")
11
+ || message.includes('Cannot find module "better-sqlite3"');
12
+ }
13
+ export function createSqliteNativeModuleUnavailableError(error) {
14
+ const detail = error instanceof Error ? error.message : String(error);
15
+ return new Error(`${SQLITE_NATIVE_MODULE_UNAVAILABLE}: ${detail}`, { cause: error });
16
+ }
17
+ export async function assertIndexerDaemonNativeDependencies(loadBetterSqlite3Impl = loadBetterSqlite3) {
18
+ try {
19
+ await loadBetterSqlite3Impl();
20
+ }
21
+ catch (error) {
22
+ if (isSqliteNativeModuleLoadFailure(error)) {
23
+ throw createSqliteNativeModuleUnavailableError(error);
24
+ }
25
+ throw error;
26
+ }
27
+ }
@@ -5,6 +5,7 @@ import { createBootstrapProgress } from "../progress/formatting.js";
5
5
  import { resolveManagedServicePaths } from "../service-paths.js";
6
6
  import { UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
7
7
  import { pauseBackgroundFollow, recordBackgroundFollowFailure, resumeBackgroundFollow, withTimeout, } from "./background-follow.js";
8
+ import { assertIndexerDaemonNativeDependencies } from "./native-dependencies.js";
8
9
  import { createIndexerDaemonServer } from "./server.js";
9
10
  import { buildIndexerDaemonStatus, deriveIndexerDaemonLeaseState, observeIndexerAppliedTip, readCoreTipStatus, refreshIndexerDaemonStatus, writeIndexerDaemonStatus, } from "./status.js";
10
11
  import { closeSnapshotLease, createSnapshotHandle, loadSnapshotMaterial, pruneExpiredSnapshotLeases, readSnapshotLease, storeSnapshotLease, } from "./snapshot-leases.js";
@@ -152,6 +153,25 @@ export function createIndexerDaemonRuntime(options) {
152
153
  return;
153
154
  }
154
155
  await mkdir(paths.indexerServiceRoot, { recursive: true });
156
+ try {
157
+ await assertIndexerDaemonNativeDependencies();
158
+ }
159
+ catch (error) {
160
+ const now = Date.now();
161
+ state.state = "failed";
162
+ state.lastError = error instanceof Error ? error.message : String(error);
163
+ state.heartbeatAtUnixMs = now;
164
+ state.updatedAtUnixMs = now;
165
+ state.bootstrapPhase = "error";
166
+ state.bootstrapProgress = {
167
+ ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
168
+ message: state.lastError,
169
+ lastError: state.lastError,
170
+ updatedAt: now,
171
+ };
172
+ await writeStatus().catch(() => undefined);
173
+ throw error;
174
+ }
155
175
  await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
156
176
  server = createIndexerDaemonServer({
157
177
  getStatus: () => buildIndexerDaemonStatus(state),
@@ -0,0 +1,44 @@
1
+ import type { ManagedServicePaths } from "../service-paths.js";
2
+ type ChildProcessLike = {
3
+ pid?: number;
4
+ once(event: "error", listener: (error: Error) => void): unknown;
5
+ once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
6
+ off(event: "error", listener: (error: Error) => void): unknown;
7
+ off(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
8
+ };
9
+ export declare class IndexerDaemonStartupError extends Error {
10
+ readonly logPath: string;
11
+ readonly logTail: string | null;
12
+ readonly exitCode: number | null;
13
+ readonly signal: NodeJS.Signals | null;
14
+ constructor(message: string, options: {
15
+ logPath: string;
16
+ logTail?: string | null;
17
+ exitCode?: number | null;
18
+ signal?: NodeJS.Signals | null;
19
+ cause?: unknown;
20
+ });
21
+ }
22
+ export declare function getIndexerDaemonStartupLogPath(error: unknown): string | null;
23
+ export declare function getIndexerDaemonStartupLogTail(error: unknown): string | null;
24
+ export declare function openIndexerDaemonStartupLog(paths: ManagedServicePaths): Promise<{
25
+ fd: number;
26
+ logPath: string;
27
+ close(): Promise<void>;
28
+ }>;
29
+ export declare function readIndexerDaemonStartupLogTail(logPath: string, maxBytes?: number): Promise<string | null>;
30
+ export declare function classifyIndexerDaemonStartupFailure(logTail: string | null): string;
31
+ export declare function waitForIndexerDaemonStartup(options: {
32
+ child: ChildProcessLike;
33
+ logPath: string;
34
+ waitForReady(): Promise<void>;
35
+ readLogTail?: (logPath: string) => Promise<string | null>;
36
+ }): Promise<void>;
37
+ export declare function recordIndexerDaemonStartupFailure(options: {
38
+ paths: ManagedServicePaths;
39
+ walletRootId: string;
40
+ binaryVersion: string;
41
+ lastError: string;
42
+ processId?: number | null;
43
+ }): Promise<void>;
44
+ export {};
@@ -0,0 +1,152 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, rm } from "node:fs/promises";
3
+ import { writeRuntimeStatusFile } from "../../wallet/fs/status-file.js";
4
+ import { readJsonFileIfPresent } from "../managed-runtime/status.js";
5
+ import { SQLITE_NATIVE_MODULE_UNAVAILABLE } from "./native-dependencies.js";
6
+ import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "../types.js";
7
+ const STARTUP_LOG_TAIL_BYTES = 4_096;
8
+ export class IndexerDaemonStartupError extends Error {
9
+ logPath;
10
+ logTail;
11
+ exitCode;
12
+ signal;
13
+ constructor(message, options) {
14
+ super(message, { cause: options.cause });
15
+ this.name = "IndexerDaemonStartupError";
16
+ this.logPath = options.logPath;
17
+ this.logTail = options.logTail ?? null;
18
+ this.exitCode = options.exitCode ?? null;
19
+ this.signal = options.signal ?? null;
20
+ }
21
+ }
22
+ export function getIndexerDaemonStartupLogPath(error) {
23
+ return error instanceof IndexerDaemonStartupError ? error.logPath : null;
24
+ }
25
+ export function getIndexerDaemonStartupLogTail(error) {
26
+ return error instanceof IndexerDaemonStartupError ? error.logTail : null;
27
+ }
28
+ export async function openIndexerDaemonStartupLog(paths) {
29
+ await mkdir(paths.indexerServiceRoot, { recursive: true });
30
+ const handle = await open(paths.indexerDaemonLogPath, "w", 0o600);
31
+ await handle.writeFile([
32
+ `Cogcoin indexer daemon startup log`,
33
+ `pid=${process.pid}`,
34
+ `node=${process.version}`,
35
+ `time=${new Date().toISOString()}`,
36
+ "",
37
+ ].join("\n"));
38
+ return {
39
+ fd: handle.fd,
40
+ logPath: paths.indexerDaemonLogPath,
41
+ async close() {
42
+ await handle.close();
43
+ },
44
+ };
45
+ }
46
+ export async function readIndexerDaemonStartupLogTail(logPath, maxBytes = STARTUP_LOG_TAIL_BYTES) {
47
+ try {
48
+ const bytes = await readFile(logPath);
49
+ const tail = bytes.subarray(Math.max(0, bytes.byteLength - maxBytes));
50
+ const text = tail.toString("utf8").trim();
51
+ return text.length > 0 ? text : null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export function classifyIndexerDaemonStartupFailure(logTail) {
58
+ return logTail?.includes(SQLITE_NATIVE_MODULE_UNAVAILABLE) === true
59
+ ? SQLITE_NATIVE_MODULE_UNAVAILABLE
60
+ : "indexer_daemon_start_failed";
61
+ }
62
+ export async function waitForIndexerDaemonStartup(options) {
63
+ const readLogTail = options.readLogTail ?? readIndexerDaemonStartupLogTail;
64
+ const ready = options.waitForReady();
65
+ const childFailure = new Promise((_, reject) => {
66
+ const onError = async (error) => {
67
+ const logTail = await readLogTail(options.logPath);
68
+ reject(new IndexerDaemonStartupError(classifyIndexerDaemonStartupFailure(logTail), {
69
+ logPath: options.logPath,
70
+ logTail,
71
+ cause: error,
72
+ }));
73
+ };
74
+ const onExit = async (code, signal) => {
75
+ const logTail = await readLogTail(options.logPath);
76
+ reject(new IndexerDaemonStartupError(classifyIndexerDaemonStartupFailure(logTail), {
77
+ logPath: options.logPath,
78
+ logTail,
79
+ exitCode: code,
80
+ signal,
81
+ }));
82
+ };
83
+ options.child.once("error", onError);
84
+ options.child.once("exit", onExit);
85
+ ready.then(() => {
86
+ options.child.off("error", onError);
87
+ options.child.off("exit", onExit);
88
+ }, () => {
89
+ options.child.off("error", onError);
90
+ options.child.off("exit", onExit);
91
+ });
92
+ });
93
+ try {
94
+ await Promise.race([
95
+ ready,
96
+ childFailure,
97
+ ]);
98
+ }
99
+ catch (error) {
100
+ if (error instanceof IndexerDaemonStartupError) {
101
+ throw error;
102
+ }
103
+ const logTail = await readLogTail(options.logPath);
104
+ throw new IndexerDaemonStartupError(error instanceof Error ? error.message : "indexer_daemon_start_timeout", {
105
+ logPath: options.logPath,
106
+ logTail,
107
+ cause: error,
108
+ });
109
+ }
110
+ }
111
+ export async function recordIndexerDaemonStartupFailure(options) {
112
+ await rm(options.paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
113
+ await mkdir(options.paths.indexerServiceRoot, { recursive: true });
114
+ const existing = await readJsonFileIfPresent(options.paths.indexerDaemonStatusPath).catch(() => null);
115
+ if (existing?.processId === (options.processId ?? null)
116
+ && (existing.state === "schema-mismatch"
117
+ || (existing.state === "failed" && existing.lastError !== null))) {
118
+ return;
119
+ }
120
+ const now = Date.now();
121
+ const status = {
122
+ serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
123
+ binaryVersion: options.binaryVersion,
124
+ buildId: null,
125
+ updatedAtUnixMs: now,
126
+ walletRootId: options.walletRootId,
127
+ daemonInstanceId: randomUUID(),
128
+ schemaVersion: INDEXER_DAEMON_SCHEMA_VERSION,
129
+ state: "failed",
130
+ processId: options.processId ?? null,
131
+ startedAtUnixMs: now,
132
+ heartbeatAtUnixMs: now,
133
+ ipcReady: false,
134
+ rpcReachable: false,
135
+ coreBestHeight: null,
136
+ coreBestHash: null,
137
+ appliedTipHeight: null,
138
+ appliedTipHash: null,
139
+ snapshotSeq: null,
140
+ backlogBlocks: null,
141
+ reorgDepth: null,
142
+ lastAppliedAtUnixMs: null,
143
+ activeSnapshotCount: 0,
144
+ lastError: options.lastError,
145
+ backgroundFollowActive: false,
146
+ bootstrapPhase: "error",
147
+ bootstrapProgress: null,
148
+ cogcoinSyncHeight: null,
149
+ cogcoinSyncTargetHeight: null,
150
+ };
151
+ await writeRuntimeStatusFile(options.paths.indexerDaemonStatusPath, status);
152
+ }
@@ -115,6 +115,16 @@ function mapIndexerStartupError(message) {
115
115
  health: "starting",
116
116
  message: "Indexer daemon is still starting.",
117
117
  };
118
+ case "indexer_daemon_start_failed":
119
+ return {
120
+ health: "failed",
121
+ message: "The managed indexer daemon exited before it opened its local IPC socket.",
122
+ };
123
+ case "sqlite_native_module_unavailable":
124
+ return {
125
+ health: "failed",
126
+ message: "The managed indexer daemon could not load its SQLite native module.",
127
+ };
118
128
  case "indexer_daemon_service_version_mismatch":
119
129
  return {
120
130
  health: "service-version-mismatch",
@@ -15,5 +15,6 @@ export interface ManagedServicePaths {
15
15
  indexerDaemonLockPath: string;
16
16
  indexerDaemonStatusPath: string;
17
17
  indexerDaemonSocketPath: string;
18
+ indexerDaemonLogPath: string;
18
19
  }
19
20
  export declare function resolveManagedServicePaths(dataDir: string, walletRootId?: string): ManagedServicePaths;
@@ -41,5 +41,6 @@ export function resolveManagedServicePaths(dataDir, walletRootId = UNINITIALIZED
41
41
  indexerDaemonLockPath: join(walletRuntimeRoot, "indexer-daemon.lock"),
42
42
  indexerDaemonStatusPath: join(indexerServiceRoot, "status.json"),
43
43
  indexerDaemonSocketPath: resolveIndexerDaemonSocketPath(serviceRootId),
44
+ indexerDaemonLogPath: join(indexerServiceRoot, "daemon.log"),
44
45
  };
45
46
  }
@@ -1,5 +1,19 @@
1
+ import { getIndexerDaemonStartupLogPath, getIndexerDaemonStartupLogTail, } from "../../../bitcoind/indexer-daemon/startup.js";
2
+ function formatIndexerStartupDetail(error) {
3
+ const logPath = getIndexerDaemonStartupLogPath(error);
4
+ const logTail = getIndexerDaemonStartupLogTail(error);
5
+ if (logPath === null && logTail === null) {
6
+ return null;
7
+ }
8
+ const tail = logTail === null
9
+ ? null
10
+ : logTail.replace(/\s+/g, " ").slice(0, 300);
11
+ return tail === null
12
+ ? `Startup log: ${logPath}.`
13
+ : `Startup log: ${logPath}. Last output: ${tail}`;
14
+ }
1
15
  export const serviceErrorRules = [
2
- ({ errorCode }) => {
16
+ ({ errorCode, error }) => {
3
17
  if (errorCode.endsWith("_requires_tty") && errorCode !== "cli_update_requires_tty") {
4
18
  return {
5
19
  what: "Interactive terminal input is required.",
@@ -14,6 +28,30 @@ export const serviceErrorRules = [
14
28
  next: "Check `cogcoin status`, wait for services to settle, and retry. If the state stays degraded, run `cogcoin repair`.",
15
29
  };
16
30
  }
31
+ if (errorCode === "sqlite_native_module_unavailable") {
32
+ const detail = formatIndexerStartupDetail(error);
33
+ return {
34
+ what: "The managed indexer daemon could not load its SQLite native module.",
35
+ why: `The active Node runtime is ${process.version}, but the installed native sqlite dependency appears to be missing or built for a different Node ABI.${detail === null ? "" : ` ${detail}`}`,
36
+ next: "Use the supported Node runtime for this checkout, then run `npm rebuild better-sqlite3 zeromq` or reinstall dependencies and retry.",
37
+ };
38
+ }
39
+ if (errorCode === "indexer_daemon_start_failed") {
40
+ const detail = formatIndexerStartupDetail(error);
41
+ return {
42
+ what: "The managed indexer daemon exited before it opened its local IPC socket.",
43
+ why: detail ?? "The daemon process died during startup before Cogcoin could read a service status.",
44
+ next: "Run `cogcoin repair` to clear stale managed indexer artifacts. If this was a local checkout, verify the Node version and rebuild native dependencies.",
45
+ };
46
+ }
47
+ if (errorCode === "indexer_daemon_start_timeout") {
48
+ const detail = formatIndexerStartupDetail(error);
49
+ return {
50
+ what: "The managed indexer daemon stayed alive but did not open its local IPC socket in time.",
51
+ why: detail ?? "Cogcoin could not attach to the daemon before the startup deadline.",
52
+ next: "Run `cogcoin repair` if this persists. If this was a local checkout, verify the Node version and rebuild native dependencies.",
53
+ };
54
+ }
17
55
  if (errorCode === "indexer_daemon_background_follow_recovery_failed") {
18
56
  return {
19
57
  what: "The managed indexer daemon could not recover automatic background follow.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogcoin/client",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "Store-backed Cogcoin client with wallet flows, SQLite persistence, and managed Bitcoin Core integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",