@lemoncat7/dsh-partner 1.11.2 → 1.12.0
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/README.md +13 -1
- package/docs/memory-quality.md +115 -0
- package/docs/releases/1.12.0.md +25 -0
- package/lib/agent-runtime.d.ts +1 -1
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +16 -11
- package/lib/agent-runtime.js.map +1 -1
- package/lib/api/features/memory-layers-api.d.ts +5 -0
- package/lib/api/features/memory-layers-api.d.ts.map +1 -0
- package/lib/api/features/memory-layers-api.js +42 -0
- package/lib/api/features/memory-layers-api.js.map +1 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +5 -1
- package/lib/api.js.map +1 -1
- package/lib/client-api.d.ts +2 -0
- package/lib/client-api.d.ts.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +2894 -2553
- package/lib/client.js.map +4 -4
- package/lib/concern-store.d.ts +1 -0
- package/lib/concern-store.d.ts.map +1 -1
- package/lib/concern-store.js +17 -0
- package/lib/concern-store.js.map +1 -1
- package/lib/daily-review.d.ts +1 -0
- package/lib/daily-review.d.ts.map +1 -1
- package/lib/daily-review.js +14 -3
- package/lib/daily-review.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -1
- package/lib/memory-artifacts.d.ts +42 -0
- package/lib/memory-artifacts.d.ts.map +1 -0
- package/lib/memory-artifacts.js +120 -0
- package/lib/memory-artifacts.js.map +1 -0
- package/lib/memory-domain.d.ts +10 -0
- package/lib/memory-domain.d.ts.map +1 -1
- package/lib/memory-journal.d.ts +16 -0
- package/lib/memory-journal.d.ts.map +1 -0
- package/lib/memory-journal.js +51 -0
- package/lib/memory-journal.js.map +1 -0
- package/lib/memory-prompt-context.d.ts.map +1 -1
- package/lib/memory-prompt-context.js +2 -1
- package/lib/memory-prompt-context.js.map +1 -1
- package/lib/memory-quality.d.ts +4 -0
- package/lib/memory-quality.d.ts.map +1 -0
- package/lib/memory-quality.js +16 -0
- package/lib/memory-quality.js.map +1 -0
- package/lib/memory-reflection.d.ts +15 -2
- package/lib/memory-reflection.d.ts.map +1 -1
- package/lib/memory-reflection.js +101 -17
- package/lib/memory-reflection.js.map +1 -1
- package/lib/memory-retrieval.d.ts +5 -0
- package/lib/memory-retrieval.d.ts.map +1 -0
- package/lib/memory-retrieval.js +20 -0
- package/lib/memory-retrieval.js.map +1 -0
- package/lib/memory-store.d.ts +26 -2
- package/lib/memory-store.d.ts.map +1 -1
- package/lib/memory-store.js +142 -50
- package/lib/memory-store.js.map +1 -1
- package/lib/memory-worker.d.ts +25 -0
- package/lib/memory-worker.d.ts.map +1 -0
- package/lib/memory-worker.js +42 -0
- package/lib/memory-worker.js.map +1 -0
- package/lib/profile-domain.d.ts.map +1 -1
- package/lib/profile-domain.js +12 -4
- package/lib/profile-domain.js.map +1 -1
- package/lib/ui/memory/automation-settings.d.ts +8 -0
- package/lib/ui/memory/automation-settings.d.ts.map +1 -0
- package/lib/ui/memory/automation-settings.js +60 -0
- package/lib/ui/memory/automation-settings.js.map +1 -0
- package/lib/ui/memory/concern-board.d.ts +12 -0
- package/lib/ui/memory/concern-board.d.ts.map +1 -0
- package/lib/ui/memory/concern-board.js +129 -0
- package/lib/ui/memory/concern-board.js.map +1 -0
- package/lib/ui/memory/concern-panel.d.ts +7 -0
- package/lib/ui/memory/concern-panel.d.ts.map +1 -0
- package/lib/ui/memory/concern-panel.js +62 -0
- package/lib/ui/memory/concern-panel.js.map +1 -0
- package/lib/ui/memory/experience-view.d.ts +8 -0
- package/lib/ui/memory/experience-view.d.ts.map +1 -0
- package/lib/ui/memory/experience-view.js +52 -0
- package/lib/ui/memory/experience-view.js.map +1 -0
- package/lib/ui/memory/history-view.d.ts +6 -0
- package/lib/ui/memory/history-view.d.ts.map +1 -0
- package/lib/ui/memory/history-view.js +40 -0
- package/lib/ui/memory/history-view.js.map +1 -0
- package/lib/ui/memory/memory-cards.d.ts +14 -0
- package/lib/ui/memory/memory-cards.d.ts.map +1 -0
- package/lib/ui/memory/memory-cards.js +17 -0
- package/lib/ui/memory/memory-cards.js.map +1 -0
- package/lib/ui/memory/memory-detail.d.ts +8 -0
- package/lib/ui/memory/memory-detail.d.ts.map +1 -0
- package/lib/ui/memory/memory-detail.js +45 -0
- package/lib/ui/memory/memory-detail.js.map +1 -0
- package/lib/ui/memory/memory-graph.d.ts +8 -0
- package/lib/ui/memory/memory-graph.d.ts.map +1 -0
- package/lib/ui/memory/memory-graph.js +43 -0
- package/lib/ui/memory/memory-graph.js.map +1 -0
- package/lib/ui/memory/memory-library.d.ts +9 -0
- package/lib/ui/memory/memory-library.d.ts.map +1 -0
- package/lib/ui/memory/memory-library.js +38 -0
- package/lib/ui/memory/memory-library.js.map +1 -0
- package/lib/ui/memory/memory-panel.d.ts +10 -0
- package/lib/ui/memory/memory-panel.d.ts.map +1 -0
- package/lib/ui/memory/memory-panel.js +48 -0
- package/lib/ui/memory/memory-panel.js.map +1 -0
- package/lib/ui/memory/types.d.ts +44 -0
- package/lib/ui/memory/types.d.ts.map +1 -0
- package/lib/ui/memory/types.js +2 -0
- package/lib/ui/memory/types.js.map +1 -0
- package/lib/ui/memory/use-memory-resource.d.ts +9 -0
- package/lib/ui/memory/use-memory-resource.d.ts.map +1 -0
- package/lib/ui/memory/use-memory-resource.js +65 -0
- package/lib/ui/memory/use-memory-resource.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function memoryTerms(value) {
|
|
2
|
+
const words = value.normalize('NFKC').toLocaleLowerCase().slice(0, 4000).match(/[a-z0-9_]+|\p{Script=Han}+/gu) ?? [];
|
|
3
|
+
return [...new Set(words.flatMap(word => /^\p{Script=Han}+$/u.test(word) && word.length > 2
|
|
4
|
+
? Array.from({ length: word.length - 1 }, (_, index) => word.slice(index, index + 2)) : [word]))].slice(0, 80);
|
|
5
|
+
}
|
|
6
|
+
export function memoryRelevance(memory, terms) {
|
|
7
|
+
const haystack = `${memory.subject} ${memory.content}`.normalize('NFKC').toLocaleLowerCase();
|
|
8
|
+
const matches = terms.filter(term => haystack.includes(term)).length;
|
|
9
|
+
return terms.length ? matches / terms.length : 0;
|
|
10
|
+
}
|
|
11
|
+
export function rankMemories(memories, query, now, relevantOnly = true) {
|
|
12
|
+
const terms = memoryTerms(query);
|
|
13
|
+
return memories.map(item => {
|
|
14
|
+
const lexical = memoryRelevance(item, terms);
|
|
15
|
+
const recency = Math.max(0, 1 - (now - item.updatedAt) / (180 * 86_400_000));
|
|
16
|
+
return { item, lexical, score: lexical * .5 + item.importance * .22 + item.confidence * .18 + recency * .1 };
|
|
17
|
+
}).filter(entry => !relevantOnly || entry.lexical > 0)
|
|
18
|
+
.sort((a, b) => b.score - a.score || b.item.updatedAt - a.item.updatedAt).map(entry => entry.item);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=memory-retrieval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-retrieval.js","sourceRoot":"","sources":["../src/memory-retrieval.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,iBAAiB,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,8BAA8B,CAAC,IAAI,EAAE,CAAA;IACpH,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YACzF,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAClH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAkD,EAAE,KAAe;IACjG,MAAM,QAAQ,GAAG,GAAG,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,iBAAiB,EAAE,CAAA;IAC5F,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;IACpE,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;AAClD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAyB,EAAE,KAAa,EAAE,GAAW,EAAE,YAAY,GAAG,IAAI;IACrG,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;IAChC,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACzB,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,GAAG,UAAU,CAAC,CAAC,CAAA;QAC5E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,UAAU,GAAG,GAAG,GAAG,IAAI,CAAC,UAAU,GAAG,GAAG,GAAG,OAAO,GAAG,EAAE,EAAE,CAAA;IAC9G,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,YAAY,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC;SACnD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AACtG,CAAC"}
|
package/lib/memory-store.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ConversationTurn, DailyReflection, DailyReviewResult, DailyReviewTarget, MemoryCandidate, MemoryRecallContext, MemoryRelation, MemoryRelationReviewContext, PartnerMemory, UserProfileSnapshot } from './memory-domain.js';
|
|
2
|
+
import { type MemoryJob } from './memory-journal.js';
|
|
3
|
+
import type { ReflectionResult } from './memory-domain.js';
|
|
4
|
+
import { readScenes, readExperienceDrafts } from './memory-artifacts.js';
|
|
2
5
|
type ReflectionResultLike = {
|
|
3
6
|
daily: Omit<DailyReflection, 'date' | 'companionId' | 'scopeId' | 'updatedAt' | 'turnCount'>;
|
|
4
7
|
memories: MemoryCandidate[];
|
|
@@ -15,13 +18,34 @@ export declare class PartnerMemoryStore {
|
|
|
15
18
|
private readonly writes;
|
|
16
19
|
constructor(root: string, timeZone?: string);
|
|
17
20
|
day(at: number): string;
|
|
21
|
+
enqueue(turn: ConversationTurn): Promise<void>;
|
|
22
|
+
hasPendingTurns(companionId: string, scopeId: string): Promise<boolean>;
|
|
23
|
+
memoryLayers(companionId: string, scopeId: string): Promise<{
|
|
24
|
+
scenes: ReturnType<typeof readScenes>;
|
|
25
|
+
experiences: ReturnType<typeof readExperienceDrafts>;
|
|
26
|
+
jobs: Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
attempts: number;
|
|
29
|
+
nextAt: number;
|
|
30
|
+
status: string;
|
|
31
|
+
}>;
|
|
32
|
+
}>;
|
|
33
|
+
reviewExperience(companionId: string, scopeId: string, id: string, version: string, action: 'approved' | 'rejected'): Promise<void>;
|
|
34
|
+
exportExperience(companionId: string, scopeId: string, id: string): Promise<string>;
|
|
35
|
+
history(companionId: string, scopeId: string, before?: number, limit?: number, beforeId?: string): Promise<ConversationTurn[]>;
|
|
36
|
+
retryJob(companionId: string, scopeId: string, id: string): Promise<void>;
|
|
37
|
+
claimJob(companionId: string, now?: number): Promise<MemoryJob | undefined>;
|
|
38
|
+
candidateMemories(companionId: string, scopeId: string, query: string): Promise<PartnerMemory[]>;
|
|
39
|
+
checkpointJob(job: MemoryJob, result: ReflectionResult): Promise<void>;
|
|
40
|
+
settleJob(job: MemoryJob, error?: string): Promise<void>;
|
|
41
|
+
private journal;
|
|
18
42
|
archive(turn: ConversationTurn): Promise<void>;
|
|
19
|
-
consolidate(turn: ConversationTurn, result: ReflectionResultLike): Promise<void>;
|
|
43
|
+
consolidate(turn: ConversationTurn, result: ReflectionResultLike, job?: MemoryJob): Promise<void>;
|
|
20
44
|
recall(companionId: string, scopeId: string, query: string, limit?: number): Promise<PartnerMemory[]>;
|
|
21
45
|
recallContext(companionId: string, scopeId: string, query: string, limit?: number): Promise<MemoryRecallContext>;
|
|
22
46
|
profileSnapshot(companionId: string, scopeId: string): Promise<UserProfileSnapshot>;
|
|
23
47
|
profileSnapshots(companionId: string, knownScopeIds?: string[]): Promise<UserProfileSnapshot[]>;
|
|
24
|
-
recentMemories(companionId: string, limit?: number): Promise<PartnerMemory[]>;
|
|
48
|
+
recentMemories(companionId: string, limit?: number, scopeId?: string): Promise<PartnerMemory[]>;
|
|
25
49
|
legacyHeartbeatFocuses(companionId: string): Promise<LegacyHeartbeatFocus[]>;
|
|
26
50
|
dropLegacyHeartbeatFocuses(companionId: string): Promise<void>;
|
|
27
51
|
updateMemory(companionId: string, memoryId: string, subject: string, content: string): Promise<PartnerMemory>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,eAAe,EAAuD,mBAAmB,EAAE,cAAc,EAAsB,2BAA2B,EAAgB,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":"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"}
|
package/lib/memory-store.js
CHANGED
|
@@ -3,6 +3,9 @@ import { appendFile, mkdir, readFile, readdir, rename, rm } from 'node:fs/promis
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
5
|
import { buildProfileSnapshot, canonicalProfileSubject, isProfileBaselineEntry } from './profile-domain.js';
|
|
6
|
+
import { initializeMemoryJournal, enqueueMemoryJob, claimMemoryJob, checkpointMemoryJob, settleMemoryJob, assertMemoryLease } from './memory-journal.js';
|
|
7
|
+
import { rankMemories, memoryRelevance, memoryTerms } from './memory-retrieval.js';
|
|
8
|
+
import { initializeMemoryArtifacts, applyMemoryArtifacts, readScenes, readExperienceDrafts, readExperienceDraft, reviewExperienceDraft, experienceMarkdown } from './memory-artifacts.js';
|
|
6
9
|
export class PartnerMemoryStore {
|
|
7
10
|
root;
|
|
8
11
|
timeZone;
|
|
@@ -12,6 +15,71 @@ export class PartnerMemoryStore {
|
|
|
12
15
|
this.timeZone = timeZone;
|
|
13
16
|
}
|
|
14
17
|
day(at) { return localDay(at, this.timeZone); }
|
|
18
|
+
async enqueue(turn) {
|
|
19
|
+
await this.journal(turn.companionId, db => enqueueMemoryJob(db, turn));
|
|
20
|
+
}
|
|
21
|
+
async hasPendingTurns(companionId, scopeId) {
|
|
22
|
+
return this.journal(companionId, db => Boolean(db.prepare('SELECT 1 FROM memory_jobs WHERE scope_id=? AND done=0 LIMIT 1').get(scopeId)));
|
|
23
|
+
}
|
|
24
|
+
async memoryLayers(companionId, scopeId) {
|
|
25
|
+
return this.journal(companionId, db => ({
|
|
26
|
+
scenes: readScenes(db, scopeId, this.memoriesForScope(db, companionId, scopeId)),
|
|
27
|
+
experiences: readExperienceDrafts(db, scopeId),
|
|
28
|
+
jobs: db.prepare('SELECT id, attempts, next_at, lease_until FROM memory_jobs WHERE scope_id=? AND done=0 ORDER BY at LIMIT 100')
|
|
29
|
+
.all(scopeId).map(row => ({ id: String(row.id), attempts: Number(row.attempts), nextAt: Number(row.next_at),
|
|
30
|
+
status: Number(row.lease_until) > Date.now() ? 'processing' : Number(row.attempts) > 0 ? 'retrying' : 'pending' })),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
async reviewExperience(companionId, scopeId, id, version, action) {
|
|
34
|
+
await this.journal(companionId, db => reviewExperienceDraft(db, scopeId, id, version, action));
|
|
35
|
+
}
|
|
36
|
+
async exportExperience(companionId, scopeId, id) {
|
|
37
|
+
return this.journal(companionId, db => {
|
|
38
|
+
const draft = readExperienceDraft(db, scopeId, id);
|
|
39
|
+
if (!draft || draft.status !== 'approved')
|
|
40
|
+
throw new Error('只能导出已审核通过的经验草稿');
|
|
41
|
+
return experienceMarkdown(draft);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async history(companionId, scopeId, before = Date.now() + 1, limit = 30, beforeId = '\uffff') {
|
|
45
|
+
return this.journal(companionId, db => db.prepare('SELECT turn_json FROM memory_jobs WHERE scope_id=? AND (at<? OR (at=? AND id<?)) ORDER BY at DESC, id DESC LIMIT ?')
|
|
46
|
+
.all(scopeId, before, before, beforeId, Math.max(1, Math.min(100, limit))).map(row => JSON.parse(String(row.turn_json))));
|
|
47
|
+
}
|
|
48
|
+
async retryJob(companionId, scopeId, id) {
|
|
49
|
+
await this.journal(companionId, db => {
|
|
50
|
+
const result = db.prepare('UPDATE memory_jobs SET next_at=0 WHERE id=? AND scope_id=? AND done=0 AND lease_until<=?').run(id, scopeId, Date.now());
|
|
51
|
+
if (!result.changes)
|
|
52
|
+
throw new Error('任务不存在、已完成或正在处理');
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async claimJob(companionId, now = Date.now()) {
|
|
56
|
+
return this.journal(companionId, db => claimMemoryJob(db, now));
|
|
57
|
+
}
|
|
58
|
+
async candidateMemories(companionId, scopeId, query) {
|
|
59
|
+
return this.journal(companionId, db => {
|
|
60
|
+
const items = this.memoriesForScope(db, companionId, scopeId).filter(item => item.status !== 'superseded');
|
|
61
|
+
const relevant = rankMemories(items, query, Date.now()).slice(0, 12);
|
|
62
|
+
const recent = [...items].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 4);
|
|
63
|
+
return [...new Map([...relevant, ...recent].map(item => [item.id, item])).values()];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async checkpointJob(job, result) {
|
|
67
|
+
await this.journal(job.turn.companionId, db => checkpointMemoryJob(db, job, result));
|
|
68
|
+
}
|
|
69
|
+
async settleJob(job, error) {
|
|
70
|
+
await this.journal(job.turn.companionId, db => settleMemoryJob(db, job, error));
|
|
71
|
+
}
|
|
72
|
+
async journal(companionId, action) {
|
|
73
|
+
return this.serialValue(this.databasePath(companionId), async () => {
|
|
74
|
+
const db = await this.open(companionId);
|
|
75
|
+
try {
|
|
76
|
+
return action(db);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
db.close();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
15
83
|
async archive(turn) {
|
|
16
84
|
const directory = this.scopeDirectory(turn.companionId, turn.scopeId);
|
|
17
85
|
const path = join(directory, 'conversations', `${localDay(turn.at, this.timeZone)}.jsonl`);
|
|
@@ -20,11 +88,19 @@ export class PartnerMemoryStore {
|
|
|
20
88
|
await appendFile(path, `${JSON.stringify(turn)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
21
89
|
});
|
|
22
90
|
}
|
|
23
|
-
async consolidate(turn, result) {
|
|
91
|
+
async consolidate(turn, result, job) {
|
|
24
92
|
await this.serial(this.databasePath(turn.companionId), async () => {
|
|
25
93
|
const database = await this.open(turn.companionId);
|
|
26
94
|
try {
|
|
27
95
|
database.exec('BEGIN IMMEDIATE');
|
|
96
|
+
if (job) {
|
|
97
|
+
assertMemoryLease(database, job);
|
|
98
|
+
const row = database.prepare('SELECT committed FROM memory_jobs WHERE id=?').get(turn.id);
|
|
99
|
+
if (row?.committed) {
|
|
100
|
+
database.exec('COMMIT');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
28
104
|
const existing = this.memoriesForScope(database, turn.companionId, turn.scopeId);
|
|
29
105
|
for (const memory of mergeMemories(existing, result.memories, turn))
|
|
30
106
|
this.upsertMemory(database, memory);
|
|
@@ -36,6 +112,8 @@ export class PartnerMemoryStore {
|
|
|
36
112
|
ON CONFLICT(scope_id, date) DO UPDATE SET summary=excluded.summary, events_json=excluded.events_json,
|
|
37
113
|
open_tasks_json=excluded.open_tasks_json, completed_tasks_json=excluded.completed_tasks_json,
|
|
38
114
|
learnings_json=excluded.learnings_json, updated_at=excluded.updated_at, turn_count=excluded.turn_count`).run(date, turn.companionId, turn.scopeId, compact(result.daily.summary, 1200), json(strings(result.daily.events, 20, 240)), json(strings(result.daily.openTasks, 20, 240)), json(strings(result.daily.completedTasks, 20, 240)), json(strings(result.daily.learnings, 20, 240)), Date.now(), number(previous?.turn_count) + 1);
|
|
115
|
+
if (job)
|
|
116
|
+
database.prepare('UPDATE memory_jobs SET committed=1 WHERE id=?').run(turn.id);
|
|
39
117
|
database.exec('COMMIT');
|
|
40
118
|
}
|
|
41
119
|
catch (error) {
|
|
@@ -54,10 +132,7 @@ export class PartnerMemoryStore {
|
|
|
54
132
|
const rows = database.prepare(`SELECT * FROM memories
|
|
55
133
|
WHERE scope_id = ? AND status = 'active' AND (expires_at IS NULL OR expires_at > ?)
|
|
56
134
|
ORDER BY updated_at DESC`).all(scopeId, now);
|
|
57
|
-
|
|
58
|
-
return rows.map(memoryFromRow).map(item => ({ item, score: recallScore(item, terms, now) }))
|
|
59
|
-
.sort((a, b) => b.score - a.score || b.item.updatedAt - a.item.updatedAt)
|
|
60
|
-
.slice(0, limit).map(entry => entry.item);
|
|
135
|
+
return rankMemories(rows.map(memoryFromRow), query, now).slice(0, Math.max(0, Math.min(100, limit)));
|
|
61
136
|
}
|
|
62
137
|
finally {
|
|
63
138
|
database.close();
|
|
@@ -71,14 +146,19 @@ export class PartnerMemoryStore {
|
|
|
71
146
|
WHERE companion_id = ? AND scope_id = ? AND status = 'active' AND (expires_at IS NULL OR expires_at > ?)
|
|
72
147
|
ORDER BY updated_at DESC`).all(companionId, scopeId, now).map(memoryFromRow);
|
|
73
148
|
const profile = buildProfileSnapshot(companionId, scopeId, memories);
|
|
74
|
-
const baselineIds = new Set(profile.entries.map(entry => entry.id));
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
149
|
+
const baselineIds = new Set([...profile.entries, ...(profile.preferences ?? [])].map(entry => entry.id));
|
|
150
|
+
const scored = rankMemories(memories.filter(item => !baselineIds.has(item.id) && (item.kind !== 'profile' || isProfileBaselineEntry(item))), query, now);
|
|
151
|
+
const recall = expandRecallWithRelations(database, companionId, scopeId, memories, scored, baselineIds, Math.max(0, Math.min(100, limit)));
|
|
152
|
+
const terms = memoryTerms(query);
|
|
153
|
+
const scenes = readScenes(database, scopeId, memories).filter(scene => memoryRelevance({ subject: scene.title, content: scene.summary }, terms) > 0).slice(0, 2);
|
|
154
|
+
const history = (database.prepare(`SELECT id, at, substr(json_extract(turn_json, '$.user'), 1, 4000) AS user,
|
|
155
|
+
substr(json_extract(turn_json, '$.assistant'), 1, 700) AS assistant
|
|
156
|
+
FROM memory_jobs WHERE scope_id=? ORDER BY at DESC LIMIT 100`).all(scopeId))
|
|
157
|
+
.map(row => ({ id: String(row.id), at: Number(row.at), user: String(row.user), assistant: String(row.assistant) }))
|
|
158
|
+
.map(turn => ({ turn, score: memoryRelevance({ subject: '', content: turn.user }, terms) }))
|
|
159
|
+
.filter(item => item.score >= .2).sort((a, b) => b.score - a.score || b.turn.at - a.turn.at).slice(0, 2)
|
|
160
|
+
.map(({ turn }) => ({ id: turn.id, at: turn.at, user: compact(turn.user, 500), assistant: compact(turn.assistant, 700) }));
|
|
161
|
+
return { profile, ...recall, scenes, history };
|
|
82
162
|
}
|
|
83
163
|
finally {
|
|
84
164
|
database.close();
|
|
@@ -88,7 +168,7 @@ export class PartnerMemoryStore {
|
|
|
88
168
|
const database = await this.open(companionId);
|
|
89
169
|
try {
|
|
90
170
|
const rows = database.prepare(`SELECT * FROM memories WHERE companion_id = ? AND scope_id = ?
|
|
91
|
-
AND kind
|
|
171
|
+
AND kind IN ('profile', 'preference') AND status = 'active' AND (expires_at IS NULL OR expires_at > ?)
|
|
92
172
|
ORDER BY updated_at DESC`).all(companionId, scopeId, Date.now());
|
|
93
173
|
return buildProfileSnapshot(companionId, scopeId, rows.map(memoryFromRow));
|
|
94
174
|
}
|
|
@@ -99,7 +179,7 @@ export class PartnerMemoryStore {
|
|
|
99
179
|
async profileSnapshots(companionId, knownScopeIds = []) {
|
|
100
180
|
const database = await this.open(companionId);
|
|
101
181
|
try {
|
|
102
|
-
const rows = database.prepare(`SELECT * FROM memories WHERE companion_id = ? AND kind
|
|
182
|
+
const rows = database.prepare(`SELECT * FROM memories WHERE companion_id = ? AND kind IN ('profile', 'preference')
|
|
103
183
|
AND status = 'active' AND (expires_at IS NULL OR expires_at > ?) ORDER BY updated_at DESC`)
|
|
104
184
|
.all(companionId, Date.now()).map(memoryFromRow);
|
|
105
185
|
const grouped = new Map();
|
|
@@ -115,10 +195,12 @@ export class PartnerMemoryStore {
|
|
|
115
195
|
database.close();
|
|
116
196
|
}
|
|
117
197
|
}
|
|
118
|
-
async recentMemories(companionId, limit = 100) {
|
|
198
|
+
async recentMemories(companionId, limit = 100, scopeId) {
|
|
119
199
|
const database = await this.open(companionId);
|
|
120
200
|
try {
|
|
121
|
-
return
|
|
201
|
+
return (scopeId === undefined
|
|
202
|
+
? database.prepare('SELECT * FROM memories ORDER BY updated_at DESC LIMIT ?').all(limit)
|
|
203
|
+
: database.prepare('SELECT * FROM memories WHERE scope_id=? ORDER BY updated_at DESC LIMIT ?').all(scopeId, limit)).map(memoryFromRow).map(item => item.status === 'active' && item.expiresAt !== undefined && item.expiresAt <= Date.now() ? { ...item, status: 'expired' } : item);
|
|
122
204
|
}
|
|
123
205
|
finally {
|
|
124
206
|
database.close();
|
|
@@ -217,7 +299,7 @@ export class PartnerMemoryStore {
|
|
|
217
299
|
if (!row)
|
|
218
300
|
throw new Error('daily reflection was not found');
|
|
219
301
|
reflection = reflectionFromRow(row);
|
|
220
|
-
memories = this.memoriesForScope(database, target.companionId, target.scopeId).filter(item => item.status === 'active');
|
|
302
|
+
memories = this.memoriesForScope(database, target.companionId, target.scopeId).filter(item => item.status === 'active' && (item.expiresAt === undefined || item.expiresAt > Date.now()));
|
|
221
303
|
const byId = new Map(memories.map(memory => [memory.id, memory]));
|
|
222
304
|
existingRelations = database.prepare('SELECT * FROM memory_relations WHERE companion_id = ? AND scope_id = ? ORDER BY updated_at DESC, confidence DESC')
|
|
223
305
|
.all(target.companionId, target.scopeId).map(relationFromRow).flatMap(relation => {
|
|
@@ -238,19 +320,26 @@ export class PartnerMemoryStore {
|
|
|
238
320
|
return [];
|
|
239
321
|
}
|
|
240
322
|
});
|
|
241
|
-
|
|
323
|
+
const dateAt = Date.parse(target.date);
|
|
324
|
+
const durable = await this.journal(target.companionId, db => (db.prepare('SELECT turn_json FROM memory_jobs WHERE scope_id=? AND at>=? AND at<? ORDER BY at, rowid')
|
|
325
|
+
.all(target.scopeId, dateAt - 86_400_000, dateAt + 2 * 86_400_000)).map(row => JSON.parse(String(row.turn_json))).filter(turn => this.day(turn.at) === target.date));
|
|
326
|
+
return { reflection, memories, existingRelations, turns: [...new Map([...turns, ...durable].map(turn => [turn.id, turn])).values()].sort((a, b) => a.at - b.at) };
|
|
242
327
|
}
|
|
243
328
|
async completeDailyReview(target, result) {
|
|
244
329
|
await this.serial(this.databasePath(target.companionId), async () => {
|
|
245
330
|
const database = await this.open(target.companionId);
|
|
246
331
|
try {
|
|
247
332
|
database.exec('BEGIN IMMEDIATE');
|
|
333
|
+
if (database.prepare('SELECT 1 FROM memory_jobs WHERE scope_id=? AND done=0 LIMIT 1').get(target.scopeId)) {
|
|
334
|
+
throw new Error('终审期间有新的待提炼轮次,稍后重试');
|
|
335
|
+
}
|
|
248
336
|
const existing = this.memoriesForScope(database, target.companionId, target.scopeId);
|
|
249
337
|
const at = Date.now();
|
|
250
338
|
const synthetic = { id: `daily-review-${target.date}`, companionId: target.companionId, scopeId: target.scopeId, sessionId: 'daily-review', at, user: `每日终审 ${target.date}`, assistant: result.daily.summary };
|
|
251
339
|
const merged = mergeMemories(existing, result.memories, synthetic);
|
|
252
340
|
for (const memory of merged)
|
|
253
341
|
this.upsertMemory(database, memory);
|
|
342
|
+
applyMemoryArtifacts(database, target.scopeId, merged, result.scenes ?? [], result.experiences ?? [], at);
|
|
254
343
|
database.prepare(`UPDATE daily_reflections SET summary=?, events_json=?, open_tasks_json=?, completed_tasks_json=?,
|
|
255
344
|
learnings_json=?, updated_at=?, reviewed_at=?, review_attempts=0, review_error=NULL, next_review_at=0
|
|
256
345
|
WHERE scope_id=? AND date=?`).run(compact(result.daily.summary, 1200), json(strings(result.daily.events, 20, 240)), json(strings(result.daily.openTasks, 20, 240)), json(strings(result.daily.completedTasks, 20, 240)), json(strings(result.daily.learnings, 20, 240)), at, at, target.scopeId, target.date);
|
|
@@ -351,8 +440,12 @@ export class PartnerMemoryStore {
|
|
|
351
440
|
const database = await this.open(companionId);
|
|
352
441
|
try {
|
|
353
442
|
database.exec('BEGIN IMMEDIATE');
|
|
354
|
-
database.prepare(
|
|
355
|
-
|
|
443
|
+
database.prepare(`DELETE FROM daily_reflections WHERE companion_id = ? AND updated_at < ? AND reviewed_at IS NOT NULL
|
|
444
|
+
AND NOT EXISTS (SELECT 1 FROM memory_jobs WHERE done=0 AND scope_id=daily_reflections.scope_id)`).run(companionId, cutoff);
|
|
445
|
+
// Retention is for history, not a silent TTL for confirmed durable facts.
|
|
446
|
+
database.prepare(`DELETE FROM memories WHERE companion_id = ? AND updated_at < ? AND locked=0
|
|
447
|
+
AND (status!='active' OR (expires_at IS NOT NULL AND expires_at<=?))`).run(companionId, cutoff, Date.now());
|
|
448
|
+
database.prepare('DELETE FROM memory_jobs WHERE done=1 AND at<?').run(cutoff);
|
|
356
449
|
database.exec('COMMIT');
|
|
357
450
|
}
|
|
358
451
|
catch (error) {
|
|
@@ -367,7 +460,8 @@ export class PartnerMemoryStore {
|
|
|
367
460
|
async open(companionId) {
|
|
368
461
|
await mkdir(this.memoryRoot(companionId), { recursive: true, mode: 0o700 });
|
|
369
462
|
const database = new DatabaseSync(this.databasePath(companionId));
|
|
370
|
-
|
|
463
|
+
try {
|
|
464
|
+
database.exec(`PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;
|
|
371
465
|
CREATE TABLE IF NOT EXISTS memories (
|
|
372
466
|
id TEXT PRIMARY KEY, companion_id TEXT NOT NULL, scope_id TEXT NOT NULL, kind TEXT NOT NULL,
|
|
373
467
|
subject TEXT NOT NULL, content TEXT NOT NULL, status TEXT NOT NULL, confidence REAL NOT NULL,
|
|
@@ -391,11 +485,18 @@ export class PartnerMemoryStore {
|
|
|
391
485
|
);
|
|
392
486
|
CREATE INDEX IF NOT EXISTS relations_companion_scope ON memory_relations(companion_id, scope_id, updated_at DESC);
|
|
393
487
|
PRAGMA user_version = 1;`);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
488
|
+
ensureColumn(database, 'daily_reflections', 'reviewed_at', 'INTEGER');
|
|
489
|
+
ensureColumn(database, 'daily_reflections', 'review_attempts', 'INTEGER NOT NULL DEFAULT 0');
|
|
490
|
+
ensureColumn(database, 'daily_reflections', 'review_error', 'TEXT');
|
|
491
|
+
ensureColumn(database, 'daily_reflections', 'next_review_at', 'INTEGER NOT NULL DEFAULT 0');
|
|
492
|
+
initializeMemoryJournal(database);
|
|
493
|
+
initializeMemoryArtifacts(database);
|
|
494
|
+
return database;
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
database.close();
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
399
500
|
}
|
|
400
501
|
memoriesForScope(database, companionId, scopeId) {
|
|
401
502
|
return database.prepare('SELECT * FROM memories WHERE companion_id = ? AND scope_id = ?').all(companionId, scopeId).map(memoryFromRow);
|
|
@@ -514,36 +615,34 @@ function mergeMemories(existing, candidates, turn) {
|
|
|
514
615
|
const memories = existing.map(item => ({ ...item, evidence: [...item.evidence] }));
|
|
515
616
|
for (const candidate of candidates.slice(0, 12)) {
|
|
516
617
|
const subject = candidate.kind === 'profile' ? canonicalProfileSubject(candidate.subject) ?? candidate.subject : candidate.subject;
|
|
517
|
-
const match = memories.find(item => item.kind === candidate.kind &&
|
|
518
|
-
|
|
618
|
+
const match = memories.find(item => item.kind === candidate.kind && item.companionId === turn.companionId && item.scopeId === turn.scopeId
|
|
619
|
+
&& (candidate.targetMemoryId ? item.id === candidate.targetMemoryId : sameMemorySubject(item, candidate.kind, subject)) && item.status !== 'superseded');
|
|
620
|
+
// A stale or foreign explicit target must never silently create another memory.
|
|
621
|
+
if (candidate.targetMemoryId && !match)
|
|
519
622
|
continue;
|
|
520
|
-
if (
|
|
521
|
-
if (match) {
|
|
522
|
-
match.status = 'superseded';
|
|
523
|
-
match.updatedAt = turn.at;
|
|
524
|
-
}
|
|
525
|
-
;
|
|
623
|
+
if (match?.locked)
|
|
526
624
|
continue;
|
|
527
|
-
}
|
|
528
|
-
if (candidate.operation === 'complete') {
|
|
625
|
+
const evidence = candidate.sourceEvidence ?? { turnId: turn.id, at: turn.at, excerpt: compact(turn.user, 300) };
|
|
626
|
+
if (candidate.operation === 'remove' || candidate.operation === 'complete') {
|
|
529
627
|
if (match) {
|
|
530
|
-
match.status = 'completed';
|
|
628
|
+
match.status = candidate.operation === 'remove' ? 'superseded' : 'completed';
|
|
531
629
|
match.updatedAt = turn.at;
|
|
630
|
+
match.evidence = [...match.evidence.filter(item => item.turnId !== evidence.turnId), evidence].slice(-8);
|
|
532
631
|
}
|
|
533
|
-
;
|
|
534
632
|
continue;
|
|
535
633
|
}
|
|
536
|
-
const evidence = { turnId: turn.id, at: turn.at, excerpt: compact(turn.user, 300) };
|
|
537
634
|
if (match) {
|
|
538
635
|
if (!match.locked)
|
|
539
636
|
match.content = compact(candidate.content, 800);
|
|
540
|
-
match.confidence =
|
|
541
|
-
match.importance =
|
|
637
|
+
match.confidence = bounded(candidate.confidence);
|
|
638
|
+
match.importance = bounded(candidate.importance);
|
|
542
639
|
match.updatedAt = turn.at;
|
|
543
640
|
match.status = 'active';
|
|
544
|
-
match.evidence = [...match.evidence.filter(item => item.turnId !==
|
|
641
|
+
match.evidence = [...match.evidence.filter(item => item.turnId !== evidence.turnId), evidence].slice(-8);
|
|
545
642
|
if (candidate.expiresInDays)
|
|
546
643
|
match.expiresAt = turn.at + candidate.expiresInDays * 86_400_000;
|
|
644
|
+
else if (candidate.kind === 'profile' || candidate.kind === 'preference' || candidate.kind === 'relationship')
|
|
645
|
+
delete match.expiresAt;
|
|
547
646
|
}
|
|
548
647
|
else
|
|
549
648
|
memories.push({
|
|
@@ -632,12 +731,6 @@ function relationFromRow(row) {
|
|
|
632
731
|
kind: string(row.kind), label: string(row.label), confidence: number(row.confidence), updatedAt: number(row.updated_at),
|
|
633
732
|
};
|
|
634
733
|
}
|
|
635
|
-
function recallScore(memory, terms, now) {
|
|
636
|
-
const haystack = normalize(`${memory.subject} ${memory.content}`);
|
|
637
|
-
const lexical = terms.length === 0 ? 0 : terms.filter(term => haystack.includes(term)).length / terms.length;
|
|
638
|
-
const recency = Math.max(0, 1 - (now - memory.updatedAt) / (180 * 86_400_000));
|
|
639
|
-
return lexical * 0.5 + memory.importance * 0.22 + memory.confidence * 0.18 + recency * 0.1;
|
|
640
|
-
}
|
|
641
734
|
function rollback(database) { try {
|
|
642
735
|
database.exec('ROLLBACK');
|
|
643
736
|
}
|
|
@@ -650,7 +743,6 @@ function ensureColumn(database, table, column, definition) {
|
|
|
650
743
|
function tableExists(database, table) {
|
|
651
744
|
return database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined;
|
|
652
745
|
}
|
|
653
|
-
function tokenize(value) { return [...new Set(normalize(value).match(/[\p{L}\p{N}]{2,}/gu) ?? [])].slice(0, 40); }
|
|
654
746
|
function normalize(value) { return value.toLocaleLowerCase().replace(/\s+/g, ''); }
|
|
655
747
|
function bounded(value) { return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0.5; }
|
|
656
748
|
function strings(value, limit, max) { return [...new Set(value.map(item => compact(item, max)).filter(Boolean))].slice(0, limit); }
|