soulbridge 0.0.106 → 0.0.107

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.
@@ -327,6 +327,17 @@ export declare class Soul {
327
327
  id: string;
328
328
  reason: 'archived' | 'quiet-turns' | 'quiet-time';
329
329
  }>;
330
+ /**
331
+ * Housekeeping that should run once per turn: close open questions nobody answered.
332
+ *
333
+ * A gap is only worth asking about while it is fresh; 14 days of silence means the moment
334
+ * passed. Closing is not deleting — the row keeps its topic, opened_at and the reason.
335
+ */
336
+ sweep(now?: number, options?: {
337
+ gapMaxAgeDays?: number;
338
+ }): {
339
+ gapsClosed: number;
340
+ };
330
341
  /** The thread the previous turn belonged to, for the attribution candidate list. */
331
342
  threadBefore(sessionId: string, turnNo: number): string | null;
332
343
  /**
@@ -535,22 +535,10 @@ export class Soul {
535
535
  slots,
536
536
  });
537
537
  facts.push(written);
538
- // Known to have happened, unknown when: keep the memory and turn the missing
539
- // date into something to ask about later, instead of inventing one.
540
- if (holder === 'world' && written.atPrecision === 'unknown') {
541
- const subject = slots.find((slot) => slot.value.kind === 'entity');
542
- gaps.push(this.store.putGap({
543
- soulId: this.soulId,
544
- userId: this.userId,
545
- aboutEntityId: subject?.value.kind === 'entity' ? subject.value.entityId : undefined,
546
- aboutFactId: written.id,
547
- // Whole statement: the gap asks "when did this happen", so the sentence it
548
- // refers to cannot be a prefix of itself.
549
- topic: `日期不明:${statement}`,
550
- tension: 0.3,
551
- openedAt: at,
552
- }));
553
- }
538
+ // A missing date is a property of the fact (`atPrecision: unknown`), not an open
539
+ // question. Turning every undated fact into a gap produced 223 open gaps in two days,
540
+ // of which 78% were '日期不明:…' — they can never be closed, and they bury the gaps
541
+ // that are worth asking about. The fact stays; the question is not created.
554
542
  return written;
555
543
  };
556
544
  // Operations carry the merge decision (Mem0's ADD/UPDATE/DELETE); plain facts are
@@ -818,6 +806,17 @@ export class Soul {
818
806
  closeIdleThreads(sessionId, options = {}) {
819
807
  return this.store.closeIdleThreads(this.soulId, this.userId, sessionId, options.idleTurns ?? 6, options.idleMs ?? 24 * 60 * 60_000, options.archivedSessionIds ?? [], options.at ?? Date.now());
820
808
  }
809
+ /**
810
+ * Housekeeping that should run once per turn: close open questions nobody answered.
811
+ *
812
+ * A gap is only worth asking about while it is fresh; 14 days of silence means the moment
813
+ * passed. Closing is not deleting — the row keeps its topic, opened_at and the reason.
814
+ */
815
+ sweep(now = Date.now(), options = {}) {
816
+ const maxAgeDays = options.gapMaxAgeDays ?? 14;
817
+ const gapsClosed = this.store.closeStaleGaps(this.soulId, this.userId, now - maxAgeDays * 86_400_000, 'expired');
818
+ return { gapsClosed };
819
+ }
821
820
  /** The thread the previous turn belonged to, for the attribution candidate list. */
