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
|
-
//
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
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 =
|
|
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 =
|
|
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 <
|
|
467
|
-
|
|
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. */
|