dsh-cortex-memory 0.2.2 → 0.3.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.
@@ -9,14 +9,15 @@ import { createSummaryRepository } from '../store/repositories/summaries.js';
9
9
  import { createJobRepository } from '../store/repositories/jobs.js';
10
10
  import { createSkillRepository } from '../store/repositories/skills.js';
11
11
  import { createPlaybookRepository } from '../store/repositories/playbook.js';
12
- import { selectTiers, renderTiers } from '../prompt/tiers.js';
12
+ import { selectTiers, renderTiers, DEFAULT_TIER_BUDGETS } from '../prompt/tiers.js';
13
13
  import { readOverrides, writeOverride, rollbackOverride } from '../core/feature-overrides.js';
14
14
  import { effectiveModelConfig, saveModelConfig, redactedModelConfig } from '../core/model-config.js';
15
15
  import { estimateTokens, truncate } from '../memory/tokenizer.js';
16
16
  import { createAuditRepository } from '../store/repositories/audit.js';
17
17
  import { createSelfStore } from '../self/state.js';
18
18
  import { createAppraisalStore } from '../self/appraisals.js';
19
- import { createInitiativeStore } from '../self/initiative.js';
19
+ import { createInitiativeStore, selectIntentions, DEFAULT_INITIATIVE_POLICY } from '../self/initiative.js';
20
+ import { createSummonRepository } from '../store/repositories/summon.js';
20
21
  import { createIdentityStore } from '../self/identity.js';
21
22
  import { createRelationshipStore } from '../self/relationship.js';
22
23
  import { createEncodeEngine } from '../engines/encode.js';
@@ -63,7 +64,7 @@ import { inspectLocalAttachment } from '../core/attachment-reader.js';
63
64
  * Facade that wires the repositories and engines together. The host plugin and
64
65
  * tests both use this; the DB is opened only here, never at import time.
65
66
  */
