soulbridge 0.0.16 → 0.0.17

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.
@@ -40,6 +40,11 @@ export interface SoulOptions {
40
40
  llm?: LlmFn;
41
41
  /** Messages longer than this are not sent to the model. Defaults to 2000. */
42
42
  llmMaxChars?: number;
43
+ /**
44
+ * A paraphrase written this many ms after a similar memory is treated as a
45
+ * duplicate rather than a second row. Set 0 to disable. Defaults to 30s.
46
+ */
47
+ dedupeWindowMs?: number;
43
48
  }
44
49
  export declare class Soul {
45
50
  readonly store: SoulStore;
@@ -50,6 +55,7 @@ export declare class Soul {
50
55
  readonly analyzer: AffectAnalyzer;
51
56
  readonly llm?: LlmFn;
52
57
  readonly llmMaxChars: number;
58
+ readonly dedupeWindowMs: number;
53
59
  constructor(options: SoulOptions);
54
60
  /** Verb 1: session start. */
55
61
  context(request?: Partial<ContextRequest>): ContextResult;
@@ -30,6 +30,7 @@ export class Soul {
30
30
  analyzer;
31
31
  llm;
32
32
  llmMaxChars;
33
+ dedupeWindowMs;
33
34
  constructor(options) {
34
35
  this.store = options.store;
35
36
  this.soulId = options.soulId;
@@ -39,6 +40,7 @@ export class Soul {
39
40
  this.analyzer = options.analyzer ?? lexiconAnalyzer;
40
41
  this.llm = options.llm;
41
42
  this.llmMaxChars = options.llmMaxChars ?? 2000;
43
+ this.dedupeWindowMs = options.dedupeWindowMs ?? 30_000;
42
44
  if (!this.store.identity(this.soulId)) {
43
45
  this.store.ensureSoul({
44
46
  soulId: this.soulId,
@@ -228,17 +230,29 @@ export class Soul {
228
230
  return { ok: false, reason: rejected[0]?.reason ?? 'rejected by write gates' };
229
231
  }
230
232
  const content = accepted[0].content;
231
- const existing = this.store
232
- .query({
233
+ const now = Date.now();
234
+ const candidates = this.store.query({
233
235
  soulId: this.soulId,
234
236
  userId: this.userId,
235
237
  status: ['active'],
236
238
  includeExpired: true,
237
- limit: 10_000,
238
- })
239
- .find((memory) => memory.content.trim() === content);
240
- if (existing)
241
- return { ok: true, id: existing.id, duplicate: true };
239
+ limit: 50,
240
+ });
241
+ // Same content anywhere: the exact duplicate.
242
+ const same = candidates.find((memory) => memory.content.trim() === content);
243
+ if (same)
244
+ return { ok: true, id: same.id, duplicate: true };
245
+ // A paraphrase of something just written (usually the automatic observer
246
+ // moments earlier) must not become a second row: a shared run of characters
247
+ // inside the dedupe window is treated as the same statement.
248
+ const window = this.dedupeWindowMs;
249
+ if (window > 0) {
250
+ const recent = candidates.filter((memory) => now - memory.createdAt <= window);
251
+ const overlapping = recent.find((memory) => sharesRun(memory.content, content, MIN_SHARED_RUN));
252
+ if (overlapping) {
253
+ return { ok: true, id: overlapping.id, duplicate: true, reason: 'already recorded this turn' };
254
+ }
255
+ }
242
256
  const id = this.remember(content, { type, importance });
243
257
  return { ok: true, id };
244
258
  }
@@ -274,3 +288,19 @@ export class Soul {
274
288
  }
275
289
  }
276
290
  }
291
+ /** Shortest shared character run that means "the same statement" (RFC 0005). */
292
+ const MIN_SHARED_RUN = 4;
293
+ function sharesRun(a, b, minRun) {
294
+ const left = a.trim();
295
+ const right = b.trim();
296
+ if (left.length === 0 || right.length === 0)
297
+ return false;
298
+ const window = Math.min(left.length, right.length);
299
+ for (let len = window; len >= minRun; len -= 1) {
300
+ for (let i = 0; i + len <= right.length; i += 1) {
301
+ if (left.includes(right.slice(i, i + len)))
302
+ return true;
303
+ }
304
+ }
305
+ return false;
306
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulbridge",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
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",