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
|
|
232
|
-
|
|
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:
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
}
|