soulbridge 0.0.105 → 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.
- package/dist/packages/core/src/config/labels.d.ts +14 -0
- package/dist/packages/core/src/config/labels.js +14 -0
- package/dist/packages/core/src/context.js +4 -5
- package/dist/packages/core/src/index.d.ts +12 -0
- package/dist/packages/core/src/index.js +16 -16
- package/dist/packages/core/src/sqlite.d.ts +2 -1
- package/dist/packages/core/src/sqlite.js +24 -4
- package/dist/packages/core/src/store.d.ts +5 -0
- package/package.json +1 -1
|
@@ -20,3 +20,17 @@ export declare const CONCLUSION_TYPE = "\u7ED3\u8BBA";
|
|
|
20
20
|
* readers; retyping it in a second filter is how a layer silently drops out.
|
|
21
21
|
*/
|
|
22
22
|
export declare const ANCHOR_TYPES: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* How a fact that came from my own reply is marked in injection (ADR 0035).
|
|
25
|
+
*
|
|
26
|
+
* Two renderers write fact lines — the core's `<memories>` block and the plugin's `<about>`
|
|
27
|
+
* block — and both must use the same marker, or the same fact reads differently depending on
|
|
28
|
+
* which path surfaced it. Declared here, imported by both.
|
|
29
|
+
*/
|
|
30
|
+
export declare const INFERRED_MARKER = "\u6211: ";
|
|
31
|
+
/**
|
|
32
|
+
* One explanation per block, not one per line.
|
|
33
|
+
*
|
|
34
|
+
* 341 of 646 facts are inferred, so a sentence on every line would double the injection.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SOURCE_NOTES = "<source-notes>\u4EE5\u300C\u6211:\u300D\u5F00\u5934\u7684\u662F\u6211\u4E0A\u4E00\u8F6E\u8BF4\u7684\u8BDD\uFF0C\u672A\u7ECF\u7528\u6237\u786E\u8BA4\uFF1B\u6CA1\u6807\u7684\u90FD\u662F\u7528\u6237\u8BF4\u7684\u3002</source-notes>";
|
|
@@ -19,3 +19,17 @@ export const CONCLUSION_TYPE = '结论';
|
|
|
19
19
|
* readers; retyping it in a second filter is how a layer silently drops out.
|
|
20
20
|
*/
|
|
21
21
|
export const ANCHOR_TYPES = ['结论', '教训', '原则', '性格'];
|
|
22
|
+
/**
|
|
23
|
+
* How a fact that came from my own reply is marked in injection (ADR 0035).
|
|
24
|
+
*
|
|
25
|
+
* Two renderers write fact lines — the core's `<memories>` block and the plugin's `<about>`
|
|
26
|
+
* block — and both must use the same marker, or the same fact reads differently depending on
|
|
27
|
+
* which path surfaced it. Declared here, imported by both.
|
|
28
|
+
*/
|
|
29
|
+
export const INFERRED_MARKER = '我: ';
|
|
30
|
+
/**
|
|
31
|
+
* One explanation per block, not one per line.
|
|
32
|
+
*
|
|
33
|
+
* 341 of 646 facts are inferred, so a sentence on every line would double the injection.
|
|
34
|
+
*/
|
|
35
|
+
export const SOURCE_NOTES = '<source-notes>以「我:」开头的是我上一轮说的话,未经用户确认;没标的都是用户说的。</source-notes>';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FACT_STATUS } from './config/status.js';
|
|
2
|
-
import { ANCHOR_TYPES } from './config/labels.js';
|
|
2
|
+
import { ANCHOR_TYPES, INFERRED_MARKER, SOURCE_NOTES } from './config/labels.js';
|
|
3
3
|
import { isActive, roleOf } from './roles.js';
|
|
4
4
|
import { escapeAttr, escapeBlockText } from './inject-text.js';
|
|
5
5
|
import { selectDirectives } from './affect/directives.js';
|
|
@@ -109,7 +109,7 @@ export function buildContext(store, request, now = Date.now()) {
|
|
|
109
109
|
}
|
|
110
110
|
// Escaping can lengthen a line (one `<` is four characters), so cost is charged
|
|
111
111
|
// on the escaped text — the budget has to measure what actually gets injected.
|
|
112
|
-
const marker = anchor.inferred ?
|
|
112
|
+
const marker = anchor.inferred ? INFERRED_MARKER : '';
|
|
113
113
|
const line = `- ${marker}[${escapeBlockText(anchor.label)}] ${escapeBlockText(anchor.text)}`;
|
|
114
114
|
if (anchor.inferred)
|
|
115
115
|
sawInferred = true;
|
|
@@ -124,9 +124,8 @@ export function buildContext(store, request, now = Date.now()) {
|
|
|
124
124
|
}
|
|
125
125
|
if (request.includeAnchors !== false && lines.length > 0)
|
|
126
126
|
sections.push(`<memories>\n${lines.join('\n')}\n</memories>`);
|
|
127
|
-
if (sawInferred)
|
|
128
|
-
sections.push(
|
|
129
|
-
}
|
|
127
|
+
if (sawInferred)
|
|
128
|
+
sections.push(SOURCE_NOTES);
|
|
130
129
|
const text = sections.join('\n\n');
|
|
131
130
|
if (request.track !== false && used.length > 0)
|
|
132
131
|
store.touch(used, now);
|
|
@@ -8,6 +8,7 @@ export * from './types.ts';
|
|
|
8
8
|
export type { SoulStore } from './store.ts';
|
|
9
9
|
export { openSqlite, SCHEMA_VERSION, AFFECT_RETENTION_MS, DbNewerThanBinaryError } from './sqlite.ts';
|
|
10
10
|
export { buildContext, styleDirectives, DEFAULT_MAX_CHARS, AFFECT_BUDGET, AFFECT_WINDOW_MS, } from './context.ts';
|
|
11
|
+
export { INFERRED_MARKER, SOURCE_NOTES } from './config/labels.ts';
|
|
11
12
|
export { reflect, ruleExtract, nextEmotion, isCorrection, correctionRule, detectCorrection, isQuestion, } from './reflect.ts';
|
|
12
13
|
export type { CorrectionLayer, CorrectionSignal, CorrectionVerdict } from './reflect.ts';
|
|
13
14
|
export { rank, scoreMemory, tokenize, relevance, bm25, buildCorpusStats } from './scoring.ts';
|
|
@@ -326,6 +327,17 @@ export declare class Soul {
|
|
|
326
327
|
id: string;
|
|
327
328
|
reason: 'archived' | 'quiet-turns' | 'quiet-time';
|
|
328
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
|
+
};
|
|
329
341
|
/** The thread the previous turn belonged to, for the attribution candidate list. */
|
|
330
342
|
threadBefore(sessionId: string, turnNo: number): string | null;
|
|
331
343
|
/**
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export * from './types.js';
|
|
8
8
|
export { openSqlite, SCHEMA_VERSION, AFFECT_RETENTION_MS, DbNewerThanBinaryError } from './sqlite.js';
|
|
9
9
|
export { buildContext, styleDirectives, DEFAULT_MAX_CHARS, AFFECT_BUDGET, AFFECT_WINDOW_MS, } from './context.js';
|
|
10
|
+
export { INFERRED_MARKER, SOURCE_NOTES } from './config/labels.js';
|
|
10
11
|
export { reflect, ruleExtract, nextEmotion, isCorrection, correctionRule, detectCorrection, isQuestion, } from './reflect.js';
|
|
11
12
|
export { rank, scoreMemory, tokenize, relevance, bm25, buildCorpusStats } from './scoring.js';
|
|
12
13
|
export { parseThreadLine } from './extract.js';
|
|
@@ -534,22 +535,10 @@ export class Soul {
|
|
|
534
535
|
slots,
|
|
535
536
|
});
|
|
536
537
|
facts.push(written);
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
gaps.push(this.store.putGap({
|
|
542
|
-
soulId: this.soulId,
|
|
543
|
-
userId: this.userId,
|
|
544
|
-
aboutEntityId: subject?.value.kind === 'entity' ? subject.value.entityId : undefined,
|
|
545
|
-
aboutFactId: written.id,
|
|
546
|
-
// Whole statement: the gap asks "when did this happen", so the sentence it
|
|
547
|
-
// refers to cannot be a prefix of itself.
|
|
548
|
-
topic: `日期不明:${statement}`,
|
|
549
|
-
tension: 0.3,
|
|
550
|
-
openedAt: at,
|
|
551
|
-
}));
|
|
552
|
-
}
|
|
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.
|
|
553
542
|
return written;
|
|
554
543
|
};
|
|
555
544
|
// Operations carry the merge decision (Mem0's ADD/UPDATE/DELETE); plain facts are
|
|
@@ -817,6 +806,17 @@ export class Soul {
|
|
|
817
806
|
closeIdleThreads(sessionId, options = {}) {
|
|
818
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());
|
|
819
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
|
+
}
|
|
820
820
|
/** The thread the previous turn belonged to, for the attribution candidate list. */
|
|
821
821
|
threadBefore(sessionId, turnNo) {
|
|
822
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. */
|