@rohirik/openltm-core 2.8.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/README.md +67 -0
- package/assets/opencode/agents/aegis.md +211 -0
- package/assets/opencode/plugins/aegis.ts +3 -0
- package/assets/opencode/skills/AgentTrustBoundaries/ContextCrushDefense.md +104 -0
- package/assets/opencode/skills/AgentTrustBoundaries/SKILL.md +31 -0
- package/assets/opencode/skills/AgentTrustBoundaries/TrustBoundaryPatterns.md +114 -0
- package/assets/opencode/skills/AgentTrustBoundaries/Workflows/DefendContextCrush.md +27 -0
- package/assets/opencode/skills/AgentTrustBoundaries/Workflows/HandleUntrustedContent.md +27 -0
- package/assets/opencode/skills/CommandPathSafety/CommandInjectionPatterns.md +95 -0
- package/assets/opencode/skills/CommandPathSafety/PathTraversalAndInstallerSafety.md +106 -0
- package/assets/opencode/skills/CommandPathSafety/SKILL.md +31 -0
- package/assets/opencode/skills/CommandPathSafety/Workflows/EnforcePathBoundaries.md +27 -0
- package/assets/opencode/skills/CommandPathSafety/Workflows/HardenCommandExecution.md +27 -0
- package/assets/opencode/skills/SecretSafeHandling/CloudCredentialPatterns.md +106 -0
- package/assets/opencode/skills/SecretSafeHandling/SKILL.md +31 -0
- package/assets/opencode/skills/SecretSafeHandling/SecretHandlingPlaybook.md +102 -0
- package/assets/opencode/skills/SecretSafeHandling/Workflows/DesignSecretSafeFlow.md +27 -0
- package/assets/opencode/skills/SecretSafeHandling/Workflows/RemoveSecretExposure.md +27 -0
- package/package.json +41 -0
- package/src/__tests__/cli/claude.test.ts +122 -0
- package/src/__tests__/cli/detect.test.ts +91 -0
- package/src/__tests__/cli/install.test.ts +161 -0
- package/src/__tests__/cli/opencode.test.ts +169 -0
- package/src/__tests__/cli/pi.test.ts +113 -0
- package/src/__tests__/cli.test.ts +70 -0
- package/src/__tests__/events/crossProcess.test.ts +82 -0
- package/src/__tests__/events/index.test.ts +32 -0
- package/src/__tests__/extensions.test.ts +81 -0
- package/src/__tests__/migrations/retention.test.ts +118 -0
- package/src/__tests__/queue/index.test.ts +61 -0
- package/src/__tests__/scheduler/index.test.ts +39 -0
- package/src/__tests__/vec/index.test.ts +130 -0
- package/src/__tests__/vec/parity.test.ts +70 -0
- package/src/adapterTypes.ts +23 -0
- package/src/cli/_shared.ts +120 -0
- package/src/cli/bin.ts +97 -0
- package/src/cli/claude.ts +124 -0
- package/src/cli/detect.ts +55 -0
- package/src/cli/hook.ts +25 -0
- package/src/cli/index.ts +22 -0
- package/src/cli/install.ts +185 -0
- package/src/cli/opencode.ts +193 -0
- package/src/cli/pi.ts +74 -0
- package/src/cli/types.ts +78 -0
- package/src/config.ts +163 -0
- package/src/context.ts +172 -0
- package/src/dao/conflicts.ts +26 -0
- package/src/dao/contextItems.ts +70 -0
- package/src/dao/embeddings.ts +78 -0
- package/src/dao/index.ts +9 -0
- package/src/dao/provenanceAudit.ts +108 -0
- package/src/dao/types.ts +142 -0
- package/src/db.ts +780 -0
- package/src/dedup.ts +12 -0
- package/src/embeddings.ts +386 -0
- package/src/events/index.ts +130 -0
- package/src/extensions.ts +140 -0
- package/src/graph.ts +268 -0
- package/src/index.ts +95 -0
- package/src/janitor/archive.ts +66 -0
- package/src/janitor/decay.ts +60 -0
- package/src/janitor/dedup.ts +333 -0
- package/src/janitor/embeddings.ts +209 -0
- package/src/janitor/index.ts +215 -0
- package/src/janitor/promote.ts +188 -0
- package/src/janitor/providers/anthropic.ts +91 -0
- package/src/janitor/providers/cohere.ts +135 -0
- package/src/janitor/providers/gemini.ts +156 -0
- package/src/janitor/providers/ollama.ts +177 -0
- package/src/janitor/providers/openai.ts +121 -0
- package/src/janitor/providers/openrouter.ts +182 -0
- package/src/janitor/providers/types.ts +154 -0
- package/src/janitor/providers/utils.ts +35 -0
- package/src/janitor/supersedes.ts +199 -0
- package/src/lib/honker.ts +54 -0
- package/src/lib/honkerTypes.ts +109 -0
- package/src/lib/jsonlLogger.ts +92 -0
- package/src/lib/writeQueue.ts +28 -0
- package/src/migrations.ts +415 -0
- package/src/paths.ts +22 -0
- package/src/proposals.ts +120 -0
- package/src/providers/disabled.ts +19 -0
- package/src/providers/embeddingProvider.ts +49 -0
- package/src/providers/gemini.ts +37 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/ollama.ts +43 -0
- package/src/providers/openai.ts +35 -0
- package/src/queue/index.ts +53 -0
- package/src/queue/worker.ts +77 -0
- package/src/recall/categorise.ts +139 -0
- package/src/recall/explainer.ts +76 -0
- package/src/scheduler/index.ts +97 -0
- package/src/schema.sql +191 -0
- package/src/secretsScrubber.ts +105 -0
- package/src/shared-db.ts +158 -0
- package/src/vec/index.ts +161 -0
- package/tsconfig.json +9 -0
package/src/graph.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graph.ts — Graph traversal engine for LTM reasoning.
|
|
3
|
+
* BFS from a seed memory, up to configurable depth.
|
|
4
|
+
* Detects conflicts, reinforcements, and infers implicit edges via LLM.
|
|
5
|
+
*/
|
|
6
|
+
import type { Database } from "bun:sqlite";
|
|
7
|
+
import { getDb } from "./shared-db.js";
|
|
8
|
+
import type { Memory, RelationshipType } from "./db.js";
|
|
9
|
+
|
|
10
|
+
export interface MemoryNode {
|
|
11
|
+
id: number;
|
|
12
|
+
content: string;
|
|
13
|
+
category: string;
|
|
14
|
+
importance: number;
|
|
15
|
+
project_scope: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MemoryPair {
|
|
19
|
+
a: MemoryNode;
|
|
20
|
+
b: MemoryNode;
|
|
21
|
+
type: RelationshipType;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface InferredRelation {
|
|
25
|
+
a: MemoryNode;
|
|
26
|
+
b: MemoryNode;
|
|
27
|
+
type: RelationshipType;
|
|
28
|
+
persisted: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TraversalResult {
|
|
32
|
+
chain: MemoryNode[];
|
|
33
|
+
conflicts: MemoryPair[];
|
|
34
|
+
reinforcements: MemoryPair[];
|
|
35
|
+
clusters: MemoryNode[][];
|
|
36
|
+
inferred: InferredRelation[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const CONFLICT_TYPES = new Set<RelationshipType>(["contradicts", "supersedes"]);
|
|
40
|
+
const REINFORCE_TYPES = new Set<RelationshipType>(["supports", "refines"]);
|
|
41
|
+
|
|
42
|
+
function edgeKey(a: number, b: number): string {
|
|
43
|
+
return `${Math.min(a, b)}:${Math.max(a, b)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface EdgeRow {
|
|
47
|
+
neighbor_id: number;
|
|
48
|
+
relationship_type: RelationshipType;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getNeighbors(db: Database, id: number): EdgeRow[] {
|
|
52
|
+
return db.query<EdgeRow, [number, number]>(
|
|
53
|
+
`SELECT target_memory_id as neighbor_id, relationship_type FROM memory_relations WHERE source_memory_id=?
|
|
54
|
+
UNION ALL
|
|
55
|
+
SELECT source_memory_id as neighbor_id, relationship_type FROM memory_relations WHERE target_memory_id=?`
|
|
56
|
+
).all(id, id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getMemoryNode(db: Database, id: number): MemoryNode | null {
|
|
60
|
+
return db.query<MemoryNode, [number]>(
|
|
61
|
+
`SELECT id, content, category, importance, project_scope FROM memories WHERE id=? AND status='active'`
|
|
62
|
+
).get(id) ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* BFS traversal from startId up to depth hops.
|
|
67
|
+
* Returns ordered chain (BFS order), plus pairs classified by edge type.
|
|
68
|
+
*/
|
|
69
|
+
export async function traverseGraph(
|
|
70
|
+
startId: number,
|
|
71
|
+
depth = 2,
|
|
72
|
+
inferImplicit = false,
|
|
73
|
+
): Promise<TraversalResult> {
|
|
74
|
+
const db = getDb();
|
|
75
|
+
|
|
76
|
+
const seedNode = getMemoryNode(db, startId);
|
|
77
|
+
if (!seedNode) {
|
|
78
|
+
return { chain: [], conflicts: [], reinforcements: [], clusters: [], inferred: [] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const visited = new Set<number>([startId]);
|
|
82
|
+
const chain: MemoryNode[] = [seedNode];
|
|
83
|
+
const conflicts: MemoryPair[] = [];
|
|
84
|
+
const reinforcements: MemoryPair[] = [];
|
|
85
|
+
|
|
86
|
+
// BFS queue: [nodeId, depth]
|
|
87
|
+
const queue: Array<[number, number]> = [[startId, 0]];
|
|
88
|
+
|
|
89
|
+
const edgeMap = new Map<string, RelationshipType>(); // "a:b" -> type
|
|
90
|
+
|
|
91
|
+
while (queue.length > 0) {
|
|
92
|
+
const [curId, curDepth] = queue.shift()!;
|
|
93
|
+
if (curDepth >= depth) continue;
|
|
94
|
+
|
|
95
|
+
const curNode = getMemoryNode(db, curId);
|
|
96
|
+
if (!curNode) continue;
|
|
97
|
+
|
|
98
|
+
const neighbors = getNeighbors(db, curId);
|
|
99
|
+
for (const edge of neighbors) {
|
|
100
|
+
const neighborNode = getMemoryNode(db, edge.neighbor_id);
|
|
101
|
+
if (!neighborNode) continue;
|
|
102
|
+
|
|
103
|
+
edgeMap.set(edgeKey(curId, edge.neighbor_id), edge.relationship_type);
|
|
104
|
+
|
|
105
|
+
if (CONFLICT_TYPES.has(edge.relationship_type)) {
|
|
106
|
+
conflicts.push({ a: curNode, b: neighborNode, type: edge.relationship_type });
|
|
107
|
+
} else if (REINFORCE_TYPES.has(edge.relationship_type)) {
|
|
108
|
+
reinforcements.push({ a: curNode, b: neighborNode, type: edge.relationship_type });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!visited.has(edge.neighbor_id)) {
|
|
112
|
+
visited.add(edge.neighbor_id);
|
|
113
|
+
chain.push(neighborNode);
|
|
114
|
+
queue.push([edge.neighbor_id, curDepth + 1]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Build connected components (clusters) from visited nodes
|
|
120
|
+
const clusters = buildClusters(db, [...visited]);
|
|
121
|
+
|
|
122
|
+
// Optionally infer implicit edges between visited nodes that lack a direct edge
|
|
123
|
+
const inferred: InferredRelation[] = [];
|
|
124
|
+
if (inferImplicit && chain.length >= 2) {
|
|
125
|
+
const { classifyRelation } = await import("./embeddings.js");
|
|
126
|
+
const { relate } = await import("./db.js");
|
|
127
|
+
|
|
128
|
+
const pairs = getPairsWithoutEdge(chain, edgeMap);
|
|
129
|
+
const results = await Promise.allSettled(
|
|
130
|
+
pairs.map(async ([a, b]) => {
|
|
131
|
+
const relType = await classifyRelation(a.content, b.content);
|
|
132
|
+
if (!relType) return null;
|
|
133
|
+
|
|
134
|
+
let persisted = false;
|
|
135
|
+
try {
|
|
136
|
+
relate({ source_id: a.id, target_id: b.id, relationship_type: relType });
|
|
137
|
+
persisted = true;
|
|
138
|
+
} catch {
|
|
139
|
+
// Memory deleted between traversal and relate — ignore
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { a, b, type: relType as RelationshipType, persisted };
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
for (const r of results) {
|
|
147
|
+
if (r.status === "fulfilled" && r.value) {
|
|
148
|
+
inferred.push(r.value);
|
|
149
|
+
if (CONFLICT_TYPES.has(r.value.type)) {
|
|
150
|
+
conflicts.push({ a: r.value.a, b: r.value.b, type: r.value.type });
|
|
151
|
+
} else if (REINFORCE_TYPES.has(r.value.type)) {
|
|
152
|
+
reinforcements.push({ a: r.value.a, b: r.value.b, type: r.value.type });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { chain, conflicts, reinforcements, clusters, inferred };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getPairsWithoutEdge(nodes: MemoryNode[], edgeMap: Map<string, RelationshipType>): Array<[MemoryNode, MemoryNode]> {
|
|
162
|
+
const pairs: Array<[MemoryNode, MemoryNode]> = [];
|
|
163
|
+
// Limit to avoid O(n^2) explosion — cap at first 6 nodes (15 pairs max)
|
|
164
|
+
const subset = nodes.slice(0, 6);
|
|
165
|
+
for (let i = 0; i < subset.length; i++) {
|
|
166
|
+
for (let j = i + 1; j < subset.length; j++) {
|
|
167
|
+
const a = subset[i]!;
|
|
168
|
+
const b = subset[j]!;
|
|
169
|
+
if (!edgeMap.has(edgeKey(a.id, b.id))) {
|
|
170
|
+
pairs.push([a, b]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return pairs;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildClusters(db: Database, ids: number[]): MemoryNode[][] {
|
|
178
|
+
if (ids.length === 0) return [];
|
|
179
|
+
const idSet = new Set(ids);
|
|
180
|
+
const adj = new Map<number, Set<number>>();
|
|
181
|
+
for (const id of ids) adj.set(id, new Set());
|
|
182
|
+
|
|
183
|
+
// Single batch query instead of N×getNeighbors calls
|
|
184
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
185
|
+
const relations = db.query<{ source: number; target: number }, number[]>(
|
|
186
|
+
`SELECT source_memory_id as source, target_memory_id as target FROM memory_relations
|
|
187
|
+
WHERE source_memory_id IN (${placeholders}) OR target_memory_id IN (${placeholders})`
|
|
188
|
+
).all(...ids, ...ids);
|
|
189
|
+
|
|
190
|
+
for (const r of relations) {
|
|
191
|
+
if (idSet.has(r.source) && idSet.has(r.target)) {
|
|
192
|
+
adj.get(r.source)!.add(r.target);
|
|
193
|
+
adj.get(r.target)!.add(r.source);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const visited = new Set<number>();
|
|
198
|
+
const clusters: MemoryNode[][] = [];
|
|
199
|
+
|
|
200
|
+
for (const id of ids) {
|
|
201
|
+
if (visited.has(id)) continue;
|
|
202
|
+
const cluster: MemoryNode[] = [];
|
|
203
|
+
const q = [id];
|
|
204
|
+
while (q.length > 0) {
|
|
205
|
+
const cur = q.pop()!;
|
|
206
|
+
if (visited.has(cur)) continue;
|
|
207
|
+
visited.add(cur);
|
|
208
|
+
const node = getMemoryNode(db, cur);
|
|
209
|
+
if (node) cluster.push(node);
|
|
210
|
+
for (const neighbor of adj.get(cur) ?? []) {
|
|
211
|
+
if (!visited.has(neighbor)) q.push(neighbor);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (cluster.length > 0) clusters.push(cluster);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return clusters;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Build a human-readable reasoning context block (max 5 bullets).
|
|
222
|
+
* Suitable for injecting into session context.
|
|
223
|
+
*/
|
|
224
|
+
export function buildReasoningContext(result: TraversalResult): string {
|
|
225
|
+
const lines: string[] = [];
|
|
226
|
+
|
|
227
|
+
if (result.chain.length > 1) {
|
|
228
|
+
const chainSnippet = result.chain
|
|
229
|
+
.slice(0, 4)
|
|
230
|
+
.map(n => n.content.substring(0, 50).replace(/\n/g, " "))
|
|
231
|
+
.join(" → ");
|
|
232
|
+
lines.push(`[Chain] ${chainSnippet}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const c of result.conflicts.slice(0, 2)) {
|
|
236
|
+
const a = c.a.content.substring(0, 40).replace(/\n/g, " ");
|
|
237
|
+
const b = c.b.content.substring(0, 40).replace(/\n/g, " ");
|
|
238
|
+
lines.push(`[Conflict] "${a}" ↔ "${b}" — verify before applying`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (result.reinforcements.length > 0) {
|
|
242
|
+
const count = result.reinforcements.length;
|
|
243
|
+
const sample = result.reinforcements[0]!;
|
|
244
|
+
const topic = sample.a.content.substring(0, 50).replace(/\n/g, " ");
|
|
245
|
+
lines.push(`[Reinforcement] ${count} memor${count === 1 ? "y" : "ies"} agree: "${topic}"`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return lines.slice(0, 5).join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// CLI entry: bun graph.ts <memoryId> [depth]
|
|
252
|
+
if (import.meta.main) {
|
|
253
|
+
const id = parseInt(process.argv[2] ?? "0", 10);
|
|
254
|
+
const depth = parseInt(process.argv[3] ?? "2", 10);
|
|
255
|
+
if (!id) {
|
|
256
|
+
console.error("Usage: bun graph.ts <memoryId> [depth=2]");
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
const result = await traverseGraph(id, depth, true);
|
|
260
|
+
console.log(JSON.stringify({
|
|
261
|
+
chainLength: result.chain.length,
|
|
262
|
+
conflicts: result.conflicts.length,
|
|
263
|
+
reinforcements: result.reinforcements.length,
|
|
264
|
+
clusters: result.clusters.length,
|
|
265
|
+
inferred: result.inferred.length,
|
|
266
|
+
context: buildReasoningContext(result),
|
|
267
|
+
}, null, 2));
|
|
268
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @rohirik/openltm-core — public API
|
|
2
|
+
export type { LtmCoreConfig, LtmAdapterContext, LtmAdapterOptions } from "./adapterTypes.js";
|
|
3
|
+
|
|
4
|
+
// DB singleton + configuration
|
|
5
|
+
export { getDb, initDb, configure, waitForInit, _setDbForTesting, withRetry, DB_PATH,
|
|
6
|
+
getSetting, setSetting, getAllSettings } from "./shared-db.js";
|
|
7
|
+
export { configureCore, configureDocs } from "./db.js";
|
|
8
|
+
|
|
9
|
+
// Core memory operations
|
|
10
|
+
export {
|
|
11
|
+
learn, recall, forget, relate, getSimilarMemories,
|
|
12
|
+
getContextMerge, getContextMergeWithGraph, computeDecayScore,
|
|
13
|
+
exportMarkdown, exportGraphJson,
|
|
14
|
+
} from "./db.js";
|
|
15
|
+
export type {
|
|
16
|
+
Memory, MemoryWithRelations, MemoryCategory, RelationshipType, MemoryRelation,
|
|
17
|
+
LearnInput, LearnResult, RecallInput, DecayResult,
|
|
18
|
+
} from "./db.js";
|
|
19
|
+
|
|
20
|
+
// Context items
|
|
21
|
+
export { addItem, getItems, exportContextMarkdown, trimProgress, promote } from "./context.js";
|
|
22
|
+
|
|
23
|
+
// Migrations
|
|
24
|
+
export { runPendingMigrations, getMigrationStatus, getMigrationFiles } from "./migrations.js";
|
|
25
|
+
|
|
26
|
+
// DAO types
|
|
27
|
+
export type {
|
|
28
|
+
ContextItemRow, ContextItemType, MemorySlim, AuditOp, ProvenanceRow, ConflictRow,
|
|
29
|
+
} from "./dao/types.js";
|
|
30
|
+
export { getRecentConflicts } from "./dao/conflicts.js";
|
|
31
|
+
export { insertProvenance, insertAudit, queryAudit, listProvenance, snapshotMemory, listProvenanceBatch } from "./dao/provenanceAudit.js";
|
|
32
|
+
export { setEmbedding, getEmbedding, deleteEmbedding, listMemoryIdsMissingEmbedding } from "./dao/embeddings.js";
|
|
33
|
+
export { listByProject, upsertGoal, appendProgress, addDecision, addGotcha } from "./dao/contextItems.js";
|
|
34
|
+
|
|
35
|
+
// Utilities
|
|
36
|
+
export { scrubSecrets } from "./secretsScrubber.js";
|
|
37
|
+
export { normalizeKey } from "./dedup.js";
|
|
38
|
+
export { embedText, getLlmConfig, callLlm } from "./embeddings.js";
|
|
39
|
+
|
|
40
|
+
// Recall utilities
|
|
41
|
+
export { categorise } from "./recall/categorise.js";
|
|
42
|
+
export { buildExplainer, computeTemperature } from "./recall/explainer.js";
|
|
43
|
+
export type { MemoryTemperature, RecallExplainer, ExplainerInput } from "./recall/explainer.js";
|
|
44
|
+
|
|
45
|
+
// Embedding providers
|
|
46
|
+
export * from "./providers/index.js";
|
|
47
|
+
export { WriteQueue, writeQueue } from "./lib/writeQueue.js";
|
|
48
|
+
export { emitEvent, readRecentEvents, getLogPath, _resetForTesting } from "./lib/jsonlLogger.js";
|
|
49
|
+
export type { LtmEvent } from "./lib/jsonlLogger.js";
|
|
50
|
+
|
|
51
|
+
// Proposals
|
|
52
|
+
export { listPendingProposals, acceptProposal, rejectProposal } from "./proposals.js";
|
|
53
|
+
export type { PendingProposal } from "./proposals.js";
|
|
54
|
+
|
|
55
|
+
// Janitor
|
|
56
|
+
export {
|
|
57
|
+
runJanitor, getJanitorStatus, startAutoRun, stopAutoRun,
|
|
58
|
+
runArchive, touchMemory,
|
|
59
|
+
approveMemory, getPendingMemories, rejectMemory,
|
|
60
|
+
mergeMemories, parseDedupSource,
|
|
61
|
+
supersede,
|
|
62
|
+
getEmbeddingProvider, semanticSearch, findSimilarMemories,
|
|
63
|
+
} from "./janitor/index.js";
|
|
64
|
+
export { runDecay } from "./janitor/decay.js";
|
|
65
|
+
export { SETTING_KEYS, SETTING_DEFAULTS } from "./janitor/providers/types.js";
|
|
66
|
+
export { anthropicLLM } from "./janitor/providers/anthropic.js";
|
|
67
|
+
export { cohereEmbedding } from "./janitor/providers/cohere.js";
|
|
68
|
+
export { geminiEmbedding, geminiLLM } from "./janitor/providers/gemini.js";
|
|
69
|
+
export { ollamaEmbedding } from "./janitor/providers/ollama.js";
|
|
70
|
+
export { openaiEmbedding } from "./janitor/providers/openai.js";
|
|
71
|
+
export { openrouterEmbedding } from "./janitor/providers/openrouter.js";
|
|
72
|
+
|
|
73
|
+
// Graph
|
|
74
|
+
export { traverseGraph, buildReasoningContext } from "./graph.js";
|
|
75
|
+
|
|
76
|
+
// Extension capability probe (custom SQLite / sqlite-vec / Honker)
|
|
77
|
+
export { getCapabilities, loadExtensions } from "./extensions.js";
|
|
78
|
+
export type { Capabilities } from "./extensions.js";
|
|
79
|
+
|
|
80
|
+
// Honker durable queue (embedding generation) — dormant without LTM_HONKER_EXT
|
|
81
|
+
export { getHonker, isHonkerAvailable } from "./lib/honker.js";
|
|
82
|
+
export { EMBED_QUEUE, getEmbeddingQueue, enqueueEmbedding, parseEmbeddingJob } from "./queue/index.js";
|
|
83
|
+
export type { EmbeddingJob } from "./queue/index.js";
|
|
84
|
+
export { startEmbeddingWorker } from "./queue/worker.js";
|
|
85
|
+
export type { EmbeddingWorkerHandle } from "./queue/worker.js";
|
|
86
|
+
export {
|
|
87
|
+
registerJanitorSchedule, startJanitorScheduler,
|
|
88
|
+
JANITOR_SCHEDULE_NAME, JANITOR_QUEUE, JANITOR_CHANNEL, DEFAULT_JANITOR_CRON,
|
|
89
|
+
} from "./scheduler/index.js";
|
|
90
|
+
export type { JanitorSchedulerHandle } from "./scheduler/index.js";
|
|
91
|
+
export {
|
|
92
|
+
notifyLtm, startLtmListener, LTM_CHANNEL, MEMORY_ADDED,
|
|
93
|
+
isCrossProcessSyncEnabled, notifyMemoryAdded, startCrossProcessSync,
|
|
94
|
+
} from "./events/index.js";
|
|
95
|
+
export type { LtmLiveEvent, LtmListenerHandle } from "./events/index.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* archive.ts — Evict fully-decayed deprecated memories into memory_archive.
|
|
3
|
+
*
|
|
4
|
+
* Eviction criteria (all must match):
|
|
5
|
+
* status = 'deprecated' AND
|
|
6
|
+
* importance <= 2 AND (high-importance memories only deprecate, never evict)
|
|
7
|
+
* recall_count <= 1 AND (recalled even once → second chance next cycle)
|
|
8
|
+
* decay_score < 0.10 (well below the 0.25 deprecation threshold)
|
|
9
|
+
*
|
|
10
|
+
* Full memory JSON is preserved in memory_archive before deletion.
|
|
11
|
+
* Max 100 evictions per run to bound transaction duration.
|
|
12
|
+
*/
|
|
13
|
+
import { getDb } from "../shared-db.js";
|
|
14
|
+
import type { Memory } from "../db.js";
|
|
15
|
+
import { insertAudit, snapshotMemory } from "../dao/provenanceAudit.js";
|
|
16
|
+
|
|
17
|
+
const BATCH_SIZE = 100;
|
|
18
|
+
|
|
19
|
+
export interface ArchiveResult {
|
|
20
|
+
/** Memories moved to memory_archive and deleted from memories. */
|
|
21
|
+
archived: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runArchive(): ArchiveResult {
|
|
25
|
+
const db = getDb();
|
|
26
|
+
|
|
27
|
+
const candidates = db.query<Memory, []>(
|
|
28
|
+
`SELECT id, content, category, importance, confidence, source, project_scope, dedup_key,
|
|
29
|
+
created_at, last_confirmed_at, last_used_at, confirm_count, status,
|
|
30
|
+
first_recalled_at, last_recalled_at, recall_count, superseded_by, superseded_at,
|
|
31
|
+
workspace_id, agent_id, decay_score
|
|
32
|
+
FROM memories
|
|
33
|
+
WHERE status = 'deprecated'
|
|
34
|
+
AND importance <= 2
|
|
35
|
+
AND COALESCE(recall_count, 0) <= 1
|
|
36
|
+
AND COALESCE(decay_score, 0) < 0.10
|
|
37
|
+
LIMIT ${BATCH_SIZE}`,
|
|
38
|
+
).all();
|
|
39
|
+
|
|
40
|
+
if (candidates.length === 0) return { archived: 0 };
|
|
41
|
+
|
|
42
|
+
const ids = candidates.map(m => m.id);
|
|
43
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
44
|
+
|
|
45
|
+
const archive = db.transaction((mems: Memory[]) => {
|
|
46
|
+
for (const mem of mems) {
|
|
47
|
+
const beforeSnap = snapshotMemory(db, mem.id);
|
|
48
|
+
db.run(
|
|
49
|
+
`INSERT OR IGNORE INTO memory_archive (id, memory_json, reason, decay_score, project_scope)
|
|
50
|
+
VALUES (?, ?, 'decay', ?, ?)`,
|
|
51
|
+
[mem.id, JSON.stringify(mem), mem.decay_score ?? null, mem.project_scope ?? null],
|
|
52
|
+
);
|
|
53
|
+
insertAudit(db, {
|
|
54
|
+
memory_id: mem.id,
|
|
55
|
+
op: "archive",
|
|
56
|
+
actor: "janitor:archive",
|
|
57
|
+
before_json: beforeSnap ? JSON.stringify(beforeSnap) : null,
|
|
58
|
+
after_json: null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
db.run(`DELETE FROM memories WHERE id IN (${placeholders})`, ids);
|
|
62
|
+
return db.query<{ n: number }, []>("SELECT changes() AS n").get()!.n;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { archived: archive(candidates) };
|
|
66
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decay.ts — Batch decay refresh using materialised decay_score column (Phase 4).
|
|
3
|
+
* Two SQL UPDATE statements inside one transaction replace the old O(N) JS loop.
|
|
4
|
+
*/
|
|
5
|
+
import { getDb } from "../shared-db.js";
|
|
6
|
+
|
|
7
|
+
export interface DecayResult {
|
|
8
|
+
/** Memories whose decay_score was refreshed. */
|
|
9
|
+
refreshed: number;
|
|
10
|
+
/** Memories that crossed the deprecation threshold this run. */
|
|
11
|
+
deprecated: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SQL_REFRESH = `
|
|
15
|
+
UPDATE memories
|
|
16
|
+
SET decay_score = CASE
|
|
17
|
+
WHEN importance = 5 THEN CAST(importance AS REAL) * confidence
|
|
18
|
+
WHEN confirm_count >= 10 THEN CAST(importance AS REAL) * confidence
|
|
19
|
+
WHEN importance = 4 THEN CAST(importance AS REAL) * confidence
|
|
20
|
+
* power(0.5, (julianday('now') - julianday(COALESCE(last_used_at, last_confirmed_at, created_at))) / 180.0)
|
|
21
|
+
WHEN importance = 3 THEN CAST(importance AS REAL) * confidence
|
|
22
|
+
* power(0.5, (julianday('now') - julianday(COALESCE(last_used_at, last_confirmed_at, created_at))) / 90.0)
|
|
23
|
+
WHEN importance = 2 THEN CAST(importance AS REAL) * confidence
|
|
24
|
+
* power(0.5, (julianday('now') - julianday(COALESCE(last_used_at, last_confirmed_at, created_at))) / 30.0)
|
|
25
|
+
ELSE CAST(importance AS REAL) * confidence
|
|
26
|
+
* power(0.5, (julianday('now') - julianday(COALESCE(last_used_at, last_confirmed_at, created_at))) / 14.0)
|
|
27
|
+
END
|
|
28
|
+
WHERE status = 'active'
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const SQL_DEPRECATE = `
|
|
32
|
+
UPDATE memories
|
|
33
|
+
SET status = 'deprecated'
|
|
34
|
+
WHERE status = 'active'
|
|
35
|
+
AND importance < 5
|
|
36
|
+
AND confirm_count < 10
|
|
37
|
+
AND decay_score < 0.25
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export function runDecay(): DecayResult {
|
|
41
|
+
const db = getDb();
|
|
42
|
+
|
|
43
|
+
const run = db.transaction(() => {
|
|
44
|
+
db.run(SQL_REFRESH);
|
|
45
|
+
const refreshed = db.query<{ n: number }, []>("SELECT changes() AS n").get()!.n;
|
|
46
|
+
|
|
47
|
+
db.run(SQL_DEPRECATE);
|
|
48
|
+
const deprecated = db.query<{ n: number }, []>("SELECT changes() AS n").get()!.n;
|
|
49
|
+
|
|
50
|
+
return { refreshed, deprecated };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return run();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Touch a memory's last_used_at timestamp when it is recalled or confirmed. */
|
|
57
|
+
export function touchMemory(memoryId: number): void {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
db.run(`UPDATE memories SET last_used_at = datetime('now') WHERE id = ?`, [memoryId]);
|
|
60
|
+
}
|