rag-memory-epf-mcp 5.1.0 → 5.2.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/index.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export declare class RAGKnowledgeGraphManager {
|
|
|
70
70
|
currentProfileId: number;
|
|
71
71
|
grandfatherAllowed: boolean;
|
|
72
72
|
coordinator: BackfillCoordinator | null;
|
|
73
|
+
readonly calendarTimeZone: string;
|
|
73
74
|
private embeddingCache;
|
|
74
75
|
private readonly EMBEDDING_CACHE_MAX;
|
|
75
76
|
private dictionaryCache;
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import { migrations } from './src/migrations/migrations.js';
|
|
|
32
32
|
import { EmbeddingGate, GateNotReadyError, GateDisabledError, TerminalConfigError } from './src/embeddingGate.js';
|
|
33
33
|
import { resolveModelCacheDir, preflightCacheDir, artifactKey, ModelDownloadLock, handleLoaderFailure } from './src/modelCache.js';
|
|
34
34
|
import { BackfillCoordinator } from './src/backfillCoordinator.js';
|
|
35
|
+
import { calendarDate, resolveCalendarTimeZone, stampDatePrefix, stripDatePrefix } from './src/observations/date-prefix.js';
|
|
35
36
|
import os from 'node:os';
|
|
36
37
|
import { createHash } from 'crypto';
|
|
37
38
|
import { createRequire } from 'module';
|
|
@@ -179,6 +180,9 @@ export class RAGKnowledgeGraphManager {
|
|
|
179
180
|
// default model config, or with the explicit trust opt-in (spec §6b guard).
|
|
180
181
|
grandfatherAllowed = IS_DEFAULT_MODEL_CONFIG || process.env.RAG_MEMORY_TRUST_LEGACY_VECTORS === '1';
|
|
181
182
|
coordinator = null;
|
|
183
|
+
// The calendar that date-only human labels are written in. Resolved once, here, so an invalid
|
|
184
|
+
// zone fails at construction instead of quietly writing wrong days for weeks.
|
|
185
|
+
calendarTimeZone = resolveCalendarTimeZone(process.env.RAG_MEMORY_CALENDAR_TZ);
|
|
182
186
|
embeddingCache = new Map();
|
|
183
187
|
EMBEDDING_CACHE_MAX = 500;
|
|
184
188
|
dictionaryCache = null;
|
|
@@ -574,11 +578,12 @@ export class RAGKnowledgeGraphManager {
|
|
|
574
578
|
}
|
|
575
579
|
// === ORIGINAL MCP FUNCTIONALITY ===
|
|
576
580
|
_timestampObservation(obs) {
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
581
|
+
// Stamp and dedup share one parser (src/observations/date-prefix.ts). They used to carry
|
|
582
|
+
// separate regexes and disagreed about "[2026-08-11 session16] ...": stamping treated it as
|
|
583
|
+
// undated and prepended a second date, while dedup could not strip it — so the same sentence
|
|
584
|
+
// written in two sessions became two observations. Measured on a live database: 82 rows with
|
|
585
|
+
// two dates, 29 with a day earlier than the day they were written.
|
|
586
|
+
return stampDatePrefix(obs, this.calendarTimeZone);
|
|
582
587
|
}
|
|
583
588
|
async createEntities(entities) {
|
|
584
589
|
if (!this.db)
|
|
@@ -590,7 +595,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
590
595
|
INSERT OR IGNORE INTO entities (id, name, entityType, observations, metadata)
|
|
591
596
|
VALUES (?, ?, ?, '[]', ?)
|
|
592
597
|
`);
|
|
593
|
-
const stripDate =
|
|
598
|
+
const stripDate = stripDatePrefix; // shared with the stamp path — see date-prefix.ts
|
|
594
599
|
for (const entity of entities) {
|
|
595
600
|
const entityId = `entity_${entity.name.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '_')}`;
|
|
596
601
|
const ts = new Date().toISOString();
|
|
@@ -700,7 +705,7 @@ export class RAGKnowledgeGraphManager {
|
|
|
700
705
|
// dedup 기준은 v3.6 과 같다: 날짜 prefix 를 뗀 본문이 active 에 이미 있으면
|
|
701
706
|
// 새 revision 을 만들지 않는다. 다만 v13 에서는 같은 사실이 다른 출처에서 다시
|
|
702
707
|
// 온 것이므로 그 revision 에 source link 를 더한다(spec §8.3 T13).
|
|
703
|
-
const stripDate =
|
|
708
|
+
const stripDate = stripDatePrefix; // shared with the stamp path — see date-prefix.ts
|
|
704
709
|
const activeRows = this.db.prepare(`SELECT observation_id, content FROM entity_observations
|
|
705
710
|
WHERE entity_id = ? AND status = 'active'`).all(entityId);
|
|
706
711
|
const activeByBare = new Map(activeRows.map(r => [stripDate(r.content), r.observation_id]));
|
|
@@ -1963,7 +1968,10 @@ export class RAGKnowledgeGraphManager {
|
|
|
1963
1968
|
// report `unchanged` for a different exclusion, which is the silent-wrong case.
|
|
1964
1969
|
const content = applyExcludePatterns(options.content !== undefined ? options.content : fsSync.readFileSync(filePath, 'utf-8'), options.excludePattern);
|
|
1965
1970
|
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
1966
|
-
|
|
1971
|
+
// Same calendar as the observation prefix. Deciding this explicitly rather than leaving it
|
|
1972
|
+
// on UTC: both are date-only labels a person reads, and "observations in Seoul days but
|
|
1973
|
+
// documents in UTC days" is not a distinction anyone could explain later.
|
|
1974
|
+
const today = calendarDate(new Date(), this.calendarTimeZone);
|
|
1967
1975
|
const contentHash = shaHex(content);
|
|
1968
1976
|
// spec §5.1: content_hash 는 system-owned — user metadata 뒤에 쓴다 (r1: spread 가 덮어쓸 수 있었다).
|
|
1969
1977
|
const metadata = { source: filePath, updated: today, ...(options.metadata || {}), content_hash: contentHash };
|
|
@@ -3452,6 +3460,9 @@ export class RAGKnowledgeGraphManager {
|
|
|
3452
3460
|
server: {
|
|
3453
3461
|
version: PKG_VERSION,
|
|
3454
3462
|
node: process.versions.node,
|
|
3463
|
+
// Which calendar produced the date-only labels in this database. Surfaced because a
|
|
3464
|
+
// wrong value is otherwise invisible: the labels look plausible either way.
|
|
3465
|
+
calendar_timezone: this.calendarTimeZone,
|
|
3455
3466
|
embeddings_mode: this.embeddingsMode,
|
|
3456
3467
|
model: `${EMBEDDING_MODEL}@${MODEL_REVISION}`,
|
|
3457
3468
|
model_state: gs.state,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The calendar day of `instant` in `timeZone`, as YYYY-MM-DD. */
|
|
2
|
+
export declare function calendarDate(instant: Date, timeZone: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve RAG_MEMORY_CALENDAR_TZ. Unset or blank means UTC. An unrecognised zone throws at
|
|
5
|
+
* boot rather than silently falling back — a wrong-but-quiet calendar writes wrong labels for
|
|
6
|
+
* as long as nobody looks, which is the failure this whole module exists to end.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveCalendarTimeZone(raw: string | undefined | null): string;
|
|
9
|
+
export interface DatePrefix {
|
|
10
|
+
/** YYYY-MM-DD, already validated as a real calendar day. */
|
|
11
|
+
date: string;
|
|
12
|
+
/** Text between the date and the closing bracket, e.g. a session marker. */
|
|
13
|
+
annotation: string | null;
|
|
14
|
+
/** The matched prefix including the brackets, without trailing whitespace. */
|
|
15
|
+
matched: string;
|
|
16
|
+
}
|
|
17
|
+
/** Parse a leading date prefix, or null when the text does not start with one. */
|
|
18
|
+
export declare function parseDatePrefix(content: string): DatePrefix | null;
|
|
19
|
+
/**
|
|
20
|
+
* The dedup key: the text with its date prefix removed. The annotation comes off with the date
|
|
21
|
+
* because it records *when and in which session the line was written*, not what the line claims
|
|
22
|
+
* — the same sentence written in two sessions is one fact, and dedup has to see that.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stripDatePrefix(content: string): string;
|
|
25
|
+
/** Prepend today's calendar day unless the text already carries a valid date prefix. */
|
|
26
|
+
export declare function stampDatePrefix(content: string, timeZone: string, now?: Date): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// One place that decides what a leading "[date]" on an observation means.
|
|
2
|
+
//
|
|
3
|
+
// Why this is a module and not two regexes inline: the stamp path and the dedup path each had
|
|
4
|
+
// their own copy of the pattern, and they disagreed. Stamping asked "does this already start
|
|
5
|
+
// with a date?" while dedup asked "what is this text without its date?", so widening one without
|
|
6
|
+
// the other let the same sentence be stored twice under two session markers. Both now call in
|
|
7
|
+
// here.
|
|
8
|
+
//
|
|
9
|
+
// Why an explicit timezone instead of the process one: a date-only label has no meaning until
|
|
10
|
+
// you say which calendar produced it. The same database is reached from a laptop, from CI and
|
|
11
|
+
// from another country; deriving the day from the ambient TZ makes the stored string mean
|
|
12
|
+
// something different on each. The default stays UTC so the product does not inherit whichever
|
|
13
|
+
// zone its first author happened to sit in — a deployment that wants local days says so.
|
|
14
|
+
/** The calendar day of `instant` in `timeZone`, as YYYY-MM-DD. */
|
|
15
|
+
export function calendarDate(instant, timeZone) {
|
|
16
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
17
|
+
timeZone,
|
|
18
|
+
year: 'numeric',
|
|
19
|
+
month: '2-digit',
|
|
20
|
+
day: '2-digit',
|
|
21
|
+
}).formatToParts(instant);
|
|
22
|
+
const part = (type) => {
|
|
23
|
+
const found = parts.find(p => p.type === type);
|
|
24
|
+
if (!found)
|
|
25
|
+
throw new Error(`calendarDate: missing ${type} for timeZone ${timeZone}`);
|
|
26
|
+
return found.value;
|
|
27
|
+
};
|
|
28
|
+
return `${part('year')}-${part('month')}-${part('day')}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve RAG_MEMORY_CALENDAR_TZ. Unset or blank means UTC. An unrecognised zone throws at
|
|
32
|
+
* boot rather than silently falling back — a wrong-but-quiet calendar writes wrong labels for
|
|
33
|
+
* as long as nobody looks, which is the failure this whole module exists to end.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveCalendarTimeZone(raw) {
|
|
36
|
+
const value = (raw ?? '').trim();
|
|
37
|
+
if (!value)
|
|
38
|
+
return 'UTC';
|
|
39
|
+
try {
|
|
40
|
+
new Intl.DateTimeFormat('en-US', { timeZone: value });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`RAG_MEMORY_CALENDAR_TZ is not a recognised IANA time zone: ${JSON.stringify(value)}. ` +
|
|
44
|
+
`Use a name like "Asia/Seoul", or leave it unset for UTC.`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
// The annotation may not contain a newline or a bracket: an unclosed or multi-line "[2026-…"
|
|
49
|
+
// is prose that happens to start with a digit, not a prefix. Requiring the closing bracket is
|
|
50
|
+
// what keeps "[2026-08-11 unclosed" and "[2026-08-111]" out.
|
|
51
|
+
const PREFIX_RE = /^\[(\d{4})-(\d{2})-(\d{2})(?:[ \t]([^\]\n]*))?\]/;
|
|
52
|
+
function isRealCalendarDay(year, month, day) {
|
|
53
|
+
const asUtc = new Date(Date.UTC(year, month - 1, day));
|
|
54
|
+
return asUtc.getUTCFullYear() === year
|
|
55
|
+
&& asUtc.getUTCMonth() === month - 1
|
|
56
|
+
&& asUtc.getUTCDate() === day;
|
|
57
|
+
}
|
|
58
|
+
/** Parse a leading date prefix, or null when the text does not start with one. */
|
|
59
|
+
export function parseDatePrefix(content) {
|
|
60
|
+
const m = PREFIX_RE.exec(content);
|
|
61
|
+
if (!m)
|
|
62
|
+
return null;
|
|
63
|
+
const [matched, year, month, day, annotation] = m;
|
|
64
|
+
if (!isRealCalendarDay(Number(year), Number(month), Number(day)))
|
|
65
|
+
return null;
|
|
66
|
+
const trimmed = annotation === undefined ? null : annotation.trim();
|
|
67
|
+
return {
|
|
68
|
+
date: `${year}-${month}-${day}`,
|
|
69
|
+
annotation: trimmed === '' ? null : trimmed,
|
|
70
|
+
matched,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The dedup key: the text with its date prefix removed. The annotation comes off with the date
|
|
75
|
+
* because it records *when and in which session the line was written*, not what the line claims
|
|
76
|
+
* — the same sentence written in two sessions is one fact, and dedup has to see that.
|
|
77
|
+
*/
|
|
78
|
+
export function stripDatePrefix(content) {
|
|
79
|
+
const prefix = parseDatePrefix(content);
|
|
80
|
+
if (!prefix)
|
|
81
|
+
return content;
|
|
82
|
+
return content.slice(prefix.matched.length).replace(/^\s+/, '');
|
|
83
|
+
}
|
|
84
|
+
/** Prepend today's calendar day unless the text already carries a valid date prefix. */
|
|
85
|
+
export function stampDatePrefix(content, timeZone, now = new Date()) {
|
|
86
|
+
if (parseDatePrefix(content))
|
|
87
|
+
return content;
|
|
88
|
+
return `[${calendarDate(now, timeZone)}] ${content}`;
|
|
89
|
+
}
|
|
@@ -200,6 +200,11 @@ Observations provide the factual foundation that supports entity existence and p
|
|
|
200
200
|
- (!important!) **Entity must exist** - this tool only adds to existing entities
|
|
201
201
|
- (!important!) Duplicates are filtered - repeating existing text with a **new** sources entry
|
|
202
202
|
adds evidence to the existing revision and returns null for that position
|
|
203
|
+
- (!important!) A \`[YYYY-MM-DD]\` prefix is prepended unless the text already starts with a valid
|
|
204
|
+
one. The day comes from \`RAG_MEMORY_CALENDAR_TZ\` (default UTC) - see \`calendar_timezone\` in
|
|
205
|
+
getKnowledgeGraphStats. Text may open with \`[YYYY-MM-DD note]\`; the note is treated as recording
|
|
206
|
+
metadata and is ignored when deduplicating, so the same sentence filed under two session markers
|
|
207
|
+
stays one observation
|
|
203
208
|
- (!important!) Observations are cumulative. **This tool never replaces anything** - use
|
|
204
209
|
correctObservation to supersede and retractObservation to withdraw
|
|
205
210
|
- (!important!) **Be specific and factual** - observations should be verifiable statements
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rag-memory-epf-mcp",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24"
|
|
6
6
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"prepare": "npm run build",
|
|
46
46
|
"watch": "tsc --watch",
|
|
47
47
|
"verify:invariants": "node test/chunk-invariants.test.mjs",
|
|
48
|
-
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/document-return-contracts.test.mjs",
|
|
48
|
+
"verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs",
|
|
49
49
|
"test": "npm run build && npm run verify:invariants && npm run verify:engine",
|
|
50
50
|
"prepublishOnly": "npm run build && npm run verify:invariants && npm run verify:engine"
|
|
51
51
|
},
|