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.
@@ -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
- process.stdout.write(`${memory.id} ${memory.type.padEnd(9)} imp=${String(memory.importance).padEnd(2)} ${preview}\n`);
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({ soulId: config.soulId, userId: config.userId, limit: 100_000 });
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
- /** Every active memory, newest first. For `list` and for auditing injection. */
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
- /** Every active memory, newest first. For `list` and for auditing injection. */
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({ soulId: this.soulId, userId: this.userId, limit });
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
- * `null` means this build does not track supersession yet — it used to report
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulbridge",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "A personality layer for any agent: continuity, forgetting, and growth across tools.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",