dsh-cortex-memory 0.0.0-stage → 0.2.0-rc.11
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 +92 -2
- package/bench/ab-engine.mjs +60 -0
- package/bench/ann-bench.mjs +78 -0
- package/bench/hnsw-bench.mjs +79 -0
- package/bench/locomo.js +89 -0
- package/bench/longmemeval.js +83 -0
- package/bench/metrics.js +67 -0
- package/bench/migration-bench.mjs +69 -0
- package/bench/run.js +77 -0
- package/bench/synthetic.js +99 -0
- package/bench/vector-ab.mjs +76 -0
- package/bin/cortex.mjs +96 -0
- package/client.js +2350 -0
- package/cordis.patch.yml +7 -0
- package/docs/install-windows.md +144 -0
- package/docs/models.md +183 -0
- package/docs/multimodal.md +112 -0
- package/docs/scopes.md +142 -0
- package/examples/README.md +72 -0
- package/examples/setup-cortex-bge.ps1 +66 -0
- package/examples/uninstall-cortex.ps1 +91 -0
- package/icon.svg +15 -0
- package/index.js +163 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +76 -4
- package/src/adapters/json.js +67 -0
- package/src/adapters/markdown-import.js +209 -0
- package/src/adapters/markdown.js +106 -0
- package/src/adapters/shiori.js +160 -0
- package/src/api/client.js +460 -0
- package/src/api/routes.js +532 -0
- package/src/api/validate.js +94 -0
- package/src/core/attachment-reader.js +31 -0
- package/src/core/attachments.js +39 -0
- package/src/core/config.js +147 -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 +128 -0
- package/src/core/profile.js +32 -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 +237 -0
- package/src/engines/evaluate.js +39 -0
- package/src/engines/extract.js +72 -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 +342 -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 +92 -0
- package/src/observability/metrics.js +45 -0
- package/src/prompt/assemble.js +78 -0
- package/src/prompt/tiers.js +50 -0
- package/src/security/scope-grants.js +73 -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 +765 -0
- package/src/store/database.js +148 -0
- package/src/store/repositories/audit.js +35 -0
- package/src/store/repositories/edges.js +130 -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 +278 -0
- package/src/store/schema.sql +484 -0
- package/src/tools/definitions.js +786 -0
- package/src/vector/ann.js +157 -0
- package/src/vector/embedder.js +50 -0
- package/src/vector/external-ann.js +37 -0
- package/src/vector/hnsw.js +246 -0
- package/src/vector/index.js +209 -0
- package/src/vector/providers.js +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 本鲸 / 凌寒
|
|
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,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-cortex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 鏈哺鐨勬柊澶ц剳銆傚垎灞傘€佹椂搴忋€佸彲鑷垜宸╁浐鐨勮蹇嗗紩鎿庛€?> 鐘舵€侊細**鍙墦鍖呫€佸彲瀹夎**锛涙湰鏈哄紑鍙戝壇鏈粛鏈畨瑁咃紝涔熸病鏈夋帴 profile銆?> Windows 鐩存帴瀹夎/鍥炴粴瑙?[docs/install-windows.md](docs/install-windows.md)銆?
|
|
4
|
+
`dsh-cortex` is a local-first memory engine for DeepSeek Harness. It is designed
|
|
5
|
+
as a successor to the `dsh-shiori` + `dsh-shiori-companion` pair, not a patch on
|
|
6
|
+
top of them.
|
|
7
|
+
|
|
8
|
+
## Why another memory plugin
|
|
9
|
+
|
|
10
|
+
The shiori stack is good at four things: a stable persona, a compact core,
|
|
11
|
+
triggered spark cards and on-demand topics. It is weaker at temporal facts,
|
|
12
|
+
episodic provenance, procedural learning, consolidation and calibrated
|
|
13
|
+
forgetting. `dsh-cortex` keeps the good parts and adds the missing layers:
|
|
14
|
+
|
|
15
|
+
| Layer | What it holds | Lifetime |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| L0 identity | self-model, values, boundaries, stable traits | long, explicit promotion |
|
|
18
|
+
| L1 working | current goals, focus, pinned context, recent turn buffer | minutes鈥揾ours |
|
|
19
|
+
| L2 episodic | what happened, when, with whom, provenance, emotion | days鈥搚ears |
|
|
20
|
+
| L3 semantic | entities, facts with validity windows, preferences | until invalidated |
|
|
21
|
+
| L4 procedural | skills, workflows, preconditions, success/failure stats | until superseded |
|
|
22
|
+
| L5 reflective | episode summaries, insights, narratives | derived, regenerable |
|
|
23
|
+
| archive | cold but searchable, wake-word recall | until privacy deletion |
|
|
24
|
+
|
|
25
|
+
## Engines
|
|
26
|
+
|
|
27
|
+
- **event log** 鈥?append-only, hash-chained, `event_id` / `operation_id`; the
|
|
28
|
+
only source of truth.
|
|
29
|
+
- **write pipeline** 鈥?scan secrets/injection 鈫?classify 鈫?dedupe 鈫? conflict-resolve 鈫?link 鈫?persist; async jobs with idempotent operation IDs.
|
|
30
|
+
- **temporal graph** 鈥?entities, facts and edges with `valid_from` /
|
|
31
|
+
`valid_to`; old facts are invalidated, never silently overwritten.
|
|
32
|
+
- **hybrid retrieval** 鈥?FTS5 BM25 + optional vector + graph activation +
|
|
33
|
+
recency / importance / feedback, fused with RRF, diversified with a greedy
|
|
34
|
+
MMR-style marginal-gain pass; query filters (`kind:`, `tag:`, `after:`,
|
|
35
|
+
`importance:>`, 鈥?.
|
|
36
|
+
- **DSR forgetting** 鈥?FSRS-inspired stability / difficulty / retrievability;
|
|
37
|
+
archive instead of delete, revive on successful recall.
|
|
38
|
+
- **compression / retention** 鈥?archive only when a derived model covers the
|
|
39
|
+
trace, or when a reviewed retention plan says it is old/low-value; important
|
|
40
|
+
memories are never auto-archived.
|
|
41
|
+
- **reflection** 鈥?episode segmentation, summaries, narrative threads,
|
|
42
|
+
duplicate/contradiction detection, optional LLM dream/summary.
|
|
43
|
+
- **self model** 鈥?drives, mood, attention, appraisal, relationships,
|
|
44
|
+
initiatives, 24-category emotion/affect, identity versions.
|
|
45
|
+
- **development** 鈥?maturity/stage from memory depth, feedback, calibration,
|
|
46
|
+
skills, narrative and relationships.
|
|
47
|
+
- **evolution** 鈥?policy proposals 鈫?replay evaluation 鈫?explicit approval 鈫? rollback; identity changes are separate and approval-only.
|
|
48
|
+
- **guardian** 鈥?invariants, circuit breaker, budget, backup/validate/offline
|
|
49
|
+
restore, FTS repair, privacy cascade, secret/injection scanner.
|
|
50
|
+
- **host runtime** 鈥?`session/event` turn capture + `user/message` /
|
|
51
|
+
`agent/pre-step` recall prefetch into the systemPrompt section.
|
|
52
|
+
- **API/UI/CLI** 鈥?loopback-only HTTP API, sidebar panel, `bin/cortex.mjs`.
|
|
53
|
+
|
|
54
|
+
## Status
|
|
55
|
+
|
|
56
|
+
This is a source-only development tree. `npm test` uses `node:test` and the
|
|
57
|
+
built-in `node:sqlite`; no native dependency and no network are required for
|
|
58
|
+
the core suite. Profiles install a built tgz explicitly; this tree is not
|
|
59
|
+
auto-linked into them.
|
|
60
|
+
|
|
61
|
+
- **395** tests passing (unit + integration).
|
|
62
|
+
- 21 model-facing tools.
|
|
63
|
+
- **`cortex_as_of`**锛歴trict / historical / belief / associative銆?- **`cortex_playbook`**锛歀1.5 宸ヤ綔鍑嗗垯锛屽甫 helpful/harmful銆乿alidTo銆佽繃鏈熶笉娉ㄥ叆銆?- **`cortex_consolidate`**锛歀LM 鍐崇瓥娓呭崟 + 鏍¢獙 + `dream_runs` 鍙洖鏀捐褰曪紱鏃?LLM 鏃惰褰?skipped 骞堕浂鏀瑰姩銆?- **`cortex_search({tiered:true})`**锛歱erm/hot/cold 鍒嗗眰棰勭畻锛宑old 鍙粰绱㈠紩銆?- **寮傛娣峰悎妫€绱?*锛歚GET /search?async=true` / QA 璧?`searchAsync`锛涜繙绋?embedder 鍙储寮曪紝涓嶉樆濉炲啓銆?- 5k synthetic benchmark (needle, vector off): ~2.6-2.8k writes/s, p50 ~20-35ms; 100k migration ~2.3s. These are local numbers, not an official score.
|
|
64
|
+
- Source/tests/docs 鈮?33k lines (includes `docs/book/`); source-only, not installed here.
|
|
65
|
+
- Windows tgz: `dist/dsh-cortex-0.2.0-rc.11.tgz` (zero runtime deps; Node >= 24).
|
|
66
|
+
|
|
67
|
+
## Design docs
|
|
68
|
+
|
|
69
|
+
- [research notes](docs/research/README.md)
|
|
70
|
+
- [architecture](docs/architecture.md)
|
|
71
|
+
- [Anima: from memory engine to a growing individual](docs/anima.md)
|
|
72
|
+
- [modules and invariants](docs/modules.md)
|
|
73
|
+
- [operations / CLI / backup](docs/operations.md)
|
|
74
|
+
- [security and privacy](docs/security.md)
|
|
75
|
+
- [scope isolation and read-only sharing](docs/scopes.md)
|
|
76
|
+
- [multimodal attachment metadata](docs/multimodal.md)
|
|
77
|
+
- [benchmark](docs/benchmark.md)
|
|
78
|
+
- [OpenAPI 3.1](docs/openapi.yaml)
|
|
79
|
+
- [API client](src/api/client.js)
|
|
80
|
+
- [UI guide](docs/ui.md)
|
|
81
|
+
- [design book](docs/book/README.md)
|
|
82
|
+
- [four-plugin comparison](docs/comparison-2026-11-11.md)
|
|
83
|
+
- [Windows install and rollback](docs/install-windows.md)
|
|
84
|
+
- [optional models](docs/models.md)
|
|
85
|
+
- [shiori companion review](docs/companion-feature-review.md)
|
|
86
|
+
- [ecosystem comparison 2026-11-11](docs/ecosystem-comparison-2026-11-11.md)
|
|
87
|
+
- [roadmap](docs/roadmap.md)
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT. External ideas are cited in the research notes; no third-party source
|
|
92
|
+
code is copied into this package.
|
|
93
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { generateSynthetic } from './synthetic.js';
|
|
6
|
+
import { evaluateQueries, percentile } from './metrics.js';
|
|
7
|
+
|
|
8
|
+
const engineRoot = process.argv[2];
|
|
9
|
+
const task = process.argv[3] ?? 'paraphrase';
|
|
10
|
+
const count = Number(process.argv[4] ?? 5000);
|
|
11
|
+
const queryCount = Number(process.argv[5] ?? 100);
|
|
12
|
+
|
|
13
|
+
if (!engineRoot) {
|
|
14
|
+
console.error('usage: node bench/ab-engine.mjs <engine-root> [needle|theme|paraphrase] [count] [queries]');
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { createCortex } = await import(pathToFileURL(path.join(engineRoot, 'src', 'service', 'cortex.js')).href);
|
|
19
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), 'cortex-ab-'));
|
|
20
|
+
const cortex = createCortex({ config: { dataRoot: dir, dbFile: path.join(dir, 'cortex.sqlite'), contextBudget: 2200 } });
|
|
21
|
+
try {
|
|
22
|
+
const dataset = generateSynthetic({ count, queries: queryCount, seed: 42, task });
|
|
23
|
+
const written = cortex.writeMany(dataset.traces.map((trace) => ({
|
|
24
|
+
text: trace.text,
|
|
25
|
+
title: trace.title,
|
|
26
|
+
tags: trace.tags,
|
|
27
|
+
importance: trace.importance,
|
|
28
|
+
explicit: true,
|
|
29
|
+
operationId: `ab:${trace.id}`,
|
|
30
|
+
now: Date.parse(trace.recordedAt),
|
|
31
|
+
})), { chunkSize: 250 });
|
|
32
|
+
const idMap = new Map();
|
|
33
|
+
for (let i = 0; i < written.length; i += 1) {
|
|
34
|
+
if (written[i].trace) idMap.set(dataset.traces[i].id, written[i].trace.id);
|
|
35
|
+
}
|
|
36
|
+
const results = [];
|
|
37
|
+
const latencies = [];
|
|
38
|
+
for (const query of dataset.queries) {
|
|
39
|
+
const start = performance.now();
|
|
40
|
+
const result = cortex.search(query.query, { limit: 10, budget: 2200 });
|
|
41
|
+
latencies.push(performance.now() - start);
|
|
42
|
+
results.push({
|
|
43
|
+
items: result.items,
|
|
44
|
+
relevant: query.relevant.map((id) => idMap.get(id)).filter(Boolean),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const metrics = evaluateQueries(results, { k: 10 });
|
|
48
|
+
console.log(JSON.stringify({
|
|
49
|
+
engine: engineRoot,
|
|
50
|
+
task,
|
|
51
|
+
count,
|
|
52
|
+
queries: dataset.queries.length,
|
|
53
|
+
p50Ms: Math.round((percentile(latencies, 50) ?? 0) * 100) / 100,
|
|
54
|
+
p95Ms: Math.round((percentile(latencies, 95) ?? 0) * 100) / 100,
|
|
55
|
+
...metrics,
|
|
56
|
+
}, null, 2));
|
|
57
|
+
} finally {
|
|
58
|
+
cortex.close();
|
|
59
|
+
rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createAnnIndex } from '../src/vector/ann.js';
|
|
2
|
+
|
|
3
|
+
function randomFactory(seed) {
|
|
4
|
+
let state = seed >>> 0;
|
|
5
|
+
return () => {
|
|
6
|
+
state = (state * 1664525 + 1013904223) >>> 0;
|
|
7
|
+
return state / 4294967296;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeVector(random, dim = 64) {
|
|
12
|
+
const value = Array.from({ length: dim }, () => random() * 2 - 1);
|
|
13
|
+
const norm = Math.sqrt(value.reduce((sum, item) => sum + item * item, 0)) || 1;
|
|
14
|
+
return value.map((item) => item / norm);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cosine(a, b) {
|
|
18
|
+
let dot = 0;
|
|
19
|
+
for (let i = 0; i < a.length; i += 1) dot += a[i] * b[i];
|
|
20
|
+
return dot;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function percentile(values, p) {
|
|
24
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
25
|
+
if (!sorted.length) return null;
|
|
26
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const count = Number(process.argv[2] ?? 5000);
|
|
30
|
+
const queries = Number(process.argv[3] ?? 100);
|
|
31
|
+
const ef = Number(process.argv[4] ?? 120);
|
|
32
|
+
const random = randomFactory(20261111);
|
|
33
|
+
const items = Array.from({ length: count }, (_, index) => ({ id: `a${index}`, vector: makeVector(random) }));
|
|
34
|
+
const queryVectors = Array.from({ length: queries }, () => makeVector(random));
|
|
35
|
+
const ann = createAnnIndex({ M: 24, efConstruction: 200, efSearch: ef, seed: 20261111 });
|
|
36
|
+
const buildStart = performance.now();
|
|
37
|
+
ann.rebuild(items);
|
|
38
|
+
const buildMs = performance.now() - buildStart;
|
|
39
|
+
|
|
40
|
+
const exactTimes = [];
|
|
41
|
+
const annTimes = [];
|
|
42
|
+
const exactResults = [];
|
|
43
|
+
const annResults = [];
|
|
44
|
+
for (const query of queryVectors) {
|
|
45
|
+
const exactStart = performance.now();
|
|
46
|
+
const exact = items
|
|
47
|
+
.map((item) => ({ id: item.id, score: cosine(item.vector, query) }))
|
|
48
|
+
.sort((a, b) => b.score - a.score)
|
|
49
|
+
.slice(0, 10);
|
|
50
|
+
exactTimes.push(performance.now() - exactStart);
|
|
51
|
+
exactResults.push(exact);
|
|
52
|
+
|
|
53
|
+
const annStart = performance.now();
|
|
54
|
+
const approx = ann.search(query, 10, { ef }).slice(0, 10);
|
|
55
|
+
annTimes.push(performance.now() - annStart);
|
|
56
|
+
annResults.push(approx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let hits = 0;
|
|
60
|
+
let expectedTotal = 0;
|
|
61
|
+
for (let index = 0; index < exactResults.length; index += 1) {
|
|
62
|
+
const expected = new Set(exactResults[index].map((item) => item.id));
|
|
63
|
+
for (const item of annResults[index]) if (expected.has(item.id)) hits += 1;
|
|
64
|
+
expectedTotal += expected.size;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(JSON.stringify({
|
|
68
|
+
count,
|
|
69
|
+
queries,
|
|
70
|
+
dim: 64,
|
|
71
|
+
ef,
|
|
72
|
+
buildMs: Math.round(buildMs * 100) / 100,
|
|
73
|
+
recallAt10: expectedTotal ? Math.round((hits / expectedTotal) * 10000) / 10000 : null,
|
|
74
|
+
exactP50Ms: Math.round((percentile(exactTimes, 50) ?? 0) * 1000) / 1000,
|
|
75
|
+
annP50Ms: Math.round((percentile(annTimes, 50) ?? 0) * 1000) / 1000,
|
|
76
|
+
exactP95Ms: Math.round((percentile(exactTimes, 95) ?? 0) * 1000) / 1000,
|
|
77
|
+
annP95Ms: Math.round((percentile(annTimes, 95) ?? 0) * 1000) / 1000,
|
|
78
|
+
}, null, 2));
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHnswIndex } from '../src/vector/hnsw.js';
|
|
2
|
+
|
|
3
|
+
function randomFactory(seed) {
|
|
4
|
+
let state = seed >>> 0;
|
|
5
|
+
return () => {
|
|
6
|
+
state = (state * 1664525 + 1013904223) >>> 0;
|
|
7
|
+
return state / 4294967296;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeVector(random, dim = 64) {
|
|
12
|
+
const value = Array.from({ length: dim }, () => random() * 2 - 1);
|
|
13
|
+
const norm = Math.sqrt(value.reduce((sum, item) => sum + item * item, 0)) || 1;
|
|
14
|
+
return value.map((item) => item / norm);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cosine(a, b) {
|
|
18
|
+
let dot = 0;
|
|
19
|
+
for (let i = 0; i < a.length; i += 1) dot += a[i] * b[i];
|
|
20
|
+
return dot;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function percentile(values, p) {
|
|
24
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
25
|
+
if (!sorted.length) return null;
|
|
26
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const count = Number(process.argv[2] ?? 5000);
|
|
30
|
+
const queries = Number(process.argv[3] ?? 100);
|
|
31
|
+
const ef = Number(process.argv[4] ?? 200);
|
|
32
|
+
const random = randomFactory(20261111);
|
|
33
|
+
const items = Array.from({ length: count }, (_, index) => ({ id: `h${index}`, vector: makeVector(random) }));
|
|
34
|
+
const queryVectors = Array.from({ length: queries }, () => makeVector(random));
|
|
35
|
+
const index = createHnswIndex({ M: 24, efConstruction: 200, efSearch: ef, seed: 20261111 });
|
|
36
|
+
const buildStart = performance.now();
|
|
37
|
+
index.rebuild(items);
|
|
38
|
+
const buildMs = performance.now() - buildStart;
|
|
39
|
+
|
|
40
|
+
const exactTimes = [];
|
|
41
|
+
const hnswTimes = [];
|
|
42
|
+
const exactResults = [];
|
|
43
|
+
const hnswResults = [];
|
|
44
|
+
for (const query of queryVectors) {
|
|
45
|
+
const exactStart = performance.now();
|
|
46
|
+
const exact = items
|
|
47
|
+
.map((item) => ({ id: item.id, score: cosine(item.vector, query) }))
|
|
48
|
+
.sort((a, b) => b.score - a.score)
|
|
49
|
+
.slice(0, 10);
|
|
50
|
+
exactTimes.push(performance.now() - exactStart);
|
|
51
|
+
exactResults.push(exact);
|
|
52
|
+
|
|
53
|
+
const hnswStart = performance.now();
|
|
54
|
+
const approx = index.search(query, 10, { ef }).slice(0, 10);
|
|
55
|
+
hnswTimes.push(performance.now() - hnswStart);
|
|
56
|
+
hnswResults.push(approx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let hits = 0;
|
|
60
|
+
let expectedTotal = 0;
|
|
61
|
+
for (let index = 0; index < exactResults.length; index += 1) {
|
|
62
|
+
const expected = new Set(exactResults[index].map((item) => item.id));
|
|
63
|
+
for (const item of hnswResults[index]) if (expected.has(item.id)) hits += 1;
|
|
64
|
+
expectedTotal += expected.size;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(JSON.stringify({
|
|
68
|
+
engine: 'hnsw',
|
|
69
|
+
count,
|
|
70
|
+
queries,
|
|
71
|
+
dim: 64,
|
|
72
|
+
ef,
|
|
73
|
+
buildMs: Math.round(buildMs * 100) / 100,
|
|
74
|
+
recallAt10: expectedTotal ? Math.round((hits / expectedTotal) * 10000) / 10000 : null,
|
|
75
|
+
exactP50Ms: Math.round((percentile(exactTimes, 50) ?? 0) * 1000) / 1000,
|
|
76
|
+
hnswP50Ms: Math.round((percentile(hnswTimes, 50) ?? 0) * 1000) / 1000,
|
|
77
|
+
exactP95Ms: Math.round((percentile(exactTimes, 95) ?? 0) * 1000) / 1000,
|
|
78
|
+
hnswP95Ms: Math.round((percentile(hnswTimes, 95) ?? 0) * 1000) / 1000,
|
|
79
|
+
}, null, 2));
|
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,67 @@
|
|
|
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 hitAtK(ranked, relevant, k = 10) {
|
|
8
|
+
const rel = new Set(relevant);
|
|
9
|
+
if (rel.size === 0) return 0;
|
|
10
|
+
return ranked.slice(0, Math.max(1, k)).some((id) => rel.has(id)) ? 1 : 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function precisionAtK(ranked, relevant, k = 10) {
|
|
14
|
+
const rel = new Set(relevant);
|
|
15
|
+
const top = ranked.slice(0, Math.max(1, k));
|
|
16
|
+
return top.length === 0 ? 0 : top.filter((id) => rel.has(id)).length / top.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function reciprocalRank(ranked, relevant, k = 10) {
|
|
20
|
+
const rel = new Set(relevant);
|
|
21
|
+
for (let i = 0; i < Math.min(ranked.length, k); i += 1) {
|
|
22
|
+
if (rel.has(ranked[i])) return 1 / (i + 1);
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ndcgAtK(ranked, relevant, k = 10) {
|
|
28
|
+
const rel = new Set(relevant);
|
|
29
|
+
if (rel.size === 0) return 0;
|
|
30
|
+
let dcg = 0;
|
|
31
|
+
for (let i = 0; i < Math.min(ranked.length, k); i += 1) {
|
|
32
|
+
if (rel.has(ranked[i])) dcg += 1 / Math.log2(i + 2);
|
|
33
|
+
}
|
|
34
|
+
let idcg = 0;
|
|
35
|
+
for (let i = 0; i < Math.min(rel.size, k); i += 1) idcg += 1 / Math.log2(i + 2);
|
|
36
|
+
return idcg === 0 ? 0 : dcg / idcg;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function percentile(values, p) {
|
|
40
|
+
const list = values.filter((value) => Number.isFinite(value)).slice().sort((a, b) => a - b);
|
|
41
|
+
if (list.length === 0) return null;
|
|
42
|
+
const index = Math.min(list.length - 1, Math.max(0, Math.ceil((p / 100) * list.length) - 1));
|
|
43
|
+
return list[index];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function evaluateQueries(results, { k = 10 } = {}) {
|
|
47
|
+
const rows = results.map((result) => {
|
|
48
|
+
const ranked = (result.items ?? []).map((item) => item.id);
|
|
49
|
+
const relevant = result.relevant ?? [];
|
|
50
|
+
return {
|
|
51
|
+
recall: recallAtK(ranked, relevant, k),
|
|
52
|
+
hit: hitAtK(ranked, relevant, k),
|
|
53
|
+
precision: precisionAtK(ranked, relevant, k),
|
|
54
|
+
mrr: reciprocalRank(ranked, relevant, k),
|
|
55
|
+
ndcg: ndcgAtK(ranked, relevant, k),
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
const mean = (key) => rows.length ? rows.reduce((sum, row) => sum + row[key], 0) / rows.length : 0;
|
|
59
|
+
return {
|
|
60
|
+
n: rows.length,
|
|
61
|
+
recallAtK: mean('recall'),
|
|
62
|
+
hitAtK: mean('hit'),
|
|
63
|
+
precisionAtK: mean('precision'),
|
|
64
|
+
mrrAtK: mean('mrr'),
|
|
65
|
+
ndcgAtK: mean('ndcg'),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { openDatabase, migrate, getMeta, closeDatabase, FTS_INDEX_VERSION } from '../src/store/database.js';
|
|
6
|
+
import { createTraceRepository } from '../src/store/repositories/traces.js';
|
|
7
|
+
import { ftsText } from '../src/memory/tokenizer.js';
|
|
8
|
+
|
|
9
|
+
const count = Number(process.argv[2] ?? 5000);
|
|
10
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), `cortex-migrate-${count}-`));
|
|
11
|
+
const dbFile = path.join(dir, 'cortex.sqlite');
|
|
12
|
+
const totalStart = performance.now();
|
|
13
|
+
const phases = {};
|
|
14
|
+
let cortex;
|
|
15
|
+
try {
|
|
16
|
+
cortex = createCortex({ config: { dataRoot: dir, dbFile, contextBudget: 2200 } });
|
|
17
|
+
console.error(`phase create done (${Math.round(performance.now() - totalStart)}ms)`);
|
|
18
|
+
const texts = [];
|
|
19
|
+
for (let i = 0; i < count; i += 1) {
|
|
20
|
+
texts.push({
|
|
21
|
+
text: i === 0 ? '白饭:升级后仍然应该能通过“米饭”的 soft 查询找到。' : `老库迁移样本 ${i}:普通记忆内容。`,
|
|
22
|
+
title: `迁移样本 #${i}`,
|
|
23
|
+
explicit: true,
|
|
24
|
+
operationId: `migrate:${count}:${i}`,
|
|
25
|
+
now: Date.UTC(2026, 0, 1) + i * 1000,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const writeStart = performance.now();
|
|
29
|
+
cortex.writeMany(texts, { chunkSize: 500 });
|
|
30
|
+
phases.writeMs = Math.round((performance.now() - writeStart) * 100) / 100;
|
|
31
|
+
console.error(`phase write done (${phases.writeMs}ms)`);
|
|
32
|
+
cortex.close();
|
|
33
|
+
cortex = null;
|
|
34
|
+
|
|
35
|
+
let opened = openDatabase({ dataRoot: dir, dbFile });
|
|
36
|
+
opened.db.exec('DELETE FROM traces_fts_index');
|
|
37
|
+
opened.db.prepare('DELETE FROM meta WHERE key = ?').run('fts_index_version');
|
|
38
|
+
const rows = opened.db.prepare('SELECT id, title, content, summary FROM traces').all();
|
|
39
|
+
const insert = opened.db.prepare('INSERT INTO traces_fts_index(trace_id, text) VALUES (?, ?)');
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
insert.run(row.id, ftsText(`${row.title ?? ''} ${row.content ?? ''} ${row.summary ?? ''}`));
|
|
42
|
+
}
|
|
43
|
+
closeDatabase(opened.db);
|
|
44
|
+
phases.oldIndexMs = Math.round((performance.now() - writeStart) * 100) / 100;
|
|
45
|
+
console.error(`phase old-index done (${phases.oldIndexMs}ms total from write)`);
|
|
46
|
+
|
|
47
|
+
const started = performance.now();
|
|
48
|
+
opened = openDatabase({ dataRoot: dir, dbFile });
|
|
49
|
+
const result = migrate(opened.db);
|
|
50
|
+
const elapsedMs = performance.now() - started;
|
|
51
|
+
phases.migrateMs = Math.round(elapsedMs * 100) / 100;
|
|
52
|
+
console.error(`phase migrate done (${phases.migrateMs}ms)`);
|
|
53
|
+
const softHits = createTraceRepository(opened.db).searchFts('米饭', { soft: true });
|
|
54
|
+
phases.totalMs = Math.round((performance.now() - totalStart) * 100) / 100;
|
|
55
|
+
console.log(JSON.stringify({
|
|
56
|
+
count,
|
|
57
|
+
ftsRebuilt: result.ftsRebuilt,
|
|
58
|
+
elapsedMs: Math.round(elapsedMs * 100) / 100,
|
|
59
|
+
phases,
|
|
60
|
+
ftsVersion: getMeta(opened.db, 'fts_index_version'),
|
|
61
|
+
expectedVersion: String(FTS_INDEX_VERSION),
|
|
62
|
+
softHits: softHits.length,
|
|
63
|
+
dbBytes: opened.db.prepare('PRAGMA page_count').get().page_count * opened.db.prepare('PRAGMA page_size').get().page_size,
|
|
64
|
+
}, null, 2));
|
|
65
|
+
closeDatabase(opened.db);
|
|
66
|
+
} finally {
|
|
67
|
+
if (cortex) cortex.close();
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|