dsh-cortex-memory 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +153 -2
- package/bench/locomo.js +89 -0
- package/bench/longmemeval.js +83 -0
- package/bench/metrics.js +59 -0
- package/bench/run.js +74 -0
- package/bench/synthetic.js +55 -0
- package/bin/cortex.mjs +97 -0
- package/client.js +2085 -0
- package/cordis.patch.yml +7 -0
- package/docs/compatibility.md +39 -0
- package/docs/install-windows.md +56 -0
- package/docs/models.md +48 -0
- package/icon.svg +15 -0
- package/index.js +149 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +83 -4
- package/src/adapters/json.js +67 -0
- package/src/adapters/markdown-import.js +190 -0
- package/src/adapters/markdown.js +96 -0
- package/src/adapters/shiori.js +160 -0
- package/src/api/client.js +460 -0
- package/src/api/routes.js +392 -0
- package/src/api/validate.js +68 -0
- package/src/core/config.js +112 -0
- package/src/core/errors.js +36 -0
- package/src/core/feature-overrides.js +71 -0
- package/src/core/ids.js +70 -0
- package/src/core/json.js +51 -0
- package/src/core/model-config.js +126 -0
- package/src/core/time.js +83 -0
- package/src/engines/autonomy.js +64 -0
- package/src/engines/compression.js +69 -0
- package/src/engines/consolidate.js +163 -0
- package/src/engines/develop.js +164 -0
- package/src/engines/digest.js +51 -0
- package/src/engines/encode.js +199 -0
- package/src/engines/evaluate.js +39 -0
- package/src/engines/extract.js +67 -0
- package/src/engines/graph-analytics.js +116 -0
- package/src/engines/graph.js +132 -0
- package/src/engines/growth.js +149 -0
- package/src/engines/health.js +113 -0
- package/src/engines/induct.js +28 -0
- package/src/engines/jobs.js +37 -0
- package/src/engines/life.js +147 -0
- package/src/engines/llm-facts.js +48 -0
- package/src/engines/narrative.js +85 -0
- package/src/engines/privacy.js +120 -0
- package/src/engines/qa.js +72 -0
- package/src/engines/reflect.js +95 -0
- package/src/engines/retention.js +41 -0
- package/src/engines/retrieve.js +311 -0
- package/src/engines/skills.js +62 -0
- package/src/guardian/circuit.js +66 -0
- package/src/guardian/guardian.js +78 -0
- package/src/guardian/invariants.js +59 -0
- package/src/guardian/rotation.js +33 -0
- package/src/guardian/scanner.js +66 -0
- package/src/host/runtime.js +197 -0
- package/src/host/scheduler.js +77 -0
- package/src/llm/adapter.js +90 -0
- package/src/memory/dsr.js +201 -0
- package/src/memory/query.js +58 -0
- package/src/memory/temporal.js +64 -0
- package/src/memory/tokenizer.js +77 -0
- package/src/observability/metrics.js +45 -0
- package/src/prompt/assemble.js +78 -0
- package/src/prompt/tiers.js +50 -0
- package/src/self/appraisal.js +135 -0
- package/src/self/appraisals.js +59 -0
- package/src/self/development.js +156 -0
- package/src/self/emotion.js +126 -0
- package/src/self/evolution.js +163 -0
- package/src/self/identity.js +114 -0
- package/src/self/initiative.js +187 -0
- package/src/self/policies.js +80 -0
- package/src/self/relationship.js +86 -0
- package/src/self/state.js +184 -0
- package/src/self/working.js +153 -0
- package/src/service/cortex.js +577 -0
- package/src/store/database.js +116 -0
- package/src/store/repositories/audit.js +35 -0
- package/src/store/repositories/edges.js +86 -0
- package/src/store/repositories/entities.js +72 -0
- package/src/store/repositories/events.js +116 -0
- package/src/store/repositories/facts.js +249 -0
- package/src/store/repositories/feedback.js +53 -0
- package/src/store/repositories/jobs.js +94 -0
- package/src/store/repositories/playbook.js +86 -0
- package/src/store/repositories/skills.js +104 -0
- package/src/store/repositories/summaries.js +52 -0
- package/src/store/repositories/traces.js +267 -0
- package/src/store/schema.sql +483 -0
- package/src/tools/definitions.js +729 -0
- package/src/vector/ann.js +157 -0
- package/src/vector/embedder.js +50 -0
- package/src/vector/index.js +126 -0
- package/src/vector/providers.js +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-cortex-memory contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,154 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Cortex Memory for DeepSeek Harness
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A layered, temporal, self-developing memory engine for DeepSeek Harness (DSH).
|
|
4
|
+
It is designed for long-lived personal agents: memory that can be recalled,
|
|
5
|
+
audited, consolidated and changed **without silently rewriting identity**.
|
|
6
|
+
|
|
7
|
+
> Current public version: **0.1.0**
|
|
8
|
+
> Package: `dsh-cortex-memory` (the npm name `dsh-cortex` is already taken by another project)
|
|
9
|
+
> License: MIT
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Most memory plugins stop at "store and recall". Cortex adds four things that are
|
|
14
|
+
hard to bolt on later:
|
|
15
|
+
|
|
16
|
+
1. **Layered memory** — L0 identity, L1 working, L1.5 playbook, L2 episodic,
|
|
17
|
+
L3 semantic/temporal graph, L4 procedural, L5 reflection, archive.
|
|
18
|
+
2. **Temporal truth** — event ledger, bitemporal facts, `strict / historical /
|
|
19
|
+
belief / associative` queries, DSR forgetting.
|
|
20
|
+
3. **Gated growth** — consolidation, reflection, playbook candidates, policy
|
|
21
|
+
proposals; identity and core changes require approval and can be rolled back.
|
|
22
|
+
4. **Guardian** — health audit, budgets, circuit breaker, backup/validate/restore,
|
|
23
|
+
privacy cascade, secret/injection scanning.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Requirements:
|
|
28
|
+
|
|
29
|
+
- Node.js **>= 24** (uses the built-in `node:sqlite`)
|
|
30
|
+
- DSH `>=0.2.1-rc.2 <0.4.0`
|
|
31
|
+
|
|
32
|
+
### From a GitHub release tgz
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
dsh plugin --profile desktop add "C:\path\to\dsh-cortex-memory-0.1.0.tgz"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### From npm (after publishing)
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
dsh plugin --profile desktop add dsh-cortex-memory
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The plugin owns `~/.dsh/cortex` and does not read or write another memory
|
|
45
|
+
plugin's data.
|
|
46
|
+
|
|
47
|
+
## Models
|
|
48
|
+
|
|
49
|
+
Cortex works with **no model** by default:
|
|
50
|
+
|
|
51
|
+
| Mode | What it uses | Key |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `hash` | local lexical fallback | no |
|
|
54
|
+
| `local` | OpenAI-compatible local service, default Ollama `bge-m3` | no |
|
|
55
|
+
| `remote` | OpenAI-compatible `/embeddings` | **required** |
|
|
56
|
+
|
|
57
|
+
For local semantic search:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
ollama pull bge-m3
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then in **Settings → Embedding / rerank models** choose `local`, keep
|
|
64
|
+
`http://127.0.0.1:11434/v1`, model `bge-m3`, dimension `1024`.
|
|
65
|
+
|
|
66
|
+
Remote mode refuses to save without an API key. Keys are stored in the local
|
|
67
|
+
data directory and are never returned by the settings API.
|
|
68
|
+
|
|
69
|
+
Optional rerank uses an OpenAI-compatible `/rerank` endpoint; if it is off or
|
|
70
|
+
fails, fusion order is kept.
|
|
71
|
+
|
|
72
|
+
## UI
|
|
73
|
+
|
|
74
|
+
The client panel includes:
|
|
75
|
+
|
|
76
|
+
- Self / working memory / search / timeline / graph
|
|
77
|
+
- Emotion and life views
|
|
78
|
+
- Guardian, evolution, audit, retention
|
|
79
|
+
- **Playbook** with candidate review before activation
|
|
80
|
+
- Settings: feature switches, embedding/rerank configuration, permission
|
|
81
|
+
overrides and rollback
|
|
82
|
+
|
|
83
|
+
All settings changes that affect runtime take effect after a plugin reload or
|
|
84
|
+
DSH restart.
|
|
85
|
+
|
|
86
|
+
## CLI
|
|
87
|
+
|
|
88
|
+
```powershell
|
|
89
|
+
node bin/cortex.mjs --data C:\path\to\data health
|
|
90
|
+
node bin/cortex.mjs --data C:\path\to\data search --query "米食"
|
|
91
|
+
node bin/cortex.mjs --data C:\path\to\data export-md --dir C:\path\to\mirror
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Compatibility
|
|
95
|
+
|
|
96
|
+
Tested in our environment:
|
|
97
|
+
|
|
98
|
+
- Windows 11
|
|
99
|
+
- DSH 0.2.2-rc.1
|
|
100
|
+
- Node.js 26.13.0
|
|
101
|
+
- Fresh `~/.dsh/cortex` data root
|
|
102
|
+
- No native runtime dependencies
|
|
103
|
+
|
|
104
|
+
Requirements:
|
|
105
|
+
|
|
106
|
+
- **Node.js >= 24** (the engine uses `node:sqlite`)
|
|
107
|
+
- DSH `>=0.2.1-rc.2 <0.4.0`
|
|
108
|
+
- One profile should run only one `cortex` host entry
|
|
109
|
+
|
|
110
|
+
It is **not** guaranteed to work on Node 20/22, older DSH releases, or a
|
|
111
|
+
profile that already has another plugin using the same `cortex` id. This is a
|
|
112
|
+
real compatibility boundary, not a marketing detail.
|
|
113
|
+
|
|
114
|
+
## Testing
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
npm test
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The repository includes unit and integration tests for storage, retrieval,
|
|
121
|
+
temporal facts, playbook, consolidation, guardian, API, client contract,
|
|
122
|
+
installation packaging and edge cases. Public copy test result: **373 passing**.
|
|
123
|
+
|
|
124
|
+
A package-level smoke test is also run from the packed tgz before release.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## Architecture
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
Host events -> capture -> encode -> event ledger + trace + facts + FTS
|
|
131
|
+
|
|
|
132
|
+
prompt <- budgets <- tiered recall <- hybrid retrieval (FTS + graph + vector)
|
|
133
|
+
|
|
|
134
|
+
reflection / consolidation / playbook / growth
|
|
135
|
+
|
|
|
136
|
+
identity versions / policies / guardian / audit
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
See [docs/install-windows.md](docs/install-windows.md),
|
|
140
|
+
[docs/models.md](docs/models.md) and
|
|
141
|
+
[docs/compatibility.md](docs/compatibility.md).
|
|
142
|
+
|
|
143
|
+
## Data and privacy
|
|
144
|
+
|
|
145
|
+
- Default data root: `~/.dsh/cortex`
|
|
146
|
+
- No telemetry and no network calls by default
|
|
147
|
+
- `hash` mode is fully offline
|
|
148
|
+
- `local` mode talks only to the configured local service
|
|
149
|
+
- `remote` mode sends text to the configured embedding endpoint
|
|
150
|
+
- Do not put API keys in prompts or issue reports
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT. See [LICENSE](LICENSE).
|
package/bench/locomo.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { evaluateQueries } from './metrics.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LoCoMo retrieval adapter (offline, no dataset bundled).
|
|
5
|
+
* Dataset/repo: https://github.com/snap-research/locomo
|
|
6
|
+
*
|
|
7
|
+
* The adapter is intentionally shape-tolerant: official samples expose
|
|
8
|
+
* conversation sessions as `session_1`, `session_2`, ... with `evidence` like
|
|
9
|
+
* `D1:2`. It evaluates retrieval only; it does not claim official QA scores.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function sessionKeys(conversation) {
|
|
13
|
+
return Object.keys(conversation ?? {})
|
|
14
|
+
.filter((key) => /^session_\d+$/.test(key))
|
|
15
|
+
.sort((a, b) => Number(a.split('_')[1]) - Number(b.split('_')[1]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseEvidence(evidence) {
|
|
19
|
+
const list = Array.isArray(evidence) ? evidence : [evidence];
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const item of list) {
|
|
22
|
+
const match = String(item ?? '').match(/D(\d+):(\d+)/i);
|
|
23
|
+
if (match) out.push({ session: Number(match[1]), turn: Number(match[2]) });
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildLoCoMoIndex({ cortex, samples = [], scopeId = 'global' } = {}) {
|
|
29
|
+
if (!cortex) throw new Error('buildLoCoMoIndex needs cortex');
|
|
30
|
+
const turnTraceIds = new Map();
|
|
31
|
+
const queries = [];
|
|
32
|
+
let traces = 0;
|
|
33
|
+
|
|
34
|
+
for (const sample of samples) {
|
|
35
|
+
const sampleId = String(sample.sample_id ?? `sample-${queries.length}`);
|
|
36
|
+
const conversation = sample.conversation ?? {};
|
|
37
|
+
for (const key of sessionKeys(conversation)) {
|
|
38
|
+
const number = Number(key.split('_')[1]);
|
|
39
|
+
const date = conversation[`${key}_date_time`] ?? conversation[`${key}_date`] ?? null;
|
|
40
|
+
const turns = Array.isArray(conversation[key]) ? conversation[key] : [];
|
|
41
|
+
turns.forEach((turn, index) => {
|
|
42
|
+
const text = `${turn.speaker ?? turn.role ?? '用户'}:${turn.text ?? turn.content ?? ''}`;
|
|
43
|
+
if (!text.trim()) return;
|
|
44
|
+
const written = cortex.write({
|
|
45
|
+
text,
|
|
46
|
+
kind: 'episode',
|
|
47
|
+
title: `LoCoMo ${sampleId} D${number}:${index + 1}`,
|
|
48
|
+
tags: ['locomo'],
|
|
49
|
+
explicit: true,
|
|
50
|
+
importance: 0.5,
|
|
51
|
+
operationId: `locomo:${sampleId}:D${number}:${index + 1}`,
|
|
52
|
+
now: date ? Date.parse(date) : undefined,
|
|
53
|
+
});
|
|
54
|
+
if (written.trace) {
|
|
55
|
+
turnTraceIds.set(`${sampleId}:D${number}:${index + 1}`, written.trace.id);
|
|
56
|
+
traces += 1;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (const item of sample.qa ?? []) {
|
|
61
|
+
const relevant = [];
|
|
62
|
+
for (const ref of parseEvidence(item.evidence)) {
|
|
63
|
+
const id = turnTraceIds.get(`${sampleId}:D${ref.session}:${ref.turn}`);
|
|
64
|
+
if (id) relevant.push(id);
|
|
65
|
+
}
|
|
66
|
+
queries.push({
|
|
67
|
+
id: `${sampleId}:${queries.length}`,
|
|
68
|
+
query: item.question ?? '',
|
|
69
|
+
answer: item.answer ?? null,
|
|
70
|
+
type: String(item.category ?? 'unknown'),
|
|
71
|
+
relevant,
|
|
72
|
+
evidence: item.evidence ?? [],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { scopeId, traces, queries, turnTraceIds };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function evaluateLoCoMoRetrieval({ cortex, samples = [], limit = 10, includeAdversarial = false } = {}) {
|
|
80
|
+
const built = buildLoCoMoIndex({ cortex, samples });
|
|
81
|
+
const evaluated = built.queries.filter((query) => includeAdversarial || query.type !== '5');
|
|
82
|
+
const results = evaluated.map((query) => ({ items: cortex.search(query.query, { limit, budget: 3000 }).items, relevant: query.relevant, type: query.type }));
|
|
83
|
+
const overall = evaluateQueries(results, { k: limit });
|
|
84
|
+
const byType = {};
|
|
85
|
+
const buckets = {};
|
|
86
|
+
for (const result of results) (buckets[result.type] ??= []).push(result);
|
|
87
|
+
for (const [type, bucket] of Object.entries(buckets)) byType[type] = evaluateQueries(bucket, { k: limit });
|
|
88
|
+
return { ...overall, byType, built: { traces: built.traces, queries: built.queries.length, evaluated: evaluated.length } };
|
|
89
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { evaluateQueries } from './metrics.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LongMemEval retrieval adapter (offline, no dataset bundled).
|
|
5
|
+
* Dataset: https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned
|
|
6
|
+
* Paper: https://arxiv.org/abs/2410.10813
|
|
7
|
+
*
|
|
8
|
+
* This adapter evaluates session/turn *retrieval*, not end-to-end QA. It does
|
|
9
|
+
* not claim official LongMemEval scores.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function instanceType(instance) {
|
|
13
|
+
if (String(instance.question_id ?? '').endsWith('_abs')) return 'abstention';
|
|
14
|
+
return instance.question_type ?? 'unknown';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildLongMemEvalIndex({ cortex, instances = [], scopeId = 'global' } = {}) {
|
|
18
|
+
if (!cortex) throw new Error('buildLongMemEvalIndex needs cortex');
|
|
19
|
+
const sessionTraceIds = new Map();
|
|
20
|
+
const queries = [];
|
|
21
|
+
let traces = 0;
|
|
22
|
+
|
|
23
|
+
for (const instance of instances) {
|
|
24
|
+
const qid = String(instance.question_id ?? `q${queries.length}`);
|
|
25
|
+
const questionDate = instance.question_date ? Date.parse(instance.question_date) : Date.now();
|
|
26
|
+
const sessions = Array.isArray(instance.haystack_sessions) ? instance.haystack_sessions : [];
|
|
27
|
+
const sessionIds = Array.isArray(instance.haystack_session_ids) ? instance.haystack_session_ids : [];
|
|
28
|
+
const dates = Array.isArray(instance.haystack_dates) ? instance.haystack_dates : [];
|
|
29
|
+
const turnRelevant = [];
|
|
30
|
+
sessions.forEach((turns, sessionIndex) => {
|
|
31
|
+
const sessionId = String(sessionIds[sessionIndex] ?? `${qid}-s${sessionIndex}`);
|
|
32
|
+
const text = (Array.isArray(turns) ? turns : [])
|
|
33
|
+
.map((turn) => `${turn.role === 'assistant' ? '助手' : '用户'}:${turn.content ?? ''}`)
|
|
34
|
+
.join('\n');
|
|
35
|
+
if (!text.trim()) return;
|
|
36
|
+
const written = cortex.write({
|
|
37
|
+
text,
|
|
38
|
+
kind: 'episode',
|
|
39
|
+
title: `LongMemEval ${qid} ${sessionId}`,
|
|
40
|
+
tags: ['longmemeval', instanceType(instance)],
|
|
41
|
+
explicit: true,
|
|
42
|
+
importance: 0.5,
|
|
43
|
+
operationId: `lme:${qid}:${sessionId}`,
|
|
44
|
+
now: dates[sessionIndex] ? Date.parse(dates[sessionIndex]) : questionDate,
|
|
45
|
+
});
|
|
46
|
+
if (written.trace) {
|
|
47
|
+
sessionTraceIds.set(`${qid}:${sessionId}`, written.trace.id);
|
|
48
|
+
traces += 1;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const relevant = (instance.answer_session_ids ?? [])
|
|
52
|
+
.map((sessionId) => sessionTraceIds.get(`${qid}:${sessionId}`))
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
queries.push({
|
|
55
|
+
id: qid,
|
|
56
|
+
query: instance.question ?? '',
|
|
57
|
+
type: instanceType(instance),
|
|
58
|
+
questionDate: new Date(questionDate).toISOString(),
|
|
59
|
+
relevant,
|
|
60
|
+
answer: instance.answer ?? null,
|
|
61
|
+
abstention: instanceType(instance) === 'abstention',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { scopeId, traces, queries, sessionTraceIds };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function evaluateLongMemEvalRetrieval({ cortex, instances = [], limit = 10, includeAbstention = false } = {}) {
|
|
69
|
+
const built = buildLongMemEvalIndex({ cortex, instances });
|
|
70
|
+
const evaluated = built.queries.filter((query) => includeAbstention || !query.abstention);
|
|
71
|
+
const results = evaluated.map((query) => {
|
|
72
|
+
const output = cortex.search(query.query, { limit, budget: 3000 });
|
|
73
|
+
return { items: output.items, relevant: query.relevant, type: query.type };
|
|
74
|
+
});
|
|
75
|
+
const overall = evaluateQueries(results, { k: limit });
|
|
76
|
+
const byType = {};
|
|
77
|
+
const typeBuckets = {};
|
|
78
|
+
results.forEach((result) => {
|
|
79
|
+
(typeBuckets[result.type] ??= []).push(result);
|
|
80
|
+
});
|
|
81
|
+
for (const [type, bucket] of Object.entries(typeBuckets)) byType[type] = evaluateQueries(bucket, { k: limit });
|
|
82
|
+
return { ...overall, byType, built: { traces: built.traces, queries: built.queries.length, evaluated: evaluated.length } };
|
|
83
|
+
}
|
package/bench/metrics.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function recallAtK(ranked, relevant, k = 10) {
|
|
2
|
+
const rel = new Set(relevant);
|
|
3
|
+
const top = ranked.slice(0, Math.max(1, k));
|
|
4
|
+
return rel.size === 0 ? 0 : top.filter((id) => rel.has(id)).length / rel.size;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function precisionAtK(ranked, relevant, k = 10) {
|
|
8
|
+
const rel = new Set(relevant);
|
|
9
|
+
const top = ranked.slice(0, Math.max(1, k));
|
|
10
|
+
return top.length === 0 ? 0 : top.filter((id) => rel.has(id)).length / top.length;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function reciprocalRank(ranked, relevant, k = 10) {
|
|
14
|
+
const rel = new Set(relevant);
|
|
15
|
+
for (let i = 0; i < Math.min(ranked.length, k); i += 1) {
|
|
16
|
+
if (rel.has(ranked[i])) return 1 / (i + 1);
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ndcgAtK(ranked, relevant, k = 10) {
|
|
22
|
+
const rel = new Set(relevant);
|
|
23
|
+
if (rel.size === 0) return 0;
|
|
24
|
+
let dcg = 0;
|
|
25
|
+
for (let i = 0; i < Math.min(ranked.length, k); i += 1) {
|
|
26
|
+
if (rel.has(ranked[i])) dcg += 1 / Math.log2(i + 2);
|
|
27
|
+
}
|
|
28
|
+
let idcg = 0;
|
|
29
|
+
for (let i = 0; i < Math.min(rel.size, k); i += 1) idcg += 1 / Math.log2(i + 2);
|
|
30
|
+
return idcg === 0 ? 0 : dcg / idcg;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function percentile(values, p) {
|
|
34
|
+
const list = values.filter((value) => Number.isFinite(value)).slice().sort((a, b) => a - b);
|
|
35
|
+
if (list.length === 0) return null;
|
|
36
|
+
const index = Math.min(list.length - 1, Math.max(0, Math.ceil((p / 100) * list.length) - 1));
|
|
37
|
+
return list[index];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function evaluateQueries(results, { k = 10 } = {}) {
|
|
41
|
+
const rows = results.map((result) => {
|
|
42
|
+
const ranked = (result.items ?? []).map((item) => item.id);
|
|
43
|
+
const relevant = result.relevant ?? [];
|
|
44
|
+
return {
|
|
45
|
+
recall: recallAtK(ranked, relevant, k),
|
|
46
|
+
precision: precisionAtK(ranked, relevant, k),
|
|
47
|
+
mrr: reciprocalRank(ranked, relevant, k),
|
|
48
|
+
ndcg: ndcgAtK(ranked, relevant, k),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const mean = (key) => rows.length ? rows.reduce((sum, row) => sum + row[key], 0) / rows.length : 0;
|
|
52
|
+
return {
|
|
53
|
+
n: rows.length,
|
|
54
|
+
recallAtK: mean('recall'),
|
|
55
|
+
precisionAtK: mean('precision'),
|
|
56
|
+
mrrAtK: mean('mrr'),
|
|
57
|
+
ndcgAtK: mean('ndcg'),
|
|
58
|
+
};
|
|
59
|
+
}
|
package/bench/run.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createCortex } from '../src/service/cortex.js';
|
|
5
|
+
import { generateSynthetic } from './synthetic.js';
|
|
6
|
+
import { evaluateQueries, percentile } from './metrics.js';
|
|
7
|
+
|
|
8
|
+
export function runBenchmark({
|
|
9
|
+
count = Number(process.env.CORTEX_BENCH_COUNT ?? 10000),
|
|
10
|
+
queries = Number(process.env.CORTEX_BENCH_QUERIES ?? 100),
|
|
11
|
+
seed = 42,
|
|
12
|
+
vector = process.env.CORTEX_BENCH_VECTOR === '1',
|
|
13
|
+
limit = 10,
|
|
14
|
+
} = {}) {
|
|
15
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), 'cortex-bench-'));
|
|
16
|
+
const cortex = createCortex({ config: { dataRoot: dir, dbFile: path.join(dir, 'cortex.sqlite'), vectorEnabled: vector, contextBudget: 2200 } });
|
|
17
|
+
try {
|
|
18
|
+
const dataset = generateSynthetic({ count, queries, seed });
|
|
19
|
+
const idMap = new Map();
|
|
20
|
+
const writeStart = performance.now();
|
|
21
|
+
const inputs = dataset.traces.map((trace) => ({
|
|
22
|
+
text: trace.text,
|
|
23
|
+
title: trace.title,
|
|
24
|
+
tags: trace.tags,
|
|
25
|
+
importance: trace.importance,
|
|
26
|
+
explicit: true,
|
|
27
|
+
operationId: `bench:${trace.id}`,
|
|
28
|
+
now: Date.parse(trace.recordedAt),
|
|
29
|
+
}));
|
|
30
|
+
const written = cortex.writeMany(inputs, { chunkSize: 250 });
|
|
31
|
+
for (let i = 0; i < written.length; i += 1) {
|
|
32
|
+
if (written[i].trace) idMap.set(dataset.traces[i].id, written[i].trace.id);
|
|
33
|
+
}
|
|
34
|
+
const writeMs = performance.now() - writeStart;
|
|
35
|
+
const results = [];
|
|
36
|
+
const searchLatencies = [];
|
|
37
|
+
for (const query of dataset.queries) {
|
|
38
|
+
const start = performance.now();
|
|
39
|
+
const result = cortex.search(query.query, { limit, budget: 2200 });
|
|
40
|
+
searchLatencies.push(performance.now() - start);
|
|
41
|
+
results.push({ items: result.items, relevant: query.relevant.map((id) => idMap.get(id)).filter(Boolean) });
|
|
42
|
+
}
|
|
43
|
+
const metrics = evaluateQueries(results, { k: limit });
|
|
44
|
+
return {
|
|
45
|
+
generatedAt: dataset.generatedAt,
|
|
46
|
+
seed,
|
|
47
|
+
count,
|
|
48
|
+
queries: dataset.queries.length,
|
|
49
|
+
vector,
|
|
50
|
+
writeMs: Math.round(writeMs),
|
|
51
|
+
writesPerSecond: Math.round((count / writeMs) * 1000),
|
|
52
|
+
search: {
|
|
53
|
+
p50Ms: Math.round((percentile(searchLatencies, 50) ?? 0) * 100) / 100,
|
|
54
|
+
p95Ms: Math.round((percentile(searchLatencies, 95) ?? 0) * 100) / 100,
|
|
55
|
+
...metrics,
|
|
56
|
+
},
|
|
57
|
+
dbBytes: (() => {
|
|
58
|
+
try { return cortex.db.prepare('PRAGMA page_count').get().page_count * cortex.db.prepare('PRAGMA page_size').get().page_size; } catch { return null; }
|
|
59
|
+
})(),
|
|
60
|
+
};
|
|
61
|
+
} finally {
|
|
62
|
+
cortex.close();
|
|
63
|
+
rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isMain = process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replace(/\\/g, '/')}`).href;
|
|
68
|
+
if (isMain) {
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
const count = Number(args.find((arg) => arg.startsWith('--count='))?.split('=')[1] ?? 10000);
|
|
71
|
+
const queries = Number(args.find((arg) => arg.startsWith('--queries='))?.split('=')[1] ?? 100);
|
|
72
|
+
const result = runBenchmark({ count, queries, vector: args.includes('--vector') });
|
|
73
|
+
console.log(JSON.stringify(result, null, 2));
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic synthetic memory workload. It is not a public benchmark and
|
|
3
|
+
* must not be reported as LoCoMo/LongMemEval numbers; it is a local regression
|
|
4
|
+
* and latency harness.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function mulberry32(seed) {
|
|
8
|
+
let a = seed >>> 0;
|
|
9
|
+
return function random() {
|
|
10
|
+
a += 0x6D2B79F5;
|
|
11
|
+
let t = a;
|
|
12
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
13
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
14
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PLACES = ['米饭', '插件', '记忆引擎', '鲸鱼', '用户', '伙伴', '直播', '语音', '项目', '代码', '测试', 'D:\\legacy-data', 'QQ', 'B站', '声音', '梦', '日记'];
|
|
19
|
+
const VERBS = ['修好了', '想起来了', '决定继续', '记录一下', '有点担心', '很开心', '测试了', '重新整理了', '发现了', '计划明天'];
|
|
20
|
+
const TOPICS = ['图检索', '向量检索', '遗忘曲线', '工作卡', '火花卡', '身份版本', '关系模型', '反思', '巩固', '守护', '备份', '权限'];
|
|
21
|
+
|
|
22
|
+
export function generateSynthetic({ count = 1000, queries = 50, seed = 42 } = {}) {
|
|
23
|
+
const random = mulberry32(seed);
|
|
24
|
+
const pick = (list) => list[Math.floor(random() * list.length)];
|
|
25
|
+
const traces = [];
|
|
26
|
+
const idByToken = new Map();
|
|
27
|
+
for (let i = 0; i < count; i += 1) {
|
|
28
|
+
const place = pick(PLACES);
|
|
29
|
+
const verb = pick(VERBS);
|
|
30
|
+
const topic = pick(TOPICS);
|
|
31
|
+
const day = 1 + Math.floor(random() * 300);
|
|
32
|
+
const text = `${place} ${verb} ${topic},第 ${i} 条合成记忆。`;
|
|
33
|
+
const id = `trc_syn_${i}`;
|
|
34
|
+
traces.push({
|
|
35
|
+
id,
|
|
36
|
+
text,
|
|
37
|
+
title: `${topic} #${i}`,
|
|
38
|
+
tags: [place, topic],
|
|
39
|
+
importance: Math.round(random() * 100) / 100,
|
|
40
|
+
recordedAt: new Date(Date.UTC(2026, 0, 1) + day * 86400000).toISOString(),
|
|
41
|
+
});
|
|
42
|
+
for (const token of [place, topic]) {
|
|
43
|
+
const list = idByToken.get(token) ?? [];
|
|
44
|
+
list.push(id);
|
|
45
|
+
idByToken.set(token, list);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const queryList = [];
|
|
49
|
+
for (let i = 0; i < queries; i += 1) {
|
|
50
|
+
const token = pick([...idByToken.keys()]);
|
|
51
|
+
const relevant = (idByToken.get(token) ?? []).slice(0, 8);
|
|
52
|
+
queryList.push({ query: token, type: 'keyword', relevant });
|
|
53
|
+
}
|
|
54
|
+
return { traces, queries: queryList, seed, generatedAt: new Date().toISOString() };
|
|
55
|
+
}
|
package/bin/cortex.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createCortex } from '../src/service/cortex.js';
|
|
5
|
+
import { createShioriImporter } from '../src/adapters/shiori.js';
|
|
6
|
+
import { exportMarkdown } from '../src/adapters/markdown.js';
|
|
7
|
+
import { importMarkdown } from '../src/adapters/markdown-import.js';
|
|
8
|
+
import { effectiveFeatures } from '../src/core/feature-overrides.js';
|
|
9
|
+
|
|
10
|
+
function parse(argv) {
|
|
11
|
+
const out = { _: [] };
|
|
12
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13
|
+
const arg = argv[i];
|
|
14
|
+
if (arg.startsWith('--')) {
|
|
15
|
+
const eq = arg.indexOf('=');
|
|
16
|
+
if (eq > 0) out[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
17
|
+
else if (argv[i + 1] && !argv[i + 1].startsWith('--')) { out[arg.slice(2)] = argv[i + 1]; i += 1; }
|
|
18
|
+
else out[arg.slice(2)] = true;
|
|
19
|
+
} else {
|
|
20
|
+
out._.push(arg);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function usage() {
|
|
27
|
+
console.log(`dsh-cortex CLI (source only)
|
|
28
|
+
|
|
29
|
+
--data <dir> cortex data directory (default ~/.dsh/cortex)
|
|
30
|
+
|
|
31
|
+
Commands:
|
|
32
|
+
write --text <text> [--kind <kind>] [--importance 0.5]
|
|
33
|
+
search --query <text> [--limit 10]
|
|
34
|
+
self
|
|
35
|
+
health
|
|
36
|
+
guard
|
|
37
|
+
import-shiori --root <legacy-data>
|
|
38
|
+
export-md --dir <target>
|
|
39
|
+
import-md --dir <vault> [--dry-run]
|
|
40
|
+
bench [--count 10000] [--queries 100]
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function main() {
|
|
45
|
+
const args = parse(process.argv.slice(2));
|
|
46
|
+
const command = args._[0];
|
|
47
|
+
const dataRoot = path.resolve(args.data ?? path.join(os.homedir(), '.dsh', 'cortex'));
|
|
48
|
+
if (!command || command === 'help' || args.help) { usage(); return; }
|
|
49
|
+
|
|
50
|
+
if (command === 'bench') {
|
|
51
|
+
const count = Number(args.count ?? 10000);
|
|
52
|
+
const queries = Number(args.queries ?? 100);
|
|
53
|
+
import('../bench/run.js').then(({ runBenchmark }) => {
|
|
54
|
+
const result = runBenchmark({ count, queries, vector: args.vector === true });
|
|
55
|
+
console.log(JSON.stringify(result, null, 2));
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const baseConfig = { dataRoot, dbFile: path.join(dataRoot, 'cortex.sqlite') };
|
|
61
|
+
const effective = effectiveFeatures(baseConfig);
|
|
62
|
+
const cortex = createCortex({ config: { ...baseConfig, features: effective.features } });
|
|
63
|
+
try {
|
|
64
|
+
if (command === 'write') {
|
|
65
|
+
const result = cortex.write({ text: args.text, kind: args.kind, importance: args.importance ? Number(args.importance) : undefined, explicit: true });
|
|
66
|
+
console.log(JSON.stringify({ skipped: result.skipped, id: result.trace?.id, kind: result.trace?.kind }, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (command === 'search') {
|
|
70
|
+
const result = cortex.search(args.query ?? '', { limit: Number(args.limit ?? 10) });
|
|
71
|
+
for (const item of result.items) console.log(`[${item.layer}] ${item.score} ${item.id} ${item.title}\n ${item.snippet}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (command === 'self') { console.log(JSON.stringify(cortex.self(), null, 2)); return; }
|
|
75
|
+
if (command === 'health') { console.log(JSON.stringify(cortex.health(), null, 2)); return; }
|
|
76
|
+
if (command === 'guard') { console.log(JSON.stringify(cortex.guardian.check(), null, 2)); return; }
|
|
77
|
+
if (command === 'import-shiori') {
|
|
78
|
+
const importer = createShioriImporter({ cortex });
|
|
79
|
+
console.log(JSON.stringify(importer.importFrom(args.root), null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (command === 'export-md') {
|
|
83
|
+
console.log(JSON.stringify(exportMarkdown(cortex, args.dir ?? path.join(dataRoot, 'mirror')), null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (command === 'import-md') {
|
|
87
|
+
console.log(JSON.stringify(importMarkdown(cortex, args.dir ?? process.cwd(), { dryRun: args['dry-run'] === true }), null, 2));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
usage();
|
|
91
|
+
} finally {
|
|
92
|
+
cortex.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main();
|
|
97
|
+
|