@andespindola/brainlink 0.1.0-beta.37 → 0.1.0-beta.38

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
@@ -69,6 +69,7 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
69
69
  - Backlinks, broken-link reports, orphan detection and validation.
70
70
  - Full-text, semantic and hybrid retrieval on a local file index.
71
71
  - Middle-out context assembly around the strongest chunk per document.
72
+ - In-process index and context caching with automatic invalidation on index updates.
72
73
  - Compressed-space prefiltering for `.blpk` packs before decryption and scan.
73
74
  - Agent namespaces under `agents/<agent-id>/`.
74
75
  - S3-compatible bucket vaults through `s3://bucket/prefix` URIs.
@@ -764,6 +765,7 @@ blink context "question" --vault ./vault --agent coding-agent --mode hybrid --js
764
765
  ```
765
766
 
766
767
  Builds a compact context package for an agent.
768
+ Repeated calls with the same vault, agent, query, mode and token/limit settings are served from a short in-memory cache while the index is unchanged.
767
769
 
768
770
  ### `links`
769
771
 
@@ -1,13 +1,68 @@
1
+ import { stat } from 'node:fs/promises';
1
2
  import { formatContextPackage, selectContextSections } from '../domain/context.js';
3
+ import { indexStoragePath } from '../infrastructure/file-index.js';
2
4
  import { searchKnowledge } from './search-knowledge.js';
5
+ const contextCacheTtlMs = 45_000;
6
+ const contextCacheMaxEntries = 200;
7
+ const contextCache = new Map();
8
+ const readIndexMtimeMs = async (vaultPath) => {
9
+ try {
10
+ return (await stat(indexStoragePath(vaultPath))).mtimeMs;
11
+ }
12
+ catch {
13
+ return 0;
14
+ }
15
+ };
16
+ const toCacheKey = (vaultPath, query, limit, maxTokens, agentId, mode) => JSON.stringify({
17
+ vaultPath,
18
+ query: query.trim().toLowerCase(),
19
+ limit,
20
+ maxTokens,
21
+ agentId: agentId?.trim().toLowerCase() ?? '*',
22
+ mode: mode ?? 'default'
23
+ });
24
+ const contextCacheGet = (key, indexMtimeMs) => {
25
+ const entry = contextCache.get(key);
26
+ if (!entry) {
27
+ return undefined;
28
+ }
29
+ const fresh = Date.now() - entry.createdAt <= contextCacheTtlMs && entry.indexMtimeMs === indexMtimeMs;
30
+ if (!fresh) {
31
+ contextCache.delete(key);
32
+ return undefined;
33
+ }
34
+ return entry.context;
35
+ };
36
+ const contextCacheSet = (entry) => {
37
+ contextCache.set(entry.key, entry);
38
+ if (contextCache.size <= contextCacheMaxEntries) {
39
+ return;
40
+ }
41
+ const overflow = contextCache.size - contextCacheMaxEntries;
42
+ const keys = Array.from(contextCache.keys()).slice(0, overflow);
43
+ keys.forEach((key) => contextCache.delete(key));
44
+ };
3
45
  export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
46
+ const cacheKey = toCacheKey(vaultPath, query, limit, maxTokens, agentId, mode);
47
+ const indexMtimeMs = await readIndexMtimeMs(vaultPath);
48
+ const cached = contextCacheGet(cacheKey, indexMtimeMs);
49
+ if (cached) {
50
+ return cached;
51
+ }
4
52
  const results = await searchKnowledge(vaultPath, query, limit, agentId, mode);
5
53
  const sections = selectContextSections(results, maxTokens);
6
- return {
54
+ const context = {
7
55
  query,
8
56
  sections,
9
57
  content: formatContextPackage(query, sections)
10
58
  };
59
+ contextCacheSet({
60
+ key: cacheKey,
61
+ createdAt: Date.now(),
62
+ indexMtimeMs,
63
+ context
64
+ });
65
+ return context;
11
66
  };
12
67
  export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
13
68
  const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode);
@@ -1,7 +1,9 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { cosineSimilarity } from '../domain/embeddings.js';
4
4
  const queryTokenPattern = /[\p{L}\p{N}_-]+/gu;
5
+ const indexCacheMaxEntries = 16;
6
+ const indexCache = new Map();
5
7
  const emptyIndex = () => ({
6
8
  version: 1,
7
9
  updatedAt: new Date().toISOString(),
@@ -11,18 +13,44 @@ const emptyIndex = () => ({
11
13
  });
12
14
  export const indexStoragePath = (vaultPath) => join(vaultPath, '.brainlink', 'index.json');
13
15
  const readIndex = async (vaultPath) => {
16
+ const path = indexStoragePath(vaultPath);
17
+ let stats = null;
14
18
  try {
15
- const parsed = JSON.parse(await readFile(indexStoragePath(vaultPath), 'utf8'));
16
- return {
19
+ const fileStats = await stat(path);
20
+ stats = { mtimeMs: fileStats.mtimeMs, size: fileStats.size };
21
+ }
22
+ catch (error) {
23
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
24
+ indexCache.delete(path);
25
+ return emptyIndex();
26
+ }
27
+ return emptyIndex();
28
+ }
29
+ const cached = indexCache.get(path);
30
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
31
+ return cached.index;
32
+ }
33
+ try {
34
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
35
+ const loaded = {
17
36
  version: 1,
18
37
  updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
19
38
  documents: Array.isArray(parsed.documents) ? parsed.documents : [],
20
39
  chunks: Array.isArray(parsed.chunks) ? parsed.chunks : [],
21
40
  links: Array.isArray(parsed.links) ? parsed.links : []
22
41
  };
42
+ indexCache.set(path, { ...stats, index: loaded });
43
+ if (indexCache.size > indexCacheMaxEntries) {
44
+ const oldest = indexCache.keys().next().value;
45
+ if (typeof oldest === 'string') {
46
+ indexCache.delete(oldest);
47
+ }
48
+ }
49
+ return loaded;
23
50
  }
24
51
  catch (error) {
25
52
  if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
53
+ indexCache.delete(path);
26
54
  return emptyIndex();
27
55
  }
28
56
  return emptyIndex();
@@ -34,6 +62,12 @@ const writeIndex = async (vaultPath, index) => {
34
62
  await mkdir(dirname(target), { recursive: true, mode: 0o700 });
35
63
  await writeFile(temp, `${JSON.stringify(index)}\n`, { encoding: 'utf8', mode: 0o600 });
36
64
  await rename(temp, target);
65
+ const fileStats = await stat(target);
66
+ indexCache.set(target, {
67
+ mtimeMs: fileStats.mtimeMs,
68
+ size: fileStats.size,
69
+ index
70
+ });
37
71
  };
38
72
  const normalizeToken = (value) => value
39
73
  .normalize('NFKD')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.37",
3
+ "version": "0.1.0-beta.38",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",