@sidurijs/core 1.0.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/action-executor.d.ts +12 -0
- package/dist/action-executor.js +54 -0
- package/dist/action-policy.d.ts +79 -0
- package/dist/action-policy.js +439 -0
- package/dist/action-policy.test.d.ts +1 -0
- package/dist/action-policy.test.js +363 -0
- package/dist/action.d.ts +72 -0
- package/dist/action.js +2 -0
- package/dist/adversarial.test.d.ts +1 -0
- package/dist/adversarial.test.js +491 -0
- package/dist/architecture-boundary.test.d.ts +1 -0
- package/dist/architecture-boundary.test.js +108 -0
- package/dist/capability.d.ts +78 -0
- package/dist/capability.js +168 -0
- package/dist/capability.test.d.ts +1 -0
- package/dist/capability.test.js +311 -0
- package/dist/chat-contract.d.ts +94 -0
- package/dist/chat-contract.js +104 -0
- package/dist/cognition-planner.d.ts +15 -0
- package/dist/cognition-planner.js +23 -0
- package/dist/container.d.ts +74 -0
- package/dist/container.js +81 -0
- package/dist/context-retriever.d.ts +33 -0
- package/dist/context-retriever.js +260 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +85 -0
- package/dist/context.test.d.ts +1 -0
- package/dist/context.test.js +76 -0
- package/dist/conversational-teach.test.d.ts +1 -0
- package/dist/conversational-teach.test.js +941 -0
- package/dist/database.d.ts +1 -0
- package/dist/database.js +17 -0
- package/dist/dispatcher.d.ts +14 -0
- package/dist/dispatcher.js +40 -0
- package/dist/dispatcher.test.d.ts +1 -0
- package/dist/dispatcher.test.js +58 -0
- package/dist/ear-types.d.ts +33 -0
- package/dist/ear-types.js +2 -0
- package/dist/evidence.d.ts +76 -0
- package/dist/evidence.js +27 -0
- package/dist/evidence.test.d.ts +1 -0
- package/dist/evidence.test.js +75 -0
- package/dist/experience-emitter.d.ts +21 -0
- package/dist/experience-emitter.js +47 -0
- package/dist/experience.d.ts +55 -0
- package/dist/experience.js +74 -0
- package/dist/experience.test.d.ts +1 -0
- package/dist/experience.test.js +55 -0
- package/dist/gating.d.ts +45 -0
- package/dist/gating.js +185 -0
- package/dist/gating.test.d.ts +1 -0
- package/dist/gating.test.js +186 -0
- package/dist/index.d.ts +367 -0
- package/dist/index.js +56 -0
- package/dist/input-normalizer.d.ts +14 -0
- package/dist/input-normalizer.js +76 -0
- package/dist/input-normalizer.test.d.ts +1 -0
- package/dist/input-normalizer.test.js +48 -0
- package/dist/intent-classifier.d.ts +37 -0
- package/dist/intent-classifier.js +150 -0
- package/dist/intent-classifier.test.d.ts +1 -0
- package/dist/intent-classifier.test.js +118 -0
- package/dist/memory-settler.d.ts +49 -0
- package/dist/memory-settler.js +161 -0
- package/dist/mouth-types.d.ts +90 -0
- package/dist/mouth-types.js +2 -0
- package/dist/perception-cycle.test.d.ts +1 -0
- package/dist/perception-cycle.test.js +301 -0
- package/dist/perception-pipeline.d.ts +77 -0
- package/dist/perception-pipeline.js +304 -0
- package/dist/perception-pipeline.test.d.ts +1 -0
- package/dist/perception-pipeline.test.js +65 -0
- package/dist/prompt-compiler.d.ts +26 -0
- package/dist/prompt-compiler.js +81 -0
- package/dist/prompt-compiler.test.d.ts +1 -0
- package/dist/prompt-compiler.test.js +90 -0
- package/dist/proposals.d.ts +33 -0
- package/dist/proposals.js +2 -0
- package/dist/response-envelope.d.ts +30 -0
- package/dist/response-envelope.js +90 -0
- package/dist/runtime-facades.test.d.ts +1 -0
- package/dist/runtime-facades.test.js +95 -0
- package/dist/runtime.d.ts +116 -0
- package/dist/runtime.js +778 -0
- package/dist/schema-validator.d.ts +9 -0
- package/dist/schema-validator.js +107 -0
- package/dist/schema-validator.test.d.ts +1 -0
- package/dist/schema-validator.test.js +124 -0
- package/dist/session-history.d.ts +20 -0
- package/dist/session-history.js +55 -0
- package/dist/session-history.test.d.ts +1 -0
- package/dist/session-history.test.js +38 -0
- package/dist/siduri-db.d.ts +235 -0
- package/dist/siduri-db.js +1405 -0
- package/dist/siduri-db.test.d.ts +1 -0
- package/dist/siduri-db.test.js +1021 -0
- package/dist/sqlite-action-store.d.ts +21 -0
- package/dist/sqlite-action-store.js +281 -0
- package/dist/sqlite-action-store.test.d.ts +1 -0
- package/dist/sqlite-action-store.test.js +261 -0
- package/dist/teaching.d.ts +14 -0
- package/dist/teaching.js +14 -0
- package/package.json +43 -0
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SiduriDatabase = void 0;
|
|
4
|
+
exports.safeJsonParse = safeJsonParse;
|
|
5
|
+
exports.normalizeStatus = normalizeStatus;
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
9
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
10
|
+
function safeJsonParse(str, fallback = undefined) {
|
|
11
|
+
if (!str)
|
|
12
|
+
return fallback;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(str);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function normalizeStatus(status, defaultStatus = 'pending') {
|
|
21
|
+
if (!status)
|
|
22
|
+
return defaultStatus;
|
|
23
|
+
return status.toLowerCase().replace(/_/g, '-');
|
|
24
|
+
}
|
|
25
|
+
class SiduriDatabase {
|
|
26
|
+
db;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
const dbPath = options.dbPath || ':memory:';
|
|
29
|
+
this.db = new DatabaseSync(dbPath);
|
|
30
|
+
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
31
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
32
|
+
this.initSchema();
|
|
33
|
+
}
|
|
34
|
+
initSchema() {
|
|
35
|
+
const schema = `
|
|
36
|
+
-- Self Tables
|
|
37
|
+
CREATE TABLE IF NOT EXISTS self_identity (
|
|
38
|
+
companion_id TEXT PRIMARY KEY,
|
|
39
|
+
name TEXT NOT NULL,
|
|
40
|
+
archetype TEXT,
|
|
41
|
+
role TEXT,
|
|
42
|
+
origin TEXT,
|
|
43
|
+
ethos TEXT,
|
|
44
|
+
version TEXT NOT NULL,
|
|
45
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS self_personality (
|
|
49
|
+
companion_id TEXT PRIMARY KEY,
|
|
50
|
+
warmth REAL DEFAULT 0.5,
|
|
51
|
+
formality REAL DEFAULT 0.5,
|
|
52
|
+
sarcasm REAL DEFAULT 0.5,
|
|
53
|
+
verbosity REAL DEFAULT 0.5,
|
|
54
|
+
curiosity REAL DEFAULT 0.5,
|
|
55
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS self_directives (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
companion_id TEXT NOT NULL,
|
|
61
|
+
priority INTEGER DEFAULT 50,
|
|
62
|
+
directive TEXT NOT NULL,
|
|
63
|
+
status TEXT DEFAULT 'active',
|
|
64
|
+
category TEXT DEFAULT 'behavioral',
|
|
65
|
+
scope_actor TEXT,
|
|
66
|
+
supersedes_id TEXT,
|
|
67
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS self_relationships (
|
|
71
|
+
companion_id TEXT NOT NULL,
|
|
72
|
+
entity_id TEXT NOT NULL,
|
|
73
|
+
entity_type TEXT NOT NULL DEFAULT 'human',
|
|
74
|
+
name TEXT,
|
|
75
|
+
affiliation TEXT,
|
|
76
|
+
role TEXT DEFAULT 'user',
|
|
77
|
+
stance TEXT DEFAULT 'neutral',
|
|
78
|
+
trust_score REAL DEFAULT 0.5,
|
|
79
|
+
familiarity REAL DEFAULT 0.5,
|
|
80
|
+
interaction_conventions TEXT,
|
|
81
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
82
|
+
PRIMARY KEY(companion_id, entity_id)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE IF NOT EXISTS self_exemplars (
|
|
86
|
+
id TEXT PRIMARY KEY,
|
|
87
|
+
companion_id TEXT NOT NULL,
|
|
88
|
+
user_prompt TEXT NOT NULL,
|
|
89
|
+
companion_response TEXT NOT NULL,
|
|
90
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
-- Knowledge Tables
|
|
94
|
+
CREATE TABLE IF NOT EXISTS life_inventory (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
companion_id TEXT NOT NULL,
|
|
97
|
+
domain TEXT NOT NULL,
|
|
98
|
+
entity_name TEXT NOT NULL,
|
|
99
|
+
properties TEXT NOT NULL,
|
|
100
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
CREATE TABLE IF NOT EXISTS life_finance (
|
|
104
|
+
id TEXT PRIMARY KEY,
|
|
105
|
+
companion_id TEXT NOT NULL,
|
|
106
|
+
category TEXT NOT NULL,
|
|
107
|
+
amount REAL NOT NULL,
|
|
108
|
+
currency TEXT DEFAULT 'USD',
|
|
109
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
110
|
+
metadata TEXT
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS life_schedule (
|
|
114
|
+
id TEXT PRIMARY KEY,
|
|
115
|
+
companion_id TEXT NOT NULL,
|
|
116
|
+
title TEXT NOT NULL,
|
|
117
|
+
start_time TEXT NOT NULL,
|
|
118
|
+
end_time TEXT,
|
|
119
|
+
is_recurring INTEGER DEFAULT 0,
|
|
120
|
+
status TEXT DEFAULT 'active'
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE TABLE IF NOT EXISTS life_preferences (
|
|
124
|
+
id TEXT PRIMARY KEY,
|
|
125
|
+
companion_id TEXT NOT NULL,
|
|
126
|
+
preference_key TEXT NOT NULL,
|
|
127
|
+
preference_value TEXT NOT NULL,
|
|
128
|
+
category TEXT NOT NULL,
|
|
129
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
CREATE TABLE IF NOT EXISTS life_entities (
|
|
133
|
+
id TEXT PRIMARY KEY,
|
|
134
|
+
companion_id TEXT NOT NULL,
|
|
135
|
+
entity_type TEXT NOT NULL,
|
|
136
|
+
domain TEXT NOT NULL,
|
|
137
|
+
name TEXT NOT NULL,
|
|
138
|
+
properties TEXT NOT NULL,
|
|
139
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
CREATE TABLE IF NOT EXISTS life_events (
|
|
143
|
+
id TEXT PRIMARY KEY,
|
|
144
|
+
companion_id TEXT NOT NULL,
|
|
145
|
+
stream TEXT NOT NULL,
|
|
146
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
147
|
+
metric_value REAL,
|
|
148
|
+
metadata TEXT
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
CREATE TABLE IF NOT EXISTS life_tasks (
|
|
152
|
+
id TEXT PRIMARY KEY,
|
|
153
|
+
companion_id TEXT NOT NULL,
|
|
154
|
+
title TEXT NOT NULL,
|
|
155
|
+
status TEXT DEFAULT 'backlog',
|
|
156
|
+
priority INTEGER DEFAULT 0,
|
|
157
|
+
target_date TEXT,
|
|
158
|
+
metadata TEXT,
|
|
159
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_life_entities_comp ON life_entities(companion_id, entity_type);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_life_events_comp ON life_events(companion_id, stream, timestamp);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_life_tasks_comp ON life_tasks(companion_id, status);
|
|
165
|
+
|
|
166
|
+
-- Memory Tables
|
|
167
|
+
CREATE TABLE IF NOT EXISTS memory_events (
|
|
168
|
+
id TEXT PRIMARY KEY,
|
|
169
|
+
companion_id TEXT NOT NULL,
|
|
170
|
+
source_type TEXT NOT NULL,
|
|
171
|
+
occurred_at TEXT DEFAULT (datetime('now')),
|
|
172
|
+
payload TEXT NOT NULL
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS memory_claims (
|
|
176
|
+
id TEXT PRIMARY KEY,
|
|
177
|
+
companion_id TEXT NOT NULL,
|
|
178
|
+
subject TEXT NOT NULL,
|
|
179
|
+
predicate TEXT NOT NULL,
|
|
180
|
+
value TEXT NOT NULL,
|
|
181
|
+
status TEXT DEFAULT 'pending',
|
|
182
|
+
confidence REAL DEFAULT 1.0,
|
|
183
|
+
valid_from TEXT,
|
|
184
|
+
valid_until TEXT,
|
|
185
|
+
evidence TEXT,
|
|
186
|
+
asserted_at TEXT DEFAULT (datetime('now')),
|
|
187
|
+
supersedes TEXT,
|
|
188
|
+
source_event_id TEXT
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
-- FTS5 Virtual Table for Memory Claims
|
|
192
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_search USING fts5(
|
|
193
|
+
subject,
|
|
194
|
+
predicate,
|
|
195
|
+
value,
|
|
196
|
+
content='memory_claims',
|
|
197
|
+
content_rowid='rowid'
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
-- FTS5 Triggers
|
|
201
|
+
CREATE TRIGGER IF NOT EXISTS memory_claims_ai AFTER INSERT ON memory_claims BEGIN
|
|
202
|
+
INSERT INTO memory_search(rowid, subject, predicate, value)
|
|
203
|
+
VALUES (new.rowid, new.subject, new.predicate, new.value);
|
|
204
|
+
END;
|
|
205
|
+
|
|
206
|
+
CREATE TRIGGER IF NOT EXISTS memory_claims_ad AFTER DELETE ON memory_claims BEGIN
|
|
207
|
+
INSERT INTO memory_search(memory_search, rowid, subject, predicate, value)
|
|
208
|
+
VALUES('delete', old.rowid, old.subject, old.predicate, old.value);
|
|
209
|
+
END;
|
|
210
|
+
|
|
211
|
+
CREATE TRIGGER IF NOT EXISTS memory_claims_au AFTER UPDATE ON memory_claims BEGIN
|
|
212
|
+
INSERT INTO memory_search(memory_search, rowid, subject, predicate, value)
|
|
213
|
+
VALUES('delete', old.rowid, old.subject, old.predicate, old.value);
|
|
214
|
+
INSERT INTO memory_search(rowid, subject, predicate, value)
|
|
215
|
+
VALUES (new.rowid, new.subject, new.predicate, new.value);
|
|
216
|
+
END;
|
|
217
|
+
|
|
218
|
+
-- System Logs Table
|
|
219
|
+
CREATE TABLE IF NOT EXISTS system_logs (
|
|
220
|
+
id TEXT PRIMARY KEY,
|
|
221
|
+
companion_id TEXT NOT NULL,
|
|
222
|
+
level TEXT NOT NULL,
|
|
223
|
+
subsystem TEXT NOT NULL,
|
|
224
|
+
message TEXT NOT NULL,
|
|
225
|
+
metadata TEXT,
|
|
226
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
CREATE INDEX IF NOT EXISTS idx_system_logs_comp ON system_logs(companion_id, level, created_at DESC);
|
|
230
|
+
`;
|
|
231
|
+
this.db.exec(schema);
|
|
232
|
+
try {
|
|
233
|
+
this.db.exec("ALTER TABLE memory_claims ADD COLUMN supersedes TEXT");
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Column already exists
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
this.db.exec("ALTER TABLE memory_claims ADD COLUMN source_event_id TEXT");
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Column already exists
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
this.db.exec("ALTER TABLE self_identity ADD COLUMN origin TEXT");
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Column already exists
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
this.db.exec("ALTER TABLE self_identity ADD COLUMN ethos TEXT");
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Column already exists
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
this.db.exec("ALTER TABLE self_directives ADD COLUMN scope_actor TEXT");
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Column already exists
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
this.db.exec("ALTER TABLE self_relationships ADD COLUMN role TEXT DEFAULT 'user'");
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Column already exists
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
this.db.exec("ALTER TABLE self_relationships ADD COLUMN stance TEXT DEFAULT 'neutral'");
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// Column already exists
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
this.db.exec("ALTER TABLE self_relationships ADD COLUMN updated_at TEXT");
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Column already exists
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
this.db.exec("ALTER TABLE self_identity ADD COLUMN role TEXT");
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Column already exists
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
this.db.exec("ALTER TABLE self_relationships ADD COLUMN name TEXT");
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Column already exists
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
this.db.exec("ALTER TABLE self_relationships ADD COLUMN affiliation TEXT");
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Column already exists
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
// Reconcile creator claims that may have been recorded before origin dual-promotion
|
|
300
|
+
const creatorClaims = this.db.prepare(`
|
|
301
|
+
SELECT * FROM memory_claims
|
|
302
|
+
WHERE predicate = 'stated_relationship'
|
|
303
|
+
AND LOWER(value) LIKE '%creator%'
|
|
304
|
+
AND LOWER(status) = 'approved'
|
|
305
|
+
`).all();
|
|
306
|
+
for (const claim of creatorClaims) {
|
|
307
|
+
const identity = this.db.prepare(`SELECT * FROM self_identity WHERE companion_id = ?`).get(claim.companion_id);
|
|
308
|
+
if (identity && !identity.origin) {
|
|
309
|
+
const nameClaim = this.db.prepare(`
|
|
310
|
+
SELECT value FROM memory_claims
|
|
311
|
+
WHERE companion_id = ? AND subject = ? AND predicate = 'name' AND LOWER(status) = 'approved'
|
|
312
|
+
ORDER BY asserted_at DESC LIMIT 1
|
|
313
|
+
`).get(claim.companion_id, claim.subject);
|
|
314
|
+
const originName = nameClaim?.value || (claim.subject.startsWith('actor:') ? claim.subject.slice(6) : claim.subject);
|
|
315
|
+
this.db.prepare(`UPDATE self_identity SET origin = ? WHERE companion_id = ?`).run(originName, claim.companion_id);
|
|
316
|
+
}
|
|
317
|
+
const rel = this.db.prepare(`SELECT * FROM self_relationships WHERE companion_id = ? AND entity_id = ?`).get(claim.companion_id, claim.subject);
|
|
318
|
+
if (rel && (rel.role !== 'creator' || rel.stance !== 'familiar_loyal')) {
|
|
319
|
+
this.db.prepare(`UPDATE self_relationships SET role = 'creator', stance = 'familiar_loyal', trust_score = 1.0 WHERE companion_id = ? AND entity_id = ?`).run(claim.companion_id, claim.subject);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Best-effort auto-reconciliation
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
close() {
|
|
328
|
+
this.db.close();
|
|
329
|
+
}
|
|
330
|
+
// ==========================================
|
|
331
|
+
// Self Domain Methods
|
|
332
|
+
// ==========================================
|
|
333
|
+
getIdentity(companionId) {
|
|
334
|
+
const stmt = this.db.prepare('SELECT * FROM self_identity WHERE companion_id = ?');
|
|
335
|
+
const row = stmt.get(companionId);
|
|
336
|
+
if (!row)
|
|
337
|
+
return undefined;
|
|
338
|
+
return {
|
|
339
|
+
companionId: row.companion_id,
|
|
340
|
+
name: row.name,
|
|
341
|
+
archetype: row.archetype || undefined,
|
|
342
|
+
role: row.role || row.archetype || undefined,
|
|
343
|
+
origin: row.origin || undefined,
|
|
344
|
+
ethos: row.ethos || undefined,
|
|
345
|
+
version: row.version,
|
|
346
|
+
updatedAt: row.updated_at
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
setIdentity(identity) {
|
|
350
|
+
const role = identity.role || identity.archetype || null;
|
|
351
|
+
const archetype = identity.archetype || identity.role || null;
|
|
352
|
+
const stmt = this.db.prepare(`
|
|
353
|
+
INSERT INTO self_identity (companion_id, name, archetype, role, origin, ethos, version, updated_at)
|
|
354
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
355
|
+
ON CONFLICT(companion_id) DO UPDATE SET
|
|
356
|
+
name = excluded.name,
|
|
357
|
+
archetype = excluded.archetype,
|
|
358
|
+
role = excluded.role,
|
|
359
|
+
origin = excluded.origin,
|
|
360
|
+
ethos = excluded.ethos,
|
|
361
|
+
version = excluded.version,
|
|
362
|
+
updated_at = datetime('now')
|
|
363
|
+
`);
|
|
364
|
+
stmt.run(identity.companionId, identity.name, archetype, role, identity.origin || null, identity.ethos || null, identity.version);
|
|
365
|
+
}
|
|
366
|
+
getPersonality(companionId) {
|
|
367
|
+
const stmt = this.db.prepare('SELECT * FROM self_personality WHERE companion_id = ?');
|
|
368
|
+
const row = stmt.get(companionId);
|
|
369
|
+
if (!row)
|
|
370
|
+
return undefined;
|
|
371
|
+
return {
|
|
372
|
+
warmth: row.warmth,
|
|
373
|
+
formality: row.formality,
|
|
374
|
+
sarcasm: row.sarcasm,
|
|
375
|
+
verbosity: row.verbosity,
|
|
376
|
+
curiosity: row.curiosity
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
setPersonality(companionId, traits) {
|
|
380
|
+
const stmt = this.db.prepare(`
|
|
381
|
+
INSERT INTO self_personality (companion_id, warmth, formality, sarcasm, verbosity, curiosity, updated_at)
|
|
382
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
383
|
+
ON CONFLICT(companion_id) DO UPDATE SET
|
|
384
|
+
warmth = excluded.warmth,
|
|
385
|
+
formality = excluded.formality,
|
|
386
|
+
sarcasm = excluded.sarcasm,
|
|
387
|
+
verbosity = excluded.verbosity,
|
|
388
|
+
curiosity = excluded.curiosity,
|
|
389
|
+
updated_at = datetime('now')
|
|
390
|
+
`);
|
|
391
|
+
stmt.run(companionId, traits.warmth, traits.formality, traits.sarcasm, traits.verbosity, traits.curiosity);
|
|
392
|
+
}
|
|
393
|
+
rowToSelfDirective(row) {
|
|
394
|
+
return {
|
|
395
|
+
id: row.id,
|
|
396
|
+
companionId: row.companion_id,
|
|
397
|
+
priority: row.priority,
|
|
398
|
+
directive: row.directive,
|
|
399
|
+
status: normalizeStatus(row.status, 'active'),
|
|
400
|
+
category: row.category,
|
|
401
|
+
scopeActor: row.scope_actor || undefined,
|
|
402
|
+
supersedesId: row.supersedes_id || undefined,
|
|
403
|
+
createdAt: row.created_at,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
getActiveDirectives(companionId) {
|
|
407
|
+
const stmt = this.db.prepare(`
|
|
408
|
+
SELECT * FROM self_directives
|
|
409
|
+
WHERE companion_id = ? AND LOWER(status) = 'active'
|
|
410
|
+
ORDER BY priority DESC, created_at ASC
|
|
411
|
+
`);
|
|
412
|
+
return stmt.all(companionId).map((row) => this.rowToSelfDirective(row));
|
|
413
|
+
}
|
|
414
|
+
getAllDirectives(companionId) {
|
|
415
|
+
const stmt = this.db.prepare(`
|
|
416
|
+
SELECT * FROM self_directives
|
|
417
|
+
WHERE companion_id = ?
|
|
418
|
+
ORDER BY priority DESC, created_at ASC
|
|
419
|
+
`);
|
|
420
|
+
return stmt.all(companionId).map((row) => this.rowToSelfDirective(row));
|
|
421
|
+
}
|
|
422
|
+
commitDirective(directive) {
|
|
423
|
+
const stmt = this.db.prepare(`
|
|
424
|
+
INSERT INTO self_directives (id, companion_id, priority, directive, status, category, scope_actor, supersedes_id, created_at)
|
|
425
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
426
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
427
|
+
companion_id = excluded.companion_id,
|
|
428
|
+
priority = excluded.priority,
|
|
429
|
+
directive = excluded.directive,
|
|
430
|
+
status = excluded.status,
|
|
431
|
+
category = excluded.category,
|
|
432
|
+
scope_actor = excluded.scope_actor,
|
|
433
|
+
supersedes_id = excluded.supersedes_id
|
|
434
|
+
`);
|
|
435
|
+
stmt.run(directive.id, directive.companionId, directive.priority !== undefined ? directive.priority : 50, directive.directive, normalizeStatus(directive.status, 'active'), directive.category || 'behavioral', directive.scopeActor || null, directive.supersedesId || null, directive.createdAt || new Date().toISOString());
|
|
436
|
+
}
|
|
437
|
+
getDirective(id, companionId) {
|
|
438
|
+
const stmt = companionId
|
|
439
|
+
? this.db.prepare('SELECT * FROM self_directives WHERE id = ? AND companion_id = ?')
|
|
440
|
+
: this.db.prepare('SELECT * FROM self_directives WHERE id = ?');
|
|
441
|
+
const row = (companionId ? stmt.get(id, companionId) : stmt.get(id));
|
|
442
|
+
if (!row)
|
|
443
|
+
return undefined;
|
|
444
|
+
return this.rowToSelfDirective(row);
|
|
445
|
+
}
|
|
446
|
+
approveDirective(id, companionId) {
|
|
447
|
+
const findStmt = companionId
|
|
448
|
+
? this.db.prepare("SELECT id, companion_id, directive, scope_actor, status, priority, supersedes_id FROM self_directives WHERE id = ? AND companion_id = ?")
|
|
449
|
+
: this.db.prepare("SELECT id, companion_id, directive, scope_actor, status, priority, supersedes_id FROM self_directives WHERE id = ?");
|
|
450
|
+
const row = (companionId ? findStmt.get(id, companionId) : findStmt.get(id));
|
|
451
|
+
if (!row) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (normalizeStatus(row.status) !== 'pending' && normalizeStatus(row.status) !== 'superseded') {
|
|
455
|
+
if (normalizeStatus(row.status) === 'active') {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
throw new Error(`Cannot approve directive '${id}': invalid transition from status '${row.status}' to 'active' (only pending or superseded directives can be approved)`);
|
|
459
|
+
}
|
|
460
|
+
// If this directive supersedes an earlier directive, transition that prior directive to superseded
|
|
461
|
+
if (row.supersedes_id) {
|
|
462
|
+
const supersededId = row.supersedes_id;
|
|
463
|
+
const effectiveCompanionId = companionId || row.companion_id;
|
|
464
|
+
if (effectiveCompanionId) {
|
|
465
|
+
const supersedeStmt = this.db.prepare("UPDATE self_directives SET status = 'superseded' WHERE id = ? AND companion_id = ?");
|
|
466
|
+
supersedeStmt.run(supersededId, effectiveCompanionId);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const supersedeStmt = this.db.prepare("UPDATE self_directives SET status = 'superseded' WHERE id = ?");
|
|
470
|
+
supersedeStmt.run(supersededId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Automatic Supersession: If an address directive or single-value directive is approved,
|
|
474
|
+
// transition conflicting older active/pending directives to 'superseded'
|
|
475
|
+
const addressMatch = (row.directive || '').match(/^address\s+(actor:[^\s]+|the user|user)\s+as\s+["“']?([^"”'.]+)["”']?/i);
|
|
476
|
+
let targetPriority = row.priority ?? 50;
|
|
477
|
+
if (addressMatch) {
|
|
478
|
+
targetPriority = 85;
|
|
479
|
+
const targetActor = addressMatch[1].toLowerCase();
|
|
480
|
+
const existingDirectives = this.getAllDirectives(companionId || 'default');
|
|
481
|
+
for (const d of existingDirectives) {
|
|
482
|
+
if ((d.status === 'active' || d.status === 'pending') &&
|
|
483
|
+
d.id !== id &&
|
|
484
|
+
d.directive &&
|
|
485
|
+
d.directive.toLowerCase().startsWith(`address ${targetActor} as`)) {
|
|
486
|
+
this.supersedeDirective(d.id, companionId);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const targetEntityId = targetActor === 'the user' || targetActor === 'user' ? 'actor:user' : targetActor;
|
|
490
|
+
const existingRel = this.getRelationship(companionId || 'default', targetEntityId);
|
|
491
|
+
if (existingRel) {
|
|
492
|
+
const cleanedAddress = addressMatch[2].trim();
|
|
493
|
+
const convs = (existingRel.interactionConventions || []).filter((c) => !c.toLowerCase().startsWith('address as'));
|
|
494
|
+
convs.push(`Address as ${cleanedAddress}`);
|
|
495
|
+
this.upsertRelationship({
|
|
496
|
+
...existingRel,
|
|
497
|
+
companionId: companionId || 'default',
|
|
498
|
+
interactionConventions: convs,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Single-value directive canonical supersession (e.g. role, language, response style)
|
|
503
|
+
const roleMatch = (row.directive || '').match(/^acknowledge role as\s+([^"”'.]+)/i);
|
|
504
|
+
if (roleMatch) {
|
|
505
|
+
const existingDirectives = this.getAllDirectives(companionId || 'default');
|
|
506
|
+
for (const d of existingDirectives) {
|
|
507
|
+
if ((d.status === 'active' || d.status === 'pending') &&
|
|
508
|
+
d.id !== id &&
|
|
509
|
+
d.directive &&
|
|
510
|
+
d.directive.toLowerCase().startsWith('acknowledge role as')) {
|
|
511
|
+
this.supersedeDirective(d.id, companionId);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Companion name directive detection: "Address companion as X", "Your name is X", "Call yourself X", "Acknowledge name as X"
|
|
516
|
+
const compNameMatch = (row.directive || '').match(/^(?:address\s+companion\s+as|your\s+name\s+is|call\s+yourself|acknowledge\s+name\s+as|companion\s+name\s+is)\s+["“']?([^"”'.]+)["”']?/i);
|
|
517
|
+
if (compNameMatch) {
|
|
518
|
+
const newCompanionName = compNameMatch[1].trim();
|
|
519
|
+
if (newCompanionName) {
|
|
520
|
+
const targetId = companionId || row.companion_id || 'default';
|
|
521
|
+
const currentIdentity = this.getIdentity(targetId) || {
|
|
522
|
+
companionId: targetId,
|
|
523
|
+
name: '',
|
|
524
|
+
version: '1.0.0',
|
|
525
|
+
updatedAt: new Date().toISOString(),
|
|
526
|
+
};
|
|
527
|
+
currentIdentity.name = newCompanionName;
|
|
528
|
+
currentIdentity.updatedAt = new Date().toISOString();
|
|
529
|
+
this.setIdentity(currentIdentity);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (companionId) {
|
|
533
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'active', priority = MAX(priority, ?) WHERE id = ? AND companion_id = ? AND LOWER(status) IN ('pending', 'superseded')");
|
|
534
|
+
stmt.run(targetPriority, id, companionId);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'active', priority = MAX(priority, ?) WHERE id = ? AND LOWER(status) IN ('pending', 'superseded')");
|
|
538
|
+
stmt.run(targetPriority, id);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
rejectDirective(id, companionId) {
|
|
542
|
+
const findStmt = companionId
|
|
543
|
+
? this.db.prepare("SELECT id, companion_id, status FROM self_directives WHERE id = ? AND companion_id = ?")
|
|
544
|
+
: this.db.prepare("SELECT id, companion_id, status FROM self_directives WHERE id = ?");
|
|
545
|
+
const row = (companionId ? findStmt.get(id, companionId) : findStmt.get(id));
|
|
546
|
+
if (!row) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (normalizeStatus(row.status) !== 'pending' && normalizeStatus(row.status) !== 'superseded') {
|
|
550
|
+
if (normalizeStatus(row.status) === 'rejected') {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
throw new Error(`Cannot reject directive '${id}': invalid transition from status '${row.status}' to 'rejected' (only pending or superseded directives can be rejected)`);
|
|
554
|
+
}
|
|
555
|
+
if (companionId) {
|
|
556
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'rejected' WHERE id = ? AND companion_id = ? AND LOWER(status) IN ('pending', 'superseded')");
|
|
557
|
+
stmt.run(id, companionId);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'rejected' WHERE id = ? AND LOWER(status) IN ('pending', 'superseded')");
|
|
561
|
+
stmt.run(id);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
supersedeDirective(id, companionId) {
|
|
565
|
+
if (companionId) {
|
|
566
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'superseded' WHERE id = ? AND companion_id = ?");
|
|
567
|
+
stmt.run(id, companionId);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'superseded' WHERE id = ?");
|
|
571
|
+
stmt.run(id);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
revokeDirective(id, companionId) {
|
|
575
|
+
if (companionId) {
|
|
576
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'revoked' WHERE id = ? AND companion_id = ?");
|
|
577
|
+
stmt.run(id, companionId);
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'revoked' WHERE id = ?");
|
|
581
|
+
stmt.run(id);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
expireDirective(id, companionId) {
|
|
585
|
+
if (companionId) {
|
|
586
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'expired' WHERE id = ? AND companion_id = ?");
|
|
587
|
+
stmt.run(id, companionId);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'expired' WHERE id = ?");
|
|
591
|
+
stmt.run(id);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
disableDirective(id, companionId) {
|
|
595
|
+
if (companionId) {
|
|
596
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'disabled' WHERE id = ? AND companion_id = ?");
|
|
597
|
+
stmt.run(id, companionId);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
const stmt = this.db.prepare("UPDATE self_directives SET status = 'disabled' WHERE id = ?");
|
|
601
|
+
stmt.run(id);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
getRelationship(companionId, entityId) {
|
|
605
|
+
const stripped = entityId.startsWith('actor:') ? entityId.slice(6) : entityId;
|
|
606
|
+
const prefixed = entityId.startsWith('actor:') ? entityId : `actor:${entityId}`;
|
|
607
|
+
const stmt = this.db.prepare('SELECT * FROM self_relationships WHERE companion_id = ? AND (entity_id = ? OR entity_id = ? OR entity_id = ?)');
|
|
608
|
+
let row = stmt.get(companionId, entityId, stripped, prefixed);
|
|
609
|
+
if (!row && (entityId === 'owner-user' || entityId === 'local-user' || entityId === 'owner' || entityId === 'primary' || entityId === 'user')) {
|
|
610
|
+
const fallbackStmt = this.db.prepare("SELECT * FROM self_relationships WHERE companion_id = ? AND entity_type = 'human' ORDER BY updated_at DESC LIMIT 1");
|
|
611
|
+
row = fallbackStmt.get(companionId);
|
|
612
|
+
}
|
|
613
|
+
if (!row)
|
|
614
|
+
return undefined;
|
|
615
|
+
return {
|
|
616
|
+
companionId: row.companion_id,
|
|
617
|
+
entityId: row.entity_id,
|
|
618
|
+
entityType: row.entity_type || 'human',
|
|
619
|
+
name: row.name || undefined,
|
|
620
|
+
affiliation: row.affiliation || undefined,
|
|
621
|
+
role: row.role || 'user',
|
|
622
|
+
stance: row.stance || 'neutral',
|
|
623
|
+
trustScore: row.trust_score !== undefined && row.trust_score !== null ? row.trust_score : 0.5,
|
|
624
|
+
familiarity: row.familiarity !== undefined && row.familiarity !== null ? row.familiarity : 0.5,
|
|
625
|
+
interactionConventions: row.interaction_conventions ? JSON.parse(row.interaction_conventions) : [],
|
|
626
|
+
updatedAt: row.updated_at || undefined,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
getRelationships(companionId) {
|
|
630
|
+
const stmt = this.db.prepare('SELECT * FROM self_relationships WHERE companion_id = ? ORDER BY entity_id ASC');
|
|
631
|
+
return stmt.all(companionId).map((row) => ({
|
|
632
|
+
companionId: row.companion_id,
|
|
633
|
+
entityId: row.entity_id,
|
|
634
|
+
entityType: row.entity_type || 'human',
|
|
635
|
+
name: row.name || undefined,
|
|
636
|
+
affiliation: row.affiliation || undefined,
|
|
637
|
+
role: row.role || 'user',
|
|
638
|
+
stance: row.stance || 'neutral',
|
|
639
|
+
trustScore: row.trust_score !== undefined && row.trust_score !== null ? row.trust_score : 0.5,
|
|
640
|
+
familiarity: row.familiarity !== undefined && row.familiarity !== null ? row.familiarity : 0.5,
|
|
641
|
+
interactionConventions: row.interaction_conventions ? JSON.parse(row.interaction_conventions) : [],
|
|
642
|
+
updatedAt: row.updated_at || undefined,
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
upsertRelationship(rel) {
|
|
646
|
+
const stmt = this.db.prepare(`
|
|
647
|
+
INSERT INTO self_relationships (companion_id, entity_id, entity_type, name, affiliation, role, stance, trust_score, familiarity, interaction_conventions, updated_at)
|
|
648
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
649
|
+
ON CONFLICT(companion_id, entity_id) DO UPDATE SET
|
|
650
|
+
entity_type = excluded.entity_type,
|
|
651
|
+
name = COALESCE(excluded.name, self_relationships.name),
|
|
652
|
+
affiliation = COALESCE(excluded.affiliation, self_relationships.affiliation),
|
|
653
|
+
role = COALESCE(excluded.role, self_relationships.role),
|
|
654
|
+
stance = COALESCE(excluded.stance, self_relationships.stance),
|
|
655
|
+
trust_score = excluded.trust_score,
|
|
656
|
+
familiarity = excluded.familiarity,
|
|
657
|
+
interaction_conventions = excluded.interaction_conventions,
|
|
658
|
+
updated_at = datetime('now')
|
|
659
|
+
`);
|
|
660
|
+
stmt.run(rel.companionId, rel.entityId, rel.entityType || 'human', rel.name || null, rel.affiliation || null, rel.role || 'user', rel.stance || 'neutral', rel.trustScore !== undefined && rel.trustScore !== null ? rel.trustScore : 0.5, rel.familiarity !== undefined && rel.familiarity !== null ? rel.familiarity : 0.5, JSON.stringify(rel.interactionConventions || []));
|
|
661
|
+
}
|
|
662
|
+
getExemplars(companionId) {
|
|
663
|
+
const stmt = this.db.prepare('SELECT * FROM self_exemplars WHERE companion_id = ? ORDER BY created_at ASC');
|
|
664
|
+
return stmt.all(companionId).map((row) => ({
|
|
665
|
+
id: row.id,
|
|
666
|
+
companionId: row.companion_id,
|
|
667
|
+
user: row.user_prompt,
|
|
668
|
+
assistant: row.companion_response,
|
|
669
|
+
createdAt: row.created_at,
|
|
670
|
+
}));
|
|
671
|
+
}
|
|
672
|
+
setExemplars(companionId, exemplars) {
|
|
673
|
+
const delStmt = this.db.prepare('DELETE FROM self_exemplars WHERE companion_id = ?');
|
|
674
|
+
delStmt.run(companionId);
|
|
675
|
+
const insertStmt = this.db.prepare(`
|
|
676
|
+
INSERT INTO self_exemplars (id, companion_id, user_prompt, companion_response, created_at)
|
|
677
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
678
|
+
`);
|
|
679
|
+
for (let i = 0; i < exemplars.length; i++) {
|
|
680
|
+
const ex = exemplars[i];
|
|
681
|
+
const id = ex.id || `ex-${i + 1}-${Date.now()}`;
|
|
682
|
+
insertStmt.run(id, companionId, ex.user, ex.assistant);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// ==========================================
|
|
686
|
+
// Knowledge / Life DB Methods
|
|
687
|
+
// ==========================================
|
|
688
|
+
getInventory(companionId, domain) {
|
|
689
|
+
let stmt;
|
|
690
|
+
let rows;
|
|
691
|
+
if (domain) {
|
|
692
|
+
stmt = this.db.prepare('SELECT * FROM life_inventory WHERE companion_id = ? AND domain = ?');
|
|
693
|
+
rows = stmt.all(companionId, domain);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
stmt = this.db.prepare('SELECT * FROM life_inventory WHERE companion_id = ?');
|
|
697
|
+
rows = stmt.all(companionId);
|
|
698
|
+
}
|
|
699
|
+
return rows.map((row) => ({
|
|
700
|
+
id: row.id,
|
|
701
|
+
companionId: row.companion_id,
|
|
702
|
+
domain: row.domain,
|
|
703
|
+
entityName: row.entity_name,
|
|
704
|
+
properties: JSON.parse(row.properties),
|
|
705
|
+
updatedAt: row.updated_at
|
|
706
|
+
}));
|
|
707
|
+
}
|
|
708
|
+
upsertInventoryItem(item) {
|
|
709
|
+
const stmt = this.db.prepare(`
|
|
710
|
+
INSERT INTO life_inventory (id, companion_id, domain, entity_name, properties, updated_at)
|
|
711
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
712
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
713
|
+
domain = excluded.domain,
|
|
714
|
+
entity_name = excluded.entity_name,
|
|
715
|
+
properties = excluded.properties,
|
|
716
|
+
updated_at = datetime('now')
|
|
717
|
+
`);
|
|
718
|
+
stmt.run(item.id, item.companionId, item.domain, item.entityName, JSON.stringify(item.properties));
|
|
719
|
+
}
|
|
720
|
+
getFinanceEntries(companionId, limit = 50) {
|
|
721
|
+
const stmt = this.db.prepare('SELECT * FROM life_finance WHERE companion_id = ? ORDER BY timestamp DESC LIMIT ?');
|
|
722
|
+
return stmt.all(companionId, limit).map((row) => ({
|
|
723
|
+
id: row.id,
|
|
724
|
+
companionId: row.companion_id,
|
|
725
|
+
category: row.category,
|
|
726
|
+
amount: row.amount,
|
|
727
|
+
currency: row.currency,
|
|
728
|
+
timestamp: row.timestamp,
|
|
729
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined
|
|
730
|
+
}));
|
|
731
|
+
}
|
|
732
|
+
addFinanceEntry(entry) {
|
|
733
|
+
const stmt = this.db.prepare(`
|
|
734
|
+
INSERT INTO life_finance (id, companion_id, category, amount, currency, timestamp, metadata)
|
|
735
|
+
VALUES (?, ?, ?, ?, ?, coalesce(?, datetime('now')), ?)
|
|
736
|
+
`);
|
|
737
|
+
stmt.run(entry.id, entry.companionId, entry.category, entry.amount, entry.currency || 'USD', entry.timestamp || null, entry.metadata ? JSON.stringify(entry.metadata) : null);
|
|
738
|
+
}
|
|
739
|
+
getSchedule(companionId) {
|
|
740
|
+
const stmt = this.db.prepare('SELECT * FROM life_schedule WHERE companion_id = ? AND status = ? ORDER BY start_time ASC');
|
|
741
|
+
return stmt.all(companionId, 'active').map((row) => ({
|
|
742
|
+
id: row.id,
|
|
743
|
+
companionId: row.companion_id,
|
|
744
|
+
title: row.title,
|
|
745
|
+
startTime: row.start_time,
|
|
746
|
+
endTime: row.end_time || undefined,
|
|
747
|
+
isRecurring: row.is_recurring === 1,
|
|
748
|
+
status: row.status
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
upsertScheduleItem(item) {
|
|
752
|
+
const stmt = this.db.prepare(`
|
|
753
|
+
INSERT INTO life_schedule (id, companion_id, title, start_time, end_time, is_recurring, status)
|
|
754
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
755
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
756
|
+
title = excluded.title,
|
|
757
|
+
start_time = excluded.start_time,
|
|
758
|
+
end_time = excluded.end_time,
|
|
759
|
+
is_recurring = excluded.is_recurring,
|
|
760
|
+
status = excluded.status
|
|
761
|
+
`);
|
|
762
|
+
stmt.run(item.id, item.companionId, item.title, item.startTime, item.endTime || null, item.isRecurring ? 1 : 0, item.status || 'active');
|
|
763
|
+
}
|
|
764
|
+
deleteScheduleItem(id) {
|
|
765
|
+
const stmt = this.db.prepare('DELETE FROM life_schedule WHERE id = ?');
|
|
766
|
+
const result = stmt.run(id);
|
|
767
|
+
return result.changes > 0;
|
|
768
|
+
}
|
|
769
|
+
getPreferences(companionId) {
|
|
770
|
+
const stmt = this.db.prepare('SELECT * FROM life_preferences WHERE companion_id = ?');
|
|
771
|
+
return stmt.all(companionId).map((row) => ({
|
|
772
|
+
id: row.id,
|
|
773
|
+
companionId: row.companion_id,
|
|
774
|
+
preferenceKey: row.preference_key,
|
|
775
|
+
preferenceValue: row.preference_value,
|
|
776
|
+
category: row.category,
|
|
777
|
+
updatedAt: row.updated_at
|
|
778
|
+
}));
|
|
779
|
+
}
|
|
780
|
+
upsertPreference(pref) {
|
|
781
|
+
const stmt = this.db.prepare(`
|
|
782
|
+
INSERT INTO life_preferences (id, companion_id, preference_key, preference_value, category, updated_at)
|
|
783
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
784
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
785
|
+
preference_key = excluded.preference_key,
|
|
786
|
+
preference_value = excluded.preference_value,
|
|
787
|
+
category = excluded.category,
|
|
788
|
+
updated_at = datetime('now')
|
|
789
|
+
`);
|
|
790
|
+
stmt.run(pref.id, pref.companionId, pref.preferenceKey, pref.preferenceValue, pref.category);
|
|
791
|
+
}
|
|
792
|
+
// --- Generic Life Entities Methods ---
|
|
793
|
+
getEntities(companionId, entityType, domain) {
|
|
794
|
+
let query = 'SELECT * FROM life_entities WHERE companion_id = ?';
|
|
795
|
+
const params = [companionId];
|
|
796
|
+
if (entityType) {
|
|
797
|
+
query += ' AND entity_type = ?';
|
|
798
|
+
params.push(entityType);
|
|
799
|
+
}
|
|
800
|
+
if (domain) {
|
|
801
|
+
query += ' AND domain = ?';
|
|
802
|
+
params.push(domain);
|
|
803
|
+
}
|
|
804
|
+
query += ' ORDER BY updated_at DESC';
|
|
805
|
+
const stmt = this.db.prepare(query);
|
|
806
|
+
return stmt.all(...params).map((row) => ({
|
|
807
|
+
id: row.id,
|
|
808
|
+
companionId: row.companion_id,
|
|
809
|
+
entityType: row.entity_type,
|
|
810
|
+
domain: row.domain,
|
|
811
|
+
name: row.name,
|
|
812
|
+
properties: JSON.parse(row.properties || '{}'),
|
|
813
|
+
updatedAt: row.updated_at,
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
getEntityById(id) {
|
|
817
|
+
const stmt = this.db.prepare('SELECT * FROM life_entities WHERE id = ?');
|
|
818
|
+
const row = stmt.get(id);
|
|
819
|
+
if (!row)
|
|
820
|
+
return undefined;
|
|
821
|
+
return {
|
|
822
|
+
id: row.id,
|
|
823
|
+
companionId: row.companion_id,
|
|
824
|
+
entityType: row.entity_type,
|
|
825
|
+
domain: row.domain,
|
|
826
|
+
name: row.name,
|
|
827
|
+
properties: JSON.parse(row.properties || '{}'),
|
|
828
|
+
updatedAt: row.updated_at,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
upsertEntity(entity) {
|
|
832
|
+
const stmt = this.db.prepare(`
|
|
833
|
+
INSERT INTO life_entities (id, companion_id, entity_type, domain, name, properties, updated_at)
|
|
834
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
835
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
836
|
+
entity_type = excluded.entity_type,
|
|
837
|
+
domain = excluded.domain,
|
|
838
|
+
name = excluded.name,
|
|
839
|
+
properties = excluded.properties,
|
|
840
|
+
updated_at = datetime('now')
|
|
841
|
+
`);
|
|
842
|
+
stmt.run(entity.id, entity.companionId, entity.entityType, entity.domain, entity.name, JSON.stringify(entity.properties || {}));
|
|
843
|
+
}
|
|
844
|
+
deleteEntity(id) {
|
|
845
|
+
const stmt = this.db.prepare('DELETE FROM life_entities WHERE id = ?');
|
|
846
|
+
const res = stmt.run(id);
|
|
847
|
+
return res.changes > 0;
|
|
848
|
+
}
|
|
849
|
+
// --- Generic Life Events (Telemetry & Logs) Methods ---
|
|
850
|
+
getEvents(companionId, stream, limit = 50) {
|
|
851
|
+
let query = 'SELECT * FROM life_events WHERE companion_id = ?';
|
|
852
|
+
const params = [companionId];
|
|
853
|
+
if (stream) {
|
|
854
|
+
query += ' AND stream = ?';
|
|
855
|
+
params.push(stream);
|
|
856
|
+
}
|
|
857
|
+
query += ' ORDER BY timestamp DESC LIMIT ?';
|
|
858
|
+
params.push(limit);
|
|
859
|
+
const stmt = this.db.prepare(query);
|
|
860
|
+
return stmt.all(...params).map((row) => ({
|
|
861
|
+
id: row.id,
|
|
862
|
+
companionId: row.companion_id,
|
|
863
|
+
stream: row.stream,
|
|
864
|
+
timestamp: row.timestamp,
|
|
865
|
+
metricValue: row.metric_value != null ? row.metric_value : undefined,
|
|
866
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
867
|
+
}));
|
|
868
|
+
}
|
|
869
|
+
addEvent(event) {
|
|
870
|
+
const stmt = this.db.prepare(`
|
|
871
|
+
INSERT INTO life_events (id, companion_id, stream, timestamp, metric_value, metadata)
|
|
872
|
+
VALUES (?, ?, ?, coalesce(?, datetime('now')), ?, ?)
|
|
873
|
+
`);
|
|
874
|
+
stmt.run(event.id, event.companionId, event.stream, event.timestamp || null, event.metricValue != null ? event.metricValue : null, event.metadata ? JSON.stringify(event.metadata) : null);
|
|
875
|
+
}
|
|
876
|
+
// --- Generic Life Tasks (Verbs & Progress) Methods ---
|
|
877
|
+
getTasks(companionId, status) {
|
|
878
|
+
let query = 'SELECT * FROM life_tasks WHERE companion_id = ?';
|
|
879
|
+
const params = [companionId];
|
|
880
|
+
if (status) {
|
|
881
|
+
query += ' AND status = ?';
|
|
882
|
+
params.push(status);
|
|
883
|
+
}
|
|
884
|
+
query += ' ORDER BY priority DESC, updated_at DESC';
|
|
885
|
+
const stmt = this.db.prepare(query);
|
|
886
|
+
return stmt.all(...params).map((row) => ({
|
|
887
|
+
id: row.id,
|
|
888
|
+
companionId: row.companion_id,
|
|
889
|
+
title: row.title,
|
|
890
|
+
status: row.status,
|
|
891
|
+
priority: row.priority,
|
|
892
|
+
targetDate: row.target_date || undefined,
|
|
893
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
894
|
+
updatedAt: row.updated_at,
|
|
895
|
+
}));
|
|
896
|
+
}
|
|
897
|
+
upsertTask(task) {
|
|
898
|
+
const stmt = this.db.prepare(`
|
|
899
|
+
INSERT INTO life_tasks (id, companion_id, title, status, priority, target_date, metadata, updated_at)
|
|
900
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
901
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
902
|
+
title = excluded.title,
|
|
903
|
+
status = excluded.status,
|
|
904
|
+
priority = excluded.priority,
|
|
905
|
+
target_date = excluded.target_date,
|
|
906
|
+
metadata = excluded.metadata,
|
|
907
|
+
updated_at = datetime('now')
|
|
908
|
+
`);
|
|
909
|
+
stmt.run(task.id, task.companionId, task.title, task.status || 'backlog', task.priority ?? 0, task.targetDate || null, task.metadata ? JSON.stringify(task.metadata) : null);
|
|
910
|
+
}
|
|
911
|
+
deleteTask(id) {
|
|
912
|
+
const stmt = this.db.prepare('DELETE FROM life_tasks WHERE id = ?');
|
|
913
|
+
const res = stmt.run(id);
|
|
914
|
+
return res.changes > 0;
|
|
915
|
+
}
|
|
916
|
+
// ==========================================
|
|
917
|
+
// Memory Domain Methods
|
|
918
|
+
// ==========================================
|
|
919
|
+
recordEvent(event) {
|
|
920
|
+
const stmt = this.db.prepare(`
|
|
921
|
+
INSERT INTO memory_events (id, companion_id, source_type, occurred_at, payload)
|
|
922
|
+
VALUES (?, ?, ?, coalesce(?, datetime('now')), ?)
|
|
923
|
+
`);
|
|
924
|
+
stmt.run(event.id, event.companionId, event.sourceType, event.occurredAt || null, JSON.stringify(event.payload));
|
|
925
|
+
}
|
|
926
|
+
getRecentEvents(companionId, limit = 50) {
|
|
927
|
+
const stmt = this.db.prepare('SELECT * FROM memory_events WHERE companion_id = ? ORDER BY occurred_at DESC LIMIT ?');
|
|
928
|
+
return stmt.all(companionId, limit).map((row) => ({
|
|
929
|
+
id: row.id,
|
|
930
|
+
companionId: row.companion_id,
|
|
931
|
+
sourceType: row.source_type,
|
|
932
|
+
occurredAt: row.occurred_at,
|
|
933
|
+
payload: JSON.parse(row.payload)
|
|
934
|
+
}));
|
|
935
|
+
}
|
|
936
|
+
getEvent(id) {
|
|
937
|
+
const stmt = this.db.prepare('SELECT * FROM memory_events WHERE id = ?');
|
|
938
|
+
const row = stmt.get(id);
|
|
939
|
+
if (!row)
|
|
940
|
+
return undefined;
|
|
941
|
+
return {
|
|
942
|
+
id: row.id,
|
|
943
|
+
companionId: row.companion_id,
|
|
944
|
+
sourceType: row.source_type,
|
|
945
|
+
occurredAt: row.occurred_at,
|
|
946
|
+
payload: JSON.parse(row.payload)
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
rowToMemoryClaim(row) {
|
|
950
|
+
return {
|
|
951
|
+
id: row.id,
|
|
952
|
+
companionId: row.companion_id,
|
|
953
|
+
subject: row.subject,
|
|
954
|
+
predicate: row.predicate,
|
|
955
|
+
value: row.value,
|
|
956
|
+
status: normalizeStatus(row.status, 'pending'),
|
|
957
|
+
confidence: row.confidence,
|
|
958
|
+
validFrom: row.valid_from || undefined,
|
|
959
|
+
validUntil: row.valid_until || undefined,
|
|
960
|
+
evidence: row.evidence ? (typeof row.evidence === 'string' ? JSON.parse(row.evidence) : row.evidence) : undefined,
|
|
961
|
+
assertedAt: row.asserted_at,
|
|
962
|
+
supersedes: row.supersedes || undefined,
|
|
963
|
+
sourceEventId: row.source_event_id || undefined,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
proposeClaim(claim) {
|
|
967
|
+
const id = claim.id || crypto.randomUUID();
|
|
968
|
+
const status = normalizeStatus(claim.status, 'pending');
|
|
969
|
+
const confidence = claim.confidence ?? 1.0;
|
|
970
|
+
const assertedAt = claim.assertedAt || new Date().toISOString();
|
|
971
|
+
const stmt = this.db.prepare(`
|
|
972
|
+
INSERT INTO memory_claims (id, companion_id, subject, predicate, value, status, confidence, valid_from, valid_until, evidence, asserted_at, supersedes, source_event_id)
|
|
973
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, coalesce(?, datetime('now')), ?, ?)
|
|
974
|
+
`);
|
|
975
|
+
stmt.run(id, claim.companionId || 'default', claim.subject, claim.predicate, claim.value, status, confidence, claim.validFrom || null, claim.validUntil || null, claim.evidence ? JSON.stringify(claim.evidence) : null, assertedAt || null, claim.supersedes || null, claim.sourceEventId || null);
|
|
976
|
+
return {
|
|
977
|
+
...claim,
|
|
978
|
+
id,
|
|
979
|
+
status: status,
|
|
980
|
+
confidence,
|
|
981
|
+
assertedAt,
|
|
982
|
+
supersedes: claim.supersedes,
|
|
983
|
+
sourceEventId: claim.sourceEventId,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
approveClaim(id, companionId) {
|
|
987
|
+
const findClaim = companionId
|
|
988
|
+
? this.db.prepare("SELECT id, subject, predicate, status, supersedes FROM memory_claims WHERE id = ? AND companion_id = ?")
|
|
989
|
+
: this.db.prepare("SELECT id, subject, predicate, status, supersedes FROM memory_claims WHERE id = ?");
|
|
990
|
+
const row = (companionId ? findClaim.get(id, companionId) : findClaim.get(id));
|
|
991
|
+
if (!row) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (normalizeStatus(row.status) !== 'pending') {
|
|
995
|
+
if (normalizeStatus(row.status) === 'approved') {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
throw new Error(`Cannot approve claim '${id}': invalid transition from status '${row.status}' to 'approved' (only pending claims can be approved)`);
|
|
999
|
+
}
|
|
1000
|
+
// Explicit supersession transition: if claim explicitly targets an earlier claim
|
|
1001
|
+
if (row.supersedes) {
|
|
1002
|
+
const supersededId = row.supersedes;
|
|
1003
|
+
if (companionId) {
|
|
1004
|
+
const supersedeStmt = this.db.prepare("UPDATE memory_claims SET status = 'superseded' WHERE id = ? AND companion_id = ?");
|
|
1005
|
+
supersedeStmt.run(supersededId, companionId);
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
const supersedeStmt = this.db.prepare("UPDATE memory_claims SET status = 'superseded' WHERE id = ?");
|
|
1009
|
+
supersedeStmt.run(supersededId);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// Automatic supersession for single-value predicates:
|
|
1013
|
+
// When a single-value predicate (e.g. name, role, creator, preferred_address) is approved,
|
|
1014
|
+
// transition previous active/approved claims on the same subject & predicate to 'superseded'
|
|
1015
|
+
const SINGLE_VALUE_PREDICATES = new Set([
|
|
1016
|
+
'name',
|
|
1017
|
+
'preferred_name',
|
|
1018
|
+
'preferred_address',
|
|
1019
|
+
'preferred_form_of_address',
|
|
1020
|
+
'form_of_address',
|
|
1021
|
+
'title',
|
|
1022
|
+
'honorific',
|
|
1023
|
+
'role',
|
|
1024
|
+
'archetype',
|
|
1025
|
+
'origin',
|
|
1026
|
+
'created_by',
|
|
1027
|
+
'stated_relationship',
|
|
1028
|
+
'relationship',
|
|
1029
|
+
'relationship_to_siduri',
|
|
1030
|
+
'ethos',
|
|
1031
|
+
'server',
|
|
1032
|
+
'uid',
|
|
1033
|
+
'preferred_language',
|
|
1034
|
+
]);
|
|
1035
|
+
const lowerPred = (row.predicate || '').toLowerCase();
|
|
1036
|
+
if (SINGLE_VALUE_PREDICATES.has(lowerPred)) {
|
|
1037
|
+
const effCompanionId = companionId || 'default';
|
|
1038
|
+
const autoSupersedeStmt = this.db.prepare(`
|
|
1039
|
+
UPDATE memory_claims
|
|
1040
|
+
SET status = 'superseded'
|
|
1041
|
+
WHERE companion_id = ?
|
|
1042
|
+
AND LOWER(subject) = LOWER(?)
|
|
1043
|
+
AND LOWER(predicate) = LOWER(?)
|
|
1044
|
+
AND id != ?
|
|
1045
|
+
AND LOWER(status) IN ('approved', 'pending')
|
|
1046
|
+
`);
|
|
1047
|
+
autoSupersedeStmt.run(effCompanionId, row.subject, row.predicate, id);
|
|
1048
|
+
}
|
|
1049
|
+
if (companionId) {
|
|
1050
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'approved' WHERE id = ? AND companion_id = ? AND LOWER(status) = 'pending'");
|
|
1051
|
+
stmt.run(id, companionId);
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'approved' WHERE id = ? AND LOWER(status) = 'pending'");
|
|
1055
|
+
stmt.run(id);
|
|
1056
|
+
}
|
|
1057
|
+
// Canonically promote approved claim to Self domain state (identity, role, relationships)
|
|
1058
|
+
const approvedClaim = this.getClaim(id);
|
|
1059
|
+
if (approvedClaim) {
|
|
1060
|
+
this.promoteClaimToSelf(approvedClaim);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Canonically promotes a Claim into Self domain tables.
|
|
1065
|
+
*/
|
|
1066
|
+
promoteClaimToSelf(claim) {
|
|
1067
|
+
const companionId = claim.companionId || 'default';
|
|
1068
|
+
const subject = (claim.subject || '').toLowerCase();
|
|
1069
|
+
const predicate = (claim.predicate || '').toLowerCase();
|
|
1070
|
+
const value = claim.value || '';
|
|
1071
|
+
if (!value)
|
|
1072
|
+
return;
|
|
1073
|
+
// 1. Identity mutations: companion identity/role/origin/name/ethos
|
|
1074
|
+
const isCompanionTarget = subject.startsWith('companion') ||
|
|
1075
|
+
subject === 'siduri' ||
|
|
1076
|
+
subject === 'self' ||
|
|
1077
|
+
subject === 'assistant' ||
|
|
1078
|
+
subject === 'persona' ||
|
|
1079
|
+
subject === 'ai' ||
|
|
1080
|
+
subject === 'me' ||
|
|
1081
|
+
((predicate === 'name' || predicate === 'role' || predicate === 'archetype' || predicate === 'ethos' || predicate === 'origin') &&
|
|
1082
|
+
!subject.startsWith('actor:') &&
|
|
1083
|
+
subject !== 'user' &&
|
|
1084
|
+
subject !== 'primary_user' &&
|
|
1085
|
+
subject !== 'owner' &&
|
|
1086
|
+
subject !== 'creator');
|
|
1087
|
+
if (isCompanionTarget) {
|
|
1088
|
+
const existing = this.getIdentity(companionId) || {
|
|
1089
|
+
companionId,
|
|
1090
|
+
name: '',
|
|
1091
|
+
version: '1.0.0',
|
|
1092
|
+
updatedAt: new Date().toISOString(),
|
|
1093
|
+
};
|
|
1094
|
+
if (predicate === 'role' || predicate === 'archetype') {
|
|
1095
|
+
existing.archetype = value;
|
|
1096
|
+
existing.role = value;
|
|
1097
|
+
this.setIdentity(existing);
|
|
1098
|
+
this.commitDirective({
|
|
1099
|
+
id: `dir-role-${claim.id || Date.now()}`,
|
|
1100
|
+
companionId,
|
|
1101
|
+
priority: 70,
|
|
1102
|
+
directive: `Acknowledge role as ${value}`,
|
|
1103
|
+
status: 'active',
|
|
1104
|
+
category: 'relational',
|
|
1105
|
+
createdAt: new Date().toISOString(),
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
else if (predicate === 'origin' || predicate === 'created_by') {
|
|
1109
|
+
existing.origin = value;
|
|
1110
|
+
this.setIdentity(existing);
|
|
1111
|
+
}
|
|
1112
|
+
else if (predicate === 'name') {
|
|
1113
|
+
existing.name = value;
|
|
1114
|
+
this.setIdentity(existing);
|
|
1115
|
+
}
|
|
1116
|
+
else if (predicate === 'ethos') {
|
|
1117
|
+
existing.ethos = value;
|
|
1118
|
+
this.setIdentity(existing);
|
|
1119
|
+
}
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
// 2. Relationship mutations: creator or user stated relationship, name, preferred address, or affiliation
|
|
1123
|
+
const isPreferredAddress = [
|
|
1124
|
+
'preferred_address',
|
|
1125
|
+
'preferred_form_of_address',
|
|
1126
|
+
'preferred_name',
|
|
1127
|
+
'form_of_address',
|
|
1128
|
+
'title',
|
|
1129
|
+
'honorific',
|
|
1130
|
+
'call_me',
|
|
1131
|
+
'addressed_as',
|
|
1132
|
+
].includes(predicate);
|
|
1133
|
+
const isName = (predicate === 'name' && (subject.startsWith('actor:') || subject === 'user' || subject === 'primary_user')) || isPreferredAddress;
|
|
1134
|
+
if (claim.claimType === 'relationship' ||
|
|
1135
|
+
predicate === 'stated_relationship' ||
|
|
1136
|
+
predicate === 'relationship' ||
|
|
1137
|
+
predicate === 'relationship_to_siduri' ||
|
|
1138
|
+
predicate === 'affiliation' ||
|
|
1139
|
+
isName) {
|
|
1140
|
+
const rawSubject = (claim.subject || 'actor:user').replace(/^actor:actor:/, 'actor:');
|
|
1141
|
+
const isCreator = value.toLowerCase().includes('creator');
|
|
1142
|
+
const isAffil = predicate === 'affiliation';
|
|
1143
|
+
const existingRel = this.getRelationship(companionId, rawSubject);
|
|
1144
|
+
const isPriorCreator = existingRel?.role === 'creator' || (existingRel?.stance === 'familiar_loyal' && existingRel.trustScore === 1.0);
|
|
1145
|
+
const role = isCreator
|
|
1146
|
+
? 'creator'
|
|
1147
|
+
: (isPriorCreator ? 'creator' : (existingRel?.role && existingRel.role !== 'user' ? existingRel.role : (isName || isAffil ? existingRel?.role || 'user' : value)));
|
|
1148
|
+
const name = isName ? (isPreferredAddress && existingRel?.name ? existingRel.name : value) : existingRel?.name;
|
|
1149
|
+
const affiliation = isAffil ? value : existingRel?.affiliation;
|
|
1150
|
+
const stance = isCreator || isPriorCreator ? 'familiar_loyal' : (existingRel?.stance || 'neutral');
|
|
1151
|
+
const trustScore = isCreator || isPriorCreator ? 1.0 : (existingRel?.trustScore ?? 0.8);
|
|
1152
|
+
const familiarity = isCreator || isPriorCreator ? 0.9 : (existingRel?.familiarity ?? 0.5);
|
|
1153
|
+
const conventions = isCreator || isPriorCreator
|
|
1154
|
+
? Array.from(new Set([...(existingRel?.interactionConventions || []), 'Direct communication', 'Highest administrative trust']))
|
|
1155
|
+
: [...(existingRel?.interactionConventions || [])];
|
|
1156
|
+
if (isPreferredAddress) {
|
|
1157
|
+
const filteredConvs = conventions.filter((c) => !c.toLowerCase().startsWith('address as'));
|
|
1158
|
+
filteredConvs.push(`Address as ${value}`);
|
|
1159
|
+
conventions.length = 0;
|
|
1160
|
+
conventions.push(...filteredConvs);
|
|
1161
|
+
}
|
|
1162
|
+
this.upsertRelationship({
|
|
1163
|
+
companionId,
|
|
1164
|
+
entityId: rawSubject,
|
|
1165
|
+
entityType: 'human',
|
|
1166
|
+
name,
|
|
1167
|
+
affiliation,
|
|
1168
|
+
role,
|
|
1169
|
+
stance,
|
|
1170
|
+
trustScore,
|
|
1171
|
+
familiarity,
|
|
1172
|
+
interactionConventions: conventions,
|
|
1173
|
+
});
|
|
1174
|
+
// Dual promotion: If this actor is established as creator, also populate companion's origin in self_identity
|
|
1175
|
+
if (isCreator || (isName && isPriorCreator)) {
|
|
1176
|
+
const existingIdentity = this.getIdentity(companionId) || {
|
|
1177
|
+
companionId,
|
|
1178
|
+
name: 'Siduri',
|
|
1179
|
+
version: '1.0.0',
|
|
1180
|
+
updatedAt: new Date().toISOString(),
|
|
1181
|
+
};
|
|
1182
|
+
const creatorName = name || existingRel?.name || (rawSubject.startsWith('actor:') && rawSubject !== 'actor:user' && rawSubject !== 'actor:primary' ? rawSubject.slice(6) : value);
|
|
1183
|
+
existingIdentity.origin = creatorName !== 'user' && creatorName !== 'primary' ? creatorName : value;
|
|
1184
|
+
existingIdentity.updatedAt = new Date().toISOString();
|
|
1185
|
+
this.setIdentity(existingIdentity);
|
|
1186
|
+
}
|
|
1187
|
+
if (isName) {
|
|
1188
|
+
const priority = isPreferredAddress ? 85 : 75;
|
|
1189
|
+
const directiveId = isPreferredAddress ? `dir-prefaddr-${claim.id || Date.now()}` : `dir-name-${claim.id || Date.now()}`;
|
|
1190
|
+
// If preferred address, supersede/revoke older address directives for this subject
|
|
1191
|
+
if (isPreferredAddress) {
|
|
1192
|
+
const existingDirectives = this.getAllDirectives(companionId);
|
|
1193
|
+
for (const d of existingDirectives) {
|
|
1194
|
+
const lowerDir = (d.directive || '').toLowerCase();
|
|
1195
|
+
const lowerSubj = rawSubject.toLowerCase();
|
|
1196
|
+
if ((d.status === 'active' || d.status === 'pending') &&
|
|
1197
|
+
d.id !== directiveId &&
|
|
1198
|
+
(lowerDir.startsWith(`address ${lowerSubj} as`) ||
|
|
1199
|
+
lowerDir.startsWith('address the user as') ||
|
|
1200
|
+
lowerDir.startsWith('address actor:user as') ||
|
|
1201
|
+
lowerDir.startsWith('address user as'))) {
|
|
1202
|
+
this.supersedeDirective(d.id, companionId);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
this.commitDirective({
|
|
1207
|
+
id: directiveId,
|
|
1208
|
+
companionId,
|
|
1209
|
+
priority,
|
|
1210
|
+
directive: `Address ${rawSubject} as ${value}`,
|
|
1211
|
+
status: 'active',
|
|
1212
|
+
category: 'relational',
|
|
1213
|
+
createdAt: new Date().toISOString(),
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
// 3. Behavioral rule claim
|
|
1219
|
+
if (predicate === 'behavioral_rule' || predicate === 'rule') {
|
|
1220
|
+
this.commitDirective({
|
|
1221
|
+
id: `dir-rule-${claim.id || Date.now()}`,
|
|
1222
|
+
companionId,
|
|
1223
|
+
priority: 60,
|
|
1224
|
+
directive: value,
|
|
1225
|
+
status: 'active',
|
|
1226
|
+
category: 'behavioral',
|
|
1227
|
+
createdAt: new Date().toISOString(),
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
rejectClaim(id, companionId) {
|
|
1232
|
+
if (companionId) {
|
|
1233
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'rejected' WHERE id = ? AND companion_id = ?");
|
|
1234
|
+
stmt.run(id, companionId);
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'rejected' WHERE id = ?");
|
|
1238
|
+
stmt.run(id);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
revokeClaim(id, companionId) {
|
|
1242
|
+
if (companionId) {
|
|
1243
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'revoked' WHERE id = ? AND companion_id = ?");
|
|
1244
|
+
stmt.run(id, companionId);
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'revoked' WHERE id = ?");
|
|
1248
|
+
stmt.run(id);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
expireClaim(id, companionId) {
|
|
1252
|
+
if (companionId) {
|
|
1253
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'expired' WHERE id = ? AND companion_id = ?");
|
|
1254
|
+
stmt.run(id, companionId);
|
|
1255
|
+
}
|
|
1256
|
+
else {
|
|
1257
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'expired' WHERE id = ?");
|
|
1258
|
+
stmt.run(id);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
markClaimSessionOnly(id, companionId) {
|
|
1262
|
+
if (companionId) {
|
|
1263
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'session-only' WHERE id = ? AND companion_id = ?");
|
|
1264
|
+
stmt.run(id, companionId);
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
const stmt = this.db.prepare("UPDATE memory_claims SET status = 'session-only' WHERE id = ?");
|
|
1268
|
+
stmt.run(id);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
searchClaims(companionId, query, limit = 20) {
|
|
1272
|
+
const cleanTokens = query
|
|
1273
|
+
.replace(/[^\p{L}\p{N}\s_]/gu, ' ')
|
|
1274
|
+
.trim()
|
|
1275
|
+
.split(/\s+/)
|
|
1276
|
+
.filter((t) => t.length > 0)
|
|
1277
|
+
.map((t) => `"${t.replace(/"/g, '""')}"`);
|
|
1278
|
+
if (cleanTokens.length === 0)
|
|
1279
|
+
return [];
|
|
1280
|
+
const ftsQuery = cleanTokens.join(' OR ');
|
|
1281
|
+
const stmt = this.db.prepare(`
|
|
1282
|
+
SELECT c.* FROM memory_claims c
|
|
1283
|
+
JOIN memory_search s ON c.rowid = s.rowid
|
|
1284
|
+
WHERE c.companion_id = ? AND LOWER(c.status) = 'approved' AND memory_search MATCH ?
|
|
1285
|
+
ORDER BY rank
|
|
1286
|
+
LIMIT ?
|
|
1287
|
+
`);
|
|
1288
|
+
return stmt.all(companionId, ftsQuery, limit).map((row) => this.rowToMemoryClaim(row));
|
|
1289
|
+
}
|
|
1290
|
+
getPendingClaims(companionId, limit = 50) {
|
|
1291
|
+
const stmt = this.db.prepare("SELECT * FROM memory_claims WHERE companion_id = ? AND LOWER(status) = 'pending' ORDER BY asserted_at DESC LIMIT ?");
|
|
1292
|
+
return stmt.all(companionId, limit).map((row) => this.rowToMemoryClaim(row));
|
|
1293
|
+
}
|
|
1294
|
+
getApprovedClaims(companionId, limit = 50) {
|
|
1295
|
+
const stmt = this.db.prepare("SELECT * FROM memory_claims WHERE companion_id = ? AND LOWER(status) = 'approved' ORDER BY asserted_at DESC LIMIT ?");
|
|
1296
|
+
return stmt.all(companionId, limit).map((row) => this.rowToMemoryClaim(row));
|
|
1297
|
+
}
|
|
1298
|
+
getAllClaims(companionId, limit = 100) {
|
|
1299
|
+
const stmt = companionId
|
|
1300
|
+
? this.db.prepare("SELECT * FROM memory_claims WHERE companion_id = ? ORDER BY asserted_at DESC LIMIT ?")
|
|
1301
|
+
: this.db.prepare("SELECT * FROM memory_claims ORDER BY asserted_at DESC LIMIT ?");
|
|
1302
|
+
const rows = companionId ? stmt.all(companionId, limit) : stmt.all(limit);
|
|
1303
|
+
return rows.map((row) => this.rowToMemoryClaim(row));
|
|
1304
|
+
}
|
|
1305
|
+
getClaim(id) {
|
|
1306
|
+
const stmt = this.db.prepare("SELECT * FROM memory_claims WHERE id = ?");
|
|
1307
|
+
const row = stmt.get(id);
|
|
1308
|
+
if (!row)
|
|
1309
|
+
return undefined;
|
|
1310
|
+
return this.rowToMemoryClaim(row);
|
|
1311
|
+
}
|
|
1312
|
+
resetMemory(companionId) {
|
|
1313
|
+
const deleteClaims = this.db.prepare("DELETE FROM memory_claims WHERE companion_id = ?");
|
|
1314
|
+
deleteClaims.run(companionId);
|
|
1315
|
+
const deleteEvents = this.db.prepare("DELETE FROM memory_events WHERE companion_id = ?");
|
|
1316
|
+
deleteEvents.run(companionId);
|
|
1317
|
+
}
|
|
1318
|
+
// --- System Logs ---
|
|
1319
|
+
insertLog(entry) {
|
|
1320
|
+
const id = entry.id || crypto.randomUUID();
|
|
1321
|
+
const companionId = entry.companionId || 'default';
|
|
1322
|
+
const level = (entry.level || 'info').toLowerCase();
|
|
1323
|
+
const subsystem = (entry.subsystem || 'system').toLowerCase();
|
|
1324
|
+
const message = entry.message || '';
|
|
1325
|
+
const metadata = entry.metadata ? JSON.stringify(entry.metadata) : null;
|
|
1326
|
+
const createdAt = entry.createdAt || new Date().toISOString();
|
|
1327
|
+
const stmt = this.db.prepare(`
|
|
1328
|
+
INSERT INTO system_logs (id, companion_id, level, subsystem, message, metadata, created_at)
|
|
1329
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1330
|
+
`);
|
|
1331
|
+
stmt.run(id, companionId, level, subsystem, message, metadata, createdAt);
|
|
1332
|
+
// Prune logs beyond 5000 per companion to prevent unbounded growth
|
|
1333
|
+
try {
|
|
1334
|
+
this.db.prepare(`
|
|
1335
|
+
DELETE FROM system_logs
|
|
1336
|
+
WHERE companion_id = ? AND rowid NOT IN (
|
|
1337
|
+
SELECT rowid FROM system_logs WHERE companion_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 5000
|
|
1338
|
+
)
|
|
1339
|
+
`).run(companionId, companionId);
|
|
1340
|
+
}
|
|
1341
|
+
catch {
|
|
1342
|
+
// Best effort pruning
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
id,
|
|
1346
|
+
companionId,
|
|
1347
|
+
level,
|
|
1348
|
+
subsystem,
|
|
1349
|
+
message,
|
|
1350
|
+
metadata: entry.metadata,
|
|
1351
|
+
createdAt,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
queryLogs(options = {}) {
|
|
1355
|
+
const conditions = [];
|
|
1356
|
+
const params = [];
|
|
1357
|
+
if (options.companionId) {
|
|
1358
|
+
conditions.push('companion_id = ?');
|
|
1359
|
+
params.push(options.companionId);
|
|
1360
|
+
}
|
|
1361
|
+
if (options.level && options.level !== 'all') {
|
|
1362
|
+
conditions.push('level = ?');
|
|
1363
|
+
params.push(options.level.toLowerCase());
|
|
1364
|
+
}
|
|
1365
|
+
if (options.subsystem && options.subsystem !== 'all') {
|
|
1366
|
+
conditions.push('subsystem = ?');
|
|
1367
|
+
params.push(options.subsystem.toLowerCase());
|
|
1368
|
+
}
|
|
1369
|
+
if (options.q && options.q.trim().length > 0) {
|
|
1370
|
+
conditions.push('(message LIKE ? OR metadata LIKE ?)');
|
|
1371
|
+
const term = `%${options.q.trim()}%`;
|
|
1372
|
+
params.push(term, term);
|
|
1373
|
+
}
|
|
1374
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
1375
|
+
const limit = Math.min(Math.max(Number(options.limit) || 100, 1), 1000);
|
|
1376
|
+
const offset = Math.max(Number(options.offset) || 0, 0);
|
|
1377
|
+
const query = `
|
|
1378
|
+
SELECT id, companion_id, level, subsystem, message, metadata, created_at
|
|
1379
|
+
FROM system_logs
|
|
1380
|
+
${whereClause}
|
|
1381
|
+
ORDER BY created_at DESC, rowid DESC
|
|
1382
|
+
LIMIT ? OFFSET ?
|
|
1383
|
+
`;
|
|
1384
|
+
params.push(limit, offset);
|
|
1385
|
+
const rows = this.db.prepare(query).all(...params);
|
|
1386
|
+
return rows.map((row) => ({
|
|
1387
|
+
id: row.id,
|
|
1388
|
+
companionId: row.companion_id,
|
|
1389
|
+
level: row.level,
|
|
1390
|
+
subsystem: row.subsystem,
|
|
1391
|
+
message: row.message,
|
|
1392
|
+
metadata: row.metadata ? safeJsonParse(row.metadata) : undefined,
|
|
1393
|
+
createdAt: row.created_at,
|
|
1394
|
+
}));
|
|
1395
|
+
}
|
|
1396
|
+
clearLogs(companionId) {
|
|
1397
|
+
if (companionId) {
|
|
1398
|
+
this.db.prepare('DELETE FROM system_logs WHERE companion_id = ?').run(companionId);
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
this.db.prepare('DELETE FROM system_logs').run();
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
exports.SiduriDatabase = SiduriDatabase;
|