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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -2
  3. package/bench/locomo.js +89 -0
  4. package/bench/longmemeval.js +83 -0
  5. package/bench/metrics.js +59 -0
  6. package/bench/run.js +74 -0
  7. package/bench/synthetic.js +55 -0
  8. package/bin/cortex.mjs +97 -0
  9. package/client.js +2085 -0
  10. package/cordis.patch.yml +7 -0
  11. package/docs/compatibility.md +39 -0
  12. package/docs/install-windows.md +56 -0
  13. package/docs/models.md +48 -0
  14. package/icon.svg +15 -0
  15. package/index.js +149 -0
  16. package/locale/en.json +6 -0
  17. package/locale/zh.json +6 -0
  18. package/package.json +83 -4
  19. package/src/adapters/json.js +67 -0
  20. package/src/adapters/markdown-import.js +190 -0
  21. package/src/adapters/markdown.js +96 -0
  22. package/src/adapters/shiori.js +160 -0
  23. package/src/api/client.js +460 -0
  24. package/src/api/routes.js +392 -0
  25. package/src/api/validate.js +68 -0
  26. package/src/core/config.js +112 -0
  27. package/src/core/errors.js +36 -0
  28. package/src/core/feature-overrides.js +71 -0
  29. package/src/core/ids.js +70 -0
  30. package/src/core/json.js +51 -0
  31. package/src/core/model-config.js +126 -0
  32. package/src/core/time.js +83 -0
  33. package/src/engines/autonomy.js +64 -0
  34. package/src/engines/compression.js +69 -0
  35. package/src/engines/consolidate.js +163 -0
  36. package/src/engines/develop.js +164 -0
  37. package/src/engines/digest.js +51 -0
  38. package/src/engines/encode.js +199 -0
  39. package/src/engines/evaluate.js +39 -0
  40. package/src/engines/extract.js +67 -0
  41. package/src/engines/graph-analytics.js +116 -0
  42. package/src/engines/graph.js +132 -0
  43. package/src/engines/growth.js +149 -0
  44. package/src/engines/health.js +113 -0
  45. package/src/engines/induct.js +28 -0
  46. package/src/engines/jobs.js +37 -0
  47. package/src/engines/life.js +147 -0
  48. package/src/engines/llm-facts.js +48 -0
  49. package/src/engines/narrative.js +85 -0
  50. package/src/engines/privacy.js +120 -0
  51. package/src/engines/qa.js +72 -0
  52. package/src/engines/reflect.js +95 -0
  53. package/src/engines/retention.js +41 -0
  54. package/src/engines/retrieve.js +311 -0
  55. package/src/engines/skills.js +62 -0
  56. package/src/guardian/circuit.js +66 -0
  57. package/src/guardian/guardian.js +78 -0
  58. package/src/guardian/invariants.js +59 -0
  59. package/src/guardian/rotation.js +33 -0
  60. package/src/guardian/scanner.js +66 -0
  61. package/src/host/runtime.js +197 -0
  62. package/src/host/scheduler.js +77 -0
  63. package/src/llm/adapter.js +90 -0
  64. package/src/memory/dsr.js +201 -0
  65. package/src/memory/query.js +58 -0
  66. package/src/memory/temporal.js +64 -0
  67. package/src/memory/tokenizer.js +77 -0
  68. package/src/observability/metrics.js +45 -0
  69. package/src/prompt/assemble.js +78 -0
  70. package/src/prompt/tiers.js +50 -0
  71. package/src/self/appraisal.js +135 -0
  72. package/src/self/appraisals.js +59 -0
  73. package/src/self/development.js +156 -0
  74. package/src/self/emotion.js +126 -0
  75. package/src/self/evolution.js +163 -0
  76. package/src/self/identity.js +114 -0
  77. package/src/self/initiative.js +187 -0
  78. package/src/self/policies.js +80 -0
  79. package/src/self/relationship.js +86 -0
  80. package/src/self/state.js +184 -0
  81. package/src/self/working.js +153 -0
  82. package/src/service/cortex.js +577 -0
  83. package/src/store/database.js +116 -0
  84. package/src/store/repositories/audit.js +35 -0
  85. package/src/store/repositories/edges.js +86 -0
  86. package/src/store/repositories/entities.js +72 -0
  87. package/src/store/repositories/events.js +116 -0
  88. package/src/store/repositories/facts.js +249 -0
  89. package/src/store/repositories/feedback.js +53 -0
  90. package/src/store/repositories/jobs.js +94 -0
  91. package/src/store/repositories/playbook.js +86 -0
  92. package/src/store/repositories/skills.js +104 -0
  93. package/src/store/repositories/summaries.js +52 -0
  94. package/src/store/repositories/traces.js +267 -0
  95. package/src/store/schema.sql +483 -0
  96. package/src/tools/definitions.js +729 -0
  97. package/src/vector/ann.js +157 -0
  98. package/src/vector/embedder.js +50 -0
  99. package/src/vector/index.js +126 -0
  100. 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
- # Temporary Holding Version
1
+ # Cortex Memory for DeepSeek Harness
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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).
@@ -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
+ }
@@ -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
+