66
- export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAdapter = null } = {}) {
67
+ export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAdapter = null, webSearch = null } = {}) {
67
68
  const { db, config } = openDatabase(rawConfig);
68
69
  migrate(db);
69
70
 
@@ -78,6 +79,7 @@ export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAd
78
79
  const skillsRepo = createSkillRepository(db);
79
80
  const playbookRepo = createPlaybookRepository(db);
80
81
  const audit = createAuditRepository(db);
82
+ const summons = createSummonRepository(db);
81
83
  const selfStore = createSelfStore(db);
82
84
  const appraisalStore = createAppraisalStore(db);
83
85
  const initiativeStore = createInitiativeStore(db);
@@ -131,6 +133,35 @@ export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAd
131
133
  const growth = createGrowthEngine({ traces, feedback, selfStore, relationshipStore, audit, config });
132
134
  const reflect = createReflectEngine({ traces, summaries, encode, config });
133
135
  const autonomy = createAutonomyEngine({ initiativeStore, reflect, retrieve, working: workingStore, selfStore, scopeId: config.scope });
136
+
137
+ async function reviewGraphCandidate(edge = {}) {
138
+ if (!llmAdapter?.reviewEdge) return null;
139
+ const review = await llmAdapter.reviewEdge({
140
+ fromId: edge.fromId,
141
+ toId: edge.toId,
142
+ rel: edge.rel,
143
+ confidence: Number(edge.metadata?.confidence ?? edge.weight ?? 0),
144
+ evidence: edge.metadata?.evidence ?? edge.evidence ?? '',
145
+ source: edge.sourceTraceId ?? edge.metadata?.sourceTraceId ?? '',
146
+ });
147
+ if (review) {
148
+ audit.record?.({
149
+ actor: 'self:edgeReview',
150
+ action: 'edge.candidate.review',
151
+ targetType: 'edge',
152
+ targetId: edge.id,
153
+ detail: {
154
+ fromId: edge.fromId,
155
+ toId: edge.toId,
156
+ rel: edge.rel,
157
+ decision: review.decision,
158
+ confidence: review.confidence,
159
+ reason: review.reason,
160
+ },
161
+ });
162
+ }
163
+ return review;
164
+ }
134
165
  const reranker = (modelConfig.rerank.enabled || config.rerankEnabled === true || config.features?.rerank === true)
135
166
  && (modelConfig.rerank.baseUrl || config.rerankBaseUrl)
136
167
  ? createHttpReranker({
@@ -162,6 +193,34 @@ export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAd
162
193
  db, traces, reflect, development, working: workingStore, emotion, scopeId: config.scope,
163
194
  consolidation, features: config.features ?? {},
164
195
  llm: llmAdapter ? (args) => llmAdapter.dream(args) : null,
196
+ diaryLlm: llmAdapter ? (args) => llmAdapter.diary(args) : null,
197
+ feelingLlm: llmAdapter ? (args) => llmAdapter.feeling(args) : null,
198
+ webSearch: webSearch ? async (request = {}) => {
199
+ try {
200
+ const result = await webSearch(request);
201
+ audit.record?.({
202
+ actor: 'self:web',
203
+ action: 'web.search',
204
+ targetType: 'web',
205
+ detail: {
206
+ query: String(request.query ?? '').slice(0, 300),
207
+ sources: (result?.sources ?? []).slice(0, 5).map((source) => String(source.url ?? '').slice(0, 500)),
208
+ },
209
+ });
210
+ return result;
211
+ } catch (error) {
212
+ audit.record?.({
213
+ actor: 'self:web',
214
+ action: 'web.search.failed',
215
+ targetType: 'web',
216
+ detail: {
217
+ query: String(request.query ?? '').slice(0, 300),
218
+ error: error instanceof Error ? error.message : String(error),
219
+ },
220
+ });
221
+ throw error;
222
+ }
223
+ } : null,
165
224
  });
166
225
  const develop = createDevelopEngine({
167
226
  events, traces, facts, entities, encode, retrieve, selfStore, appraisalStore,
@@ -414,18 +473,602 @@ export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAd
414
473
  }
415
474
 
416
475
  function self(scopeId = config.scope) {
476
+ const identity = identityStore.active(scopeId);
477
+ const identityText = identity ? `${identity.content.narrative}\n${(identity.content.values ?? []).join('、')}\n${(identity.content.boundaries ?? []).join('、')}` : '';
417
478
  return {
418
479
  self: selfStore.get(scopeId),
419
- identity: identityStore.active(scopeId),
480
+ identity,
420
481
  relationships: relationshipStore.list(scopeId),
421
482
  intentions: initiativeStore.pending(scopeId),
422
483
  working: workingStore.list({ scopeId, limit: 20 }),
423
484
  goals: goalStore.list({ scopeId, status: 'active', limit: 20 }),
424
485
  emotion: emotion.text(),
425
486
  calibration: growth.calibration({ scopeId }),
487
+ capacity: {
488
+ identityBudget: config.identityBudget,
489
+ identityTokens: estimateTokens(identityText),
490
+ workingBudget: config.workingBudget,
491
+ contextBudget: config.contextBudget,
492
+ promptBudget: 1600,
493
+ tiers: DEFAULT_TIER_BUDGETS,
494
+ resultLimit: config.resultLimit,
495
+ ftsLimit: config.ftsLimit,
496
+ valuesLimit: 20,
497
+ boundariesLimit: 30,
498
+ },
499
+ };
500
+ }
501
+
502
+ let memoryEditEnabled = false;
503
+ let memoryEditChangedAt = null;
504
+
505
+ function memoryEditState() {
506
+ return { enabled: memoryEditEnabled, changedAt: memoryEditChangedAt };
507
+ }
508
+
509
+ function setMemoryEdit(enabled) {
510
+ memoryEditEnabled = enabled === true;
511
+ memoryEditChangedAt = new Date().toISOString();
512
+ audit.record?.({
513
+ actor: 'user',
514
+ action: memoryEditEnabled ? 'memory.edit.unlock' : 'memory.edit.lock',
515
+ targetType: 'memory',
516
+ targetId: config.scope,
517
+ detail: {},
518
+ });
519
+ return memoryEditState();
520
+ }
521
+
522
+ function memoryDays({ limit = 60, scopeId = config.scope } = {}) {
523
+ const count = Math.max(1, Math.min(365, Number(limit) || 60));
524
+ const traceDays = db.prepare("SELECT date(recorded_at, 'localtime') AS day, COUNT(*) AS count FROM traces WHERE scope_id = ? GROUP BY day ORDER BY day DESC LIMIT ?").all(scopeId, count);
525
+ const emotionDays = db.prepare("SELECT date(at, 'localtime') AS day, COUNT(*) AS count FROM emotion_events WHERE scope_id = ? GROUP BY day ORDER BY day DESC LIMIT ?").all(scopeId, count);
526
+ const map = new Map();
527
+ for (const row of traceDays) map.set(row.day, { date: row.day, work: Number(row.count), emotion: 0 });
528
+ for (const row of emotionDays) {
529
+ const day = map.get(row.day) ?? { date: row.day, work: 0, emotion: 0 };
530
+ day.emotion = Number(row.count);
531
+ map.set(row.day, day);
532
+ }
533
+ return [...map.values()].filter((day) => day.date).sort((a, b) => b.date.localeCompare(a.date)).slice(0, count);
534
+ }
535
+
536
+ function memoryDay({ date, scopeId = config.scope, limit = 600 } = {}) {
537
+ const value = String(date ?? '').trim();
538
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) throw new Error('date must be YYYY-MM-DD');
539
+ const start = new Date(`${value}T00:00:00`).toISOString();
540
+ const end = new Date(new Date(`${value}T00:00:00`).getTime() + 86_400_000).toISOString();
541
+ const cap = Math.max(1, Math.min(2000, Number(limit) || 600));
542
+ const traceRows = db.prepare('SELECT * FROM traces WHERE scope_id = ? AND recorded_at >= ? AND recorded_at < ? ORDER BY recorded_at ASC LIMIT ?').all(scopeId, start, end, cap);
543
+ const emotionRows = db.prepare('SELECT * FROM emotion_events WHERE scope_id = ? AND at >= ? AND at < ? ORDER BY at ASC LIMIT ?').all(scopeId, start, end, cap);
544
+ const factRows = db.prepare('SELECT id, subject_id, predicate, object_id, object_literal, confidence, status, recorded_at FROM facts WHERE scope_id = ? AND recorded_at >= ? AND recorded_at < ? ORDER BY recorded_at ASC LIMIT ?').all(scopeId, start, end, cap);
545
+ const summaryRows = db.prepare('SELECT id, level, content, coverage, created_at FROM summaries WHERE scope_id = ? AND created_at >= ? AND created_at < ? ORDER BY created_at ASC LIMIT ?').all(scopeId, start, end, cap);
546
+ const diaryRows = db.prepare('SELECT id, text, mood, source, at FROM diary_entries WHERE scope_id = ? AND at >= ? AND at < ? ORDER BY at ASC LIMIT ?').all(scopeId, start, end, cap);
547
+ const dreamRows = db.prepare('SELECT id, text, source_count, model, at FROM dreams WHERE scope_id = ? AND at >= ? AND at < ? ORDER BY at ASC LIMIT ?').all(scopeId, start, end, cap);
548
+ const json = (value, fallback) => { try { return JSON.parse(value); } catch { return fallback; } };
549
+ return {
550
+ date: value,
551
+ scopeId,
552
+ emotion: emotionRows.map((row) => ({
553
+ id: row.id, at: row.at, target: row.target, source: row.source, text: row.text,
554
+ emotions: json(row.emotions_json, []),
555
+ })),
556
+ work: {
557
+ traces: traceRows.map((row) => ({
558
+ id: row.id, kind: row.kind, title: row.title, content: row.content,
559
+ summary: row.summary, tags: json(row.tags_json, []), importance: Number(row.importance),
560
+ recordedAt: row.recorded_at, state: row.state, source: row.source,
561
+ })),
562
+ facts: factRows.map((row) => ({
563
+ id: row.id, subjectId: row.subject_id, predicate: row.predicate,
564
+ objectId: row.object_id, objectLiteral: row.object_literal,
565
+ confidence: Number(row.confidence), status: row.status, recordedAt: row.recorded_at,
566
+ })),
567
+ summaries: summaryRows.map((row) => ({ id: row.id, level: row.level, content: row.content, coverage: row.coverage, createdAt: row.created_at })),
568
+ diary: diaryRows.map((row) => ({ id: row.id, text: row.text, mood: row.mood, source: row.source, at: row.at })),
569
+ dreams: dreamRows.map((row) => ({ id: row.id, text: row.text, sourceCount: row.source_count, model: row.model, at: row.at })),
570
+ },
571
+ counts: {
572
+ emotion: emotionRows.length,
573
+ traces: traceRows.length,
574
+ facts: factRows.length,
575
+ summaries: summaryRows.length,
576
+ diary: diaryRows.length,
577
+ dreams: dreamRows.length,
578
+ },
426
579
  };
427
580
  }
428
581
 
582
+ function updateMemoryTrace({ id, title, content, tags, summary } = {}) {
583
+ if (!memoryEditEnabled) throw new Error('memory edit is locked');
584
+ const current = traces.get(id);
585
+ if (!current) throw new Error(`trace not found: ${id}`);
586
+ const patch = {};
587
+ if (title != null) patch.title = String(title).slice(0, 240);
588
+ if (content != null) patch.content = String(content).slice(0, 20_000);
589
+ if (summary != null) patch.summary = String(summary).slice(0, 4_000);
590
+ if (Array.isArray(tags)) patch.tags = tags.map(String).filter(Boolean).slice(0, 40);
591
+ const updated = traces.update(id, patch);
592
+ audit.record?.({
593
+ actor: 'user',
594
+ action: 'memory.trace.edit',
595
+ targetType: 'trace',
596
+ targetId: id,
597
+ detail: { changed: Object.keys(patch), previous: { title: current.title, content: current.content, summary: current.summary, tags: current.tags } },
598
+ });
599
+ return updated;
600
+ }
601
+
602
+ function updateMemoryEmotion({ id, text, target } = {}) {
603
+ if (!memoryEditEnabled) throw new Error('memory edit is locked');
604
+ const current = db.prepare('SELECT * FROM emotion_events WHERE id = ? AND scope_id = ?').get(id, config.scope);
605
+ if (!current) throw new Error(`emotion event not found: ${id}`);
606
+ const nextText = text == null ? current.text : String(text).slice(0, 10_000);
607
+ const nextTarget = target == null ? current.target : String(target).slice(0, 120);
608
+ db.prepare('UPDATE emotion_events SET text = ?, target = ? WHERE id = ?').run(nextText, nextTarget, id);
609
+ audit.record?.({
610
+ actor: 'user',
611
+ action: 'memory.emotion.edit',
612
+ targetType: 'emotion',
613
+ targetId: id,
614
+ detail: { previous: { text: current.text, target: current.target }, next: { text: nextText, target: nextTarget } },
615
+ });
616
+ return { id, text: nextText, target: nextTarget, at: current.at };
617
+ }
618
+
619
+ function reviewIntentions({ at = Date.now() } = {}) {
620
+ const developmentReport = development.evaluate({ scopeId: config.scope, now: at });
621
+ const selfState = selfStore.get(config.scope);
622
+ const recent = initiativeStore.recent(config.scope, { now: at, hours: 24, limit: 100 });
623
+ const selected = selectIntentions({
624
+ selfState,
625
+ appraisal: null,
626
+ pendingQuestions: 0,
627
+ unresolvedDisputes: 0,
628
+ lastConsolidationAt: null,
629
+ store: initiativeStore,
630
+ scopeId: config.scope,
631
+ now: at,
632
+ recent,
633
+ policy: DEFAULT_INITIATIVE_POLICY,
634
+ });
635
+ const identity = identityStore.active(config.scope);
636
+ const coreHighlights = traces.listSince(config.scope, new Date(at - 30 * 86400000).toISOString(), { limit: 200 })
637
+ .filter((trace) => trace.kind !== 'reflection')
638
+ .sort((a, b) => Number(b.importance) - Number(a.importance))
639
+ .slice(0, 5)
640
+ .map((trace) => ({ id: trace.id, title: trace.title || String(trace.content ?? '').slice(0, 80), importance: trace.importance }));
641
+ let review = selected;
642
+ if (review.length === 0) {
643
+ const existing = initiativeStore.pending(config.scope, { now: at, limit: 20 })
644
+ .find((item) => item.payload?.topic === '1.5h-review');
645
+ review = existing ? [existing] : [initiativeStore.insert({
646
+ kind: 'question',
647
+ priority: 0.48,
648
+ reason: '1.5 小时复盘:人设、核心记忆与情感状态需要重新对齐',
649
+ payload: {
650
+ topic: '1.5h-review',
651
+ identityVersion: identity?.version ?? null,
652
+ emotion: emotion.text(),
653
+ maturity: developmentReport.maturity,
654
+ stage: developmentReport.stage,
655
+ coreHighlights,
656
+ },
657
+ createdAt: new Date(at).toISOString(),
658
+ dueAt: new Date(at + 90 * 60_000).toISOString(),
659
+ expiresAt: new Date(at + 6 * 60 * 60_000).toISOString(),
660
+ }, { scopeId: config.scope })];
661
+ }
662
+ audit.record?.({
663
+ actor: 'self:review',
664
+ action: 'intention.review',
665
+ targetType: 'self',
666
+ targetId: config.scope,
667
+ detail: {
668
+ maturity: developmentReport.maturity,
669
+ stage: developmentReport.stage,
670
+ coreHighlights: coreHighlights.map((item) => item.title),
671
+ intentions: review.map((item) => ({ id: item.id, kind: item.kind, reason: item.reason })),
672
+ },
673
+ });
674
+ return { at: new Date(at).toISOString(), development: developmentReport, intentions: review };
675
+ }
676
+
677
+ const SUMMON_STOPWORDS = new Set([
678
+ '工作', '记忆', '今天', '现在', '我们', '你们', '他们', '可以', '什么', '一个', '没有',
679
+ '自己', '这个', '那个', '还是', '就是', '因为', '所以', '然后', '如果', '但是', '不是',
680
+ '只是', '这样', '时候', '东西', '事情', '帮我', '整理', '一下', '昨天', '最近', '看看',
681
+ '记得', '知道', '怎样', '如何', '为什么', '需要', '应该', '已经', '还在', '有点', '真的',
682
+ '比较', '很多', '这些', '那些', '一起', '进行', '目前', '现在', '里面', '上面', '下面',
683
+ 'the', 'and', 'for', 'with', 'that', 'this', 'you', 'are', 'from', 'have', 'not',
684
+ ]);
685
+
686
+ function summonTriggers(text, extra = []) {
687
+ const value = String(text ?? '');
688
+ const quoted = [...value.matchAll(/[「“"]([^」”"]{2,24})[」”"]/g)].map((match) => match[1]);
689
+ const words = value.match(/[\p{Script=Han}]{2,8}|[A-Za-z0-9_]{3,24}/gu) ?? [];
690
+ const candidates = [...extra.map(String), ...quoted, ...words]
691
+ .map((item) => String(item).trim())
692
+ .filter((item) => item.length >= 2 && !SUMMON_STOPWORDS.has(item.toLowerCase()));
693
+ return [...new Set(candidates)].slice(0, 24);
694
+ }
695
+
696
+ function localDay(value) {
697
+ const date = value ? new Date(value) : new Date();
698
+ if (Number.isNaN(date.getTime())) return new Date().toISOString().slice(0, 10);
699
+ return date.toLocaleDateString('en-CA');
700
+ }
701
+
702
+ function mergeSummonVolumes({ scopeId = config.scope } = {}) {
703
+ const cards = summons.listCards({ scopeId, limit: 1000 });
704
+ const groups = new Map();
705
+ for (const card of cards) {
706
+ const key = `${card.side}|${card.category ?? '未分类'}|${card.day}`;
707
+ const group = groups.get(key) ?? [];
708
+ group.push(card);
709
+ groups.set(key, group);
710
+ }
711
+ let volumes = 0;
712
+ for (const group of groups.values()) {
713
+ const first = group[0];
714
+ const sideLabel = first.side === 'left' ? '左脑 · 情感' : '右脑 · 工作';
715
+ const title = `${sideLabel} · ${first.category ?? '未分类'} · ${first.day}`;
716
+ const index = group.slice(0, 40).map((card) => `- ${card.title}`).join('\n');
717
+ const representatives = [...group]
718
+ .sort((a, b) => Number(b.importance) - Number(a.importance))
719
+ .slice(0, 5);
720
+ const content = representatives.map((card) => {
721
+ const firstSentence = String(card.content ?? '').split(/[。!?\n]/).find((sentence) => sentence.trim()) ?? '';
722
+ return `## ${card.title}\n${firstSentence.trim().slice(0, 500)}`;
723
+ }).join('\n\n');
724
+ summons.upsertVolume({
725
+ scopeId,
726
+ side: first.side,
727
+ title,
728
+ category: first.category ?? null,
729
+ index,
730
+ content,
731
+ cardIds: group.map((card) => card.id),
732
+ periodStart: `${first.day}T00:00:00.000Z`,
733
+ periodEnd: `${first.day}T23:59:59.999Z`,
734
+ });
735
+ volumes += 1;
736
+ }
737
+ return { cards: cards.length, volumes };
738
+ }
739
+
740
+ function rebuildSummonBook({ scopeId = config.scope } = {}) {
741
+ const emotions = db.prepare('SELECT * FROM emotion_events WHERE scope_id = ? ORDER BY at DESC LIMIT 1000').all(scopeId);
742
+ for (const row of emotions) {
743
+ const parsed = (() => { try { return JSON.parse(row.emotions_json); } catch { return []; } })();
744
+ const labels = parsed.map((item) => item.name).filter(Boolean);
745
+ summons.upsertCard({
746
+ scopeId,
747
+ side: 'left',
748
+ title: `${labels.slice(0, 2).join(' / ') || '情感'} · ${row.target ?? '主人'}`,
749
+ content: row.text,
750
+ triggers: summonTriggers(row.text, labels),
751
+ category: labels[0] ?? '情感',
752
+ sourceKind: 'emotion',
753
+ sourceId: row.id,
754
+ day: localDay(row.at),
755
+ importance: Math.min(1, 0.55 + labels.length * 0.08),
756
+ });
757
+ }
758
+ const facts = db.prepare(`
759
+ SELECT f.id, f.subject_id, f.predicate, f.object_id, f.object_literal,
760
+ f.confidence, f.recorded_at, s.name AS subject_name, o.name AS object_name
761
+ FROM facts f
762
+ LEFT JOIN entities s ON s.id = f.subject_id
763
+ LEFT JOIN entities o ON o.id = f.object_id
764
+ WHERE f.scope_id = ? AND f.status = 'active' AND f.confidence >= 0.65
765
+ ORDER BY f.recorded_at DESC LIMIT 500
766
+ `).all(scopeId);
767
+ for (const fact of facts) {
768
+ const title = `${fact.subject_name || fact.subject_id} ${fact.predicate} ${fact.object_literal || fact.object_name || fact.object_id || ''}`.trim();
769
+ summons.upsertCard({
770
+ scopeId,
771
+ side: 'right',
772
+ title,
773
+ content: title,
774
+ triggers: summonTriggers(title),
775
+ category: 'fact',
776
+ sourceKind: 'fact',
777
+ sourceId: fact.id,
778
+ day: localDay(fact.recorded_at),
779
+ importance: Number(fact.confidence ?? 0.7),
780
+ });
781
+ }
782
+ const traces = db.prepare(`
783
+ SELECT * FROM traces WHERE scope_id = ?
784
+ AND (kind IN ('decision', 'reflection', 'identity') OR importance >= 0.6)
785
+ ORDER BY recorded_at DESC LIMIT 800
786
+ `).all(scopeId);
787
+ for (const row of traces) {
788
+ const tags = (() => { try { return JSON.parse(row.tags_json); } catch { return []; } })();
789
+ const content = String(row.content ?? '');
790
+ const fallbackTitle = content.split(/\n/).map((line) => line.replace(/^\s*(用户|本鲸|assistant|user)[::]\s*/i, '').trim()).find(Boolean) ?? '';
791
+ const title = /^会话\s*\d+\s*的对话/.test(String(row.title ?? ''))
792
+ ? fallbackTitle.slice(0, 60)
793
+ : (row.title || fallbackTitle.slice(0, 60) || '工作记忆');
794
+ summons.upsertCard({
795
+ scopeId,
796
+ side: 'right',
797
+ title,
798
+ content: String(row.content ?? '').slice(0, 20_000),
799
+ triggers: summonTriggers(`${row.title ?? ''} ${row.content ?? ''}`, tags),
800
+ category: row.kind || 'work',
801
+ sourceKind: 'trace',
802
+ sourceId: row.id,
803
+ day: localDay(row.recorded_at),
804
+ importance: Number(row.importance ?? 0.5),
805
+ });
806
+ }
807
+ const merged = mergeSummonVolumes({ scopeId });
808
+ audit.record?.({ actor: 'self:summon', action: 'summon.rebuild', targetType: 'summon', targetId: scopeId, detail: { emotions: emotions.length, facts: facts.length, traces: traces.length, ...merged } });
809
+ return { emotions: emotions.length, facts: facts.length, traces: traces.length, ...merged };
810
+ }
811
+
812
+ function summonBook({ date = null, scopeId = config.scope } = {}) {
813
+ const day = date || localDay(new Date());
814
+ const cards = summons.listCards({ scopeId, day, limit: 1000 });
815
+ return {
816
+ date: day,
817
+ left: cards.filter((card) => card.side === 'left'),
818
+ right: cards.filter((card) => card.side === 'right'),
819
+ volumes: summons.listVolumes({ scopeId, limit: 100 }),
820
+ days: [...new Set(summons.listCards({ scopeId, limit: 1000 }).map((card) => card.day))].sort().reverse(),
821
+ };
822
+ }
823
+
824
+ function matchSummonCards({ text = '', maxCards = 3, maxChars = 900, scopeId = config.scope } = {}) {
825
+ const query = String(text ?? '').trim();
826
+ if (!query) return { cards: [], block: '' };
827
+ const allCards = summons.listCards({ scopeId, limit: 1000 });
828
+ const triggerFrequency = new Map();
829
+ for (const card of allCards) {
830
+ for (const trigger of new Set((card.triggers ?? []).filter((item) => item.length >= 2 && item.length <= 12))) {
831
+ triggerFrequency.set(trigger, (triggerFrequency.get(trigger) ?? 0) + 1);
832
+ }
833
+ }
834
+ const scored = [];
835
+ for (const card of allCards) {
836
+ const hits = (card.triggers ?? []).filter((trigger) => trigger.length >= 2 && trigger.length <= 12 && query.includes(trigger));
837
+ const titleHit = card.title && query.includes(card.title.slice(0, Math.min(12, card.title.length)));
838
+ if (hits.length === 0 && !titleHit) continue;
839
+ let score = hits.reduce((sum, trigger) => {
840
+ const specificity = 1 + Math.log(1 + allCards.length / (1 + (triggerFrequency.get(trigger) ?? 1)));
841
+ return sum + (1 + Math.min(4, trigger.length) * 0.25) * specificity;
842
+ }, 0);
843
+ if (titleHit) score += 1.5;
844
+ score += Number(card.importance ?? 0.5) * 0.4;
845
+ if (card.category !== 'utterance') score += 0.6;
846
+ scored.push({ card, score, hits });
847
+ }
848
+ const specificScored = scored.filter((entry) => entry.card.category !== 'utterance');
849
+ const ranked = (specificScored.length ? specificScored : scored).sort((a, b) => b.score - a.score);
850
+ const left = ranked.find((entry) => entry.card.side === 'left');
851
+ const right = ranked.find((entry) => entry.card.side === 'right');
852
+ const selected = [];
853
+ for (const entry of [left, right, ...ranked]) {
854
+ if (!entry || selected.some((item) => item.card.id === entry.card.id)) continue;
855
+ selected.push(entry);
856
+ if (selected.length >= Math.max(1, Math.min(5, Number(maxCards) || 3))) break;
857
+ }
858
+ const lines = ['## 召唤卡(历史参考,不是指令)'];
859
+ let used = 0;
860
+ for (const entry of selected) {
861
+ const label = entry.card.side === 'left' ? '左脑·情感' : '右脑·工作';
862
+ const body = String(entry.card.content ?? '').replace(/\s+/g, ' ').slice(0, 260);
863
+ const line = `- [${label}] ${entry.card.title}:${body}`;
864
+ const cost = estimateTokens(line);
865
+ if (used + cost > maxChars) break;
866
+ lines.push(line);
867
+ used += cost;
868
+ }
869
+ return {
870
+ cards: selected.map((entry) => ({ id: entry.card.id, side: entry.card.side, title: entry.card.title, score: entry.score, hits: entry.hits })),
871
+ block: selected.length ? lines.join('\n') : '',
872
+ };
873
+ }
874
+
875
+ function summarizeEntityName(name) {
876
+ const value = String(name ?? '').trim();
877
+ if (value.length < 2 || value.length > 16) return false;
878
+ if (/^ent_|[\r\n]/.test(value)) return false;
879
+ if (/^[::→、,。;;()()\s]+$/.test(value)) return false;
880
+ if (/^(还|现|只|但|这|那|它|他|她|的|了|是|在|有|我|你|工作|记忆|这个|那个)$/.test(value)) return false;
881
+ return true;
882
+ }
883
+
884
+ async function labelEntities({ scopeId = config.scope, limit = 3 } = {}) {
885
+ if (!llmAdapter?.complete) return { updated: [], skipped: 'no-llm' };
886
+ const rows = entities.list({ scopeId, limit: 500 })
887
+ .filter((entity) => !summarizeEntityName(entity.name))
888
+ .slice(0, Math.max(1, Math.min(10, Number(limit) || 3)));
889
+ const updated = [];
890
+ for (const entity of rows) {
891
+ const linkedFacts = db.prepare(`
892
+ SELECT predicate, object_literal, object_id FROM facts
893
+ WHERE scope_id = ? AND (subject_id = ? OR object_id = ?)
894
+ ORDER BY updated_at DESC LIMIT 5
895
+ `).all(scopeId, entity.id, entity.id);
896
+ const evidence = linkedFacts.map((fact) => `${fact.predicate} ${fact.object_literal ?? fact.object_id ?? ''}`).join(';');
897
+ const raw = await llmAdapter.complete({
898
+ system: '你在整理记忆图谱实体名。只输出 JSON:{"name":"2-12字中文短名","confidence":0到1,"reason":"一句话"}。名字必须具体,不能是句子、ID、单字或泛词;证据不足就低置信。',
899
+ prompt: `实体原值:${entity.name}\n类型:${entity.type}\n关联事实:${evidence || '无更多上下文'}`,
900
+ maxTokens: 160,
901
+ temperature: 0.2,
902
+ purpose: 'cortex-entity-name',
903
+ }).catch(() => null);
904
+ if (!raw) continue;
905
+ try {
906
+ const match = raw.match(/\{[\s\S]*\}/);
907
+ if (!match) continue;
908
+ const parsed = JSON.parse(match[0]);
909
+ const name = String(parsed.name ?? '').trim();
910
+ const confidence = Number(parsed.confidence ?? 0);
911
+ if (confidence < 0.6 || !summarizeEntityName(name)) continue;
912
+ db.prepare('UPDATE entities SET name = ?, updated_at = ? WHERE id = ?').run(name, new Date().toISOString(), entity.id);
913
+ audit.record?.({ actor: 'self:entity-label', action: 'entity.rename', targetType: 'entity', targetId: entity.id, detail: { from: entity.name, to: name, confidence, reason: parsed.reason ?? '' } });
914
+ updated.push({ id: entity.id, from: entity.name, to: name, confidence });
915
+ } catch { /* skip malformed proposal */ }
916
+ }
917
+ return { updated, count: updated.length, scanned: rows.length };
918
+ }
919
+
920
+ function diagnostics({ scopeId = config.scope } = {}) {
921
+ const report = health.audit({ scopeId });
922
+ const disputes = db.prepare(`
923
+ SELECT f.id, f.subject_id, f.predicate, f.object_id, f.object_literal,
924
+ f.confidence, f.recorded_at, e.name AS subject_name
925
+ FROM facts f LEFT JOIN entities e ON e.id = f.subject_id
926
+ WHERE f.scope_id = ? AND f.status = 'disputed'
927
+ ORDER BY f.recorded_at DESC LIMIT 30
928
+ `).all(scopeId).map((row) => ({
929
+ id: row.id,
930
+ subjectId: row.subject_id,
931
+ subject: row.subject_name || row.subject_id,
932
+ predicate: row.predicate,
933
+ objectId: row.object_id,
934
+ objectLiteral: row.object_literal,
935
+ confidence: Number(row.confidence),
936
+ recordedAt: row.recorded_at,
937
+ }));
938
+ const skillList = skillsRepo.list({ scopeId, limit: 100 });
939
+ return {
940
+ scopeId,
941
+ score: report.score,
942
+ issues: report.issues ?? [],
943
+ disputes,
944
+ skills: {
945
+ active: skillList.filter((skill) => skill.state === 'active').length,
946
+ candidates: skillList.filter((skill) => skill.state === 'candidate' || skill.state === 'trial').length,
947
+ items: skillList.slice(0, 20),
948
+ },
949
+ };
950
+ }
951
+
952
+ function suggestSkills({ scopeId = config.scope } = {}) {
953
+ const existing = new Set(skillsRepo.list({ scopeId, limit: 1000 }).map((skill) => skill.name));
954
+ const suggestions = skills.suggest({ scopeId, minSupport: 4 });
955
+ const created = [];
956
+ for (const suggestion of suggestions.slice(0, 3)) {
957
+ if (existing.has(suggestion.name)) continue;
958
+ const skill = skills.create({
959
+ scopeId,
960
+ name: suggestion.name,
961
+ intent: suggestion.intent,
962
+ steps: suggestion.steps,
963
+ preconditions: suggestion.preconditions,
964
+ contraindications: suggestion.contraindications,
965
+ sourceTraceIds: suggestion.sourceTraceIds,
966
+ state: 'candidate',
967
+ });
968
+ audit.record?.({ actor: 'self:skill', action: 'skill.suggest', targetType: 'skill', targetId: skill.id, detail: { support: suggestion.support } });
969
+ created.push(skill);
970
+ }
971
+ return { created, count: created.length };
972
+ }
973
+
974
+ function promoteSkill({ id } = {}) {
975
+ const skill = skillsRepo.get(id);
976
+ if (!skill) throw new Error(`skill not found: ${id}`);
977
+ const promoted = skills.promote(id);
978
+ audit.record?.({ actor: 'user', action: 'skill.promote', targetType: 'skill', targetId: id, detail: { from: skill.state, to: promoted.state } });
979
+ return promoted;
980
+ }
981
+
982
+ function resolveFactDispute({ id, decision = 'dismiss', reason = '' } = {}) {
983
+ const fact = facts.get(id);
984
+ if (!fact) throw new Error(`fact not found: ${id}`);
985
+ if (!['confirm', 'dismiss'].includes(decision)) throw new Error('decision must be confirm or dismiss');
986
+ const status = decision === 'confirm' ? 'active' : 'invalidated';
987
+ db.prepare('UPDATE facts SET status = ?, invalidated_by = ?, updated_at = ? WHERE id = ?')
988
+ .run(status, decision === 'dismiss' ? 'self:health-review' : null, new Date().toISOString(), id);
989
+ audit.record?.({
990
+ actor: 'self:health-review',
991
+ action: 'fact.dispute.resolve',
992
+ targetType: 'fact',
993
+ targetId: id,
994
+ detail: { decision, reason, previousStatus: fact.status, nextStatus: status },
995
+ });
996
+ return { id, status, decision, reason };
997
+ }
998
+
999
+ function feedbackSummonCard({ id, signal = 'helpful', source = 'summon:card' } = {}) {
1000
+ const card = summons.getCard(id);
1001
+ if (!card) throw new Error(`summon card not found: ${id}`);
1002
+ const allowed = new Set(['helpful', 'used', 'ignored', 'hard', 'incorrect', 'corrected', 'easy', 'good', 'again']);
1003
+ if (!allowed.has(String(signal))) throw new Error(`unsupported summon feedback signal: ${signal}`);
1004
+ let feedback = null;
1005
+ if (card.sourceKind === 'trace' && card.sourceId) {
1006
+ feedback = growth.applyFeedback({
1007
+ traceId: card.sourceId,
1008
+ signal,
1009
+ source,
1010
+ });
1011
+ }
1012
+ audit.record?.({
1013
+ actor: 'user',
1014
+ action: 'summon.card.feedback',
1015
+ targetType: 'summon-card',
1016
+ targetId: id,
1017
+ detail: { signal, sourceKind: card.sourceKind, sourceId: card.sourceId, hasTraceFeedback: Boolean(feedback) },
1018
+ });
1019
+ return { cardId: id, sourceKind: card.sourceKind, sourceId: card.sourceId, signal, feedback };
1020
+ }
1021
+
1022
+ function updateSummonCard({ id, title, content, triggers, importance } = {}) {
1023
+ if (!memoryEditEnabled) throw new Error('memory edit is locked');
1024
+ const current = summons.getCard(id);
1025
+ if (!current) throw new Error(`summon card not found: ${id}`);
1026
+ const updated = summons.updateCard(id, { title, content, triggers, importance });
1027
+ audit.record?.({ actor: 'user', action: 'summon.card.edit', targetType: 'summon-card', targetId: id, detail: { previous: current, changed: { title, content, triggers, importance } } });
1028
+ return updated;
1029
+ }
1030
+
1031
+ function insertAscensionSection(narrative, section) {
1032
+ const body = String(narrative ?? '');
1033
+ const addendum = String(section ?? '').trim();
1034
+ if (!addendum || body.includes(addendum.slice(0, 40))) return body;
1035
+ const lines = body.split('\n');
1036
+ if (lines[0]?.startsWith('# ')) {
1037
+ return [lines[0], '', addendum, '', ...lines.slice(1)].join('\n');
1038
+ }
1039
+ return `${addendum}\n\n${body}`;
1040
+ }
1041
+
1042
+ function ascendIdentity({ addendum = '', rationale = 'core memory ascension', values = null } = {}) {
1043
+ const current = identityStore.active(config.scope);
1044
+ if (!current) throw new Error('no active identity');
1045
+ const narrative = insertAscensionSection(current.content.narrative, addendum);
1046
+ const nextContent = {
1047
+ ...current.content,
1048
+ narrative,
1049
+ values: Array.isArray(values) && values.length
1050
+ ? [...new Set([...(current.content.values ?? []), ...values.map(String)])]
1051
+ : current.content.values,
1052
+ };
1053
+ const updated = identityStore.amendActive(config.scope, nextContent, {
1054
+ rationale,
1055
+ amendedBy: 'self:ascend',
1056
+ });
1057
+ audit.record?.({
1058
+ actor: 'self:ascend',
1059
+ action: 'identity.ascend',
1060
+ targetType: 'identity',
1061
+ targetId: updated.id,
1062
+ detail: {
1063
+ version: updated.version,
1064
+ rationale,
1065
+ previousNarrative: current.content.narrative,
1066
+ nextNarrative: updated.content.narrative,
1067
+ },
1068
+ });
1069
+ return { ...updated, ascended: true };
1070
+ }
1071
+
429
1072
  return {
430
1073
  db,
431
1074
  config,
@@ -634,19 +1277,43 @@ export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAd
634
1277
  });
635
1278
  return { subject: entity.name, subjectId: entity.id, mode, at: atIso, facts: view };
636
1279
  },
637
- graphSummary: ({ scopeId = config.scope, limit = 2000 } = {}) => summarizeGraph(edges.list({ scopeId, limit })),
1280
+ graphSummary: ({ scopeId = config.scope, limit = 2000 } = {}) => {
1281
+ const summary = summarizeGraph(edges.list({ scopeId, limit }));
1282
+ const names = new Map(entities.list({ scopeId, limit: 5000 }).map((entity) => [entity.id, entity.name]));
1283
+ return { ...summary, top: summary.top.map((item) => ({ ...item, name: names.get(item.id) ?? item.id })) };
1284
+ },
638
1285
  graphData: ({ scopeId = config.scope, limit = 2000 } = {}) => {
639
1286
  const edgeRows = edges.list({ scopeId, limit });
640
1287
  const entityRows = entities.list({ scopeId, limit: 5000 });
641
1288
  const names = new Map(entityRows.map((entity) => [entity.id, entity.name]));
642
1289
  return {
643
1290
  nodes: entityRows.map((entity) => ({ id: entity.id, name: entity.name, type: entity.type })),
644
- edges: edgeRows.map((edge) => ({ id: edge.id, from: edge.fromId, to: edge.toId, type: edge.rel, rel: edge.rel, weight: edge.weight, label: `${names.get(edge.fromId) ?? edge.fromId} → ${names.get(edge.toId) ?? edge.toId}` })),
1291
+ edges: edgeRows.map((edge) => ({ id: edge.id, from: edge.fromId, to: edge.toId, fromName: names.get(edge.fromId) ?? edge.fromId, toName: names.get(edge.toId) ?? edge.toId, type: edge.rel, rel: edge.rel, weight: edge.weight, label: `${names.get(edge.fromId) ?? edge.fromId} → ${names.get(edge.toId) ?? edge.toId}` })),
645
1292
  summary: summarizeGraph(edgeRows),
646
1293
  };
647
1294
  },
1295
+ ascendIdentity,
1296
+ reviewIntentions,
1297
+ rebuildSummonBook,
1298
+ mergeSummonVolumes,
1299
+ summonBook,
1300
+ matchSummonCards,
1301
+ feedbackSummonCard,
1302
+ resolveFactDispute,
1303
+ diagnostics,
1304
+ labelEntities,
1305
+ suggestSkills,
1306
+ promoteSkill,
1307
+ updateSummonCard,
1308
+ memoryEditState,
1309
+ setMemoryEdit,
1310
+ memoryDays,
1311
+ memoryDay,
1312
+ updateMemoryTrace,
1313
+ updateMemoryEmotion,
648
1314
  graphCandidates: ({ scopeId = config.scope, limit = 200 } = {}) => edges.listCandidates({ scopeId, limit }),
649
- graphCandidateAction: ({ action, id, scopeId = config.scope, fromId, toId, rel, weight, confidence, sourceEventId, sourceTraceId, evidence, metadata } = {}) => {
1315
+ reviewGraphCandidate,
1316
+ graphCandidateAction: ({ action, id, scopeId = config.scope, fromId, toId, rel, weight, confidence, sourceEventId, sourceTraceId, evidence, metadata, review = null } = {}) => {
650
1317
  if (!scopeWritable(scopeId)) throw new Error('scope-read-only');
651
1318
  let result = null;
652
1319
  if (action === 'add') {
@@ -660,7 +1327,7 @@ export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAd
660
1327
  }
661
1328
  if (result) {
662
1329
  audit.record?.({
663
- actor: 'user',
1330
+ actor: review ? 'self:edgeReview' : 'user',
664
1331
  action: `edge.candidate.${action}`,
665
1332
  targetType: 'edge',
666
1333
  targetId: result.id,
@@ -670,6 +1337,7 @@ export function createCortex({ config: rawConfig = {}, logger = null, llm: llmAd
670
1337
  rel: result.rel,
671
1338
  confidence: result.metadata?.confidence ?? null,
672
1339
  sourceTraceId: result.metadata?.sourceTraceId ?? null,
1340
+ review,
673
1341
  },
674
1342
  });
675
1343
  }