@karmaniverous/jeeves-watcher 0.6.4 → 0.6.6

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.
@@ -456,7 +456,7 @@ z.object({
456
456
  * @returns The configured AJV instance.
457
457
  */
458
458
  function createRuleAjv() {
459
- const ajv = new Ajv({ allErrors: true });
459
+ const ajv = new Ajv({ allErrors: true, strict: false });
460
460
  addFormats(ajv);
461
461
  ajv.addKeyword({
462
462
  keyword: 'glob',
@@ -2786,6 +2786,53 @@ function createReindexHandler(deps) {
2786
2786
  }, deps.logger, 'Reindex');
2787
2787
  }
2788
2788
 
2789
+ /**
2790
+ * @module api/handlers/rulesReapply
2791
+ * Fastify route handler for POST /rules/reapply.
2792
+ * Re-applies current inference rules to already-indexed files matching given globs.
2793
+ */
2794
+ /**
2795
+ * Create handler for POST /rules/reapply.
2796
+ *
2797
+ * Scrolls through all indexed points, finds files matching the given globs,
2798
+ * and re-applies current inference rules without re-embedding.
2799
+ */
2800
+ function createRulesReapplyHandler(deps) {
2801
+ return wrapHandler(async (request) => {
2802
+ await Promise.resolve();
2803
+ const { globs } = request.body;
2804
+ if (!Array.isArray(globs) || globs.length === 0) {
2805
+ throw new Error('Missing required field: globs (non-empty string array)');
2806
+ }
2807
+ const normalizedGlobs = globs.map((g) => normalizeSlashes(g));
2808
+ const isMatch = picomatch(normalizedGlobs, { dot: true, nocase: true });
2809
+ // Collect unique file paths matching the globs
2810
+ const matchingFiles = new Set();
2811
+ for await (const point of deps.vectorStore.scroll()) {
2812
+ const filePath = point.payload['file_path'];
2813
+ if (typeof filePath === 'string' && isMatch(filePath)) {
2814
+ matchingFiles.add(filePath);
2815
+ }
2816
+ }
2817
+ deps.logger.info({ globs: normalizedGlobs, matchCount: matchingFiles.size }, 'Re-applying rules to matching files');
2818
+ let updated = 0;
2819
+ for (const filePath of matchingFiles) {
2820
+ try {
2821
+ const result = await deps.processor.processRulesUpdate(filePath);
2822
+ if (result !== null)
2823
+ updated++;
2824
+ }
2825
+ catch (error) {
2826
+ deps.logger.warn({ filePath, err: error }, 'Failed to re-apply rules to file');
2827
+ }
2828
+ }
2829
+ return {
2830
+ matched: matchingFiles.size,
2831
+ updated,
2832
+ };
2833
+ }, deps.logger, 'RulesReapply');
2834
+ }
2835
+
2789
2836
  /**
2790
2837
  * @module api/handlers/rulesRegister
2791
2838
  * Fastify route handler for POST /rules/register.
@@ -3028,6 +3075,7 @@ function createApiServer(options) {
3028
3075
  onRulesChanged,
3029
3076
  }));
3030
3077
  app.post('/points/delete', createPointsDeleteHandler({ vectorStore, logger }));
3078
+ app.post('/rules/reapply', createRulesReapplyHandler({ processor, vectorStore, logger }));
3031
3079
  }
3032
3080
  return app;
3033
3081
  }
@@ -3949,7 +3997,10 @@ class DocumentProcessor {
3949
3997
  if (customMapLib !== undefined) {
3950
3998
  this.config = { ...this.config, customMapLib };
3951
3999
  }
3952
- this.logger.info({ rules: compiledRules.length }, 'Inference rules updated');
4000
+ this.logger.info({
4001
+ rules: compiledRules.length,
4002
+ ruleNames: compiledRules.map((r) => r.rule.name),
4003
+ }, 'Inference rules updated');
3953
4004
  }
3954
4005
  }
3955
4006
 
@@ -4330,6 +4381,7 @@ async function* scrollCollection(client, collectionName, filter, limit = 100) {
4330
4381
  */
4331
4382
  class VectorStoreClient {
4332
4383
  client;
4384
+ clientConfig;
4333
4385
  collectionName;
4334
4386
  dims;
4335
4387
  log;
@@ -4342,16 +4394,27 @@ class VectorStoreClient {
4342
4394
  * @param logger - Optional pino logger for retry warnings.
4343
4395
  */
4344
4396
  constructor(config, dimensions, logger) {
4345
- this.client = new QdrantClient({
4346
- url: config.url,
4347
- apiKey: config.apiKey,
4348
- checkCompatibility: false,
4349
- });
4397
+ this.clientConfig = { url: config.url, apiKey: config.apiKey };
4398
+ this.client = this.createClient();
4350
4399
  this.collectionName = config.collectionName;
4351
4400
  this.dims = dimensions;
4352
4401
  this.log = getLogger(logger);
4353
4402
  this.pinoLogger = logger;
4354
4403
  }
4404
+ /**
4405
+ * Create a fresh QdrantClient instance.
4406
+ *
4407
+ * Used to avoid stale HTTP keep-alive connections. The Qdrant JS client's
4408
+ * internal undici Agent uses keepAliveTimeout: 10s, which causes ECONNRESET
4409
+ * when connections sit idle during slow embedding calls (Gemini p99 ~8s).
4410
+ * Creating a fresh client for write operations ensures clean TCP connections.
4411
+ */
4412
+ createClient() {
4413
+ return new QdrantClient({
4414
+ ...this.clientConfig,
4415
+ checkCompatibility: false,
4416
+ });
4417
+ }
4355
4418
  /**
4356
4419
  * Ensure the collection exists with correct dimensions and Cosine distance.
4357
4420
  */
@@ -4399,13 +4462,18 @@ class VectorStoreClient {
4399
4462
  /**
4400
4463
  * Upsert points into the collection.
4401
4464
  *
4465
+ * Uses a fresh QdrantClient per attempt to avoid stale keep-alive connections.
4466
+ * Between embedding calls and upserts, idle connections may be closed by the
4467
+ * server, causing ECONNRESET on reuse.
4468
+ *
4402
4469
  * @param points - The points to upsert.
4403
4470
  */
4404
4471
  async upsert(points) {
4405
4472
  if (points.length === 0)
4406
4473
  return;
4407
4474
  await this.retryOperation('upsert', async () => {
4408
- await this.client.upsert(this.collectionName, {
4475
+ const freshClient = this.createClient();
4476
+ await freshClient.upsert(this.collectionName, {
4409
4477
  wait: true,
4410
4478
  points: points.map((p) => ({
4411
4479
  id: p.id,
@@ -4418,13 +4486,16 @@ class VectorStoreClient {
4418
4486
  /**
4419
4487
  * Delete points by their IDs.
4420
4488
  *
4489
+ * Uses a fresh QdrantClient per attempt to avoid stale keep-alive connections.
4490
+ *
4421
4491
  * @param ids - The point IDs to delete.
4422
4492
  */
4423
4493
  async delete(ids) {
4424
4494
  if (ids.length === 0)
4425
4495
  return;
4426
4496
  await this.retryOperation('delete', async () => {
4427
- await this.client.delete(this.collectionName, {
4497
+ const freshClient = this.createClient();
4498
+ await freshClient.delete(this.collectionName, {
4428
4499
  wait: true,
4429
4500
  points: ids,
4430
4501
  });
package/dist/index.d.ts CHANGED
@@ -742,6 +742,7 @@ interface VectorStore {
742
742
  */
743
743
  declare class VectorStoreClient implements VectorStore {
744
744
  private readonly client;
745
+ private readonly clientConfig;
745
746
  private readonly collectionName;
746
747
  private readonly dims;
747
748
  private readonly log;
@@ -754,6 +755,15 @@ declare class VectorStoreClient implements VectorStore {
754
755
  * @param logger - Optional pino logger for retry warnings.
755
756
  */
756
757
  constructor(config: VectorStoreConfig, dimensions: number, logger?: pino.Logger);
758
+ /**
759
+ * Create a fresh QdrantClient instance.
760
+ *
761
+ * Used to avoid stale HTTP keep-alive connections. The Qdrant JS client's
762
+ * internal undici Agent uses keepAliveTimeout: 10s, which causes ECONNRESET
763
+ * when connections sit idle during slow embedding calls (Gemini p99 ~8s).
764
+ * Creating a fresh client for write operations ensures clean TCP connections.
765
+ */
766
+ private createClient;
757
767
  /**
758
768
  * Ensure the collection exists with correct dimensions and Cosine distance.
759
769
  */
@@ -768,12 +778,18 @@ declare class VectorStoreClient implements VectorStore {
768
778
  /**
769
779
  * Upsert points into the collection.
770
780
  *
781
+ * Uses a fresh QdrantClient per attempt to avoid stale keep-alive connections.
782
+ * Between embedding calls and upserts, idle connections may be closed by the
783
+ * server, causing ECONNRESET on reuse.
784
+ *
771
785
  * @param points - The points to upsert.
772
786
  */
773
787
  upsert(points: VectorPoint[]): Promise<void>;
774
788
  /**
775
789
  * Delete points by their IDs.
776
790
  *
791
+ * Uses a fresh QdrantClient per attempt to avoid stale keep-alive connections.
792
+ *
777
793
  * @param ids - The point IDs to delete.
778
794
  */
779
795
  delete(ids: string[]): Promise<void>;
package/dist/index.js CHANGED
@@ -905,7 +905,7 @@ function buildAttributes(filePath, stats, extractedFrontmatter, extractedJson) {
905
905
  * @returns The configured AJV instance.
906
906
  */
907
907
  function createRuleAjv() {
908
- const ajv = new Ajv({ allErrors: true });
908
+ const ajv = new Ajv({ allErrors: true, strict: false });
909
909
  addFormats(ajv);
910
910
  ajv.addKeyword({
911
911
  keyword: 'glob',
@@ -2478,6 +2478,53 @@ function createReindexHandler(deps) {
2478
2478
  }, deps.logger, 'Reindex');
2479
2479
  }
2480
2480
 
2481
+ /**
2482
+ * @module api/handlers/rulesReapply
2483
+ * Fastify route handler for POST /rules/reapply.
2484
+ * Re-applies current inference rules to already-indexed files matching given globs.
2485
+ */
2486
+ /**
2487
+ * Create handler for POST /rules/reapply.
2488
+ *
2489
+ * Scrolls through all indexed points, finds files matching the given globs,
2490
+ * and re-applies current inference rules without re-embedding.
2491
+ */
2492
+ function createRulesReapplyHandler(deps) {
2493
+ return wrapHandler(async (request) => {
2494
+ await Promise.resolve();
2495
+ const { globs } = request.body;
2496
+ if (!Array.isArray(globs) || globs.length === 0) {
2497
+ throw new Error('Missing required field: globs (non-empty string array)');
2498
+ }
2499
+ const normalizedGlobs = globs.map((g) => normalizeSlashes(g));
2500
+ const isMatch = picomatch(normalizedGlobs, { dot: true, nocase: true });
2501
+ // Collect unique file paths matching the globs
2502
+ const matchingFiles = new Set();
2503
+ for await (const point of deps.vectorStore.scroll()) {
2504
+ const filePath = point.payload['file_path'];
2505
+ if (typeof filePath === 'string' && isMatch(filePath)) {
2506
+ matchingFiles.add(filePath);
2507
+ }
2508
+ }
2509
+ deps.logger.info({ globs: normalizedGlobs, matchCount: matchingFiles.size }, 'Re-applying rules to matching files');
2510
+ let updated = 0;
2511
+ for (const filePath of matchingFiles) {
2512
+ try {
2513
+ const result = await deps.processor.processRulesUpdate(filePath);
2514
+ if (result !== null)
2515
+ updated++;
2516
+ }
2517
+ catch (error) {
2518
+ deps.logger.warn({ filePath, err: error }, 'Failed to re-apply rules to file');
2519
+ }
2520
+ }
2521
+ return {
2522
+ matched: matchingFiles.size,
2523
+ updated,
2524
+ };
2525
+ }, deps.logger, 'RulesReapply');
2526
+ }
2527
+
2481
2528
  /**
2482
2529
  * @module api/handlers/rulesRegister
2483
2530
  * Fastify route handler for POST /rules/register.
@@ -2720,6 +2767,7 @@ function createApiServer(options) {
2720
2767
  onRulesChanged,
2721
2768
  }));
2722
2769
  app.post('/points/delete', createPointsDeleteHandler({ vectorStore, logger }));
2770
+ app.post('/rules/reapply', createRulesReapplyHandler({ processor, vectorStore, logger }));
2723
2771
  }
2724
2772
  return app;
2725
2773
  }
@@ -3931,7 +3979,10 @@ class DocumentProcessor {
3931
3979
  if (customMapLib !== undefined) {
3932
3980
  this.config = { ...this.config, customMapLib };
3933
3981
  }
3934
- this.logger.info({ rules: compiledRules.length }, 'Inference rules updated');
3982
+ this.logger.info({
3983
+ rules: compiledRules.length,
3984
+ ruleNames: compiledRules.map((r) => r.rule.name),
3985
+ }, 'Inference rules updated');
3935
3986
  }
3936
3987
  }
3937
3988
 
@@ -4312,6 +4363,7 @@ async function* scrollCollection(client, collectionName, filter, limit = 100) {
4312
4363
  */
4313
4364
  class VectorStoreClient {
4314
4365
  client;
4366
+ clientConfig;
4315
4367
  collectionName;
4316
4368
  dims;
4317
4369
  log;
@@ -4324,16 +4376,27 @@ class VectorStoreClient {
4324
4376
  * @param logger - Optional pino logger for retry warnings.
4325
4377
  */
4326
4378
  constructor(config, dimensions, logger) {
4327
- this.client = new QdrantClient({
4328
- url: config.url,
4329
- apiKey: config.apiKey,
4330
- checkCompatibility: false,
4331
- });
4379
+ this.clientConfig = { url: config.url, apiKey: config.apiKey };
4380
+ this.client = this.createClient();
4332
4381
  this.collectionName = config.collectionName;
4333
4382
  this.dims = dimensions;
4334
4383
  this.log = getLogger(logger);
4335
4384
  this.pinoLogger = logger;
4336
4385
  }
4386
+ /**
4387
+ * Create a fresh QdrantClient instance.
4388
+ *
4389
+ * Used to avoid stale HTTP keep-alive connections. The Qdrant JS client's
4390
+ * internal undici Agent uses keepAliveTimeout: 10s, which causes ECONNRESET
4391
+ * when connections sit idle during slow embedding calls (Gemini p99 ~8s).
4392
+ * Creating a fresh client for write operations ensures clean TCP connections.
4393
+ */
4394
+ createClient() {
4395
+ return new QdrantClient({
4396
+ ...this.clientConfig,
4397
+ checkCompatibility: false,
4398
+ });
4399
+ }
4337
4400
  /**
4338
4401
  * Ensure the collection exists with correct dimensions and Cosine distance.
4339
4402
  */
@@ -4381,13 +4444,18 @@ class VectorStoreClient {
4381
4444
  /**
4382
4445
  * Upsert points into the collection.
4383
4446
  *
4447
+ * Uses a fresh QdrantClient per attempt to avoid stale keep-alive connections.
4448
+ * Between embedding calls and upserts, idle connections may be closed by the
4449
+ * server, causing ECONNRESET on reuse.
4450
+ *
4384
4451
  * @param points - The points to upsert.
4385
4452
  */
4386
4453
  async upsert(points) {
4387
4454
  if (points.length === 0)
4388
4455
  return;
4389
4456
  await this.retryOperation('upsert', async () => {
4390
- await this.client.upsert(this.collectionName, {
4457
+ const freshClient = this.createClient();
4458
+ await freshClient.upsert(this.collectionName, {
4391
4459
  wait: true,
4392
4460
  points: points.map((p) => ({
4393
4461
  id: p.id,
@@ -4400,13 +4468,16 @@ class VectorStoreClient {
4400
4468
  /**
4401
4469
  * Delete points by their IDs.
4402
4470
  *
4471
+ * Uses a fresh QdrantClient per attempt to avoid stale keep-alive connections.
4472
+ *
4403
4473
  * @param ids - The point IDs to delete.
4404
4474
  */
4405
4475
  async delete(ids) {
4406
4476
  if (ids.length === 0)
4407
4477
  return;
4408
4478
  await this.retryOperation('delete', async () => {
4409
- await this.client.delete(this.collectionName, {
4479
+ const freshClient = this.createClient();
4480
+ await freshClient.delete(this.collectionName, {
4410
4481
  wait: true,
4411
4482
  points: ids,
4412
4483
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-watcher",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "Filesystem watcher that keeps a Qdrant vector store in sync with document changes",
6
6
  "license": "BSD-3-Clause",