@siftd/connect-agent 0.2.10 → 0.2.12
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/core/memory-interface.d.ts +66 -0
- package/dist/core/memory-interface.js +6 -0
- package/dist/core/memory-postgres.d.ts +109 -0
- package/dist/core/memory-postgres.js +374 -0
- package/dist/heartbeat.js +1 -1
- package/dist/orchestrator.d.ts +4 -1
- package/dist/orchestrator.js +48 -11
- package/package.json +3 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Store Interface
|
|
3
|
+
*
|
|
4
|
+
* Common interface for memory backends (SQLite, Postgres, etc.)
|
|
5
|
+
*/
|
|
6
|
+
export type MemoryType = 'episodic' | 'semantic' | 'procedural' | 'working';
|
|
7
|
+
export interface Memory {
|
|
8
|
+
id: string;
|
|
9
|
+
type: MemoryType;
|
|
10
|
+
content: string;
|
|
11
|
+
summary?: string;
|
|
12
|
+
source: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
lastAccessed: string;
|
|
15
|
+
importance: number;
|
|
16
|
+
accessCount: number;
|
|
17
|
+
decayRate: number;
|
|
18
|
+
associations: string[];
|
|
19
|
+
tags: string[];
|
|
20
|
+
hasEmbedding: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface MemorySearchOptions {
|
|
23
|
+
type?: MemoryType;
|
|
24
|
+
limit?: number;
|
|
25
|
+
minImportance?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface MemoryStore {
|
|
28
|
+
/**
|
|
29
|
+
* Store a new memory
|
|
30
|
+
*/
|
|
31
|
+
remember(content: string, options?: {
|
|
32
|
+
type?: MemoryType;
|
|
33
|
+
source?: string;
|
|
34
|
+
importance?: number;
|
|
35
|
+
tags?: string[];
|
|
36
|
+
}): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Search memories using semantic similarity
|
|
39
|
+
*/
|
|
40
|
+
search(query: string, options?: MemorySearchOptions): Promise<Memory[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Get a specific memory by ID
|
|
43
|
+
*/
|
|
44
|
+
get(id: string): Promise<Memory | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Delete a memory
|
|
47
|
+
*/
|
|
48
|
+
forget(id: string): Promise<boolean>;
|
|
49
|
+
/**
|
|
50
|
+
* Boost importance of a memory
|
|
51
|
+
*/
|
|
52
|
+
reinforce(id: string, boost?: number): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Get memory statistics
|
|
55
|
+
*/
|
|
56
|
+
stats(): {
|
|
57
|
+
total: number;
|
|
58
|
+
byType: Record<string, number>;
|
|
59
|
+
avgImportance: number;
|
|
60
|
+
totalAssociations: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Close the store and release resources
|
|
64
|
+
*/
|
|
65
|
+
close(): void | Promise<void>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Memory Store with pgvector
|
|
3
|
+
*
|
|
4
|
+
* Cloud-native memory backend for Railway deployments.
|
|
5
|
+
* Uses pgvector extension for efficient vector similarity search.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - All AdvancedMemoryStore features
|
|
9
|
+
* - Scalable Postgres backend
|
|
10
|
+
* - pgvector for vector similarity
|
|
11
|
+
* - Shared across multiple agent instances
|
|
12
|
+
*/
|
|
13
|
+
export type MemoryType = 'episodic' | 'semantic' | 'procedural' | 'working';
|
|
14
|
+
export interface Memory {
|
|
15
|
+
id: string;
|
|
16
|
+
type: MemoryType;
|
|
17
|
+
content: string;
|
|
18
|
+
summary?: string;
|
|
19
|
+
source: string;
|
|
20
|
+
timestamp: string;
|
|
21
|
+
lastAccessed: string;
|
|
22
|
+
importance: number;
|
|
23
|
+
accessCount: number;
|
|
24
|
+
decayRate: number;
|
|
25
|
+
associations: string[];
|
|
26
|
+
tags: string[];
|
|
27
|
+
hasEmbedding: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface PostgresMemoryConfig {
|
|
30
|
+
connectionString: string;
|
|
31
|
+
userId?: string;
|
|
32
|
+
dimensions?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class PostgresMemoryStore {
|
|
35
|
+
private pool;
|
|
36
|
+
private userId;
|
|
37
|
+
private dimensions;
|
|
38
|
+
private initialized;
|
|
39
|
+
constructor(config: PostgresMemoryConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Initialize database schema
|
|
42
|
+
*/
|
|
43
|
+
initialize(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Store a memory
|
|
46
|
+
*/
|
|
47
|
+
remember(content: string, options?: {
|
|
48
|
+
type?: MemoryType;
|
|
49
|
+
source?: string;
|
|
50
|
+
importance?: number;
|
|
51
|
+
tags?: string[];
|
|
52
|
+
}): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Search memories using vector similarity
|
|
55
|
+
*/
|
|
56
|
+
search(query: string, options?: {
|
|
57
|
+
type?: MemoryType;
|
|
58
|
+
limit?: number;
|
|
59
|
+
minImportance?: number;
|
|
60
|
+
}): Promise<Memory[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Get memory by ID
|
|
63
|
+
*/
|
|
64
|
+
get(id: string): Promise<Memory | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Delete a memory
|
|
67
|
+
*/
|
|
68
|
+
forget(id: string): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Update memory importance
|
|
71
|
+
*/
|
|
72
|
+
reinforce(id: string, boost?: number): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Get memory statistics
|
|
75
|
+
*/
|
|
76
|
+
stats(): {
|
|
77
|
+
total: number;
|
|
78
|
+
byType: Record<string, number>;
|
|
79
|
+
avgImportance: number;
|
|
80
|
+
totalAssociations: number;
|
|
81
|
+
};
|
|
82
|
+
private _cachedStats;
|
|
83
|
+
/**
|
|
84
|
+
* Update stats cache (call periodically)
|
|
85
|
+
*/
|
|
86
|
+
updateStatsCache(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Apply temporal decay to all memories
|
|
89
|
+
*/
|
|
90
|
+
applyDecay(): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Cleanup old low-importance memories
|
|
93
|
+
*/
|
|
94
|
+
cleanup(options?: {
|
|
95
|
+
maxAge?: number;
|
|
96
|
+
minImportance?: number;
|
|
97
|
+
}): Promise<number>;
|
|
98
|
+
/**
|
|
99
|
+
* Close the connection pool
|
|
100
|
+
*/
|
|
101
|
+
close(): Promise<void>;
|
|
102
|
+
private inferType;
|
|
103
|
+
private calculateImportance;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if Postgres is available and configured
|
|
107
|
+
*/
|
|
108
|
+
export declare function isPostgresConfigured(): boolean;
|
|
109
|
+
export {};
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Memory Store with pgvector
|
|
3
|
+
*
|
|
4
|
+
* Cloud-native memory backend for Railway deployments.
|
|
5
|
+
* Uses pgvector extension for efficient vector similarity search.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - All AdvancedMemoryStore features
|
|
9
|
+
* - Scalable Postgres backend
|
|
10
|
+
* - pgvector for vector similarity
|
|
11
|
+
* - Shared across multiple agent instances
|
|
12
|
+
*/
|
|
13
|
+
import pg from 'pg';
|
|
14
|
+
const { Pool } = pg;
|
|
15
|
+
// Simple local embedding using character n-grams (same as SQLite version)
|
|
16
|
+
function computeEmbedding(text, dimensions = 384) {
|
|
17
|
+
const embedding = new Array(dimensions).fill(0);
|
|
18
|
+
const normalized = text.toLowerCase();
|
|
19
|
+
// Character trigrams
|
|
20
|
+
for (let i = 0; i < normalized.length - 2; i++) {
|
|
21
|
+
const trigram = normalized.slice(i, i + 3);
|
|
22
|
+
const hash = hashString(trigram);
|
|
23
|
+
const idx = Math.abs(hash) % dimensions;
|
|
24
|
+
embedding[idx] += 1;
|
|
25
|
+
}
|
|
26
|
+
// Word unigrams
|
|
27
|
+
const words = normalized.split(/\s+/);
|
|
28
|
+
for (const word of words) {
|
|
29
|
+
const hash = hashString(word);
|
|
30
|
+
const idx = Math.abs(hash) % dimensions;
|
|
31
|
+
embedding[idx] += 2;
|
|
32
|
+
}
|
|
33
|
+
// Normalize
|
|
34
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
|
|
35
|
+
if (magnitude > 0) {
|
|
36
|
+
for (let i = 0; i < embedding.length; i++) {
|
|
37
|
+
embedding[i] /= magnitude;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return embedding;
|
|
41
|
+
}
|
|
42
|
+
function hashString(str) {
|
|
43
|
+
let hash = 0;
|
|
44
|
+
for (let i = 0; i < str.length; i++) {
|
|
45
|
+
const char = str.charCodeAt(i);
|
|
46
|
+
hash = ((hash << 5) - hash) + char;
|
|
47
|
+
hash = hash & hash;
|
|
48
|
+
}
|
|
49
|
+
return hash;
|
|
50
|
+
}
|
|
51
|
+
export class PostgresMemoryStore {
|
|
52
|
+
pool;
|
|
53
|
+
userId;
|
|
54
|
+
dimensions;
|
|
55
|
+
initialized = false;
|
|
56
|
+
constructor(config) {
|
|
57
|
+
this.pool = new Pool({
|
|
58
|
+
connectionString: config.connectionString,
|
|
59
|
+
ssl: { rejectUnauthorized: false } // Required for Railway
|
|
60
|
+
});
|
|
61
|
+
this.userId = config.userId || 'default';
|
|
62
|
+
this.dimensions = config.dimensions || 384;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Initialize database schema
|
|
66
|
+
*/
|
|
67
|
+
async initialize() {
|
|
68
|
+
if (this.initialized)
|
|
69
|
+
return;
|
|
70
|
+
const client = await this.pool.connect();
|
|
71
|
+
try {
|
|
72
|
+
// Enable pgvector extension
|
|
73
|
+
await client.query('CREATE EXTENSION IF NOT EXISTS vector');
|
|
74
|
+
// Create memories table
|
|
75
|
+
await client.query(`
|
|
76
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
77
|
+
id TEXT PRIMARY KEY,
|
|
78
|
+
user_id TEXT NOT NULL,
|
|
79
|
+
type TEXT NOT NULL,
|
|
80
|
+
content TEXT NOT NULL,
|
|
81
|
+
summary TEXT,
|
|
82
|
+
source TEXT,
|
|
83
|
+
timestamp TIMESTAMPTZ DEFAULT NOW(),
|
|
84
|
+
last_accessed TIMESTAMPTZ DEFAULT NOW(),
|
|
85
|
+
importance REAL DEFAULT 0.5,
|
|
86
|
+
access_count INTEGER DEFAULT 0,
|
|
87
|
+
decay_rate REAL DEFAULT 0.01,
|
|
88
|
+
associations TEXT[] DEFAULT '{}',
|
|
89
|
+
tags TEXT[] DEFAULT '{}',
|
|
90
|
+
embedding vector(${this.dimensions}),
|
|
91
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
92
|
+
)
|
|
93
|
+
`);
|
|
94
|
+
// Create indexes
|
|
95
|
+
await client.query(`
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_memories_user_id ON memories(user_id)
|
|
97
|
+
`);
|
|
98
|
+
await client.query(`
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type)
|
|
100
|
+
`);
|
|
101
|
+
await client.query(`
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance DESC)
|
|
103
|
+
`);
|
|
104
|
+
// Create vector similarity index (IVFFlat for performance)
|
|
105
|
+
await client.query(`
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_memories_embedding
|
|
107
|
+
ON memories USING ivfflat (embedding vector_cosine_ops)
|
|
108
|
+
WITH (lists = 100)
|
|
109
|
+
`).catch(() => {
|
|
110
|
+
// IVFFlat requires at least 100 rows, fall back to exact search
|
|
111
|
+
console.log('[POSTGRES] IVFFlat index skipped (will use exact search)');
|
|
112
|
+
});
|
|
113
|
+
this.initialized = true;
|
|
114
|
+
console.log('[POSTGRES] Memory store initialized');
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
client.release();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Store a memory
|
|
122
|
+
*/
|
|
123
|
+
async remember(content, options) {
|
|
124
|
+
await this.initialize();
|
|
125
|
+
const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
126
|
+
const type = options?.type || this.inferType(content);
|
|
127
|
+
const importance = options?.importance ?? this.calculateImportance(content);
|
|
128
|
+
const embedding = computeEmbedding(content, this.dimensions);
|
|
129
|
+
await this.pool.query(`
|
|
130
|
+
INSERT INTO memories (id, user_id, type, content, source, importance, tags, embedding)
|
|
131
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
132
|
+
`, [
|
|
133
|
+
id,
|
|
134
|
+
this.userId,
|
|
135
|
+
type,
|
|
136
|
+
content,
|
|
137
|
+
options?.source || 'user',
|
|
138
|
+
importance,
|
|
139
|
+
options?.tags || [],
|
|
140
|
+
`[${embedding.join(',')}]`
|
|
141
|
+
]);
|
|
142
|
+
console.log(`[POSTGRES] Stored memory ${id} (${type}, importance=${importance.toFixed(2)})`);
|
|
143
|
+
return id;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Search memories using vector similarity
|
|
147
|
+
*/
|
|
148
|
+
async search(query, options) {
|
|
149
|
+
await this.initialize();
|
|
150
|
+
const embedding = computeEmbedding(query, this.dimensions);
|
|
151
|
+
const limit = options?.limit || 10;
|
|
152
|
+
const minImportance = options?.minImportance || 0;
|
|
153
|
+
let sql = `
|
|
154
|
+
SELECT
|
|
155
|
+
id, type, content, summary, source, timestamp, last_accessed,
|
|
156
|
+
importance, access_count, decay_rate, associations, tags,
|
|
157
|
+
1 - (embedding <=> $1::vector) as similarity
|
|
158
|
+
FROM memories
|
|
159
|
+
WHERE user_id = $2
|
|
160
|
+
AND importance >= $3
|
|
161
|
+
`;
|
|
162
|
+
const params = [
|
|
163
|
+
`[${embedding.join(',')}]`,
|
|
164
|
+
this.userId,
|
|
165
|
+
minImportance
|
|
166
|
+
];
|
|
167
|
+
if (options?.type) {
|
|
168
|
+
sql += ` AND type = $${params.length + 1}`;
|
|
169
|
+
params.push(options.type);
|
|
170
|
+
}
|
|
171
|
+
sql += ` ORDER BY similarity DESC LIMIT $${params.length + 1}`;
|
|
172
|
+
params.push(limit);
|
|
173
|
+
const result = await this.pool.query(sql, params);
|
|
174
|
+
// Update access counts for retrieved memories
|
|
175
|
+
if (result.rows.length > 0) {
|
|
176
|
+
const ids = result.rows.map(r => r.id);
|
|
177
|
+
await this.pool.query(`
|
|
178
|
+
UPDATE memories
|
|
179
|
+
SET access_count = access_count + 1,
|
|
180
|
+
last_accessed = NOW()
|
|
181
|
+
WHERE id = ANY($1)
|
|
182
|
+
`, [ids]);
|
|
183
|
+
}
|
|
184
|
+
return result.rows.map(row => ({
|
|
185
|
+
id: row.id,
|
|
186
|
+
type: row.type,
|
|
187
|
+
content: row.content,
|
|
188
|
+
summary: row.summary,
|
|
189
|
+
source: row.source,
|
|
190
|
+
timestamp: row.timestamp.toISOString(),
|
|
191
|
+
lastAccessed: row.last_accessed.toISOString(),
|
|
192
|
+
importance: row.importance,
|
|
193
|
+
accessCount: row.access_count,
|
|
194
|
+
decayRate: row.decay_rate,
|
|
195
|
+
associations: row.associations || [],
|
|
196
|
+
tags: row.tags || [],
|
|
197
|
+
hasEmbedding: true
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get memory by ID
|
|
202
|
+
*/
|
|
203
|
+
async get(id) {
|
|
204
|
+
await this.initialize();
|
|
205
|
+
const result = await this.pool.query(`
|
|
206
|
+
SELECT * FROM memories WHERE id = $1 AND user_id = $2
|
|
207
|
+
`, [id, this.userId]);
|
|
208
|
+
if (result.rows.length === 0)
|
|
209
|
+
return null;
|
|
210
|
+
const row = result.rows[0];
|
|
211
|
+
return {
|
|
212
|
+
id: row.id,
|
|
213
|
+
type: row.type,
|
|
214
|
+
content: row.content,
|
|
215
|
+
summary: row.summary,
|
|
216
|
+
source: row.source,
|
|
217
|
+
timestamp: row.timestamp.toISOString(),
|
|
218
|
+
lastAccessed: row.last_accessed.toISOString(),
|
|
219
|
+
importance: row.importance,
|
|
220
|
+
accessCount: row.access_count,
|
|
221
|
+
decayRate: row.decay_rate,
|
|
222
|
+
associations: row.associations || [],
|
|
223
|
+
tags: row.tags || [],
|
|
224
|
+
hasEmbedding: true
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Delete a memory
|
|
229
|
+
*/
|
|
230
|
+
async forget(id) {
|
|
231
|
+
await this.initialize();
|
|
232
|
+
const result = await this.pool.query(`
|
|
233
|
+
DELETE FROM memories WHERE id = $1 AND user_id = $2
|
|
234
|
+
`, [id, this.userId]);
|
|
235
|
+
return (result.rowCount ?? 0) > 0;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Update memory importance
|
|
239
|
+
*/
|
|
240
|
+
async reinforce(id, boost = 0.1) {
|
|
241
|
+
await this.initialize();
|
|
242
|
+
await this.pool.query(`
|
|
243
|
+
UPDATE memories
|
|
244
|
+
SET importance = LEAST(1.0, importance + $1)
|
|
245
|
+
WHERE id = $2 AND user_id = $3
|
|
246
|
+
`, [boost, id, this.userId]);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get memory statistics
|
|
250
|
+
*/
|
|
251
|
+
stats() {
|
|
252
|
+
// This is synchronous in the interface, but we need async for Postgres
|
|
253
|
+
// Return cached stats or default values
|
|
254
|
+
return this._cachedStats || {
|
|
255
|
+
total: 0,
|
|
256
|
+
byType: { episodic: 0, semantic: 0, procedural: 0, working: 0 },
|
|
257
|
+
avgImportance: 0.5,
|
|
258
|
+
totalAssociations: 0
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
_cachedStats = null;
|
|
262
|
+
/**
|
|
263
|
+
* Update stats cache (call periodically)
|
|
264
|
+
*/
|
|
265
|
+
async updateStatsCache() {
|
|
266
|
+
await this.initialize();
|
|
267
|
+
const result = await this.pool.query(`
|
|
268
|
+
SELECT
|
|
269
|
+
COUNT(*) as total,
|
|
270
|
+
AVG(importance) as avg_importance,
|
|
271
|
+
SUM(array_length(associations, 1)) as total_associations
|
|
272
|
+
FROM memories
|
|
273
|
+
WHERE user_id = $1
|
|
274
|
+
`, [this.userId]);
|
|
275
|
+
const typeResult = await this.pool.query(`
|
|
276
|
+
SELECT type, COUNT(*) as count
|
|
277
|
+
FROM memories
|
|
278
|
+
WHERE user_id = $1
|
|
279
|
+
GROUP BY type
|
|
280
|
+
`, [this.userId]);
|
|
281
|
+
const byType = {
|
|
282
|
+
episodic: 0, semantic: 0, procedural: 0, working: 0
|
|
283
|
+
};
|
|
284
|
+
for (const row of typeResult.rows) {
|
|
285
|
+
byType[row.type] = parseInt(row.count);
|
|
286
|
+
}
|
|
287
|
+
this._cachedStats = {
|
|
288
|
+
total: parseInt(result.rows[0].total) || 0,
|
|
289
|
+
byType,
|
|
290
|
+
avgImportance: parseFloat(result.rows[0].avg_importance) || 0.5,
|
|
291
|
+
totalAssociations: parseInt(result.rows[0].total_associations) || 0
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Apply temporal decay to all memories
|
|
296
|
+
*/
|
|
297
|
+
async applyDecay() {
|
|
298
|
+
await this.initialize();
|
|
299
|
+
await this.pool.query(`
|
|
300
|
+
UPDATE memories
|
|
301
|
+
SET importance = GREATEST(0.1, importance * (1 - decay_rate))
|
|
302
|
+
WHERE user_id = $1
|
|
303
|
+
AND importance > 0.1
|
|
304
|
+
AND last_accessed < NOW() - INTERVAL '1 day'
|
|
305
|
+
`, [this.userId]);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Cleanup old low-importance memories
|
|
309
|
+
*/
|
|
310
|
+
async cleanup(options) {
|
|
311
|
+
await this.initialize();
|
|
312
|
+
const maxAgeDays = (options?.maxAge || 30 * 24 * 60 * 60 * 1000) / (1000 * 60 * 60 * 24);
|
|
313
|
+
const minImportance = options?.minImportance || 0.2;
|
|
314
|
+
const result = await this.pool.query(`
|
|
315
|
+
DELETE FROM memories
|
|
316
|
+
WHERE user_id = $1
|
|
317
|
+
AND importance < $2
|
|
318
|
+
AND last_accessed < NOW() - INTERVAL '1 day' * $3
|
|
319
|
+
`, [this.userId, minImportance, maxAgeDays]);
|
|
320
|
+
const count = result.rowCount ?? 0;
|
|
321
|
+
if (count > 0) {
|
|
322
|
+
console.log(`[POSTGRES] Cleaned up ${count} old memories`);
|
|
323
|
+
}
|
|
324
|
+
return count;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Close the connection pool
|
|
328
|
+
*/
|
|
329
|
+
async close() {
|
|
330
|
+
await this.pool.end();
|
|
331
|
+
console.log('[POSTGRES] Connection pool closed');
|
|
332
|
+
}
|
|
333
|
+
// Helper methods
|
|
334
|
+
inferType(content) {
|
|
335
|
+
const lower = content.toLowerCase();
|
|
336
|
+
// Procedural indicators
|
|
337
|
+
if (lower.includes('how to') || lower.includes('steps to') ||
|
|
338
|
+
lower.includes('process for') || lower.includes('when doing')) {
|
|
339
|
+
return 'procedural';
|
|
340
|
+
}
|
|
341
|
+
// Episodic indicators (past tense, specific events)
|
|
342
|
+
if (lower.includes('yesterday') || lower.includes('last week') ||
|
|
343
|
+
lower.includes('we did') || lower.includes('happened')) {
|
|
344
|
+
return 'episodic';
|
|
345
|
+
}
|
|
346
|
+
// Default to semantic (facts)
|
|
347
|
+
return 'semantic';
|
|
348
|
+
}
|
|
349
|
+
calculateImportance(content) {
|
|
350
|
+
let importance = 0.5;
|
|
351
|
+
// Boost for longer, detailed content
|
|
352
|
+
if (content.length > 200)
|
|
353
|
+
importance += 0.1;
|
|
354
|
+
if (content.length > 500)
|
|
355
|
+
importance += 0.1;
|
|
356
|
+
// Boost for structured content
|
|
357
|
+
if (content.includes(':') || content.includes('-') || content.includes('•')) {
|
|
358
|
+
importance += 0.1;
|
|
359
|
+
}
|
|
360
|
+
// Boost for preference indicators
|
|
361
|
+
const lower = content.toLowerCase();
|
|
362
|
+
if (lower.includes('prefer') || lower.includes('always') ||
|
|
363
|
+
lower.includes('never') || lower.includes('important')) {
|
|
364
|
+
importance += 0.15;
|
|
365
|
+
}
|
|
366
|
+
return Math.min(1.0, importance);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Check if Postgres is available and configured
|
|
371
|
+
*/
|
|
372
|
+
export function isPostgresConfigured() {
|
|
373
|
+
return !!process.env.DATABASE_URL;
|
|
374
|
+
}
|
package/dist/heartbeat.js
CHANGED
|
@@ -10,7 +10,7 @@ import { hostname } from 'os';
|
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
11
|
import { getServerUrl, getAgentToken, getUserId, isCloudMode } from './config.js';
|
|
12
12
|
const HEARTBEAT_INTERVAL = 10000; // 10 seconds
|
|
13
|
-
const VERSION = '0.2.
|
|
13
|
+
const VERSION = '0.2.12'; // Should match package.json
|
|
14
14
|
const state = {
|
|
15
15
|
intervalId: null,
|
|
16
16
|
runnerId: null,
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class MasterOrchestrator {
|
|
|
11
11
|
private model;
|
|
12
12
|
private maxTokens;
|
|
13
13
|
private memory;
|
|
14
|
+
private postgresMemory;
|
|
14
15
|
private scheduler;
|
|
15
16
|
private indexer;
|
|
16
17
|
private jobs;
|
|
@@ -19,6 +20,8 @@ export declare class MasterOrchestrator {
|
|
|
19
20
|
private workspaceDir;
|
|
20
21
|
private claudePath;
|
|
21
22
|
private initialized;
|
|
23
|
+
private usingPostgres;
|
|
24
|
+
private voyageApiKey?;
|
|
22
25
|
private bashTool;
|
|
23
26
|
private webTools;
|
|
24
27
|
private workerTools;
|
|
@@ -149,5 +152,5 @@ export declare class MasterOrchestrator {
|
|
|
149
152
|
/**
|
|
150
153
|
* Shutdown cleanly
|
|
151
154
|
*/
|
|
152
|
-
shutdown(): void
|
|
155
|
+
shutdown(): Promise<void>;
|
|
153
156
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -8,6 +8,7 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
8
8
|
import { spawn, execSync } from 'child_process';
|
|
9
9
|
import { existsSync } from 'fs';
|
|
10
10
|
import { AdvancedMemoryStore } from './core/memory-advanced.js';
|
|
11
|
+
import { PostgresMemoryStore, isPostgresConfigured } from './core/memory-postgres.js';
|
|
11
12
|
import { TaskScheduler } from './core/scheduler.js';
|
|
12
13
|
import { SystemIndexer } from './core/system-indexer.js';
|
|
13
14
|
import { FileWatcher } from './core/file-watcher.js';
|
|
@@ -75,6 +76,7 @@ export class MasterOrchestrator {
|
|
|
75
76
|
model;
|
|
76
77
|
maxTokens;
|
|
77
78
|
memory;
|
|
79
|
+
postgresMemory = null;
|
|
78
80
|
scheduler;
|
|
79
81
|
indexer;
|
|
80
82
|
jobs = new Map();
|
|
@@ -83,6 +85,8 @@ export class MasterOrchestrator {
|
|
|
83
85
|
workspaceDir;
|
|
84
86
|
claudePath;
|
|
85
87
|
initialized = false;
|
|
88
|
+
usingPostgres = false;
|
|
89
|
+
voyageApiKey;
|
|
86
90
|
// New tools from whatsapp-claude
|
|
87
91
|
bashTool;
|
|
88
92
|
webTools;
|
|
@@ -96,17 +100,19 @@ export class MasterOrchestrator {
|
|
|
96
100
|
this.maxTokens = options.maxTokens || 4096;
|
|
97
101
|
this.userId = options.userId;
|
|
98
102
|
this.workspaceDir = options.workspaceDir || process.env.HOME || '/tmp';
|
|
103
|
+
this.voyageApiKey = options.voyageApiKey;
|
|
99
104
|
// Find claude binary - critical for worker delegation
|
|
100
105
|
this.claudePath = this.findClaudeBinary();
|
|
101
106
|
console.log(`[ORCHESTRATOR] Claude binary: ${this.claudePath}`);
|
|
102
|
-
//
|
|
107
|
+
// Memory initialization is deferred to initialize() for async Postgres support
|
|
108
|
+
// For now, create SQLite as fallback (will be replaced if Postgres is configured)
|
|
103
109
|
this.memory = new AdvancedMemoryStore({
|
|
104
110
|
dbPath: `memories_${options.userId}.db`,
|
|
105
111
|
vectorPath: `./memory_vectors_${options.userId}`,
|
|
106
112
|
voyageApiKey: options.voyageApiKey
|
|
107
113
|
});
|
|
108
114
|
this.scheduler = new TaskScheduler(`scheduled_${options.userId}.json`);
|
|
109
|
-
// Create indexer (will
|
|
115
|
+
// Create indexer (will be re-initialized after memory setup)
|
|
110
116
|
this.indexer = new SystemIndexer(this.memory);
|
|
111
117
|
// Initialize new tools
|
|
112
118
|
this.bashTool = new BashTool(this.workspaceDir);
|
|
@@ -129,14 +135,39 @@ export class MasterOrchestrator {
|
|
|
129
135
|
if (this.initialized)
|
|
130
136
|
return;
|
|
131
137
|
console.log('[ORCHESTRATOR] Initializing...');
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
// Check for Postgres and switch memory backend if configured
|
|
139
|
+
if (isPostgresConfigured()) {
|
|
140
|
+
try {
|
|
141
|
+
console.log('[ORCHESTRATOR] DATABASE_URL detected, using PostgreSQL + pgvector');
|
|
142
|
+
this.postgresMemory = new PostgresMemoryStore({
|
|
143
|
+
connectionString: process.env.DATABASE_URL,
|
|
144
|
+
userId: this.userId
|
|
145
|
+
});
|
|
146
|
+
await this.postgresMemory.initialize();
|
|
147
|
+
this.memory = this.postgresMemory;
|
|
148
|
+
this.usingPostgres = true;
|
|
149
|
+
// Re-create indexer with Postgres memory
|
|
150
|
+
// Note: SystemIndexer expects AdvancedMemoryStore, so we skip indexing for Postgres
|
|
151
|
+
console.log('[ORCHESTRATOR] Memory backend: PostgreSQL + pgvector');
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error('[ORCHESTRATOR] PostgreSQL initialization failed, falling back to SQLite:', error);
|
|
155
|
+
this.usingPostgres = false;
|
|
156
|
+
}
|
|
136
157
|
}
|
|
137
|
-
|
|
138
|
-
console.
|
|
139
|
-
|
|
158
|
+
else {
|
|
159
|
+
console.log('[ORCHESTRATOR] Memory backend: SQLite + vectra (local)');
|
|
160
|
+
}
|
|
161
|
+
// Index the filesystem into memory (only for SQLite - Postgres typically runs in cloud)
|
|
162
|
+
if (!this.usingPostgres) {
|
|
163
|
+
try {
|
|
164
|
+
const result = await this.indexer.indexHome();
|
|
165
|
+
console.log(`[ORCHESTRATOR] Filesystem indexed: ${result.projects} projects, ${result.directories} directories`);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error('[ORCHESTRATOR] Filesystem indexing failed:', error);
|
|
169
|
+
// Continue anyway - indexing is a nice-to-have
|
|
170
|
+
}
|
|
140
171
|
}
|
|
141
172
|
this.initialized = true;
|
|
142
173
|
}
|
|
@@ -1337,9 +1368,15 @@ This enables parallel workers to coordinate.`;
|
|
|
1337
1368
|
/**
|
|
1338
1369
|
* Shutdown cleanly
|
|
1339
1370
|
*/
|
|
1340
|
-
shutdown() {
|
|
1371
|
+
async shutdown() {
|
|
1341
1372
|
this.fileWatcher.stopAll();
|
|
1342
1373
|
this.scheduler.shutdown();
|
|
1343
|
-
|
|
1374
|
+
// Close memory store (Postgres is async, SQLite is sync)
|
|
1375
|
+
if (this.usingPostgres && this.postgresMemory) {
|
|
1376
|
+
await this.postgresMemory.close();
|
|
1377
|
+
}
|
|
1378
|
+
else if ('close' in this.memory) {
|
|
1379
|
+
this.memory.close();
|
|
1380
|
+
}
|
|
1344
1381
|
}
|
|
1345
1382
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siftd/connect-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Master orchestrator agent - control Claude Code remotely via web",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"conf": "^13.0.1",
|
|
44
44
|
"node-cron": "^3.0.3",
|
|
45
45
|
"ora": "^8.1.1",
|
|
46
|
+
"pg": "^8.13.1",
|
|
46
47
|
"vectra": "^0.9.0",
|
|
47
48
|
"ws": "^8.18.3"
|
|
48
49
|
},
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"@types/better-sqlite3": "^7.6.12",
|
|
51
52
|
"@types/node": "^22.10.2",
|
|
52
53
|
"@types/node-cron": "^3.0.11",
|
|
54
|
+
"@types/pg": "^8.11.10",
|
|
53
55
|
"@types/ws": "^8.18.1",
|
|
54
56
|
"typescript": "^5.7.2"
|
|
55
57
|
}
|