soulbridge 0.0.14 → 0.0.15
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/packages/cli/src/cli.js +24 -3
- package/dist/packages/core/src/index.d.ts +6 -1
- package/dist/packages/core/src/index.js +15 -2
- package/dist/packages/core/src/sqlite.js +18 -2
- package/dist/packages/core/src/store.d.ts +6 -0
- package/dist/packages/core/src/types.d.ts +12 -16
- package/package.json +1 -1
|
@@ -312,7 +312,9 @@ async function main() {
|
|
|
312
312
|
}
|
|
313
313
|
for (const memory of memories) {
|
|
314
314
|
const preview = memory.content.replace(/\s+/g, ' ').slice(0, 72);
|
|
315
|
-
|
|
315
|
+
const status = memory.status === 'active' ? '' : ` [${memory.status}]`;
|
|
316
|
+
const superseded = memory.supersededBy ? ` -> ${memory.supersededBy}` : '';
|
|
317
|
+
process.stdout.write(`${memory.id} ${memory.type.padEnd(9)} imp=${String(memory.importance).padEnd(2)}${status}${superseded} ${preview}\n`);
|
|
316
318
|
}
|
|
317
319
|
return;
|
|
318
320
|
}
|
|
@@ -324,6 +326,14 @@ async function main() {
|
|
|
324
326
|
out({ ok: soul.forget(id), id });
|
|
325
327
|
return;
|
|
326
328
|
}
|
|
329
|
+
case 'supersede': {
|
|
330
|
+
const [oldId, newId] = positional;
|
|
331
|
+
if (!oldId || !newId)
|
|
332
|
+
throw new Error('usage: soulbridge supersede <old-id> <new-id>');
|
|
333
|
+
const { soul } = open(flags);
|
|
334
|
+
out({ ok: soul.supersede(oldId, newId), oldId, newId });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
327
337
|
case 'corrections': {
|
|
328
338
|
const { soul } = open(flags);
|
|
329
339
|
const rows = soul.corrections(flags.limit ? Number(flags.limit) : 10);
|
|
@@ -344,9 +354,19 @@ async function main() {
|
|
|
344
354
|
const config = ensureConfig();
|
|
345
355
|
const dbPath = resolveDbPath(flags.db);
|
|
346
356
|
const store = openSqlite(dbPath);
|
|
347
|
-
const memories = store.query({
|
|
357
|
+
const memories = store.query({
|
|
358
|
+
soulId: config.soulId,
|
|
359
|
+
userId: config.userId,
|
|
360
|
+
status: ['active', 'superseded'],
|
|
361
|
+
includeExpired: true,
|
|
362
|
+
limit: 100_000,
|
|
363
|
+
});
|
|
348
364
|
const samples = store.recentAffect(config.soulId, config.userId, 0);
|
|
349
365
|
store.close();
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
const activeCount = memories.filter((m) => m.status === 'active' && (m.validUntil === undefined || m.validUntil > now)).length;
|
|
368
|
+
const supersededCount = memories.filter((m) => m.status === 'superseded').length;
|
|
369
|
+
const expiredCount = memories.filter((m) => m.status === 'active' && m.validUntil !== undefined && m.validUntil <= now).length;
|
|
350
370
|
const byType = new Map();
|
|
351
371
|
for (const memory of memories)
|
|
352
372
|
byType.set(memory.type, (byType.get(memory.type) ?? 0) + 1);
|
|
@@ -373,7 +393,7 @@ async function main() {
|
|
|
373
393
|
const lines = [
|
|
374
394
|
`soul ${config.soulId} / ${config.userId}`,
|
|
375
395
|
`db ${dbPath}`,
|
|
376
|
-
`memories ${memories.length} (${typeText})`,
|
|
396
|
+
`memories ${memories.length} (有效 ${activeCount} · 被取代 ${supersededCount} · 已过期 ${expiredCount}) (${typeText})`,
|
|
377
397
|
`affect ${samples.length} samples${samples.length > 0 ? ` · last ${ago(samples[0].at)}` : ''}`,
|
|
378
398
|
plugin
|
|
379
399
|
? `plugin v${plugin.version ?? '?'} · applied ${ago(plugin.appliedAt)} · inject ${plugin.injects ?? 0} · observe ${plugin.observes ?? 0} · fail ${plugin.observeFails ?? 0}`
|
|
@@ -411,6 +431,7 @@ async function main() {
|
|
|
411
431
|
' list [--limit N] 看它记住了什么',
|
|
412
432
|
' corrections [--limit N] 看被纠正过的证据',
|
|
413
433
|
' forget <id> 忘掉一条记忆(墓碑)',
|
|
434
|
+
' supersede <old> <new> 用新记忆取代旧记忆(保留历史)',
|
|
414
435
|
' status 一眼看它是否在工作',
|
|
415
436
|
' stats 看原始状态(JSON)',
|
|
416
437
|
' demo 端到端演示(无需模型)',
|
|
@@ -52,10 +52,15 @@ export declare class Soul {
|
|
|
52
52
|
constructor(options: SoulOptions);
|
|
53
53
|
/** Verb 1: session start. */
|
|
54
54
|
context(request?: Partial<ContextRequest>): ContextResult;
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Audit view: every non-deleted row, including superseded and expired ones.
|
|
57
|
+
* Prompt builders use `context`, which filters both out.
|
|
58
|
+
*/
|
|
56
59
|
list(limit?: number): Memory[];
|
|
57
60
|
/** Tombstone one memory so it never reaches a prompt again. */
|
|
58
61
|
forget(id: string): boolean;
|
|
62
|
+
/** Mark `oldId` replaced by `newId`; neither row is deleted (RFC 0004). */
|
|
63
|
+
supersede(oldId: string, newId: string): boolean;
|
|
59
64
|
/**
|
|
60
65
|
* Record a user correction as evidence (RFC 0003 phase A). Nothing here
|
|
61
66
|
* reaches a prompt; distillation to a strategy is a later phase.
|
|
@@ -59,14 +59,27 @@ export class Soul {
|
|
|
59
59
|
maxItems: request.maxItems,
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Audit view: every non-deleted row, including superseded and expired ones.
|
|
64
|
+
* Prompt builders use `context`, which filters both out.
|
|
65
|
+
*/
|
|
63
66
|
list(limit = 50) {
|
|
64
|
-
return this.store.query({
|
|
67
|
+
return this.store.query({
|
|
68
|
+
soulId: this.soulId,
|
|
69
|
+
userId: this.userId,
|
|
70
|
+
status: ['active', 'superseded'],
|
|
71
|
+
includeExpired: true,
|
|
72
|
+
limit,
|
|
73
|
+
});
|
|
65
74
|
}
|
|
66
75
|
/** Tombstone one memory so it never reaches a prompt again. */
|
|
67
76
|
forget(id) {
|
|
68
77
|
return this.store.forget(id);
|
|
69
78
|
}
|
|
79
|
+
/** Mark `oldId` replaced by `newId`; neither row is deleted (RFC 0004). */
|
|
80
|
+
supersede(oldId, newId) {
|
|
81
|
+
return this.store.supersede(oldId, newId);
|
|
82
|
+
}
|
|
70
83
|
/**
|
|
71
84
|
* Record a user correction as evidence (RFC 0003 phase A). Nothing here
|
|
72
85
|
* reaches a prompt; distillation to a strategy is a later phase.
|
|
@@ -236,12 +236,15 @@ export function openSqlite(file) {
|
|
|
236
236
|
query(spec) {
|
|
237
237
|
const statuses = spec.status ?? ['active'];
|
|
238
238
|
const placeholders = statuses.map(() => '?').join(', ');
|
|
239
|
+
const now = spec.now ?? Date.now();
|
|
240
|
+
// Expiry is a filter, not a deletion: audit views can still see the row.
|
|
241
|
+
const expiry = spec.includeExpired ? '' : 'AND (valid_until IS NULL OR valid_until > ?)';
|
|
239
242
|
const rows = db
|
|
240
243
|
.prepare(`SELECT * FROM memories
|
|
241
244
|
WHERE soul_id = ? AND (user_id = ? OR visibility = 'shared')
|
|
242
|
-
AND status IN (${placeholders}) AND deleted_at IS NULL
|
|
245
|
+
AND status IN (${placeholders}) AND deleted_at IS NULL ${expiry}
|
|
243
246
|
ORDER BY created_at DESC LIMIT ?`)
|
|
244
|
-
.all(spec.soulId, spec.userId, ...statuses, spec.limit ?? 500);
|
|
247
|
+
.all(spec.soulId, spec.userId, ...statuses, ...(spec.includeExpired ? [] : [now]), spec.limit ?? 500);
|
|
245
248
|
const memories = rows.map(rowToMemory);
|
|
246
249
|
return spec.types ? memories.filter((m) => spec.types.includes(m.type)) : memories;
|
|
247
250
|
},
|
|
@@ -258,6 +261,19 @@ export function openSqlite(file) {
|
|
|
258
261
|
.run(now, now, id));
|
|
259
262
|
return Number(result.changes ?? 0) > 0;
|
|
260
263
|
},
|
|
264
|
+
/** Mark `oldId` as replaced by `newId`. Neither row is deleted. */
|
|
265
|
+
supersede(oldId, newId, now = Date.now()) {
|
|
266
|
+
const replacement = db
|
|
267
|
+
.prepare('SELECT id FROM memories WHERE id = ? AND deleted_at IS NULL')
|
|
268
|
+
.get(newId);
|
|
269
|
+
if (!replacement)
|
|
270
|
+
return false;
|
|
271
|
+
const result = withRetry(() => db
|
|
272
|
+
.prepare(`UPDATE memories SET status = 'superseded', superseded_by = ?, updated_at = ?
|
|
273
|
+
WHERE id = ? AND status = 'active' AND deleted_at IS NULL`)
|
|
274
|
+
.run(newId, now, oldId));
|
|
275
|
+
return Number(result.changes ?? 0) > 0;
|
|
276
|
+
},
|
|
261
277
|
saveAffect(soulId, userId, signal, at, deviceId) {
|
|
262
278
|
withRetry(() => {
|
|
263
279
|
db.prepare(`INSERT OR IGNORE INTO affect_samples
|
|
@@ -18,6 +18,12 @@ export interface SoulStore {
|
|
|
18
18
|
* merge, the deletion wins by updatedAt). Returns false when no such active row.
|
|
19
19
|
*/
|
|
20
20
|
forget(id: string, now?: number): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Mark `oldId` as replaced by `newId` (status superseded, supersededBy set).
|
|
23
|
+
* Both rows stay; returns false when the replacement is missing or the old row
|
|
24
|
+
* is not active.
|
|
25
|
+
*/
|
|
26
|
+
supersede(oldId: string, newId: string, now?: number): boolean;
|
|
21
27
|
/** Persist one affect sample (payload only; no raw text). */
|
|
22
28
|
saveAffect(soulId: string, userId: string, signal: AffectSignal, at: number, deviceId?: string): void;
|
|
23
29
|
/** Samples newer than `sinceMs`, newest first. */
|
|
@@ -23,8 +23,11 @@ export interface Memory {
|
|
|
23
23
|
keywords: string[];
|
|
24
24
|
createdAt: number;
|
|
25
25
|
lastRecalledAt?: number;
|
|
26
|
+
/** Written on create; no code filters by it yet (roadmap N3). */
|
|
26
27
|
validFrom?: number;
|
|
28
|
+
/** Reserved for expiry filtering; not enforced yet (roadmap N3). */
|
|
27
29
|
validUntil?: number;
|
|
30
|
+
/** Reserved for conflict replacement; not written yet (roadmap N3). */
|
|
28
31
|
supersededBy?: string;
|
|
29
32
|
/** Last-writer-wins clock for cross-device merge (ADR 0021). */
|
|
30
33
|
updatedAt?: number;
|
|
@@ -81,12 +84,6 @@ export interface ContextResult {
|
|
|
81
84
|
directives: string[];
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
|
-
export interface RecallRequest {
|
|
85
|
-
soulId: string;
|
|
86
|
-
userId: string;
|
|
87
|
-
q: string;
|
|
88
|
-
limit?: number;
|
|
89
|
-
}
|
|
90
87
|
export interface ScoredMemory {
|
|
91
88
|
memory: Memory;
|
|
92
89
|
score: number;
|
|
@@ -97,12 +94,20 @@ export interface QuerySpec {
|
|
|
97
94
|
status?: MemoryStatus[];
|
|
98
95
|
types?: MemoryType[];
|
|
99
96
|
limit?: number;
|
|
97
|
+
/** Clock for expiry checks; defaults to now. */
|
|
98
|
+
now?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Audit views (`list`) need to see expired rows; prompt builders must not.
|
|
101
|
+
* Defaults to false, so expiry applies unless a caller asks otherwise.
|
|
102
|
+
*/
|
|
103
|
+
includeExpired?: boolean;
|
|
100
104
|
}
|
|
101
105
|
export interface ReflectResult {
|
|
102
106
|
memories: number;
|
|
103
107
|
/**
|
|
104
108
|
* How many older memories were replaced by a newer conflicting one.
|
|
105
|
-
*
|
|
109
|
+
*
|
|
110
|
+
ull` means this build does not track supersession yet — it used to report
|
|
106
111
|
* a hardcoded 0, which claimed a capability that did not exist.
|
|
107
112
|
*/
|
|
108
113
|
superseded: number | null;
|
|
@@ -185,12 +190,3 @@ export interface Correction {
|
|
|
185
190
|
rule?: string;
|
|
186
191
|
deviceId?: string;
|
|
187
192
|
}
|
|
188
|
-
export interface CorrectionInput {
|
|
189
|
-
soulId: string;
|
|
190
|
-
userId: string;
|
|
191
|
-
userText: string;
|
|
192
|
-
assistantText?: string;
|
|
193
|
-
rule?: string;
|
|
194
|
-
deviceId?: string;
|
|
195
|
-
at?: number;
|
|
196
|
-
}
|