@karmaniverous/jeeves-watcher 0.18.8 → 0.18.9
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/config.schema.json +228 -180
- package/dist/cli/jeeves-watcher/{index-BjghvUnc.js → index-xP2XrV4W.js} +66 -83
- package/dist/cli/jeeves-watcher/index.js +148 -23
- package/dist/index.d.ts +7 -1
- package/dist/index.js +207 -101
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { join, dirname, resolve, relative, extname, basename, isAbsolute, sep } from 'node:path';
|
|
2
2
|
import { createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, createConfigApplyHandler, substituteEnvVars, DEFAULT_PORTS, getBindAddress, postJson, fetchJson, getPackageVersion } from '@karmaniverous/jeeves';
|
|
3
|
-
import { extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema, normalizeWatchPaths, vcsAuthorConfigSchema, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS, INIT_CONFIG_TEMPLATE, DEFAULT_PORT as DEFAULT_PORT$1, PLUGIN_PACKAGE, SERVICE_PACKAGE, COMPONENT_NAME } from '@karmaniverous/jeeves-watcher-core';
|
|
3
|
+
import { extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema, normalizeWatchPaths, vcsRetentionConfigSchema, vcsAuthorConfigSchema, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS, INIT_CONFIG_TEMPLATE, DEFAULT_PORT as DEFAULT_PORT$1, PLUGIN_PACKAGE, SERVICE_PACKAGE, COMPONENT_NAME } from '@karmaniverous/jeeves-watcher-core';
|
|
4
4
|
export { apiConfigSchema, configWatchConfigSchema, embeddingConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema, loggingConfigSchema, vectorStoreConfigSchema, watchConfigSchema } from '@karmaniverous/jeeves-watcher-core';
|
|
5
5
|
import Fastify from 'fastify';
|
|
6
|
-
import { readdir, stat, readFile, writeFile, mkdir, appendFile,
|
|
6
|
+
import { readdir, stat, access, readFile, writeFile, mkdir, appendFile, rm } from 'node:fs/promises';
|
|
7
7
|
import { parallel, capitalize, title, camel, snake, dash, isEqual, get, omit } from 'radash';
|
|
8
8
|
import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, rmSync, renameSync } from 'node:fs';
|
|
9
9
|
import ignore from 'ignore';
|
|
10
10
|
import picomatch from 'picomatch';
|
|
11
|
+
import { execFile } from 'node:child_process';
|
|
12
|
+
import { promisify } from 'node:util';
|
|
11
13
|
import Ajv from 'ajv';
|
|
12
14
|
import addFormats from 'ajv-formats';
|
|
13
15
|
import { z, ZodError } from 'zod';
|
|
@@ -21,8 +23,6 @@ import { toMarkdown } from 'mdast-util-to-markdown';
|
|
|
21
23
|
import rehypeParse from 'rehype-parse';
|
|
22
24
|
import { unified } from 'unified';
|
|
23
25
|
import yaml from 'js-yaml';
|
|
24
|
-
import { execFile } from 'node:child_process';
|
|
25
|
-
import { promisify } from 'node:util';
|
|
26
26
|
import crypto from 'crypto';
|
|
27
27
|
import { packageDirectorySync } from 'package-directory';
|
|
28
28
|
import { homedir } from 'node:os';
|
|
@@ -421,6 +421,77 @@ async function retry(fn, options) {
|
|
|
421
421
|
throw lastError;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
/**
|
|
425
|
+
* @module vcs/gitExec
|
|
426
|
+
* Shared git execution utilities.
|
|
427
|
+
*/
|
|
428
|
+
/** Promisified execFile for git commands. */
|
|
429
|
+
const execFileAsync = promisify(execFile);
|
|
430
|
+
/**
|
|
431
|
+
* Stage files via stdin to avoid ENAMETOOLONG on Windows.
|
|
432
|
+
*
|
|
433
|
+
* Uses `git add --pathspec-from-file=- --pathspec-file-nul` so the file list is piped
|
|
434
|
+
* through stdin (NUL-delimited) instead of passed as command-line
|
|
435
|
+
* arguments. This sidesteps the Windows CreateProcessW 32 767-char
|
|
436
|
+
* limit that triggers ENAMETOOLONG when batches contain many files
|
|
437
|
+
* with long absolute paths.
|
|
438
|
+
*
|
|
439
|
+
* @param files - Absolute paths of files to stage.
|
|
440
|
+
* @param cwd - Repository root (working directory for git).
|
|
441
|
+
*/
|
|
442
|
+
function gitAddViaStdin(files, cwd) {
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
const child = execFile('git', ['add', '--pathspec-from-file=-', '--pathspec-file-nul'], { cwd }, (error) => {
|
|
445
|
+
if (error)
|
|
446
|
+
reject(error);
|
|
447
|
+
else
|
|
448
|
+
resolve();
|
|
449
|
+
});
|
|
450
|
+
if (!child.stdin) {
|
|
451
|
+
reject(new Error('Failed to open stdin for git add'));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
// Suppress EPIPE — expected if git exits before we finish writing
|
|
455
|
+
child.stdin.on('error', (err) => {
|
|
456
|
+
if (err.code !== 'EPIPE') {
|
|
457
|
+
reject(err);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
child.stdin.end(files.join('\0'));
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Normalize a path for case-insensitive comparison on Windows.
|
|
465
|
+
* On Windows, lowercases the entire path; on other platforms, returns as-is.
|
|
466
|
+
*
|
|
467
|
+
* @param p - The path string to normalize.
|
|
468
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
469
|
+
* @returns The normalized path.
|
|
470
|
+
*/
|
|
471
|
+
function normalizePathCase(p, platform = process.platform) {
|
|
472
|
+
return platform === 'win32' ? p.toLowerCase() : p;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Find the longest-prefix-match root for a normalized path.
|
|
476
|
+
* Roots must be sorted longest-first for correct nested matching.
|
|
477
|
+
*
|
|
478
|
+
* @param roots - Root paths sorted longest-first.
|
|
479
|
+
* @param normalizedPath - Normalized absolute path (forward slashes).
|
|
480
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
481
|
+
* @returns The matching root, or undefined.
|
|
482
|
+
*/
|
|
483
|
+
function findRootForPath(roots, normalizedPath, platform = process.platform) {
|
|
484
|
+
const comparePath = normalizePathCase(normalizedPath, platform);
|
|
485
|
+
for (const root of roots) {
|
|
486
|
+
const compareRoot = normalizePathCase(root, platform);
|
|
487
|
+
if (comparePath === compareRoot ||
|
|
488
|
+
comparePath.startsWith(compareRoot + '/')) {
|
|
489
|
+
return root;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
|
|
424
495
|
/**
|
|
425
496
|
* Best-effort base directory inference for a glob pattern.
|
|
426
497
|
*
|
|
@@ -638,7 +709,7 @@ function groupByRoot(filePaths, watchPaths) {
|
|
|
638
709
|
/** Default page size for prune scroll (smaller than search to reduce connection strain). */
|
|
639
710
|
const PRUNE_SCROLL_PAGE_SIZE = 500;
|
|
640
711
|
/** Max retry attempts for scroll page failures. */
|
|
641
|
-
const SCROLL_RETRY_ATTEMPTS =
|
|
712
|
+
const SCROLL_RETRY_ATTEMPTS = 5;
|
|
642
713
|
/** Base delay for scroll retry backoff (ms). */
|
|
643
714
|
const SCROLL_RETRY_BASE_DELAY_MS = 1000;
|
|
644
715
|
/**
|
|
@@ -664,26 +735,42 @@ async function scrollPageWithRetry(vectorStore, cursor, logger) {
|
|
|
664
735
|
}
|
|
665
736
|
throw lastError;
|
|
666
737
|
}
|
|
738
|
+
/** Concurrency limit for fs.access() disk-existence checks. */
|
|
739
|
+
const FS_ACCESS_CONCURRENCY = 100;
|
|
667
740
|
/**
|
|
668
|
-
* Compute the plan for a prune operation.
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
741
|
+
* Compute the plan for a prune operation using a filter-first approach.
|
|
742
|
+
*
|
|
743
|
+
* 1. Enumerate all watched files from the filesystem → `watchedFiles` set.
|
|
744
|
+
* 2. Scroll Qdrant points. For each point:
|
|
745
|
+
* - No `file_path` → orphan.
|
|
746
|
+
* - `file_path` not in `watchedFiles` → orphan (out of watch scope).
|
|
747
|
+
* - `file_path` in `watchedFiles` → check disk existence via `fs.access()`.
|
|
748
|
+
* If missing from disk → orphan (deleted file still in index).
|
|
749
|
+
* 3. Batch `fs.access()` checks with bounded concurrency.
|
|
672
750
|
*/
|
|
673
751
|
async function computePrunePlan(deps) {
|
|
674
752
|
const { config, vectorStore, gitignoreFilter, logger } = deps;
|
|
675
753
|
if (!vectorStore) {
|
|
676
754
|
throw new Error('vectorStore is required for prune scope');
|
|
677
755
|
}
|
|
756
|
+
// Step 1: Build set of watched files from filesystem
|
|
757
|
+
const isGitignored = createIsGitignored(gitignoreFilter);
|
|
758
|
+
const watchedFileList = await listFilesFromGlobs(extractWatchPathStrings(config.watch.paths), config.watch.ignored, isGitignored);
|
|
759
|
+
const watchedFiles = new Set(watchedFileList.map((f) => normalizePathCase(normalizeSlashes(f))));
|
|
760
|
+
logger.info({ watchedFileCount: watchedFiles.size }, 'Prune: enumerated watched files from filesystem');
|
|
761
|
+
// Step 2: Scroll Qdrant and classify points
|
|
678
762
|
const seenFiles = new Set();
|
|
679
763
|
const orphanedFiles = new Set();
|
|
764
|
+
const diskCheckFiles = new Set();
|
|
680
765
|
const orphanedIds = [];
|
|
766
|
+
// Map from file_path → point IDs, for points needing disk-existence check
|
|
767
|
+
const diskCheckPointIds = new Map();
|
|
681
768
|
let totalPoints = 0;
|
|
682
769
|
let incomplete = false;
|
|
683
770
|
let cursor;
|
|
684
771
|
try {
|
|
685
|
-
|
|
686
|
-
while (
|
|
772
|
+
let hasMore = true;
|
|
773
|
+
while (hasMore) {
|
|
687
774
|
const page = await scrollPageWithRetry(vectorStore, cursor, logger);
|
|
688
775
|
for (const point of page.points) {
|
|
689
776
|
totalPoints++;
|
|
@@ -692,32 +779,62 @@ async function computePrunePlan(deps) {
|
|
|
692
779
|
orphanedIds.push(point.id);
|
|
693
780
|
continue;
|
|
694
781
|
}
|
|
695
|
-
|
|
696
|
-
|
|
782
|
+
const normalized = normalizePathCase(normalizeSlashes(filePath));
|
|
783
|
+
if (seenFiles.has(normalized)) {
|
|
784
|
+
if (orphanedFiles.has(normalized)) {
|
|
697
785
|
orphanedIds.push(point.id);
|
|
698
786
|
}
|
|
787
|
+
else if (diskCheckFiles.has(normalized)) {
|
|
788
|
+
// Already queued for disk check — track its ID
|
|
789
|
+
const ids = diskCheckPointIds.get(normalized);
|
|
790
|
+
if (ids)
|
|
791
|
+
ids.push(point.id);
|
|
792
|
+
}
|
|
699
793
|
continue;
|
|
700
794
|
}
|
|
701
|
-
seenFiles.add(
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
: false;
|
|
706
|
-
if (!watched || gitignored) {
|
|
707
|
-
orphanedFiles.add(filePath);
|
|
795
|
+
seenFiles.add(normalized);
|
|
796
|
+
if (!watchedFiles.has(normalized)) {
|
|
797
|
+
// Not in watch scope → definitely orphaned
|
|
798
|
+
orphanedFiles.add(normalized);
|
|
708
799
|
orphanedIds.push(point.id);
|
|
709
800
|
}
|
|
801
|
+
else {
|
|
802
|
+
// In watch scope → needs disk-existence check
|
|
803
|
+
diskCheckFiles.add(normalized);
|
|
804
|
+
diskCheckPointIds.set(normalized, [point.id]);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// Progress logging every 100K points
|
|
808
|
+
if (totalPoints > 0 && totalPoints % 100000 === 0) {
|
|
809
|
+
logger.info({ totalPoints, orphanedCount: orphanedIds.length }, 'Prune scroll progress');
|
|
810
|
+
}
|
|
811
|
+
if (!page.nextCursor) {
|
|
812
|
+
hasMore = false;
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
cursor = page.nextCursor;
|
|
710
816
|
}
|
|
711
|
-
if (!page.nextCursor)
|
|
712
|
-
break;
|
|
713
|
-
cursor = page.nextCursor;
|
|
714
817
|
}
|
|
715
818
|
}
|
|
716
819
|
catch (error) {
|
|
717
|
-
// All retries exhausted - return partial results
|
|
718
820
|
logger.error({ err: normalizeError(error), totalPoints, cursor }, 'Prune scroll failed after retries; returning partial plan');
|
|
719
821
|
incomplete = true;
|
|
720
822
|
}
|
|
823
|
+
// Step 3: Batch fs.access() checks for files in watch scope
|
|
824
|
+
const filesToCheck = [...diskCheckFiles];
|
|
825
|
+
await parallel(FS_ACCESS_CONCURRENCY, filesToCheck, async (filePath) => {
|
|
826
|
+
try {
|
|
827
|
+
await access(filePath);
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
// File doesn't exist on disk → orphaned
|
|
831
|
+
orphanedFiles.add(filePath);
|
|
832
|
+
const pointIds = diskCheckPointIds.get(filePath);
|
|
833
|
+
if (pointIds) {
|
|
834
|
+
orphanedIds.push(...pointIds);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
});
|
|
721
838
|
const orphanedFileList = [...orphanedFiles];
|
|
722
839
|
const byRoot = groupByRoot(orphanedFileList, extractWatchPathStrings(config.watch.paths));
|
|
723
840
|
return {
|
|
@@ -775,6 +892,12 @@ async function executeReindex(deps, scope, path, dryRun = false) {
|
|
|
775
892
|
deps.queue?.resume();
|
|
776
893
|
return { filesProcessed: 0, durationMs: 0, errors: 0, plan };
|
|
777
894
|
}
|
|
895
|
+
// Abort live prune when plan is incomplete — never silently delete partial data
|
|
896
|
+
if (plan.incomplete) {
|
|
897
|
+
deps.queue?.resume();
|
|
898
|
+
logger.error({ totalPoints: plan.total, pointsToDelete: plan.toDelete }, 'Aborting live prune — scroll was incomplete; partial deletes are unsafe');
|
|
899
|
+
return { filesProcessed: 0, durationMs: 0, errors: 1, plan };
|
|
900
|
+
}
|
|
778
901
|
// Execute prune
|
|
779
902
|
reindexTracker?.start(scope);
|
|
780
903
|
const startTime = Date.now();
|
|
@@ -3369,77 +3492,6 @@ function createSearchHandler(deps) {
|
|
|
3369
3492
|
}, deps.logger, 'Search');
|
|
3370
3493
|
}
|
|
3371
3494
|
|
|
3372
|
-
/**
|
|
3373
|
-
* @module vcs/gitExec
|
|
3374
|
-
* Shared git execution utilities.
|
|
3375
|
-
*/
|
|
3376
|
-
/** Promisified execFile for git commands. */
|
|
3377
|
-
const execFileAsync = promisify(execFile);
|
|
3378
|
-
/**
|
|
3379
|
-
* Stage files via stdin to avoid ENAMETOOLONG on Windows.
|
|
3380
|
-
*
|
|
3381
|
-
* Uses `git add --pathspec-from-file=- --pathspec-file-nul` so the file list is piped
|
|
3382
|
-
* through stdin (NUL-delimited) instead of passed as command-line
|
|
3383
|
-
* arguments. This sidesteps the Windows CreateProcessW 32 767-char
|
|
3384
|
-
* limit that triggers ENAMETOOLONG when batches contain many files
|
|
3385
|
-
* with long absolute paths.
|
|
3386
|
-
*
|
|
3387
|
-
* @param files - Absolute paths of files to stage.
|
|
3388
|
-
* @param cwd - Repository root (working directory for git).
|
|
3389
|
-
*/
|
|
3390
|
-
function gitAddViaStdin(files, cwd) {
|
|
3391
|
-
return new Promise((resolve, reject) => {
|
|
3392
|
-
const child = execFile('git', ['add', '--pathspec-from-file=-', '--pathspec-file-nul'], { cwd }, (error) => {
|
|
3393
|
-
if (error)
|
|
3394
|
-
reject(error);
|
|
3395
|
-
else
|
|
3396
|
-
resolve();
|
|
3397
|
-
});
|
|
3398
|
-
if (!child.stdin) {
|
|
3399
|
-
reject(new Error('Failed to open stdin for git add'));
|
|
3400
|
-
return;
|
|
3401
|
-
}
|
|
3402
|
-
// Suppress EPIPE — expected if git exits before we finish writing
|
|
3403
|
-
child.stdin.on('error', (err) => {
|
|
3404
|
-
if (err.code !== 'EPIPE') {
|
|
3405
|
-
reject(err);
|
|
3406
|
-
}
|
|
3407
|
-
});
|
|
3408
|
-
child.stdin.end(files.join('\0'));
|
|
3409
|
-
});
|
|
3410
|
-
}
|
|
3411
|
-
/**
|
|
3412
|
-
* Normalize a path for case-insensitive comparison on Windows.
|
|
3413
|
-
* On Windows, lowercases the entire path; on other platforms, returns as-is.
|
|
3414
|
-
*
|
|
3415
|
-
* @param p - The path string to normalize.
|
|
3416
|
-
* @param platform - The platform to check against (default: `process.platform`).
|
|
3417
|
-
* @returns The normalized path.
|
|
3418
|
-
*/
|
|
3419
|
-
function normalizePathCase(p, platform = process.platform) {
|
|
3420
|
-
return platform === 'win32' ? p.toLowerCase() : p;
|
|
3421
|
-
}
|
|
3422
|
-
/**
|
|
3423
|
-
* Find the longest-prefix-match root for a normalized path.
|
|
3424
|
-
* Roots must be sorted longest-first for correct nested matching.
|
|
3425
|
-
*
|
|
3426
|
-
* @param roots - Root paths sorted longest-first.
|
|
3427
|
-
* @param normalizedPath - Normalized absolute path (forward slashes).
|
|
3428
|
-
* @param platform - The platform to check against (default: `process.platform`).
|
|
3429
|
-
* @returns The matching root, or undefined.
|
|
3430
|
-
*/
|
|
3431
|
-
function findRootForPath(roots, normalizedPath, platform = process.platform) {
|
|
3432
|
-
const comparePath = normalizePathCase(normalizedPath, platform);
|
|
3433
|
-
for (const root of roots) {
|
|
3434
|
-
const compareRoot = normalizePathCase(root, platform);
|
|
3435
|
-
if (comparePath === compareRoot ||
|
|
3436
|
-
comparePath.startsWith(compareRoot + '/')) {
|
|
3437
|
-
return root;
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
|
-
return undefined;
|
|
3441
|
-
}
|
|
3442
|
-
|
|
3443
3495
|
/**
|
|
3444
3496
|
* @module vcs/resolveWatchRoot
|
|
3445
3497
|
* Resolves which VCS-enabled watch root(s) own a given absolute path or glob.
|
|
@@ -4975,6 +5027,7 @@ class VcsManager {
|
|
|
4975
5027
|
squashManager;
|
|
4976
5028
|
debounceTimer;
|
|
4977
5029
|
commitInFlight = Promise.resolve();
|
|
5030
|
+
consecutiveCommitFailures = 0;
|
|
4978
5031
|
started = false;
|
|
4979
5032
|
isBaseline = true;
|
|
4980
5033
|
_lastPushTime = null;
|
|
@@ -5023,6 +5076,11 @@ class VcsManager {
|
|
|
5023
5076
|
fileChanged(filePath) {
|
|
5024
5077
|
if (!this.started)
|
|
5025
5078
|
return;
|
|
5079
|
+
// Reset circuit breaker on new file change so the system can recover
|
|
5080
|
+
if (this.consecutiveCommitFailures >= this.config.maxConsecutiveFailures) {
|
|
5081
|
+
this.logger.info({ root: this.rootPath }, 'VCS circuit breaker reset — new file change received, retrying commits');
|
|
5082
|
+
this.consecutiveCommitFailures = 0;
|
|
5083
|
+
}
|
|
5026
5084
|
this.pending.add(filePath);
|
|
5027
5085
|
if (this.pending.size >= this.config.maxBatchSize) {
|
|
5028
5086
|
this.clearDebounce();
|
|
@@ -5110,12 +5168,44 @@ class VcsManager {
|
|
|
5110
5168
|
}, this.config.commitDebounceMs);
|
|
5111
5169
|
}
|
|
5112
5170
|
/**
|
|
5113
|
-
*
|
|
5171
|
+
* Check for and remove stale index.lock files before retrying.
|
|
5172
|
+
*/
|
|
5173
|
+
async removeStaleLock() {
|
|
5174
|
+
const lockPath = join(this.rootPath, '.git', 'index.lock');
|
|
5175
|
+
try {
|
|
5176
|
+
const lockStat = await stat(lockPath);
|
|
5177
|
+
const ageMs = Date.now() - lockStat.mtimeMs;
|
|
5178
|
+
if (ageMs > this.config.staleLockThresholdMs) {
|
|
5179
|
+
await rm(lockPath, { force: true });
|
|
5180
|
+
this.logger.warn({ root: this.rootPath, ageMs }, 'Removed stale index.lock');
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
catch (error) {
|
|
5184
|
+
const code = error.code;
|
|
5185
|
+
if (code !== 'ENOENT') {
|
|
5186
|
+
this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Unable to remove stale index.lock');
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
/**
|
|
5191
|
+
* Commit a batch of files to the git repo with index.lock retry,
|
|
5192
|
+
* stale lock detection, circuit breaker, and re-queue cap.
|
|
5114
5193
|
*/
|
|
5115
5194
|
async commitBatch(files) {
|
|
5116
5195
|
if (files.length === 0)
|
|
5117
5196
|
return;
|
|
5197
|
+
// Circuit breaker: skip if too many consecutive failures
|
|
5198
|
+
if (this.consecutiveCommitFailures >= this.config.maxConsecutiveFailures) {
|
|
5199
|
+
this.logger.error({
|
|
5200
|
+
root: this.rootPath,
|
|
5201
|
+
consecutiveFailures: this.consecutiveCommitFailures,
|
|
5202
|
+
discardedFiles: files.length,
|
|
5203
|
+
}, 'VCS circuit breaker tripped — pending files discarded until next successful commit');
|
|
5204
|
+
return;
|
|
5205
|
+
}
|
|
5118
5206
|
try {
|
|
5207
|
+
// Check for stale lock before first attempt
|
|
5208
|
+
await this.removeStaleLock();
|
|
5119
5209
|
await retry(async (attempt) => {
|
|
5120
5210
|
await gitAddViaStdin(files, this.rootPath);
|
|
5121
5211
|
// Build message after staging so getStagedDiff can see the changes.
|
|
@@ -5141,12 +5231,22 @@ class VcsManager {
|
|
|
5141
5231
|
this.logger.warn({ root: this.rootPath, attempt, delay: delayMs }, 'index.lock contention, retrying');
|
|
5142
5232
|
},
|
|
5143
5233
|
});
|
|
5234
|
+
// Success — reset circuit breaker
|
|
5235
|
+
this.consecutiveCommitFailures = 0;
|
|
5144
5236
|
}
|
|
5145
5237
|
catch (error) {
|
|
5146
|
-
|
|
5238
|
+
this.consecutiveCommitFailures++;
|
|
5239
|
+
// Re-queue cap: only re-queue up to maxBatchSize files
|
|
5240
|
+
const cap = this.config.maxBatchSize;
|
|
5241
|
+
const toRequeue = files.slice(0, cap);
|
|
5242
|
+
const overflow = files.length - toRequeue.length;
|
|
5243
|
+
for (const f of toRequeue) {
|
|
5147
5244
|
this.pending.add(f);
|
|
5148
5245
|
}
|
|
5149
|
-
|
|
5246
|
+
if (overflow > 0) {
|
|
5247
|
+
this.logger.warn({ root: this.rootPath, overflow, cap }, 'Re-queue cap exceeded — discarded overflow files');
|
|
5248
|
+
}
|
|
5249
|
+
this.logger.warn({ root: this.rootPath, fileCount: toRequeue.length }, 'Re-queued files after commit failure');
|
|
5150
5250
|
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'VCS commit failed');
|
|
5151
5251
|
}
|
|
5152
5252
|
}
|
|
@@ -5178,10 +5278,14 @@ class VcsCoordinator {
|
|
|
5178
5278
|
// Deduplicate: skip if another glob already resolved to this root.
|
|
5179
5279
|
if (this.managers.has(rootKey))
|
|
5180
5280
|
continue;
|
|
5281
|
+
const mergedRetention = entry.vcs?.retention ??
|
|
5282
|
+
config.vcs.retention ??
|
|
5283
|
+
vcsRetentionConfigSchema.parse({});
|
|
5181
5284
|
const mergedConfig = {
|
|
5182
5285
|
...config.vcs,
|
|
5183
5286
|
...entry.vcs,
|
|
5184
5287
|
enabled: true,
|
|
5288
|
+
retention: mergedRetention,
|
|
5185
5289
|
};
|
|
5186
5290
|
// Create CommitMessageGenerator if AI commit messages are configured
|
|
5187
5291
|
const cmConfig = mergedConfig.commitMessage;
|
|
@@ -5315,10 +5419,10 @@ function createWatcher(config, factories, queue, processor, logger, runtimeOptio
|
|
|
5315
5419
|
* Tear down and rebuild the filesystem watcher with new config.
|
|
5316
5420
|
* Falls back to the old watcher if the new one fails to start.
|
|
5317
5421
|
*/
|
|
5318
|
-
async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache) {
|
|
5422
|
+
async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
|
|
5319
5423
|
try {
|
|
5320
5424
|
await oldState.watcher.stop();
|
|
5321
|
-
const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache);
|
|
5425
|
+
const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete);
|
|
5322
5426
|
newWatcher.start();
|
|
5323
5427
|
logger.info('Filesystem watcher rebuilt successfully');
|
|
5324
5428
|
return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
|
|
@@ -6459,7 +6563,7 @@ async function reloadConfig(state, deps) {
|
|
|
6459
6563
|
// Rebuild filesystem watcher if watch config changed
|
|
6460
6564
|
if (watchConfigChanged(oldConfig, newConfig)) {
|
|
6461
6565
|
logger.info('Watch config changed, rebuilding filesystem watcher...');
|
|
6462
|
-
const newState = await rebuildWatcher(newConfig, deps.factories, deps.queue, processor, logger, deps.runtimeOptions, { watcher: state.watcher, gitignoreFilter: state.gitignoreFilter }, deps.initialScanTracker, deps.contentHashCache);
|
|
6566
|
+
const newState = await rebuildWatcher(newConfig, deps.factories, deps.queue, processor, logger, deps.runtimeOptions, { watcher: state.watcher, gitignoreFilter: state.gitignoreFilter }, deps.initialScanTracker, deps.contentHashCache, deps.onVcsFileChange, deps.onInitialScanComplete);
|
|
6463
6567
|
state.watcher = newState.watcher;
|
|
6464
6568
|
state.gitignoreFilter = newState.gitignoreFilter;
|
|
6465
6569
|
}
|
|
@@ -9140,6 +9244,8 @@ class JeevesWatcher {
|
|
|
9140
9244
|
contentHashCache: this.contentHashCache,
|
|
9141
9245
|
valuesManager: this.valuesManager,
|
|
9142
9246
|
issuesManager: this.issuesManager,
|
|
9247
|
+
onVcsFileChange: (filePath, event) => this.vcsCoordinator?.onFileChange(filePath, event),
|
|
9248
|
+
onInitialScanComplete: () => this.vcsCoordinator?.onInitialScanComplete(),
|
|
9143
9249
|
});
|
|
9144
9250
|
this.config = state.config;
|
|
9145
9251
|
this.watcher = state.watcher;
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@commander-js/extra-typings": "^14.0.0",
|
|
11
11
|
"@karmaniverous/jeeves": "^0.5.12",
|
|
12
|
-
"@karmaniverous/jeeves-watcher-core": "^0.2.
|
|
12
|
+
"@karmaniverous/jeeves-watcher-core": "^0.2.3",
|
|
13
13
|
"@karmaniverous/jsonmap": "^2.1.1",
|
|
14
14
|
"@langchain/textsplitters": "*",
|
|
15
15
|
"@qdrant/js-client-rest": "*",
|
|
@@ -149,5 +149,5 @@
|
|
|
149
149
|
},
|
|
150
150
|
"type": "module",
|
|
151
151
|
"types": "dist/index.d.ts",
|
|
152
|
-
"version": "0.18.
|
|
152
|
+
"version": "0.18.9"
|
|
153
153
|
}
|