dsh-memsys 0.1.0 → 0.2.1

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.
@@ -0,0 +1,69 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { recordDelivery } from '../src/context/delivery.mjs';
5
+ import { getMemories } from '../src/read/get.mjs';
6
+ import {
7
+ insertCandidate, insertEvidence, insertMemory, insertObservation, insertPointer, insertTimeline,
8
+ } from '../src/storage/repositories.mjs';
9
+ import { createTestContext, makeCandidate, makeMemory, makeObservation, mutableClock } from './helpers/fixtures.mjs';
10
+
11
+ function deliveryFixture(ctx, { actualIds, expectedIds, receivedIds, hash = 'h' }) {
12
+ const roundId = 'r-c1';
13
+ const snapshotId = 'snap-c1';
14
+ ctx.control
15
+ .prepare("INSERT OR REPLACE INTO compilations (id, round_id, stream_key, source_revision, policy_revision, delivery_level, payload_hash, section_text, created_at) VALUES (?, ?, 'stream', 0, '1', 'grouped', ?, '', ?)")
16
+ .run(`comp:${snapshotId}`, roundId, hash, ctx.clock.at().at);
17
+ ctx.control
18
+ .prepare("INSERT OR REPLACE INTO context_snapshots (id, compilation_id, round_id, mode, source_revision, policy_revision, delivery_level, eligible_ids, expected_ids, deferred_groups, created_at, expires_at, manifest_json) VALUES (?, ?, ?, 'work', 0, '1', 'grouped', '[]', ?, '[]', ?, ?, ?)")
19
+ .run(snapshotId, `comp:${snapshotId}`, roundId, JSON.stringify(expectedIds), ctx.clock.at().at, ctx.clock.at().at, JSON.stringify({ expected_ids: expectedIds, actual_ids: actualIds, hashes: { payload: hash } }));
20
+ return recordDelivery({ main: ctx.main, control: ctx.control, clock: ctx.clock, roundId, snapshotId, receivedIds, payloadHash: hash, manifestHash: 'm', slots: {} });
21
+ }
22
+
23
+ test('P6/C1: group-only receipt with empty actual set is incomplete', (t) => {
24
+ const ctx = createTestContext({ clock: mutableClock().clock });
25
+ t.after(() => ctx.cleanup());
26
+ const result = deliveryFixture(ctx, { actualIds: [], expectedIds: ['group:g1'], receivedIds: ['group:g1'] });
27
+ assert.equal(result.delivery, 'incomplete');
28
+ assert.equal(result.code, 'receipt_incomplete');
29
+ assert.notEqual(result.delivery, 'confirmed');
30
+ });
31
+
32
+ test('P6/C1: non-empty actual set missing one memory is incomplete', (t) => {
33
+ const ctx = createTestContext({ clock: mutableClock().clock });
34
+ t.after(() => ctx.cleanup());
35
+ const result = deliveryFixture(ctx, { actualIds: ['mem_a', 'mem_b'], expectedIds: ['mem_a', 'mem_b', 'group:g1'], receivedIds: ['mem_a', 'group:g1'] });
36
+ assert.equal(result.delivery, 'incomplete');
37
+ assert.equal(result.code, 'receipt_incomplete');
38
+ assert.deepEqual(result.missing_ids, ['mem_b']);
39
+ });
40
+
41
+ test('P6/C4: evidence text pages reassemble byte-for-byte', (t) => {
42
+ const ctx = createTestContext({ clock: mutableClock().clock });
43
+ t.after(() => ctx.cleanup());
44
+ const original = '证据原文'.repeat(600);
45
+ const observation = makeObservation(ctx, { messageSeq: 30000, text: original });
46
+ insertObservation(ctx.main, observation);
47
+ const candidate = makeCandidate(ctx, observation, { businessKey: 'c4', text: '短摘要', why: 'w', type: 'fact', scene: 'work', scope: 'work-only' });
48
+ insertCandidate(ctx.main, candidate);
49
+ const memory = makeMemory(ctx, candidate);
50
+ insertMemory(ctx.main, memory);
51
+ insertTimeline(ctx.main, { memory_id: memory.id, start_at: null, end_at: null, prec_start: 'unknown', prec_end: 'unknown', time_state: 'point', revision: 1 });
52
+ insertPointer(ctx.main, { memory_id: memory.id, status: 'unknown', location: null, search_hint: null, lossy: 0, revision: 1 });
53
+ insertEvidence(ctx.main, { id: 'ev_c4', memory_id: memory.id, candidate_id: null, observation_id: observation.id, source_memory_id: null, role: 'basis', source_span: 'bytes:0:4', at: ctx.clock.at().at });
54
+
55
+ let cursor = null;
56
+ let joined = '';
57
+ for (let page = 0; page < 20; page += 1) {
58
+ const result = getMemories({ main: ctx.main, clock: ctx.clock, ids: [memory.id], includeEvidence: true, maxChars: 1000, cursor });
59
+ assert.equal(result.status, 'ok', JSON.stringify(result));
60
+ for (const item of result.items) {
61
+ for (const evidence of item.evidence ?? []) {
62
+ if (typeof evidence.observation_text === 'string') joined += evidence.observation_text;
63
+ }
64
+ }
65
+ cursor = result.next_cursor;
66
+ if (!cursor) break;
67
+ }
68
+ assert.equal(joined, original);
69
+ });
@@ -0,0 +1,85 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { compileRound } from '../src/context/compile.mjs';
5
+ import { conservativeTokens } from '../src/context/budget.mjs';
6
+ import { getGroupPage } from '../src/context/snapshots.mjs';
7
+ import { ingestInput } from '../src/write/observe.mjs';
8
+ import { promoteCandidate, recordCandidates } from '../src/write/candidate.mjs';
9
+ import { createTestContext, mutableClock } from './helpers/fixtures.mjs';
10
+ import { fakeRuntime } from './helpers/runtime.mjs';
11
+
12
+ let seq = 0;
13
+ function addMemory(ctx, text) {
14
+ seq += 1;
15
+ const runtime = fakeRuntime({ role: 'daily', identity: 'c3a', callId: 'c3a-' + seq, messageSeq: 31000 + seq });
16
+ const observation = ingestInput(runtime, { text: 'C3-' + seq, messageSeq: 31000 + seq }, { main: ctx.main, control: ctx.control, clock: ctx.clock });
17
+ const created = recordCandidates(runtime, {
18
+ items: [{
19
+ observationId: observation.observation.id,
20
+ text,
21
+ why: 'w', type: 'fact', scene: 'work', scope: 'work-only',
22
+ evidenceRefs: [{ observationId: observation.observation.id, sourceSpan: 'bytes:0:3' }],
23
+ }],
24
+ }, { main: ctx.main, control: ctx.control, clock: ctx.clock });
25
+ promoteCandidate(runtime, created.items[0].id, null, { main: ctx.main, control: ctx.control, clock: ctx.clock });
26
+ }
27
+
28
+ function make(ctx) {
29
+ for (let i = 0; i < 12; i += 1) addMemory(ctx, 'C3混版-' + i + '-' + 'x'.repeat(500));
30
+ }
31
+
32
+ function compile(ctx, roundId) {
33
+ return compileRound({ main: ctx.main, control: ctx.control, clock: ctx.clock, sessionId: 's-c3a', roundId, mode: 'work', tokenizer: conservativeTokens, H: 300 });
34
+ }
35
+
36
+ function page(ctx, snapshotRevision, groupId, cursor = null) {
37
+ return getGroupPage({ control: ctx.control, main: ctx.main, clock: ctx.clock, snapshotRevision, groupId, cursor, maxChars: 1000 });
38
+ }
39
+
40
+ test('P6/C3: old cursor is stale after an append through the promote path', (t) => {
41
+ const ctx = createTestContext({ clock: mutableClock().clock });
42
+ t.after(() => ctx.cleanup());
43
+ make(ctx);
44
+ const first = compile(ctx, 'r-c3a');
45
+ assert.equal(first.delivery_level, 'grouped', JSON.stringify(first.delivery_level));
46
+ const groupId = first.groups[0].group_id;
47
+ const firstPage = page(ctx, first.snapshot_revision, groupId);
48
+ assert.equal(firstPage.status, 'ok', JSON.stringify(firstPage));
49
+ assert.ok(firstPage.next_cursor);
50
+ addMemory(ctx, '追加混版-' + 'y'.repeat(500));
51
+ const stale = page(ctx, first.snapshot_revision, groupId, firstPage.next_cursor);
52
+ assert.equal(stale.status, 'error', JSON.stringify(stale));
53
+ assert.equal(stale.code, 'stale_cursor');
54
+ });
55
+
56
+ test('P6/C3: same data recompiles to same id; append changes it', (t) => {
57
+ const ctx = createTestContext({ clock: mutableClock().clock });
58
+ t.after(() => ctx.cleanup());
59
+ make(ctx);
60
+ const a = compile(ctx, 'r-c3b1');
61
+ const b = compile(ctx, 'r-c3b2');
62
+ assert.equal(a.snapshot_revision, b.snapshot_revision);
63
+ addMemory(ctx, '追加换ID-' + 'z'.repeat(500));
64
+ const c = compile(ctx, 'r-c3b3');
65
+ assert.notEqual(c.snapshot_revision, a.snapshot_revision);
66
+ });
67
+
68
+ test('P6/C3: clean paging reaches the end without stale_cursor', (t) => {
69
+ const ctx = createTestContext({ clock: mutableClock().clock });
70
+ t.after(() => ctx.cleanup());
71
+ make(ctx);
72
+ const compiled = compile(ctx, 'r-c3c');
73
+ const groupId = compiled.groups[0].group_id;
74
+ let cursor = null;
75
+ let pages = 0;
76
+ for (;;) {
77
+ const result = page(ctx, compiled.snapshot_revision, groupId, cursor);
78
+ assert.equal(result.status, 'ok', JSON.stringify(result));
79
+ pages += 1;
80
+ cursor = result.next_cursor;
81
+ if (!cursor) break;
82
+ assert.ok(pages < 100);
83
+ }
84
+ assert.ok(pages >= 1);
85
+ });
@@ -0,0 +1,76 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { EventEmitter } from 'node:events';
4
+
5
+ import { createRoundInjector } from '../src/runtime/inject-round.mjs';
6
+ import { compileRound } from '../src/context/compile.mjs';
7
+ import { conservativeTokens } from '../src/context/budget.mjs';
8
+ import { ingestInput } from '../src/write/observe.mjs';
9
+ import { promoteCandidate, recordCandidates } from '../src/write/candidate.mjs';
10
+ import { createTestContext, mutableClock } from './helpers/fixtures.mjs';
11
+ import { fakeRuntime } from './helpers/runtime.mjs';
12
+
13
+ function seed(ctx, index) {
14
+ const runtime = fakeRuntime({ role: 'daily', identity: 'inj', callId: 'inj-' + index, messageSeq: 28000 + index });
15
+ const observation = ingestInput(runtime, { text: '注入目录记忆-' + index, messageSeq: 28000 + index }, { main: ctx.main, control: ctx.control, clock: ctx.clock });
16
+ const created = recordCandidates(runtime, {
17
+ items: [{
18
+ observationId: observation.observation.id,
19
+ text: '注入目录记忆-' + index,
20
+ why: 'w', type: 'fact', scene: 'work', scope: 'work-only',
21
+ evidenceRefs: [{ observationId: observation.observation.id, sourceSpan: 'bytes:0:5' }],
22
+ }],
23
+ }, { main: ctx.main, control: ctx.control, clock: ctx.clock });
24
+ promoteCandidate(runtime, created.items[0].id, null, { main: ctx.main, control: ctx.control, clock: ctx.clock });
25
+ }
26
+
27
+ test('P6/inject-round: turn/start refreshes an empty directory into compiled memory ids', (t) => {
28
+ const ctx = createTestContext({ clock: mutableClock().clock });
29
+ t.after(() => ctx.cleanup());
30
+ seed(ctx, 1);
31
+ const bus = new EventEmitter();
32
+ const injector = createRoundInjector({
33
+ ctx: bus,
34
+ handles: ctx.handles,
35
+ modules: { context: true },
36
+ compileRound,
37
+ tokenizer: conservativeTokens,
38
+ H: 1000,
39
+ clock: ctx.clock,
40
+ });
41
+ t.after(() => injector.dispose());
42
+ assert.equal(injector.current().text, '');
43
+ bus.emit('session/event', { type: 'turn/start', session: { header: { id: 'session-inj' } }, id: 'turn-1' });
44
+ const current = injector.current();
45
+ assert.ok(current.text.length > 0, JSON.stringify(current));
46
+ assert.ok(current.manifest.eligible_ids.length > 0, JSON.stringify(current.manifest));
47
+ assert.equal(current.delivery, 'unknown');
48
+ });
49
+
50
+ test('P6/inject-round: missing tokenizer/H falls back to tools-only and never pretends delivery', () => {
51
+ const ctx = createTestContext({ clock: mutableClock().clock });
52
+ try {
53
+ const injector = createRoundInjector({ ctx: { on() {} }, handles: ctx.handles, modules: {}, compileRound, tokenizer: null, H: null, clock: ctx.clock });
54
+ injector.refresh({ session: { header: { id: 's' } }, id: 't' });
55
+ assert.equal(injector.current().delivery_level, 'unavailable');
56
+ assert.equal(injector.current().delivery, 'unknown');
57
+ assert.match(injector.current().text, /tools-only/);
58
+ } finally {
59
+ ctx.cleanup();
60
+ }
61
+ });
62
+
63
+ test('P6/inject-round: context=false yields empty text and does not subscribe', () => {
64
+ const ctx = createTestContext({ clock: mutableClock().clock });
65
+ try {
66
+ let subscribed = false;
67
+ const bus = { on() { subscribed = true; } };
68
+ const injector = createRoundInjector({ ctx: bus, handles: ctx.handles, modules: { context: false }, compileRound, tokenizer: conservativeTokens, H: 1000, clock: ctx.clock });
69
+ injector.refresh({ id: 't' });
70
+ assert.equal(injector.current().text, '');
71
+ assert.equal(injector.current().delivery_level, 'disabled');
72
+ assert.equal(subscribed, false);
73
+ } finally {
74
+ ctx.cleanup();
75
+ }
76
+ });
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ compareBusinessRows,
6
+ compareDatabaseFreeze,
7
+ snapshotBusinessRows,
8
+ snapshotDatabaseFreeze,
9
+ } from '../src/import/rollback.mjs';
10
+ import { createTestContext, mutableClock } from './helpers/fixtures.mjs';
11
+
12
+ test('P6/L6: rollback detects changed row content even when counts match', (t) => {
13
+ const ctx = createTestContext({ clock: mutableClock().clock });
14
+ t.after(() => ctx.cleanup());
15
+ ctx.main
16
+ .prepare("INSERT INTO observations (id, idempotency_key, at, payload_hash, text, source, author_kind, runtime_refs, origin, lossy, state_snapshot, initial_strength, recorded_at) VALUES ('obs_l6_content','obs:l6c',?, 'h','原始内容','file','unknown','{}','migrated',0,'{}',0.5,?)")
17
+ .run(ctx.clock.at().at, ctx.clock.at().at);
18
+ const baseline = snapshotBusinessRows(ctx.main);
19
+ ctx.main.exec('DROP TRIGGER IF EXISTS trg_observations_no_update');
20
+ ctx.main.prepare('UPDATE observations SET text = ? WHERE id = ?').run('改过的内容', 'obs_l6_content');
21
+ const compared = compareBusinessRows({ db: ctx.main, baseline });
22
+ assert.equal(compared.ok, false, 'same count must not pass when content changed');
23
+ assert.ok(compared.missing.some((entry) => entry.includes('content')), JSON.stringify(compared));
24
+ });
25
+
26
+ test('P6/L6: boundary freeze detects changed content with unchanged counts', (t) => {
27
+ const ctx = createTestContext({ clock: mutableClock().clock });
28
+ t.after(() => ctx.cleanup());
29
+ ctx.main
30
+ .prepare("INSERT INTO observations (id, idempotency_key, at, payload_hash, text, source, author_kind, runtime_refs, origin, lossy, state_snapshot, initial_strength, recorded_at) VALUES ('obs_l6_freeze','obs:l6f',?, 'h','原始','file','unknown','{}','migrated',0,'{}',0.5,?)")
31
+ .run(ctx.clock.at().at, ctx.clock.at().at);
32
+ const freeze = snapshotDatabaseFreeze(ctx.handles);
33
+ ctx.main.exec('DROP TRIGGER IF EXISTS trg_observations_no_update');
34
+ ctx.main.prepare('UPDATE observations SET text = ? WHERE id = ?').run('改过', 'obs_l6_freeze');
35
+ const compared = compareDatabaseFreeze(ctx.handles, freeze);
36
+ assert.equal(compared.ok, false);
37
+ assert.ok(compared.missing.some((entry) => entry.includes('content')), JSON.stringify(compared));
38
+ });
@@ -0,0 +1,77 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { moduleEnabled, resolveModules } from '../src/runtime/modules.mjs';
5
+ import { toolDefinitions } from '../src/runtime/tools.mjs';
6
+ import { compileRound } from '../src/context/compile.mjs';
7
+ import { conservativeTokens } from '../src/context/budget.mjs';
8
+ import {
9
+ insertCandidate, insertMemory, insertObservation, insertPointer, insertTimeline,
10
+ } from '../src/storage/repositories.mjs';
11
+ import { createTestContext, makeCandidate, makeMemory, makeObservation, mutableClock } from './helpers/fixtures.mjs';
12
+
13
+ let seq = 0;
14
+ function pendingMemory(ctx) {
15
+ seq += 1;
16
+ const text = '待确认惦记';
17
+ const observation = makeObservation(ctx, { messageSeq: 27000 + seq, text });
18
+ insertObservation(ctx.main, observation);
19
+ const candidate = makeCandidate(ctx, observation, { businessKey: `mod-${seq}`, text, why: '期限', type: 'promise', scene: 'work', scope: 'work-only' });
20
+ insertCandidate(ctx.main, candidate);
21
+ const memory = makeMemory(ctx, candidate);
22
+ Object.assign(memory, { due_state: 'pending-due', scene: 'work', scope: 'work-only' });
23
+ insertMemory(ctx.main, memory);
24
+ insertTimeline(ctx.main, { memory_id: memory.id, start_at: null, end_at: null, prec_start: 'unknown', prec_end: 'unknown', time_state: 'point', revision: 1 });
25
+ insertPointer(ctx.main, { memory_id: memory.id, status: 'unknown', location: null, search_hint: null, lossy: 0, revision: 1 });
26
+ ctx.main
27
+ .prepare("INSERT INTO due_queue (memory_id, due_state, enqueued_seq, enqueued_at, opened_at, deadline_at, revision, reason) VALUES (?, 'pending-due', 1, ?, NULL, NULL, 0, 'promote')")
28
+ .run(memory.id, ctx.clock.at().at);
29
+ return memory;
30
+ }
31
+
32
+ test('P6/modules: defaults enable everything and register all 10 tools', () => {
33
+ const modules = resolveModules({});
34
+ assert.deepEqual(modules, {
35
+ context: true, read: true, write: true, dream: true, projects: true, due: true, feed: true,
36
+ });
37
+ assert.equal(toolDefinitions({}, modules).length, 10);
38
+ });
39
+
40
+ test('P6/modules: read=false removes read tools but keeps write/dream/feed', () => {
41
+ const modules = resolveModules({ modules: { read: false } });
42
+ const names = toolDefinitions({}, modules).map((tool) => tool.name);
43
+ for (const name of ['memsys_get', 'memsys_search', 'memsys_review_get', 'memsys_context_get', 'memsys_project_get', 'memsys_self_get']) {
44
+ assert.ok(!names.includes(name), name);
45
+ }
46
+ assert.ok(names.includes('memsys_record'));
47
+ assert.ok(names.includes('memsys_dream_record'));
48
+ assert.ok(names.includes('memsys_feed'));
49
+ });
50
+
51
+ test('P6/modules: context=false disables the injection section', () => {
52
+ const modules = resolveModules({ modules: { context: false } });
53
+ assert.equal(moduleEnabled(modules, 'context'), false);
54
+ assert.equal(moduleEnabled(modules, 'read'), true);
55
+ });
56
+
57
+ test('P6/modules: due=false removes review:* typed items from compile output', (t) => {
58
+ const ctx = createTestContext({ clock: mutableClock().clock });
59
+ t.after(() => ctx.cleanup());
60
+ const memory = pendingMemory(ctx);
61
+ const base = { main: ctx.main, control: ctx.control, clock: ctx.clock, sessionId: 's-mod', roundId: 'r-mod', mode: 'work', tokenizer: conservativeTokens, H: 200000 };
62
+ const enabled = compileRound(base);
63
+ assert.ok(enabled.eligible_ids.includes('review:due:' + memory.id), JSON.stringify(enabled.eligible_ids));
64
+ const disabled = compileRound({ ...base, roundId: 'r-mod2', modules: { due: false } });
65
+ assert.ok(!disabled.eligible_ids.some((id) => id.startsWith('review:')), JSON.stringify(disabled.eligible_ids));
66
+ });
67
+
68
+ test('P6/modules: legacy auto_inject maps to context without disabling other modules', () => {
69
+ const modules = resolveModules({ auto_inject: true });
70
+ assert.equal(modules.context, true);
71
+ assert.equal(modules.read, true);
72
+ assert.equal(modules.write, true);
73
+ assert.equal(modules.dream, true);
74
+ assert.equal(modules.projects, true);
75
+ assert.equal(modules.due, true);
76
+ assert.equal(modules.feed, true);
77
+ });