dsh-cortex-memory 0.2.1 → 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.
- package/LICENSE +3 -2
- package/README.md +55 -7
- package/bench/synthetic.js +1 -1
- package/client.js +576 -55
- package/docs/install-windows.md +3 -3
- package/docs/multimodal.md +1 -1
- package/index.js +17 -2
- package/package.json +14 -2
- package/src/api/client.js +5 -2
- package/src/api/routes.js +88 -0
- package/src/core/config.js +8 -0
- package/src/core/feature-overrides.js +2 -2
- package/src/engines/life.js +182 -19
- package/src/host/runtime.js +25 -16
- package/src/host/scheduler.js +177 -0
- package/src/llm/adapter.js +65 -4
- package/src/prompt/assemble.js +8 -3
- package/src/self/emotion.js +4 -2
- package/src/self/evolution.js +15 -1
- package/src/self/identity.js +10 -1
- package/src/service/cortex.js +676 -8
- package/src/store/database.js +10 -0
- package/src/store/repositories/summon.js +138 -0
- package/src/store/schema.sql +45 -0
- package/src/tools/definitions.js +13 -3
package/src/service/cortex.js
CHANGED
|
@@ -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
|
|
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 } = {}) =>
|
|
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
|
-
|
|
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
|
}
|