@loreai/core 0.18.0 → 0.20.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/bun/agents-file.d.ts.map +1 -1
- package/dist/bun/config.d.ts.map +1 -1
- package/dist/bun/curator.d.ts.map +1 -1
- package/dist/bun/db.d.ts +86 -1
- package/dist/bun/db.d.ts.map +1 -1
- package/dist/bun/distillation.d.ts +2 -13
- package/dist/bun/distillation.d.ts.map +1 -1
- package/dist/bun/embedding.d.ts +5 -1
- package/dist/bun/embedding.d.ts.map +1 -1
- package/dist/bun/git.d.ts.map +1 -1
- package/dist/bun/gradient.d.ts +13 -1
- package/dist/bun/gradient.d.ts.map +1 -1
- package/dist/bun/hosted.d.ts +36 -0
- package/dist/bun/hosted.d.ts.map +1 -0
- package/dist/bun/index.d.ts +3 -2
- package/dist/bun/index.d.ts.map +1 -1
- package/dist/bun/index.js +1049 -247
- package/dist/bun/index.js.map +4 -4
- package/dist/bun/lat-reader.d.ts.map +1 -1
- package/dist/bun/ltm.d.ts +99 -5
- package/dist/bun/ltm.d.ts.map +1 -1
- package/dist/bun/session-limiter.d.ts +26 -0
- package/dist/bun/session-limiter.d.ts.map +1 -0
- package/dist/bun/temporal.d.ts +2 -0
- package/dist/bun/temporal.d.ts.map +1 -1
- package/dist/node/agents-file.d.ts.map +1 -1
- package/dist/node/config.d.ts.map +1 -1
- package/dist/node/curator.d.ts.map +1 -1
- package/dist/node/db.d.ts +86 -1
- package/dist/node/db.d.ts.map +1 -1
- package/dist/node/distillation.d.ts +2 -13
- package/dist/node/distillation.d.ts.map +1 -1
- package/dist/node/embedding.d.ts +5 -1
- package/dist/node/embedding.d.ts.map +1 -1
- package/dist/node/git.d.ts.map +1 -1
- package/dist/node/gradient.d.ts +13 -1
- package/dist/node/gradient.d.ts.map +1 -1
- package/dist/node/hosted.d.ts +36 -0
- package/dist/node/hosted.d.ts.map +1 -0
- package/dist/node/index.d.ts +3 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1049 -247
- package/dist/node/index.js.map +4 -4
- package/dist/node/lat-reader.d.ts.map +1 -1
- package/dist/node/ltm.d.ts +99 -5
- package/dist/node/ltm.d.ts.map +1 -1
- package/dist/node/session-limiter.d.ts +26 -0
- package/dist/node/session-limiter.d.ts.map +1 -0
- package/dist/node/temporal.d.ts +2 -0
- package/dist/node/temporal.d.ts.map +1 -1
- package/dist/types/agents-file.d.ts.map +1 -1
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/curator.d.ts.map +1 -1
- package/dist/types/db.d.ts +86 -1
- package/dist/types/db.d.ts.map +1 -1
- package/dist/types/distillation.d.ts +2 -13
- package/dist/types/distillation.d.ts.map +1 -1
- package/dist/types/embedding.d.ts +5 -1
- package/dist/types/embedding.d.ts.map +1 -1
- package/dist/types/git.d.ts.map +1 -1
- package/dist/types/gradient.d.ts +13 -1
- package/dist/types/gradient.d.ts.map +1 -1
- package/dist/types/hosted.d.ts +36 -0
- package/dist/types/hosted.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lat-reader.d.ts.map +1 -1
- package/dist/types/ltm.d.ts +99 -5
- package/dist/types/ltm.d.ts.map +1 -1
- package/dist/types/session-limiter.d.ts +26 -0
- package/dist/types/session-limiter.d.ts.map +1 -0
- package/dist/types/temporal.d.ts +2 -0
- package/dist/types/temporal.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/agents-file.ts +12 -0
- package/src/config.ts +10 -5
- package/src/curator.ts +54 -2
- package/src/db.ts +386 -6
- package/src/distillation.ts +55 -14
- package/src/embedding.ts +71 -8
- package/src/git.ts +4 -0
- package/src/gradient.ts +227 -74
- package/src/hosted.ts +46 -0
- package/src/index.ts +12 -0
- package/src/lat-reader.ts +4 -0
- package/src/ltm.ts +480 -45
- package/src/session-limiter.ts +47 -0
- package/src/temporal.ts +10 -0
package/dist/types/ltm.d.ts
CHANGED
|
@@ -45,6 +45,24 @@ export declare function findFuzzyDuplicate(input: {
|
|
|
45
45
|
title: string;
|
|
46
46
|
} | null;
|
|
47
47
|
export declare function forProject(projectPath: string, includeCross?: boolean): KnowledgeEntry[];
|
|
48
|
+
/**
|
|
49
|
+
* Well-known knowledge entry categories managed by the curator.
|
|
50
|
+
* The DB column is a free-form string, but these are the standard values.
|
|
51
|
+
*/
|
|
52
|
+
export type KnowledgeCategory = "decision" | "pattern" | "preference" | "architecture" | "gotcha";
|
|
53
|
+
/** Options for `forSession()` to control entry selection. */
|
|
54
|
+
export type ForSessionOptions = {
|
|
55
|
+
/** Caller-provided context (e.g., user's current message) for relevance
|
|
56
|
+
* scoring when no session context exists in the DB yet. */
|
|
57
|
+
contextHint?: string;
|
|
58
|
+
/** Restrict to these categories (e.g., `['preference']` for turn 1). */
|
|
59
|
+
categories?: (KnowledgeCategory | (string & {}))[];
|
|
60
|
+
/** Exclude these categories (e.g., `['preference']` for context-bound
|
|
61
|
+
* entries when preferences are already injected in a separate block).
|
|
62
|
+
* Mutually exclusive with `categories` — if both are provided,
|
|
63
|
+
* `categories` (include) wins. */
|
|
64
|
+
excludeCategories?: (KnowledgeCategory | (string & {}))[];
|
|
65
|
+
};
|
|
48
66
|
/**
|
|
49
67
|
* Build a relevance-ranked, budget-capped list of knowledge entries for injection
|
|
50
68
|
* into the system prompt of a live session.
|
|
@@ -52,20 +70,24 @@ export declare function forProject(projectPath: string, includeCross?: boolean):
|
|
|
52
70
|
* Strategy:
|
|
53
71
|
* 1. Both project-specific and cross-project entries are scored for relevance
|
|
54
72
|
* against recent session context (last distillation + recent raw messages).
|
|
55
|
-
* 2.
|
|
73
|
+
* 2. When embeddings are available, vector cosine similarity is used for scoring
|
|
74
|
+
* (captures semantic matches that keyword overlap misses). Falls back to
|
|
75
|
+
* FTS5 BM25 when embeddings are unavailable.
|
|
76
|
+
* 3. Project entries get a safety net: the top PROJECT_SAFETY_NET entries by
|
|
56
77
|
* confidence are always included even if they have zero relevance score.
|
|
57
78
|
* This ensures the most important project knowledge is never lost to
|
|
58
|
-
* coarse
|
|
59
|
-
*
|
|
79
|
+
* coarse scoring.
|
|
80
|
+
* 4. All scored entries are merged into a single pool and greedily packed
|
|
60
81
|
* into the token budget by score descending.
|
|
61
|
-
*
|
|
82
|
+
* 5. If there's no session context yet (first turn), fall back to top entries
|
|
62
83
|
* by confidence only (capped at NO_CONTEXT_FALLBACK_CAP per pool).
|
|
63
84
|
*
|
|
64
85
|
* @param projectPath Current project path
|
|
65
86
|
* @param sessionID Current session ID (for context extraction)
|
|
66
87
|
* @param maxTokens Hard token budget for the entire formatted block
|
|
88
|
+
* @param options Optional category filter and context hint
|
|
67
89
|
*/
|
|
68
|
-
export declare function forSession(projectPath: string, sessionID: string | undefined, maxTokens: number): KnowledgeEntry[]
|
|
90
|
+
export declare function forSession(projectPath: string, sessionID: string | undefined, maxTokens: number, options?: ForSessionOptions): Promise<KnowledgeEntry[]>;
|
|
69
91
|
export declare function all(): KnowledgeEntry[];
|
|
70
92
|
/** Return all cross-project and global (user-level) knowledge entries. */
|
|
71
93
|
export declare function crossProject(): KnowledgeEntry[];
|
|
@@ -164,9 +186,15 @@ export type DedupCluster = {
|
|
|
164
186
|
title: string;
|
|
165
187
|
}>;
|
|
166
188
|
};
|
|
189
|
+
/** Stable pair key for two entry IDs — sorted to ensure order-independence. */
|
|
190
|
+
export declare function dedupPairKey(idA: string, idB: string): string;
|
|
167
191
|
export type DedupResult = {
|
|
168
192
|
clusters: DedupCluster[];
|
|
169
193
|
totalRemoved: number;
|
|
194
|
+
/** Pairwise embedding cosine similarities. Key: dedupPairKey(idA, idB). */
|
|
195
|
+
pairSimilarities: Map<string, number>;
|
|
196
|
+
/** All entry titles by ID — for feedback recording after entries are deleted. */
|
|
197
|
+
entryTitles: Map<string, string>;
|
|
170
198
|
};
|
|
171
199
|
export declare function deduplicate(projectPath: string, opts?: {
|
|
172
200
|
dryRun?: boolean;
|
|
@@ -175,4 +203,70 @@ export declare function deduplicate(projectPath: string, opts?: {
|
|
|
175
203
|
export declare function deduplicateGlobal(opts?: {
|
|
176
204
|
dryRun?: boolean;
|
|
177
205
|
}): Promise<DedupResult>;
|
|
206
|
+
export type DedupFeedbackSource = "auto_dedup" | "cli_yes" | "cli_interactive";
|
|
207
|
+
/** Record a single dedup feedback row. */
|
|
208
|
+
export declare function recordDedupFeedback(input: {
|
|
209
|
+
projectId: string | null;
|
|
210
|
+
entryATitle: string;
|
|
211
|
+
entryBTitle: string;
|
|
212
|
+
similarity: number;
|
|
213
|
+
accepted: boolean;
|
|
214
|
+
source: DedupFeedbackSource;
|
|
215
|
+
}): void;
|
|
216
|
+
/**
|
|
217
|
+
* Bulk-record feedback for all merged pairs in a DedupResult.
|
|
218
|
+
* Only records pairs with embedding similarity > 0 (title-overlap-only
|
|
219
|
+
* matches are excluded from calibration).
|
|
220
|
+
*/
|
|
221
|
+
export declare function recordDedupResultFeedback(projectId: string | null, result: DedupResult, accepted: boolean, source: DedupFeedbackSource): void;
|
|
222
|
+
/**
|
|
223
|
+
* Record automatic calibration signals from a post-curation dedup sweep.
|
|
224
|
+
*
|
|
225
|
+
* Only records **reject** signals — non-merged pairs with similarity in
|
|
226
|
+
* [0.80, threshold). Accept signals from auto-dedup are tautological (the
|
|
227
|
+
* pair was merged *because* its similarity exceeded the threshold), so they
|
|
228
|
+
* provide no new information and would create a self-reinforcing feedback
|
|
229
|
+
* loop. Manual signals (cli_yes, cli_interactive) provide the accept side.
|
|
230
|
+
*
|
|
231
|
+
* Caps at AUTO_SIGNAL_MAX_PAIRS most interesting pairs per run (closest
|
|
232
|
+
* to the threshold boundary) to avoid table bloat.
|
|
233
|
+
*/
|
|
234
|
+
export declare function recordAutoSignals(projectId: string | null, result: DedupResult): void;
|
|
235
|
+
/** Get all feedback for a project (for calibration). */
|
|
236
|
+
export declare function getDedupFeedback(projectId: string | null): Array<{
|
|
237
|
+
similarity: number;
|
|
238
|
+
accepted: boolean;
|
|
239
|
+
source: string;
|
|
240
|
+
}>;
|
|
241
|
+
/** Quick count of feedback rows for a project. */
|
|
242
|
+
export declare function getDedupFeedbackCount(projectId: string | null): number;
|
|
243
|
+
/**
|
|
244
|
+
* Prune old feedback rows for a project, keeping the most recent
|
|
245
|
+
* MAX_FEEDBACK_ROWS_PER_PROJECT rows. Called from recordAutoSignals
|
|
246
|
+
* to prevent unbounded table growth.
|
|
247
|
+
*/
|
|
248
|
+
export declare function pruneDedupFeedback(projectId: string | null): void;
|
|
249
|
+
/**
|
|
250
|
+
* Compute an optimal embedding dedup threshold from user feedback.
|
|
251
|
+
*
|
|
252
|
+
* Algorithm:
|
|
253
|
+
* 1. Load all (similarity, accepted) pairs for the project.
|
|
254
|
+
* 2. If fewer than MIN_CALIBRATION_SAMPLES, return null (use default).
|
|
255
|
+
* 3. If all feedback is "accept" (no rejects), return the minimum
|
|
256
|
+
* accepted similarity minus a small margin (0.005).
|
|
257
|
+
* 4. If all feedback is "reject" (no accepts), return null.
|
|
258
|
+
* 5. Otherwise, find the threshold that maximizes separation:
|
|
259
|
+
* - For each candidate threshold (midpoint between consecutive
|
|
260
|
+
* distinct similarity values), compute accuracy:
|
|
261
|
+
* correct = accepted_pairs_above + rejected_pairs_below
|
|
262
|
+
* accuracy = correct / total
|
|
263
|
+
* - Pick the threshold with highest accuracy.
|
|
264
|
+
* - Tie-break: prefer higher threshold (conservative).
|
|
265
|
+
* - Clamp to [0.85, 0.98].
|
|
266
|
+
*/
|
|
267
|
+
export declare function calibrateDedupThreshold(projectId: string | null): number | null;
|
|
268
|
+
/** Persist the calibrated threshold for a project. */
|
|
269
|
+
export declare function saveCalibratedThreshold(projectId: string | null, threshold: number, sampleSize: number): void;
|
|
270
|
+
/** Load the calibrated threshold for a project, or null if not calibrated. */
|
|
271
|
+
export declare function loadCalibratedThreshold(projectId: string | null): number | null;
|
|
178
272
|
//# sourceMappingURL=ltm.d.ts.map
|
package/dist/types/ltm.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ltm.d.ts","sourceRoot":"","sources":["../../src/ltm.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAWF,wBAAgB,MAAM,CAAC,KAAK,EAAE;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+FAA+F;IAC/F,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,MAAM,CAuFT;AAED,wBAAgB,MAAM,CACpB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,QA4BjD;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,QAEhC;AAuCD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0CvC;AAED,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,EACnB,YAAY,UAAO,GAClB,cAAc,EAAE,CAoBlB;AAqED
|
|
1
|
+
{"version":3,"file":"ltm.d.ts","sourceRoot":"","sources":["../../src/ltm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-key concurrency limiter using p-limit.
|
|
3
|
+
*
|
|
4
|
+
* Each key (typically a session ID) gets its own p-limit(1) instance,
|
|
5
|
+
* serializing async operations on the same key while allowing different
|
|
6
|
+
* keys to run fully in parallel.
|
|
7
|
+
*
|
|
8
|
+
* Two independent limiter pools are provided — one for distillation and
|
|
9
|
+
* one for curation — so they don't block each other.
|
|
10
|
+
*/
|
|
11
|
+
import pLimit from "p-limit";
|
|
12
|
+
type LimitFunction = ReturnType<typeof pLimit>;
|
|
13
|
+
/** Serializes distillation.run() and metaDistill() per session. */
|
|
14
|
+
export declare const distillLimiter: {
|
|
15
|
+
get: (key: string) => LimitFunction;
|
|
16
|
+
isBusy: (key: string) => boolean;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
};
|
|
19
|
+
/** Serializes curator.run() per session with skip-if-busy semantics. */
|
|
20
|
+
export declare const curatorLimiter: {
|
|
21
|
+
get: (key: string) => LimitFunction;
|
|
22
|
+
isBusy: (key: string) => boolean;
|
|
23
|
+
clear: () => void;
|
|
24
|
+
};
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=session-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-limiter.d.ts","sourceRoot":"","sources":["../../src/session-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AA6B/C,mEAAmE;AACnE,eAAO,MAAM,cAAc;eAxBP,MAAM,KAAG,aAAa;kBAUnB,MAAM,KAAG,OAAO;iBAMnB,IAAI;CAQyB,CAAC;AAElD,wEAAwE;AACxE,eAAO,MAAM,cAAc;eA3BP,MAAM,KAAG,aAAa;kBAUnB,MAAM,KAAG,OAAO;iBAMnB,IAAI;CAWyB,CAAC"}
|
package/dist/types/temporal.d.ts
CHANGED
|
@@ -84,6 +84,8 @@ export declare function searchScored(input: {
|
|
|
84
84
|
*/
|
|
85
85
|
export declare function temporalCnorm(timestamps: number[], now?: number): number;
|
|
86
86
|
export declare function count(projectPath: string, sessionID?: string): number;
|
|
87
|
+
/** Quick existence check — true if any temporal messages exist for this session. */
|
|
88
|
+
export declare function hasMessages(projectPath: string, sessionID: string): boolean;
|
|
87
89
|
export declare function undistilledCount(projectPath: string, sessionID?: string): number;
|
|
88
90
|
/** Sum of estimated tokens across undistilled messages for a project/session. */
|
|
89
91
|
export declare function undistilledTokens(projectPath: string, sessionID?: string): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temporal.d.ts","sourceRoot":"","sources":["../../src/temporal.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,WAAS,CAAC;AAEvC;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAarD;AAiBD,wBAAgB,KAAK,CAAC,KAAK,EAAE;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,QA8CA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,eAAe,EAAE,CASnB;AAED,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,eAAe,EAAE,CAOnB;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAQ1C;AA6BD,wBAAgB,MAAM,CAAC,KAAK,EAAE;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,eAAe,EAAE,CA8BpB;AAED,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,qBAAqB,EAAE,CAwB1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAAE,EACpB,GAAG,GAAE,MAAmB,GACvB,MAAM,CAoBR;AAED,wBAAgB,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAWrE;AAED,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAWR;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAWR;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,WAAW,CA0Ed"}
|
|
1
|
+
{"version":3,"file":"temporal.d.ts","sourceRoot":"","sources":["../../src/temporal.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAQrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,WAAS,CAAC;AAEvC;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,CAarD;AAiBD,wBAAgB,KAAK,CAAC,KAAK,EAAE;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,QA8CA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,eAAe,EAAE,CASnB;AAED,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,eAAe,EAAE,CAOnB;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAQ1C;AA6BD,wBAAgB,MAAM,CAAC,KAAK,EAAE;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,eAAe,EAAE,CA8BpB;AAED,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,qBAAqB,EAAE,CAwB1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAAE,EACpB,GAAG,GAAE,MAAmB,GACvB,MAAM,CAoBR;AAED,wBAAgB,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAWrE;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAO3E;AAED,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAWR;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAWR;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAC;IACnB,8FAA8F;IAC9F,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,WAAW,CA0Ed"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loreai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"description": "Shared memory engine for Lore — three-tier storage, distillation, gradient context management",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"types": "./dist/node/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"types": "./dist/types/index.d.ts",
|
|
11
12
|
"bun": "./src/index.ts",
|
|
12
13
|
"default": "./dist/node/index.js"
|
|
13
14
|
}
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"@huggingface/hub": "2.11.0",
|
|
27
28
|
"@huggingface/transformers": "^3.7.1",
|
|
28
29
|
"micromark": "^4.0.0",
|
|
30
|
+
"p-limit": "7",
|
|
29
31
|
"remark": "^15.0.1",
|
|
30
32
|
"uuidv7": "^1.1.0",
|
|
31
33
|
"zod": "^4.3.6"
|
package/src/agents-file.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { dirname, join } from "path";
|
|
|
13
13
|
import { db, ensureProject } from "./db";
|
|
14
14
|
import * as ltm from "./ltm";
|
|
15
15
|
import { serialize, inline, h, ul, liph, strong, t, root, unescapeMarkdown } from "./markdown";
|
|
16
|
+
import { isHostedMode } from "./hosted";
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Constants
|
|
@@ -334,6 +335,8 @@ export function exportToFile(input: {
|
|
|
334
335
|
projectPath: string;
|
|
335
336
|
filePath: string;
|
|
336
337
|
}): void {
|
|
338
|
+
if (isHostedMode()) return;
|
|
339
|
+
|
|
337
340
|
// Write the actual entries to .lore.md first.
|
|
338
341
|
exportLoreFile(input.projectPath);
|
|
339
342
|
|
|
@@ -378,6 +381,7 @@ export function shouldImport(input: {
|
|
|
378
381
|
projectPath: string;
|
|
379
382
|
filePath: string;
|
|
380
383
|
}): boolean {
|
|
384
|
+
if (isHostedMode()) return false;
|
|
381
385
|
if (!existsSync(input.filePath)) return false;
|
|
382
386
|
|
|
383
387
|
const fileContent = readFileSync(input.filePath, "utf8");
|
|
@@ -483,6 +487,7 @@ export function importFromFile(input: {
|
|
|
483
487
|
projectPath: string;
|
|
484
488
|
filePath: string;
|
|
485
489
|
}): void {
|
|
490
|
+
if (isHostedMode()) return;
|
|
486
491
|
if (!existsSync(input.filePath)) return;
|
|
487
492
|
|
|
488
493
|
const fileContent = readFileSync(input.filePath, "utf8");
|
|
@@ -507,6 +512,7 @@ export function importFromFile(input: {
|
|
|
507
512
|
* Returns true if a `.lore.md` file exists in the project root.
|
|
508
513
|
*/
|
|
509
514
|
export function loreFileExists(projectPath: string): boolean {
|
|
515
|
+
if (isHostedMode()) return false;
|
|
510
516
|
return existsSync(join(projectPath, LORE_FILE));
|
|
511
517
|
}
|
|
512
518
|
|
|
@@ -519,6 +525,8 @@ export function loreFileExists(projectPath: string): boolean {
|
|
|
519
525
|
* and mtime bumps.
|
|
520
526
|
*/
|
|
521
527
|
export function exportLoreFile(projectPath: string): void {
|
|
528
|
+
if (isHostedMode()) return;
|
|
529
|
+
|
|
522
530
|
const sectionBody = buildSection(projectPath);
|
|
523
531
|
const content = LORE_FILE_HEADER + "\n" + sectionBody;
|
|
524
532
|
const contentHash = hashSection(content);
|
|
@@ -545,6 +553,8 @@ export function exportLoreFile(projectPath: string): void {
|
|
|
545
553
|
* call when the file hasn't been touched since we last processed it.
|
|
546
554
|
*/
|
|
547
555
|
export function shouldImportLoreFile(projectPath: string): boolean {
|
|
556
|
+
if (isHostedMode()) return false;
|
|
557
|
+
|
|
548
558
|
const fp = join(projectPath, LORE_FILE);
|
|
549
559
|
if (!existsSync(fp)) return false;
|
|
550
560
|
|
|
@@ -579,6 +589,8 @@ export function shouldImportLoreFile(projectPath: string): boolean {
|
|
|
579
589
|
* content hasn't changed, only the DB was updated to match it.
|
|
580
590
|
*/
|
|
581
591
|
export function importLoreFile(projectPath: string): void {
|
|
592
|
+
if (isHostedMode()) return;
|
|
593
|
+
|
|
582
594
|
const fp = join(projectPath, LORE_FILE);
|
|
583
595
|
if (!existsSync(fp)) return;
|
|
584
596
|
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { isHostedMode } from "./hosted";
|
|
4
5
|
|
|
5
6
|
export const LoreConfig = z.object({
|
|
6
7
|
model: z
|
|
@@ -251,11 +252,15 @@ export function config(): LoreConfig {
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
export async function load(directory: string): Promise<LoreConfig> {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
255
|
+
// In hosted mode, never read config from client-controlled paths —
|
|
256
|
+
// a crafted .lore.json could alter gateway behavior (budget, model, thresholds).
|
|
257
|
+
if (!isHostedMode()) {
|
|
258
|
+
const path = join(directory, ".lore.json");
|
|
259
|
+
if (existsSync(path)) {
|
|
260
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
261
|
+
current = LoreConfig.parse(raw);
|
|
262
|
+
return current;
|
|
263
|
+
}
|
|
259
264
|
}
|
|
260
265
|
current = LoreConfig.parse({});
|
|
261
266
|
return current;
|
package/src/curator.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { config } from "./config";
|
|
2
|
+
import { saveSessionTracking, loadSessionTracking, ensureProject } from "./db";
|
|
2
3
|
import * as temporal from "./temporal";
|
|
3
4
|
import * as ltm from "./ltm";
|
|
4
5
|
import * as log from "./log";
|
|
5
6
|
import { CURATOR_SYSTEM, curatorUser, CONSOLIDATION_SYSTEM, consolidationUser } from "./prompt";
|
|
6
7
|
import { detectAndFormat } from "./instruction-detect";
|
|
8
|
+
import { curatorLimiter } from "./session-limiter";
|
|
7
9
|
import type { LLMClient } from "./types";
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -121,8 +123,20 @@ export function applyOps(
|
|
|
121
123
|
// Track which messages we've already curated — per session to prevent
|
|
122
124
|
// cross-session leaking (curation on session A advancing the timestamp
|
|
123
125
|
// past session B's messages, causing B's curation to find < 3 recent).
|
|
126
|
+
// In-memory cache backed by session_state DB table so it survives restarts.
|
|
124
127
|
const lastCuratedAt = new Map<string, number>();
|
|
125
128
|
|
|
129
|
+
/** Get the last-curated timestamp for a session, loading from DB if needed. */
|
|
130
|
+
function getLastCuratedAt(sessionID: string): number {
|
|
131
|
+
const cached = lastCuratedAt.get(sessionID);
|
|
132
|
+
if (cached !== undefined) return cached;
|
|
133
|
+
// Load from DB on first access
|
|
134
|
+
const persisted = loadSessionTracking(sessionID);
|
|
135
|
+
const ts = persisted?.lastCuratedAt ?? 0;
|
|
136
|
+
lastCuratedAt.set(sessionID, ts);
|
|
137
|
+
return ts;
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
export async function run(input: {
|
|
127
141
|
llm: LLMClient;
|
|
128
142
|
projectPath: string;
|
|
@@ -132,9 +146,33 @@ export async function run(input: {
|
|
|
132
146
|
const cfg = config();
|
|
133
147
|
if (!cfg.curator.enabled) return { created: 0, updated: 0, deleted: 0 };
|
|
134
148
|
|
|
149
|
+
// Skip-if-busy: curation is periodic, not accumulative. If a curation is
|
|
150
|
+
// already running for this session, skip — the next trigger will pick up
|
|
151
|
+
// any new messages. Serializing would waste an LLM call.
|
|
152
|
+
//
|
|
153
|
+
// The isBusy() check and get()() enqueue are both synchronous — in Node's
|
|
154
|
+
// single-threaded event loop no microtask can interleave between them, so
|
|
155
|
+
// there is no TOCTOU race. The p-limit(1) serialization is a safety net
|
|
156
|
+
// if this invariant is ever violated.
|
|
157
|
+
if (curatorLimiter.isBusy(input.sessionID)) {
|
|
158
|
+
log.info(`curation skipped: already running for session ${input.sessionID.slice(0, 16)}`);
|
|
159
|
+
return { created: 0, updated: 0, deleted: 0 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return curatorLimiter.get(input.sessionID)(() => runInner(input));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runInner(input: {
|
|
166
|
+
llm: LLMClient;
|
|
167
|
+
projectPath: string;
|
|
168
|
+
sessionID: string;
|
|
169
|
+
model?: { providerID: string; modelID: string };
|
|
170
|
+
}): Promise<{ created: number; updated: number; deleted: number }> {
|
|
171
|
+
const cfg = config();
|
|
172
|
+
|
|
135
173
|
// Get recent messages since last curation
|
|
136
174
|
const all = temporal.bySession(input.projectPath, input.sessionID);
|
|
137
|
-
const sessionCuratedAt =
|
|
175
|
+
const sessionCuratedAt = getLastCuratedAt(input.sessionID);
|
|
138
176
|
const recent = all.filter((m) => m.created_at > sessionCuratedAt);
|
|
139
177
|
if (recent.length < 3) return { created: 0, updated: 0, deleted: 0 };
|
|
140
178
|
|
|
@@ -190,12 +228,26 @@ export async function run(input: {
|
|
|
190
228
|
log.info(`post-curation dedup: merged ${dupes.totalRemoved} duplicate entries`);
|
|
191
229
|
result.deleted += dupes.totalRemoved;
|
|
192
230
|
}
|
|
231
|
+
// Record auto-signals for adaptive threshold calibration.
|
|
232
|
+
// Merged pairs → accept; non-merged high-similarity pairs → reject.
|
|
233
|
+
if (dupes.pairSimilarities.size > 0) {
|
|
234
|
+
const pid = ensureProject(input.projectPath);
|
|
235
|
+
ltm.recordAutoSignals(pid, dupes);
|
|
236
|
+
// Recalibrate if enough data has accumulated
|
|
237
|
+
const newThreshold = ltm.calibrateDedupThreshold(pid);
|
|
238
|
+
if (newThreshold !== null) {
|
|
239
|
+
const count = ltm.getDedupFeedbackCount(pid);
|
|
240
|
+
ltm.saveCalibratedThreshold(pid, newThreshold, count);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
193
243
|
} catch (err) {
|
|
194
244
|
log.warn("post-curation dedup failed (non-fatal):", err);
|
|
195
245
|
}
|
|
196
246
|
}
|
|
197
247
|
|
|
198
|
-
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
lastCuratedAt.set(input.sessionID, now);
|
|
250
|
+
saveSessionTracking(input.sessionID, { lastCuratedAt: now });
|
|
199
251
|
return result;
|
|
200
252
|
}
|
|
201
253
|
|