@coderule/mcp 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { createHash } from 'crypto';
5
5
  import envPaths from 'env-paths';
6
6
  import pino from 'pino';
7
7
  import Database from 'better-sqlite3';
8
- import { Qulite, enqueueFsEvent } from '@coderule/qulite';
8
+ import { Qulite, JobStatus, enqueueFsEvent } from '@coderule/qulite';
9
9
  import { CoderuleClients, ASTHttpClient, SyncHttpClient } from '@coderule/clients';
10
10
  import fs2 from 'fs';
11
11
  import { Worker } from 'worker_threads';
@@ -29,7 +29,7 @@ var DEFAULT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
29
29
  var DEFAULT_QUEUE_POLL_INTERVAL_MS = 500;
30
30
  var DEFAULT_HASH_BATCH_SIZE = 32;
31
31
  var DEFAULT_MAX_SNAPSHOT_ATTEMPTS = 5;
32
- var DEFAULT_HTTP_TIMEOUT_MS = 3e4;
32
+ var DEFAULT_HTTP_TIMEOUT_MS = 12e4;
33
33
 
34
34
  // src/config/Configurator.ts
35
35
  var DEFAULT_RETRIEVAL_FORMATTER = "standard";
@@ -534,6 +534,16 @@ var Outbox = class {
534
534
  if (purged > 0) {
535
535
  this.log.warn({ purged }, "Purged legacy fs_control jobs without kind");
536
536
  }
537
+ try {
538
+ const counts = {
539
+ pending: this.queue.countByStatus(JobStatus.Pending),
540
+ processing: this.queue.countByStatus(JobStatus.Processing),
541
+ done: this.queue.countByStatus(JobStatus.Done),
542
+ failed: this.queue.countByStatus(JobStatus.Failed)
543
+ };
544
+ this.log.debug({ counts }, "Outbox initialized");
545
+ } catch {
546
+ }
537
547
  }
538
548
  getQueue() {
539
549
  return this.queue;
@@ -1045,44 +1055,109 @@ function computeSnapshot(filesRepo) {
1045
1055
  totalSize
1046
1056
  };
1047
1057
  }