822
821
  threadBefore(sessionId, turnNo) {
823
822
  return this.store.threadBefore(sessionId, turnNo);
@@ -1,6 +1,6 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import type { SoulStore } from './store.ts';
3
- export declare const SCHEMA_VERSION = 11;
3
+ export declare const SCHEMA_VERSION = 12;
4
4
  export declare const AFFECT_RETENTION_MS: number;
5
5
  export declare class DbNewerThanBinaryError extends Error {
6
6
  readonly code = "DB_NEWER_THAN_BINARY";
@@ -13,6 +13,7 @@ export declare class DbNewerThanBinaryError extends Error {
13
13
  * old table stays as a read-only legacy copy.
14
14
  */
15
15
  /** v6 → v7: turn threads (a topic's turns need not be adjacent) plus turns.thread_id. */
16
+ export declare function upgradeToV12(db: DatabaseSync): void;
16
17
  export declare function upgradeToV11(db: DatabaseSync): void;
17
18
  export declare function upgradeToV10(db: DatabaseSync): void;
18
19
  export declare function upgradeToV9(db: DatabaseSync): void;
@@ -12,7 +12,7 @@ import { DEFAULT_EMOTION } from './types.js';
12
12
  import { FACT_STATUS, MEMORY_STATUS } from './config/status.js';
13
13
  import { FACT_ROLES } from './config/domains.js';
14
14
  import { bm25, buildCorpusStats, tokenize } from './scoring.js';
15
- export const SCHEMA_VERSION = 11;
15
+ export const SCHEMA_VERSION = 12;
16
16
  /** How many recent pairs are considered before scoring. Bounded so recall stays O(candidates). */
17
17
  const RECALL_CANDIDATES = 500;
18
18
  export const AFFECT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
@@ -212,7 +212,8 @@ CREATE TABLE IF NOT EXISTS gaps (
212
212
  status TEXT NOT NULL DEFAULT 'open',
213
213
  opened_at INTEGER NOT NULL,
214
214
  closed_at INTEGER,
215
- closed_by_fact_id TEXT
215
+ closed_by_fact_id TEXT,
216
+ close_reason TEXT
216
217
  );
217
218
  CREATE INDEX IF NOT EXISTS idx_gaps_open ON gaps(soul_id, user_id, status, tension DESC);
218
219
 
@@ -299,6 +300,14 @@ function upgradeToV3(db) {
299
300
  * old table stays as a read-only legacy copy.
300
301
  */
301
302
  /** v6 → v7: turn threads (a topic's turns need not be adjacent) plus turns.thread_id. */
303
+ export function upgradeToV12(db) {
304
+ // A missing date is a property of the fact, not an open question. Existing gaps created by
305
+ // that old rule are closed with the reason, so the remaining open gaps are the ones that
306
+ // were worth asking about.
307
+ ensureColumn(db, 'gaps', 'close_reason', 'TEXT');
308
+ db.prepare(`UPDATE gaps SET status = 'closed', closed_at = ?, close_reason = 'time-unknown-not-a-gap'
309
+ WHERE status = 'open' AND topic LIKE '日期不明:%'`).run(Date.now());
310
+ }
302
311
  export function upgradeToV11(db) {
303
312
  // 原则候选 stored a decision as a slot; it is role+status now. The row becomes a lesson
304
313
  // candidate — held and shown to the classifier, injected nowhere until it is placed.
@@ -463,8 +472,8 @@ export function openSqlite(file) {
463
472
  upgradeToV7(db);
464
473
  if (version < 8)
465
474
  upgradeToV8(db);
466
- if (version < 11)
467
- upgradeToV11(db);
475
+ if (version < 12)
476
+ upgradeToV12(db);
468
477
  // v6 adds tables only; SCHEMA creates them on every open, nothing to copy.
469
478
  setVersion(db, SCHEMA_VERSION);
470
479
  db.exec('COMMIT');
@@ -918,6 +927,17 @@ export function openSqlite(file) {
918
927
  WHERE id = ?`)
919
928
  .run(at, closedByFactId ?? null, id));
920
929
  },
930
+ closeStaleGaps(soulId, userId, beforeAt, reason) {
931
+ let changed = 0;
932
+ withRetry(() => {
933
+ const result = db
934
+ .prepare(`UPDATE gaps SET status = 'closed', closed_at = ?, close_reason = ?
935
+ WHERE soul_id = ? AND user_id = ? AND status = 'open' AND opened_at < ?`)
936
+ .run(Date.now(), reason, soulId, userId, beforeAt);
937
+ changed = Number(result.changes);
938
+ });
939
+ return changed;
940
+ },
921
941
  turn(id) {
922
942
  const row = db.prepare('SELECT * FROM turns WHERE id = ?').get(id);
923
943
  return row ? rowToTurnPair(row) : null;
@@ -103,6 +103,11 @@ export interface SoulStore {
103
103
  putGap(spec: GapSpec): Gap;
104
104
  openGaps(soulId: string, userId: string, limit?: number): Gap[];
105
105
  closeGap(id: string, closedByFactId?: string, at?: number): void;
106
+ /**
107
+ * Close every open gap older than `beforeAt`, recording why. Returns how many were closed.
108
+ * An open question that nobody answered in weeks is not a question any more.
109
+ */
110
+ closeStaleGaps(soulId: string, userId: string, beforeAt: number, reason: string): number;
106
111
  /** Corrections the user made, newest first — the objective signal for behaviour changes. */
107
112
  corrections(soulId: string, userId: string, limit?: number): Correction[];
108
113
  /** Everything a portable bundle needs, scoped to one soul/user. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulbridge",
3
- "version": "0.0.106",
3
+ "version": "0.0.107",
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",