soulbridge 0.0.15 → 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.
@@ -20,6 +20,7 @@ export * from './storage/transcript.ts';
20
20
  import type { AffectSignal, ContextRequest, Correction, Memory, ContextResult, Emotion, LlmFn, MemoryType, ObserveResult, ReflectResult, ScoredMemory, Turn, Visibility } from './types.ts';
21
21
  import type { SoulStore } from './store.ts';
22
22
  import type { AffectAnalyzer } from './affect/port.ts';
23
+ export { gateDrafts } from './reflect.ts';
23
24
  export interface SoulOptions {
24
25
  store: SoulStore;
25
26
  soulId: string;
@@ -39,6 +40,11 @@ export interface SoulOptions {
39
40
  llm?: LlmFn;
40
41
  /** Messages longer than this are not sent to the model. Defaults to 2000. */
41
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;
42
48
  }
43
49
  export declare class Soul {
44
50
  readonly store: SoulStore;
@@ -49,6 +55,7 @@ export declare class Soul {
49
55
  readonly analyzer: AffectAnalyzer;
50
56
  readonly llm?: LlmFn;
51
57
  readonly llmMaxChars: number;
58
+ readonly dedupeWindowMs: number;
52
59
  constructor(options: SoulOptions);
53
60
  /** Verb 1: session start. */
54
61
  context(request?: Partial<ContextRequest>): ContextResult;
@@ -91,6 +98,21 @@ export declare class Soul {
91
98
  importance?: number;
92
99
  visibility?: Visibility;
93
100
  }): string;
101
+ /**
102
+ * Explicit write requested by a host or its model (RFC 0005). Unlike
103
+ * `remember`, it passes the same gates as `observe` and dedupes by content,
104
+ * so a tool cannot bypass shape/semantics or flood duplicates.
105
+ */
106
+ rememberExplicit(input: {
107
+ content: string;
108
+ type?: MemoryType;
109
+ importance?: number;
110
+ }): {
111
+ ok: boolean;
112
+ id?: string;
113
+ duplicate?: boolean;
114
+ reason?: string;
115
+ };
94
116
  /** Verb 4: session end. */
95
117
  reflect(turns: Turn[], options?: {
96
118
  updateEmotion?: boolean;
@@ -20,6 +20,7 @@ import { reflect, ruleExtract, nextEmotion } from './reflect.js';
20
20
  import { rank, tokenize } from './scoring.js';
21
21
  import { analyzeAffect, emptyAffect, lexiconAnalyzer } from './affect/analyze.js';
22
22
  import { gateDrafts, llmExtractTurn } from './reflect.js';
23
+ export { gateDrafts } from './reflect.js';
23
24
  export class Soul {
24
25
  store;
25
26
  soulId;
@@ -29,6 +30,7 @@ export class Soul {
29
30
  analyzer;
30
31
  llm;
31
32
  llmMaxChars;
33
+ dedupeWindowMs;
32
34
  constructor(options) {
33
35
  this.store = options.store;
34
36
  this.soulId = options.soulId;
@@ -38,6 +40,7 @@ export class Soul {
38
40
  this.analyzer = options.analyzer ?? lexiconAnalyzer;
39
41
  this.llm = options.llm;
40
42
  this.llmMaxChars = options.llmMaxChars ?? 2000;
43
+ this.dedupeWindowMs = options.dedupeWindowMs ?? 30_000;
41
44
  if (!this.store.identity(this.soulId)) {
42
45
  this.store.ensureSoul({
43
46
  soulId: this.soulId,
@@ -214,6 +217,45 @@ export class Soul {
214
217
  });
215
218
  return id;
216
219
  }
220
+ /**
221
+ * Explicit write requested by a host or its model (RFC 0005). Unlike
222
+ * `remember`, it passes the same gates as `observe` and dedupes by content,
223
+ * so a tool cannot bypass shape/semantics or flood duplicates.
224
+ */
225
+ rememberExplicit(input) {
226
+ const type = input.type ?? 'fact';
227
+ const importance = Math.min(10, Math.max(1, input.importance ?? 7));
228
+ const { accepted, rejected } = gateDrafts([{ type, content: input.content, importance }]);
229
+ if (accepted.length === 0) {
230
+ return { ok: false, reason: rejected[0]?.reason ?? 'rejected by write gates' };
231
+ }
232
+ const content = accepted[0].content;
233
+ const now = Date.now();
234
+ const candidates = this.store.query({
235
+ soulId: this.soulId,
236
+ userId: this.userId,
237
+ status: ['active'],
238
+ includeExpired: 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
+ }
256
+ const id = this.remember(content, { type, importance });
257
+ return { ok: true, id };
258
+ }
217
259
  /** Verb 4: session end. */
218
260
  async reflect(turns, options = {}) {
219
261
  return reflect(this.store, {
@@ -246,3 +288,19 @@ export class Soul {
246
288
  }
247
289
  }
248
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.15",
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",