@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.
Files changed (115) hide show
  1. package/README.md +13 -1
  2. package/docs/memory-quality.md +115 -0
  3. package/docs/releases/1.12.0.md +25 -0
  4. package/lib/agent-runtime.d.ts +1 -1
  5. package/lib/agent-runtime.d.ts.map +1 -1
  6. package/lib/agent-runtime.js +16 -11
  7. package/lib/agent-runtime.js.map +1 -1
  8. package/lib/api/features/memory-layers-api.d.ts +5 -0
  9. package/lib/api/features/memory-layers-api.d.ts.map +1 -0
  10. package/lib/api/features/memory-layers-api.js +42 -0
  11. package/lib/api/features/memory-layers-api.js.map +1 -0
  12. package/lib/api.d.ts.map +1 -1
  13. package/lib/api.js +5 -1
  14. package/lib/api.js.map +1 -1
  15. package/lib/client-api.d.ts +2 -0
  16. package/lib/client-api.d.ts.map +1 -1
  17. package/lib/client.d.ts.map +1 -1
  18. package/lib/client.js +2894 -2553
  19. package/lib/client.js.map +4 -4
  20. package/lib/concern-store.d.ts +1 -0
  21. package/lib/concern-store.d.ts.map +1 -1
  22. package/lib/concern-store.js +17 -0
  23. package/lib/concern-store.js.map +1 -1
  24. package/lib/daily-review.d.ts +1 -0
  25. package/lib/daily-review.d.ts.map +1 -1
  26. package/lib/daily-review.js +14 -3
  27. package/lib/daily-review.js.map +1 -1
  28. package/lib/index.d.ts.map +1 -1
  29. package/lib/index.js +13 -0
  30. package/lib/index.js.map +1 -1
  31. package/lib/memory-artifacts.d.ts +42 -0
  32. package/lib/memory-artifacts.d.ts.map +1 -0
  33. package/lib/memory-artifacts.js +120 -0
  34. package/lib/memory-artifacts.js.map +1 -0
  35. package/lib/memory-domain.d.ts +10 -0
  36. package/lib/memory-domain.d.ts.map +1 -1
  37. package/lib/memory-journal.d.ts +16 -0
  38. package/lib/memory-journal.d.ts.map +1 -0
  39. package/lib/memory-journal.js +51 -0
  40. package/lib/memory-journal.js.map +1 -0
  41. package/lib/memory-prompt-context.d.ts.map +1 -1
  42. package/lib/memory-prompt-context.js +2 -1
  43. package/lib/memory-prompt-context.js.map +1 -1
  44. package/lib/memory-quality.d.ts +4 -0
  45. package/lib/memory-quality.d.ts.map +1 -0
  46. package/lib/memory-quality.js +16 -0
  47. package/lib/memory-quality.js.map +1 -0
  48. package/lib/memory-reflection.d.ts +15 -2
  49. package/lib/memory-reflection.d.ts.map +1 -1
  50. package/lib/memory-reflection.js +101 -17
  51. package/lib/memory-reflection.js.map +1 -1
  52. package/lib/memory-retrieval.d.ts +5 -0
  53. package/lib/memory-retrieval.d.ts.map +1 -0
  54. package/lib/memory-retrieval.js +20 -0
  55. package/lib/memory-retrieval.js.map +1 -0
  56. package/lib/memory-store.d.ts +26 -2
  57. package/lib/memory-store.d.ts.map +1 -1
  58. package/lib/memory-store.js +142 -50
  59. package/lib/memory-store.js.map +1 -1
  60. package/lib/memory-worker.d.ts +25 -0
  61. package/lib/memory-worker.d.ts.map +1 -0
  62. package/lib/memory-worker.js +42 -0
  63. package/lib/memory-worker.js.map +1 -0
  64. package/lib/profile-domain.d.ts.map +1 -1
  65. package/lib/profile-domain.js +12 -4
  66. package/lib/profile-domain.js.map +1 -1
  67. package/lib/ui/memory/automation-settings.d.ts +8 -0
  68. package/lib/ui/memory/automation-settings.d.ts.map +1 -0
  69. package/lib/ui/memory/automation-settings.js +60 -0
  70. package/lib/ui/memory/automation-settings.js.map +1 -0
  71. package/lib/ui/memory/concern-board.d.ts +12 -0
  72. package/lib/ui/memory/concern-board.d.ts.map +1 -0
  73. package/lib/ui/memory/concern-board.js +129 -0
  74. package/lib/ui/memory/concern-board.js.map +1 -0
  75. package/lib/ui/memory/concern-panel.d.ts +7 -0
  76. package/lib/ui/memory/concern-panel.d.ts.map +1 -0
  77. package/lib/ui/memory/concern-panel.js +62 -0
  78. package/lib/ui/memory/concern-panel.js.map +1 -0
  79. package/lib/ui/memory/experience-view.d.ts +8 -0
  80. package/lib/ui/memory/experience-view.d.ts.map +1 -0
  81. package/lib/ui/memory/experience-view.js +52 -0
  82. package/lib/ui/memory/experience-view.js.map +1 -0
  83. package/lib/ui/memory/history-view.d.ts +6 -0
  84. package/lib/ui/memory/history-view.d.ts.map +1 -0
  85. package/lib/ui/memory/history-view.js +40 -0
  86. package/lib/ui/memory/history-view.js.map +1 -0
  87. package/lib/ui/memory/memory-cards.d.ts +14 -0
  88. package/lib/ui/memory/memory-cards.d.ts.map +1 -0
  89. package/lib/ui/memory/memory-cards.js +17 -0
  90. package/lib/ui/memory/memory-cards.js.map +1 -0
  91. package/lib/ui/memory/memory-detail.d.ts +8 -0
  92. package/lib/ui/memory/memory-detail.d.ts.map +1 -0
  93. package/lib/ui/memory/memory-detail.js +45 -0
  94. package/lib/ui/memory/memory-detail.js.map +1 -0
  95. package/lib/ui/memory/memory-graph.d.ts +8 -0
  96. package/lib/ui/memory/memory-graph.d.ts.map +1 -0
  97. package/lib/ui/memory/memory-graph.js +43 -0
  98. package/lib/ui/memory/memory-graph.js.map +1 -0
  99. package/lib/ui/memory/memory-library.d.ts +9 -0
  100. package/lib/ui/memory/memory-library.d.ts.map +1 -0
  101. package/lib/ui/memory/memory-library.js +38 -0
  102. package/lib/ui/memory/memory-library.js.map +1 -0
  103. package/lib/ui/memory/memory-panel.d.ts +10 -0
  104. package/lib/ui/memory/memory-panel.d.ts.map +1 -0
  105. package/lib/ui/memory/memory-panel.js +48 -0
  106. package/lib/ui/memory/memory-panel.js.map +1 -0
  107. package/lib/ui/memory/types.d.ts +44 -0
  108. package/lib/ui/memory/types.d.ts.map +1 -0
  109. package/lib/ui/memory/types.js +2 -0
  110. package/lib/ui/memory/types.js.map +1 -0
  111. package/lib/ui/memory/use-memory-resource.d.ts +9 -0
  112. package/lib/ui/memory/use-memory-resource.d.ts.map +1 -0
  113. package/lib/ui/memory/use-memory-resource.js +65 -0
  114. package/lib/ui/memory/use-memory-resource.js.map +1 -0
  115. 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"}