1048
- async function uploadMissing(rootPath, missing, syncClient, logger2) {
1049
- if (!missing || missing.length === 0) return;
1050
- const map = /* @__PURE__ */ new Map();
1051
- for (const missingFile of missing) {
1052
- const absPath = path.join(rootPath, missingFile.file_path);
1058
+ async function withRetries(op, logger2, context, maxAttempts) {
1059
+ let attempt = 0;
1060
+ while (true) {
1053
1061
  try {
1054
- const buffer = await fs4.readFile(absPath);
1055
- map.set(missingFile.file_hash, {
1056
- path: missingFile.file_path,
1057
- content: buffer
1058
- });
1059
- } catch (error) {
1062
+ return await op();
1063
+ } catch (err) {
1064
+ attempt += 1;
1065
+ if (attempt >= maxAttempts) {
1066
+ logger2.error(
1067
+ { err, ...context, attempt },
1068
+ "Operation failed after retries"
1069
+ );
1070
+ throw err;
1071
+ }
1072
+ const delay = Math.min(15e3, 1e3 * 2 ** (attempt - 1));
1060
1073
  logger2.warn(
1061
- { err: error, relPath: missingFile.file_path },
1062
- "Failed to read missing file content"
1074
+ { err, ...context, attempt, delay },
1075
+ "Operation failed; retrying"
1063
1076
  );
1077
+ await sleep(delay);
1078
+ }
1079
+ }
1080
+ }
1081
+ async function uploadMissing(rootPath, missing, syncClient, logger2, maxAttempts, chunkSize = 64) {
1082
+ if (!missing || missing.length === 0) return;
1083
+ const total = missing.length;
1084
+ const chunks = [];
1085
+ for (let i = 0; i < total; i += chunkSize) {
1086
+ chunks.push(missing.slice(i, i + chunkSize));
1087
+ }
1088
+ for (let idx = 0; idx < chunks.length; idx++) {
1089
+ const list = chunks[idx];
1090
+ const map = /* @__PURE__ */ new Map();
1091
+ for (const missingFile of list) {
1092
+ const absPath = path.join(rootPath, missingFile.file_path);
1093
+ try {
1094
+ const buffer = await fs4.readFile(absPath);
1095
+ map.set(missingFile.file_hash, {
1096
+ path: missingFile.file_path,
1097
+ content: buffer
1098
+ });
1099
+ } catch (error) {
1100
+ logger2.warn(
1101
+ { err: error, relPath: missingFile.file_path },
1102
+ "Failed to read missing file content"
1103
+ );
1104
+ }
1064
1105
  }
1106
+ if (map.size === 0) continue;
1107
+ await withRetries(
1108
+ () => syncClient.uploadFileContent(map),
1109
+ logger2,
1110
+ {
1111
+ op: "uploadFileContent",
1112
+ chunkIndex: idx + 1,
1113
+ chunks: chunks.length,
1114
+ files: map.size
1115
+ },
1116
+ maxAttempts
1117
+ );
1065
1118
  }
1066
- if (map.size === 0) return;
1067
- await syncClient.uploadFileContent(map);
1068
1119
  }
1069
- async function ensureSnapshotCreated(rootPath, computation, syncClient, logger2) {
1120
+ async function ensureSnapshotCreated(rootPath, computation, syncClient, logger2, options) {
1070
1121
  const { snapshotHash, files } = computation;
1071
- let status = await syncClient.checkSnapshotStatus(snapshotHash);
1122
+ const maxAttempts = options?.maxAttempts ?? 5;
1123
+ const uploadChunkSize = options?.uploadChunkSize ?? 64;
1124
+ let status = await withRetries(
1125
+ () => syncClient.checkSnapshotStatus(snapshotHash),
1126
+ logger2,
1127
+ { op: "checkSnapshotStatus", snapshotHash },
1128
+ maxAttempts
1129
+ );
1072
1130
  if (status.status === "READY") {
1073
1131
  logger2.info({ snapshotHash }, "Snapshot already READY");
1074
1132
  return;
1075
1133
  }
1076
1134
  if (status.status === "NOT_FOUND" || status.status === "MISSING_CONTENT") {
1077
- status = await syncClient.createSnapshot(snapshotHash, files);
1135
+ status = await withRetries(
1136
+ () => syncClient.createSnapshot(snapshotHash, files),
1137
+ logger2,
1138
+ { op: "createSnapshot", snapshotHash },
1139
+ maxAttempts
1140
+ );
1078
1141
  }
1079
1142
  if (status.status === "MISSING_CONTENT" && status.missing_files?.length) {
1080
1143
  logger2.info(
1081
1144
  { missing: status.missing_files.length },
1082
1145
  "Uploading missing file content"
1083
1146
  );
1084
- await uploadMissing(rootPath, status.missing_files, syncClient, logger2);
1085
- status = await syncClient.createSnapshot(snapshotHash, files);
1147
+ await uploadMissing(
1148
+ rootPath,
1149
+ status.missing_files,
1150
+ syncClient,
1151
+ logger2,
1152
+ maxAttempts,
1153
+ uploadChunkSize
1154
+ );
1155
+ status = await withRetries(
1156
+ () => syncClient.createSnapshot(snapshotHash, files),
1157
+ logger2,
1158
+ { op: "createSnapshot", snapshotHash },
1159
+ maxAttempts
1160
+ );
1086
1161
  }
1087
1162
  let attempt = 0;
1088
1163
  while (status.status !== "READY") {
@@ -1092,13 +1167,24 @@ async function ensureSnapshotCreated(rootPath, computation, syncClient, logger2)
1092
1167
  const delay = Math.min(5e3, 1e3 * Math.max(1, 2 ** attempt));
1093
1168
  await sleep(delay);
1094
1169
  attempt += 1;
1095
- status = await syncClient.checkSnapshotStatus(snapshotHash);
1170
+ status = await withRetries(
1171
+ () => syncClient.checkSnapshotStatus(snapshotHash),
1172
+ logger2,
1173
+ { op: "checkSnapshotStatus", snapshotHash },
1174
+ maxAttempts
1175
+ );
1096
1176
  }
1097
1177
  logger2.info({ snapshotHash }, "Snapshot READY");
1098
1178
  }
1099
- async function publishSnapshot(rootPath, filesRepo, snapshotsRepo, syncClient, logger2) {
1179
+ async function publishSnapshot(rootPath, filesRepo, snapshotsRepo, syncClient, logger2, options) {
1100
1180
  const computation = computeSnapshot(filesRepo);
1101
- await ensureSnapshotCreated(rootPath, computation, syncClient, logger2);
1181
+ await ensureSnapshotCreated(
1182
+ rootPath,
1183
+ computation,
1184
+ syncClient,
1185
+ logger2,
1186
+ options
1187
+ );
1102
1188
  const createdAt = Date.now();
1103
1189
  snapshotsRepo.insert(
1104
1190
  computation.snapshotHash,
@@ -1138,7 +1224,8 @@ async function runInitialSyncPipeline(runtime) {
1138
1224
  runtime.filesRepo,
1139
1225
  runtime.snapshotsRepo,
1140
1226
  runtime.clients.sync,
1141
- syncLogger
1227
+ syncLogger,
1228
+ { maxAttempts: runtime.config.maxSnapshotAttempts }
1142
1229
  );
1143
1230
  return result;
1144
1231
  }
@@ -1541,7 +1628,8 @@ var ServiceRunner = class {
1541
1628
  this.runtime.filesRepo,
1542
1629
  this.runtime.snapshotsRepo,
1543
1630
  this.runtime.clients.sync,
1544
- log
1631
+ log,
1632
+ { maxAttempts: this.runtime.config.maxSnapshotAttempts }
1545
1633
  );
1546
1634
  this.runtime.outbox.ack(job.id, this.fsControlLeaseOwner);
1547
1635
  this.state.updateSnapshotReady(result.createdAt);
@@ -1622,15 +1710,27 @@ async function runService(params) {
1622
1710
  try {
1623
1711
  runner = new ServiceRunner(runtime);
1624
1712
  await runner.prepareWatcher(true);
1625
- const initial = await runInitialSyncPipeline(runtime);
1626
- runtime.logger.info(
1627
- {
1628
- snapshotHash: initial.snapshotHash,
1629
- filesCount: initial.filesCount
1630
- },
1631
- "Initial sync completed; entering continuous mode"
1632
- );
1633
- runner.recordInitialSnapshot(initial.createdAt);
1713
+ let initialCreatedAt;
1714
+ try {
1715
+ const initial = await runInitialSyncPipeline(runtime);
1716
+ runtime.logger.info(
1717
+ {
1718
+ snapshotHash: initial.snapshotHash,
1719
+ filesCount: initial.filesCount
1720
+ },
1721
+ "Initial sync completed; entering continuous mode"
1722
+ );
1723
+ initialCreatedAt = initial.createdAt;
1724
+ } catch (error) {
1725
+ runtime.logger.warn(
1726
+ { err: error },
1727
+ "Initial sync failed; enqueuing snapshot job and continuing"
1728
+ );
1729
+ runtime.outbox.enqueueSnapshot(runtime.config.rootId, 0);
1730
+ }
1731
+ if (initialCreatedAt) {
1732
+ runner.recordInitialSnapshot(initialCreatedAt);
1733
+ }
1634
1734
  await runner.startLoops();
1635
1735
  await runner.enableWatcherProcessing();
1636
1736
  runtime.logger.info("Coderule scanner service is running");