@postnesia/db 0.1.4 → 0.1.5
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 +63 -0
- package/dist/access.d.ts +22 -0
- package/dist/access.js +63 -0
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.js +2 -2
- package/dist/importance.d.ts +42 -0
- package/dist/importance.js +141 -0
- package/package.json +9 -1
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @postnesia/db
|
|
2
|
+
|
|
3
|
+
SQLite database layer for the Postnesia memory system. Provides schema migrations, Gemini embeddings, vector search, access tracking, and importance dynamics.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { getDb, queries, createMemory } from '@postnesia/db';
|
|
9
|
+
import { embed } from '@postnesia/db/embeddings';
|
|
10
|
+
import { logAccess } from '@postnesia/db/access';
|
|
11
|
+
import { runConsolidation } from '@postnesia/db/importance';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Environment Variables
|
|
15
|
+
|
|
16
|
+
| Variable | Required | Default | Description |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| `DATABASE_URL` | Yes | — | Absolute `file://` URL to the SQLite database |
|
|
19
|
+
| `GEMINI_API_KEY` | Yes | — | Google Gemini API key for embeddings |
|
|
20
|
+
| `EMBEDDING_MODEL` | No | `gemini-embedding-001` | Embedding model name |
|
|
21
|
+
| `EMBEDDING_DIMENSIONS` | No | `768` | Embedding vector dimensions |
|
|
22
|
+
|
|
23
|
+
`DATABASE_URL` must be an absolute path, e.g. `file:///home/user/workspace/memory.db`.
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
### First-time (copies schema into your project and migrates)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx postnesia-migrate-init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Apply pending migrations
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx postnesia-migrate-dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Seed core memories
|
|
40
|
+
|
|
41
|
+
Seeds the operational guide into the database as a core memory:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx postnesia-seed
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Bins
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `postnesia-migrate-init` | Copy `schema.prisma` to your project and run initial migration |
|
|
52
|
+
| `postnesia-migrate-dev` | Run `prisma migrate dev` against your database |
|
|
53
|
+
| `postnesia-seed` | Insert core bootstrap memories |
|
|
54
|
+
|
|
55
|
+
## Schema Overview
|
|
56
|
+
|
|
57
|
+
- `memory` — main memory records with `content`, `content_l1`, `type`, `importance`, `core`, timestamps
|
|
58
|
+
- `tag` / `memory_tag` — many-to-many tag associations
|
|
59
|
+
- `relationship` — directional links between memories
|
|
60
|
+
- `vec_memories` — sqlite-vec virtual table for vector similarity search
|
|
61
|
+
- `access_log` — read access history used by the importance system
|
|
62
|
+
- `journal` — daily journal entries
|
|
63
|
+
- `task` — task records with status and optional session grouping
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Tracking System
|
|
3
|
+
* Logs when memories are retrieved and updates last_accessed on the memory row.
|
|
4
|
+
* Feeds into L1 decay calculations.
|
|
5
|
+
*/
|
|
6
|
+
export interface AccessLogEntry {
|
|
7
|
+
memory_id: number;
|
|
8
|
+
context?: string;
|
|
9
|
+
accessed_at: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Record that a memory was accessed (updates both access_log and memory.last_accessed)
|
|
13
|
+
*/
|
|
14
|
+
export declare function logAccess(memory_id: number, context?: string): void;
|
|
15
|
+
export declare function getAccessCount(memory_id: number, daysBack?: number): number;
|
|
16
|
+
export declare function getRecentlyAccessed(limit?: number): number[];
|
|
17
|
+
export declare function getAccessHistory(memory_id: number): AccessLogEntry[];
|
|
18
|
+
/**
|
|
19
|
+
* Calculate relevance boost based on access patterns
|
|
20
|
+
* Recent access + frequency = higher relevance
|
|
21
|
+
*/
|
|
22
|
+
export declare function calculateAccessBoost(memory_id: number): number;
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Tracking System
|
|
3
|
+
* Logs when memories are retrieved and updates last_accessed on the memory row.
|
|
4
|
+
* Feeds into L1 decay calculations.
|
|
5
|
+
*/
|
|
6
|
+
import { getDb, recordAccess } from './index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Record that a memory was accessed (updates both access_log and memory.last_accessed)
|
|
9
|
+
*/
|
|
10
|
+
export function logAccess(memory_id, context) {
|
|
11
|
+
try {
|
|
12
|
+
const db = getDb(false);
|
|
13
|
+
recordAccess(db, memory_id, context);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error(`[access-tracker] Warning: Could not log access for #${memory_id}:`, error?.message);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function getAccessCount(memory_id, daysBack = 30) {
|
|
20
|
+
const db = getDb(true);
|
|
21
|
+
const result = db.prepare(`
|
|
22
|
+
SELECT COUNT(*) as count
|
|
23
|
+
FROM access_log
|
|
24
|
+
WHERE memory_id = ?
|
|
25
|
+
AND accessed_at > datetime('now', '-${daysBack} days')
|
|
26
|
+
`).get(memory_id);
|
|
27
|
+
return result.count;
|
|
28
|
+
}
|
|
29
|
+
export function getRecentlyAccessed(limit = 20) {
|
|
30
|
+
const db = getDb(true);
|
|
31
|
+
const results = db.prepare(`
|
|
32
|
+
SELECT memory_id, COUNT(*) as access_count
|
|
33
|
+
FROM access_log
|
|
34
|
+
WHERE accessed_at > datetime('now', '-7 days')
|
|
35
|
+
GROUP BY memory_id
|
|
36
|
+
ORDER BY access_count DESC, MAX(accessed_at) DESC
|
|
37
|
+
LIMIT ?
|
|
38
|
+
`).all(limit);
|
|
39
|
+
return results.map(r => r.memory_id);
|
|
40
|
+
}
|
|
41
|
+
export function getAccessHistory(memory_id) {
|
|
42
|
+
const db = getDb(true);
|
|
43
|
+
return db.prepare(`
|
|
44
|
+
SELECT memory_id, accessed_at, context
|
|
45
|
+
FROM access_log
|
|
46
|
+
WHERE memory_id = ?
|
|
47
|
+
ORDER BY accessed_at DESC
|
|
48
|
+
LIMIT 50
|
|
49
|
+
`).all(memory_id);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Calculate relevance boost based on access patterns
|
|
53
|
+
* Recent access + frequency = higher relevance
|
|
54
|
+
*/
|
|
55
|
+
export function calculateAccessBoost(memory_id) {
|
|
56
|
+
const last7Days = getAccessCount(memory_id, 7);
|
|
57
|
+
const last30Days = getAccessCount(memory_id, 30);
|
|
58
|
+
const recentWeight = last7Days * 2;
|
|
59
|
+
const olderWeight = (last30Days - last7Days);
|
|
60
|
+
const totalScore = recentWeight + olderWeight;
|
|
61
|
+
// Normalize to 0-2 range
|
|
62
|
+
return Math.min(2, totalScore / 5);
|
|
63
|
+
}
|
package/dist/embeddings.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Used by sqlite-vec for semantic similarity search (L2 retrieval)
|
|
5
5
|
*/
|
|
6
6
|
import 'dotenv/config';
|
|
7
|
-
export declare const EMBEDDING_DIMENSIONS
|
|
7
|
+
export declare const EMBEDDING_DIMENSIONS: number;
|
|
8
8
|
/**
|
|
9
9
|
* Generate an embedding vector for a text string.
|
|
10
10
|
* Returns a Float32Array suitable for sqlite-vec storage.
|
package/dist/embeddings.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import 'dotenv/config';
|
|
7
7
|
import { GoogleGenAI } from '@google/genai';
|
|
8
|
-
export const EMBEDDING_DIMENSIONS = 768;
|
|
9
|
-
const EMBEDDING_MODEL = 'gemini-embedding-001';
|
|
8
|
+
export const EMBEDDING_DIMENSIONS = parseInt(process.env.EMBEDDING_DIMENSIONS ?? '768', 10);
|
|
9
|
+
const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL ?? 'gemini-embedding-001';
|
|
10
10
|
function getClient() {
|
|
11
11
|
const key = process.env.GEMINI_API_KEY;
|
|
12
12
|
if (!key)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importance Dynamics
|
|
3
|
+
* Adjust memory importance based on age, access patterns, and relationships
|
|
4
|
+
*/
|
|
5
|
+
export interface ImportanceFactors {
|
|
6
|
+
baseImportance: number;
|
|
7
|
+
ageDecay: number;
|
|
8
|
+
accessBoost: number;
|
|
9
|
+
relationshipBoost: number;
|
|
10
|
+
finalScore: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Calculate age decay factor
|
|
14
|
+
* Recent memories maintain importance, old ones decay unless accessed
|
|
15
|
+
*/
|
|
16
|
+
export declare function calculateAgeDecay(timestamp: string): number;
|
|
17
|
+
/**
|
|
18
|
+
* Calculate dynamic importance score
|
|
19
|
+
* Base importance + access boost - age decay
|
|
20
|
+
*/
|
|
21
|
+
export declare function calculateDynamicImportance(memory_id: number): ImportanceFactors;
|
|
22
|
+
/**
|
|
23
|
+
* Get memories eligible for L1 based on dynamic scoring.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getL1Candidates(limit?: number): Array<{
|
|
26
|
+
id: number;
|
|
27
|
+
type: string;
|
|
28
|
+
content_l1: string;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
last_accessed: string;
|
|
31
|
+
baseImportance: number;
|
|
32
|
+
dynamicScore: number;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Consolidation: Review and adjust importances based on access patterns
|
|
36
|
+
*/
|
|
37
|
+
export declare function runConsolidation(): {
|
|
38
|
+
reviewed: number;
|
|
39
|
+
boosted: number;
|
|
40
|
+
decayed: number;
|
|
41
|
+
unchanged: number;
|
|
42
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importance Dynamics
|
|
3
|
+
* Adjust memory importance based on age, access patterns, and relationships
|
|
4
|
+
*/
|
|
5
|
+
import { getDb, queries } from './index.js';
|
|
6
|
+
import { calculateAccessBoost } from './access.js';
|
|
7
|
+
/**
|
|
8
|
+
* Calculate relationship boost based on graph connectivity
|
|
9
|
+
* Memories that are hubs in the relationship graph are more valuable
|
|
10
|
+
*/
|
|
11
|
+
function calculateRelationshipBoost(memory_id) {
|
|
12
|
+
const db = getDb(true);
|
|
13
|
+
// Count total connections
|
|
14
|
+
const connections = db.prepare(`
|
|
15
|
+
SELECT COUNT(*) as count
|
|
16
|
+
FROM relationship
|
|
17
|
+
WHERE from_id = ? OR to_id = ?
|
|
18
|
+
`).get(memory_id, memory_id);
|
|
19
|
+
// Count connections to high-importance memories (4+)
|
|
20
|
+
const importantConnections = db.prepare(`
|
|
21
|
+
SELECT COUNT(*) as count
|
|
22
|
+
FROM relationship r
|
|
23
|
+
JOIN memory m ON (
|
|
24
|
+
CASE
|
|
25
|
+
WHEN r.from_id = ? THEN m.id = r.to_id
|
|
26
|
+
ELSE m.id = r.from_id
|
|
27
|
+
END
|
|
28
|
+
)
|
|
29
|
+
WHERE (r.from_id = ? OR r.to_id = ?)
|
|
30
|
+
AND m.importance >= 4
|
|
31
|
+
`).get(memory_id, memory_id, memory_id);
|
|
32
|
+
// Boost calculation:
|
|
33
|
+
// - Base: 0.1 per connection (up to +1.0 for 10 connections)
|
|
34
|
+
// - Bonus: +0.2 per important connection
|
|
35
|
+
const baseBoost = Math.min(1.0, connections.count * 0.1);
|
|
36
|
+
const importantBonus = importantConnections.count * 0.2;
|
|
37
|
+
return Math.min(2.0, baseBoost + importantBonus);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Calculate age decay factor
|
|
41
|
+
* Recent memories maintain importance, old ones decay unless accessed
|
|
42
|
+
*/
|
|
43
|
+
export function calculateAgeDecay(timestamp) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const memoryDate = new Date(timestamp).getTime();
|
|
46
|
+
const ageInDays = (now - memoryDate) / (1000 * 60 * 60 * 24);
|
|
47
|
+
// No decay for first 7 days
|
|
48
|
+
if (ageInDays <= 7)
|
|
49
|
+
return 0;
|
|
50
|
+
// Gradual decay: -0.1 per week after first week
|
|
51
|
+
const weeksOld = Math.floor((ageInDays - 7) / 7);
|
|
52
|
+
const decay = Math.min(2, weeksOld * 0.1); // Max -2 importance
|
|
53
|
+
return -decay;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Calculate dynamic importance score
|
|
57
|
+
* Base importance + access boost - age decay
|
|
58
|
+
*/
|
|
59
|
+
export function calculateDynamicImportance(memory_id) {
|
|
60
|
+
const db = getDb(true);
|
|
61
|
+
const memory = db.prepare(`
|
|
62
|
+
SELECT importance, timestamp
|
|
63
|
+
FROM memory
|
|
64
|
+
WHERE id = ?
|
|
65
|
+
`).get(memory_id);
|
|
66
|
+
if (!memory) {
|
|
67
|
+
return {
|
|
68
|
+
baseImportance: 0,
|
|
69
|
+
ageDecay: 0,
|
|
70
|
+
accessBoost: 0,
|
|
71
|
+
relationshipBoost: 0,
|
|
72
|
+
finalScore: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const baseImportance = memory.importance;
|
|
76
|
+
const ageDecay = calculateAgeDecay(memory.timestamp);
|
|
77
|
+
const accessBoost = calculateAccessBoost(memory_id);
|
|
78
|
+
const relationshipBoost = calculateRelationshipBoost(memory_id);
|
|
79
|
+
// Final score: base + boosts + decay (decay is negative)
|
|
80
|
+
const finalScore = Math.max(1, Math.min(5, baseImportance + accessBoost + relationshipBoost + ageDecay));
|
|
81
|
+
return {
|
|
82
|
+
baseImportance,
|
|
83
|
+
ageDecay,
|
|
84
|
+
accessBoost,
|
|
85
|
+
relationshipBoost,
|
|
86
|
+
finalScore: Math.round(finalScore * 10) / 10,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get memories eligible for L1 based on dynamic scoring.
|
|
91
|
+
*/
|
|
92
|
+
export function getL1Candidates(limit = 50) {
|
|
93
|
+
const db = getDb(true);
|
|
94
|
+
const memories = queries.getL1Summaries(db).all();
|
|
95
|
+
return memories.slice(0, limit).map(m => ({
|
|
96
|
+
id: m.id,
|
|
97
|
+
type: m.type,
|
|
98
|
+
content_l1: m.content_l1,
|
|
99
|
+
timestamp: m.timestamp,
|
|
100
|
+
last_accessed: m.last_accessed,
|
|
101
|
+
baseImportance: m.importance,
|
|
102
|
+
dynamicScore: m.effective_importance,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Consolidation: Review and adjust importances based on access patterns
|
|
107
|
+
*/
|
|
108
|
+
export function runConsolidation() {
|
|
109
|
+
const db = getDb(false);
|
|
110
|
+
let reviewed = 0;
|
|
111
|
+
let boosted = 0;
|
|
112
|
+
let decayed = 0;
|
|
113
|
+
let unchanged = 0;
|
|
114
|
+
const memories = db.prepare(`
|
|
115
|
+
SELECT id, importance, timestamp
|
|
116
|
+
FROM memory
|
|
117
|
+
WHERE timestamp > datetime('now', '-60 days')
|
|
118
|
+
`).all();
|
|
119
|
+
for (const memory of memories) {
|
|
120
|
+
reviewed++;
|
|
121
|
+
const factors = calculateDynamicImportance(memory.id);
|
|
122
|
+
const newImportance = Math.round(factors.finalScore);
|
|
123
|
+
if (newImportance !== memory.importance) {
|
|
124
|
+
db.prepare(`
|
|
125
|
+
UPDATE memory
|
|
126
|
+
SET importance = ?
|
|
127
|
+
WHERE id = ?
|
|
128
|
+
`).run(newImportance, memory.id);
|
|
129
|
+
if (newImportance > memory.importance) {
|
|
130
|
+
boosted++;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
decayed++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
unchanged++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { reviewed, boosted, decayed, unchanged };
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postnesia/db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "AI Agent memory context database",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
"./embeddings": {
|
|
26
26
|
"types": "./dist/embeddings.d.ts",
|
|
27
27
|
"import": "./dist/embeddings.js"
|
|
28
|
+
},
|
|
29
|
+
"./access": {
|
|
30
|
+
"types": "./dist/access.d.ts",
|
|
31
|
+
"import": "./dist/access.js"
|
|
32
|
+
},
|
|
33
|
+
"./importance": {
|
|
34
|
+
"types": "./dist/importance.d.ts",
|
|
35
|
+
"import": "./dist/importance.js"
|
|
28
36
|
}
|
|
29
37
|
},
|
|
30
38
|
"dependencies": {
|