@@ -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":"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"}
1
+ {"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../src/memory-store.ts"],"names":[],"mappings":"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"}
@@ -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
- const terms = tokenize(query);
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 terms = tokenize(query);
76
- const scored = memories
77
- .filter(item => !baselineIds.has(item.id) && (item.kind !== 'profile' || isProfileBaselineEntry(item)))
78
- .map(item => ({ item, score: recallScore(item, terms, now) }))
79
- .sort((a, b) => b.score - a.score || b.item.updatedAt - a.item.updatedAt);
80
- const recall = expandRecallWithRelations(database, companionId, scopeId, memories, scored.map(entry => entry.item), baselineIds, limit);
81
- return { profile, ...recall };
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 = 'profile' AND status = 'active' AND (expires_at IS NULL OR expires_at > ?)
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 = 'profile'
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 database.prepare('SELECT * FROM memories ORDER BY updated_at DESC LIMIT ?').all(limit).map(memoryFromRow);
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
- return { reflection, memories, existingRelations, turns };
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('DELETE FROM daily_reflections WHERE companion_id = ? AND updated_at < ?').run(companionId, cutoff);
355
- database.prepare('DELETE FROM memories WHERE companion_id = ? AND updated_at < ?').run(companionId, cutoff);
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
- database.exec(`PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;
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
- ensureColumn(database, 'daily_reflections', 'reviewed_at', 'INTEGER');
395
- ensureColumn(database, 'daily_reflections', 'review_attempts', 'INTEGER NOT NULL DEFAULT 0');
396
- ensureColumn(database, 'daily_reflections', 'review_error', 'TEXT');
397
- ensureColumn(database, 'daily_reflections', 'next_review_at', 'INTEGER NOT NULL DEFAULT 0');
398
- return database;
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 && sameMemorySubject(item, candidate.kind, subject) && item.status !== 'superseded');
518
- if (match?.locked)
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 (candidate.operation === 'remove') {
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 = Math.max(match.confidence, bounded(candidate.confidence));
541
- match.importance = Math.max(match.importance, bounded(candidate.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 !== turn.id), evidence].slice(-8);
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); }