import { DatabaseSync } from "node:sqlite"; import assert from "node:assert/strict"; import { test } from "node:test"; import { ChainStore } from "../src/chain-store.js"; import { Timeline } from "../src/timeline.js"; import { CoreProjection } from "../src/core-projection.js"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { estimateTokens } from "../src/token-budget.js"; function fixture() { const db = new DatabaseSync(":memory:"), timeline = new Timeline(db), chain = new ChainStore(db, timeline); const append = (id, text = "用户讨论报销工作与发票整理。".repeat(100), kind = "user", at = 1) => timeline.append({ id, text, kind, at, source: { channel: "test", sender: "owner" } }); const claim = (mode = "compact") => { chain.request({ request_id: `test:${Math.random()}`, mode }, 10); return chain.claim({}, 20).job; }; const proposal = (job, extra = {}) => ({ job_id: job.id, token: job.token, covered_node_ids: job.nodes.map(n => n.id), summary: "讨论报销,尚未提交。详细依据可以展开原始事件。", objects: [], edges: [], core: [], catalog: [], ...extra }); return { db, timeline, chain, append, claim, proposal }; } const person = (id = "person:qhy", refs = ["meeting"]) => ({ id, type: "person", name: "钱灏宇", summary: "用户的同事,与报销工作有关。", body: "已确认是同事。具体职责尚未确认。", aliases: ["灏宇"], cues: ["发票整理找谁沟通", "同事交接报销材料"], source_refs: refs, status: "active" }); test("atomic compaction preserves raw evidence and concurrent suffix; receipt retries are idempotent", () => { const f = fixture(); try { f.append("meeting"); const job = f.claim(); f.append("new", "正在整理时用户的新消息", "user", 30); f.chain.sync(); const receipt = f.chain.commit(f.proposal(job, { objects: [person()], catalog: [{ type: "person", summary: "有同事和报销协作人的相关记忆" }] }), 40); assert.equal(f.chain.commit(f.proposal(job), 50).revision, receipt.revision); const context = f.chain.context(); assert.equal(context.ready, true); assert.ok(context.nodes.some(n => n.text.includes("新消息"))); assert.ok(context.nodes.some(n => n.id === receipt.node_id)); assert.ok(f.timeline.read({ id: "meeting" }).text.length > 1000); assert.ok(f.chain.node({ id: receipt.node_id }).children.length); assert.equal(f.chain.graph.find({ query: "钱灏宇" }).hits[0].id, "person:qhy"); assert.equal(f.chain.graph.find({ query: "灏宇" }).hits[0].id, "person:qhy"); assert.equal(f.chain.graph.find({ query: "发票整理找谁沟通" }).hits[0].id, "person:qhy"); } finally { f.db.close(); } }); test("invalid evidence rolls back graph, timeline and revision together", () => { const f = fixture(); try { f.append("meeting"); const job = f.claim(), before = f.chain.context(); assert.throws(() => f.chain.commit(f.proposal(job, { objects: [person(), person("person:bad", ["not-in-batch"])] }), 40), /Evidence/); assert.equal(f.chain.graph.object("person:qhy"), null); assert.deepEqual(f.chain.context().nodes, before.nodes); assert.equal(f.chain.revision(), 0); assert.throws(() => f.chain.commit({ ...f.proposal(job), covered_node_ids: [] }, 40), /every claimed node/); } finally { f.db.close(); } }); test("core changes invalidate stale leases and expired leases can be reclaimed", () => { const f = fixture(); try { f.append("meeting"); const job = f.claim(); f.chain.bootstrapCore({ "SOUL.md": "做可靠的工作同事。" }); assert.throws(() => f.chain.commit(f.proposal(job), 40), /Memory changed/); assert.equal(f.chain.fail({ job_id: job.id, token: job.token, error: "revision conflict" }, 50).released, true); const next = f.chain.claim({}, 30000).job; assert.equal(next.id, job.id); assert.notEqual(next.token, job.token); assert.throws(() => f.chain.commit(f.proposal(next), 300001), /Expired/); assert.equal(f.chain.claim({}, 300002).job.id, job.id); } finally { f.db.close(); } }); test("02:50 local nightly job waits five quiet minutes, is unique, and yields between batches", () => { const f = fixture(); try { f.append("meeting"); const now = Date.parse("2026-10-11T18:50:00Z"); f.chain.tick({ last_dialogue_at: now - 240000, busy: false }, now); assert.equal(f.chain.claim({}, now).job, null); f.chain.tick({ busy: true }, now + 120000); assert.equal(f.chain.claim({}, now + 120000).job, null); f.chain.tick({ busy: false }, now + 120001); const job = f.chain.claim({}, now + 120001).job; assert.equal(job.trigger, "nightly"); assert.equal(job.mode, "deep"); f.chain.commit(f.proposal(job), now + 120010); f.chain.tick({ busy: false }, now + 240000); assert.equal(f.chain.status().jobs.filter(j => j.trigger === "nightly").length, 1); assert.equal(f.chain.claim({}, now + 240000).job, null); } finally { f.db.close(); } }); test("threshold is not an every-message model trigger; explicit request is an event", () => { const f = fixture(); try { f.append("meeting", "普通聊天"); const now = Date.parse("2026-10-11T17:00:00Z"); f.chain.tick({ threshold_tokens: 8000 }, now); assert.equal(f.chain.status().jobs.length, 0); f.append("big", "财务材料".repeat(2000)); f.chain.tick({ threshold_tokens: 8000 }, now + 1); assert.equal(f.chain.status().jobs[0].trigger, "threshold"); f.chain.request({ request_id: "explicit:one", scope: "整理钱灏宇有关的记忆", mode: "deep" }, now + 2); f.chain.request({ request_id: "explicit:one", scope: "整理钱灏宇有关的记忆", mode: "deep" }, now + 3); assert.equal(f.db.prepare("SELECT count(*) AS n FROM timeline WHERE kind='organize_request'").get().n, 1); } finally { f.db.close(); } }); test("projects require user confirmation, graph links are cross-project and paginated", () => { const f = fixture(); try { f.append("meeting"); const job = f.claim(); const project = { ...person("project:expense"), type: "project", name: "报销", summary: "每月报销与日常发票收集" }; f.chain.commit(f.proposal(job, { objects: [person(), project], edges: [{ from_id: "person:qhy", relation: "参与", to_id: project.id, source_refs: ["meeting"] }] }), 40); assert.equal(f.chain.graph.object(project.id).status, "candidate"); assert.equal(f.chain.graph.find({ project_id: project.id, type: "person" }).hits[0].id, "person:qhy"); assert.equal(f.chain.graph.read({ id: "person:qhy" }).edges.length, 1); f.append("yes", "同意创建报销项目", "user", 60); assert.throws(() => f.chain.projectDecision({ id: project.id, request_id: "decision", status: "active", source_id: "yes", user_quote: "不是用户原话" }), /exact user evidence/); f.chain.projectDecision({ id: project.id, request_id: "decision", status: "active", source_id: "yes", user_quote: "同意创建报销项目" }); assert.equal(f.chain.graph.object(project.id).status, "active"); } finally { f.db.close(); } }); test("same name does not merge identities; read/cues never become new factual evidence", () => { const f = fixture(); try { f.append("meeting"); f.append("read", "钱灏宇的历史记忆", "memory_recall"); const job = f.claim(); assert.throws(() => f.chain.commit(f.proposal(job, { objects: [person("person:false", ["read"])] }), 40), /independent evidence/); f.chain.commit(f.proposal(job, { objects: [person(), person("person:another")] }), 40); assert.equal(f.chain.graph.find({ query: "钱灏宇" }).hits.length, 2); } finally { f.db.close(); } }); test("catalog remains bounded, types can be fully paginated, no implicit graph fan-out", () => { const f = fixture(); try { for (let i = 0; i < 300; i++) f.chain.graph.apply({ objects: [person(`person:${i}`)] }, i + 1, new Map([["meeting", { kind: "user", text: "" }]]), i); assert.ok(JSON.stringify(f.chain.graph.catalog()).length < 3000); assert.equal(f.chain.graph.catalog().entries[0].count, 300); assert.equal(f.chain.graph.find({ type: "person", offset: 290, limit: 20 }).hits.length, 10); assert.throws(() => f.chain.graph.find({ limit: 300 })); } finally { f.db.close(); } }); test("credential metadata permits vault refs but rejects values; core identity needs user evidence", () => { const f = fixture(); try { f.append("meeting"); const job = f.claim(); assert.throws(() => f.chain.commit(f.proposal(job, { core: [{ name: "USER.md", body: "用户喜欢自动付款", source_refs: ["meeting"] }] }), 40), /exact user evidence/); assert.throws(() => f.chain.commit(f.proposal(job, { objects: [{ ...person(), body: "sk-example-secret-01234567890" }] }), 40), /Credential/); assert.throws(() => f.chain.commit(f.proposal(job, { objects: [{ ...person("credential:bad"), type: "credential", secret_ref: "sk-example-secret-01234567890" }] }), 40), /Credential/); f.chain.commit(f.proposal(job, { objects: [{ ...person("credential:model"), type: "credential", name: "模型服务", body: "用于模型调用;真实值仅存密钥库", secret_ref: "provider/example/api_key" }] }), 40); assert.equal(f.chain.graph.object("credential:model").secret_ref, "provider/example/api_key"); } finally { f.db.close(); } }); test("large events span bounded batches without skipping sibling chunks", () => { const f = fixture(); try { f.append("big", "原始会议内容".repeat(5000)); const request = f.chain.request({ request_id: "large", mode: "deep" }, 10); let batches = 0; for (let now = 20; now < 100; now++) { const job = f.chain.claim({ max_bytes: 24000 }, now).job; if (!job) break; assert.ok(Buffer.byteLength(JSON.stringify(job)) <= 24000); f.chain.commit(f.proposal(job), now); batches++; } assert.ok(batches > 1); assert.equal(f.chain.status({ id: request.id }).jobs[0].status, "done"); assert.equal(f.db.prepare("SELECT count(*) AS n FROM memory_nodes WHERE active=1 AND event_id='big'").get().n, 0); assert.equal(f.timeline.read({ id: "big", limit: 32000 }).text.length, 30000); } finally { f.db.close(); } }); test("core files import human edits, project committed versions, and reject oversized/secret edits without data loss", () => { const f = fixture(), dir = mkdtempSync(join(tmpdir(), "memory-core-")); try { writeFileSync(join(dir, "USER.md"), "用户喜欢简洁沟通。"); const projection = new CoreProjection(f.chain, dir); projection.refresh(); assert.equal(f.chain.graph.core()[0].body, "用户喜欢简洁沟通。"); const revision = f.chain.revision(); f.chain.editCore({ name: "USER.md", body: "用户喜欢简洁且有依据的回复。", expected_revision: revision }); projection.refresh(); assert.equal(readFileSync(join(dir, "USER.md"), "utf8"), "用户喜欢简洁且有依据的回复。"); writeFileSync(join(dir, "USER.md"), "用户自己修改的新偏好。"); projection.refresh(); assert.equal(f.chain.graph.core()[0].body, "用户自己修改的新偏好。"); writeFileSync(join(dir, "USER.md"), "password=never-store-this-value"); projection.refresh(); assert.equal(f.chain.graph.core()[0].body, "用户自己修改的新偏好。"); assert.ok(f.chain.meta("core_projection_error")); assert.equal(readFileSync(join(dir, "USER.md"), "utf8"), "password=never-store-this-value"); assert.throws(() => f.chain.editCore({ name: "USER.md", body: "stale", expected_revision: revision }), /revision conflict/); } finally { f.db.close(); rmSync(dir, { recursive: true, force: true }); } }); test("organization survives SQLite reopen and a stale worker cannot overwrite a reclaimed batch", () => { const dir = mkdtempSync(join(tmpdir(), "memory-restart-")), path = join(dir, "memory.sqlite"); let db = new DatabaseSync(path); try { let timeline = new Timeline(db), chain = new ChainStore(db, timeline); timeline.append({ id: "event", kind: "user", at: 1, text: "旧事件需要保留".repeat(100), source: {} }); chain.request({ request_id: "restart" }, 10); const first = chain.claim({}, 20).job; db.close(); db = new DatabaseSync(path); timeline = new Timeline(db); chain = new ChainStore(db, timeline); assert.equal(chain.claim({}, 30).job, null); const next = chain.claim({}, 180021).job; assert.equal(next.id, first.id); assert.notEqual(next.token, first.token); const proposal = { job_id: first.id, token: first.token, summary: "保留经历", covered_node_ids: first.nodes.map(n => n.id) }; assert.throws(() => chain.commit(proposal, 180022), /stale/); chain.commit({ ...proposal, token: next.token, covered_node_ids: next.nodes.map(n => n.id) }, 180022); assert.equal(db.prepare("PRAGMA integrity_check").get().integrity_check, "ok"); assert.equal(timeline.read({ id: "event" }).text, "旧事件需要保留".repeat(100)); } finally { db.close(); rmSync(dir, { recursive: true, force: true }); } }); test("deep inspection grants evidence without letting uninspected objects be replaced", () => { const f = fixture(); try { f.append("meeting"); let job = f.claim(); f.chain.commit(f.proposal(job, { objects: [person()] }), 40); f.append("new", "用户进一步说明了同事职责".repeat(100)); f.chain.request({ request_id: "inspect", mode: "compact", scope: "完全不相关词 abcxyz" }, 50); job = f.chain.claim({}, 60).job; const change = { ...person(), body: "更正后的同事信息", source_refs: ["new"] }; assert.throws(() => f.chain.commit(f.proposal(job, { objects: [change] }), 70), /Inspect existing/); const inspected = f.chain.inspect({ job_id: job.id, token: job.token, kind: "object", id: "person:qhy" }, 70); assert.equal(inspected.name, "钱灏宇"); f.chain.commit(f.proposal(job, { objects: [change] }), 80); assert.equal(f.chain.graph.object("person:qhy").body, "更正后的同事信息"); assert.equal(f.chain.graph.read({ id: "person:qhy" }).total_sources, 2); } finally { f.db.close(); } }); test("timeline clipping returns newest nodes, explicit omission and separate byte/token accounting without deleting evidence", () => { const f = fixture(); try { for (let i=0;i<10;i++) f.append(`raw:${i}`, `事件${i}:${"中文正文".repeat(200)}`, "user", i); const full=f.chain.context({max_tokens:100000,max_bytes:350000}); const view=f.chain.context({max_tokens:4000,max_bytes:350000}); assert.equal(view.ready,true); assert.equal(view.truncated,true); assert.equal(view.nodes.at(-1).event_id,"raw:9"); assert.ok(view.view_estimated_tokens<=4000); assert.equal(view.view_estimated_tokens,2+view.nodes.reduce((n,node)=>n+estimateTokens(node)+1,0)); assert.equal(view.omitted_nodes,full.nodes.length-view.nodes.length); assert.deepEqual(f.chain.context({max_tokens:100000,max_bytes:350000}).nodes,full.nodes); assert.ok(f.timeline.read({id:"raw:0"}).text.includes("中文正文")); } finally {f.db.close();} }); test("deep review covers dormant and archived objects across durable batches and never invents timeline events", () => { const f=fixture(); try { f.append("meeting"); for(let i=0;i<13;i++) f.chain.graph.apply({objects:[{...person(`person:old-${i}`),status:i===0?"archived":"active"}]},i+1,new Map([["meeting",{kind:"user",text:"证据"}]]),i); let after, ids=[]; do { const page=f.chain.graph.browse({after,limit:3}); ids.push(...page.entries.map(x=>x.id)); after=page.next_after; } while(after); assert.equal(ids.length,14); assert.ok(ids.includes("person:old-0")); const req=f.chain.request({request_id:"global-review",mode:"deep",scope:"与旧人物无关的新工作"},10); const reviewed=new Set(); let emptyBatches=0; for(let now=20;now<40;now++) { const job=f.chain.claim({},now).job; if(!job)break; for(const obj of job.review_objects) reviewed.add(obj.id); if(!job.nodes.length)emptyBatches++; const before=f.chain.context().nodes; f.chain.commit(f.proposal(job),now); if(!job.nodes.length)assert.deepEqual(f.chain.context().nodes,before); } assert.equal(reviewed.size,14); assert.ok(emptyBatches>0); const status=f.chain.status({id:req.id}).jobs[0]; assert.equal(status.status,"done"); assert.deepEqual({...status.object_review},{total:14,reviewed:14}); assert.equal(f.chain.graph.object("person:old-0").status,"archived"); }finally{f.db.close();} }); test("failed deep batch does not advance review coverage",()=>{ const f=fixture(); try { f.append("meeting"); const first=f.claim("deep"); f.chain.fail({job_id:first.id,token:first.token,error:"test failure"},30); assert.equal(f.chain.status({id:first.id}).jobs[0].object_review.reviewed,0); const retry=f.chain.claim({},30000).job; assert.deepEqual(retry.review_objects.map(x=>x.id),first.review_objects.map(x=>x.id)); }finally{f.db.close();} }); test("opaque Chinese evidence IDs remain expandable and credential metadata is not a secret", () => { const f = fixture(); try { f.append("会议记录/十月十二日", "讨论密钥管理:用途是模型调用,引用 provider/example/api_key,未提供真实值。".repeat(10)); const job = f.claim(); assert.ok(f.chain.node({ id: "会议记录/十月十二日" }).text.includes("provider/example/api_key")); f.chain.commit(f.proposal(job, { objects: [person("person:qhy", ["会议记录/十月十二日"])] }), 40); assert.deepEqual(f.chain.graph.read({ id: "person:qhy" }).source_refs, ["会议记录/十月十二日"]); f.append("quoted-secret", '{"api_key":"sk-example-secret-01234567890"}'); assert.equal(f.chain.node({ id: "quoted-secret" }).text, "[敏感内容未写入记忆]"); } finally { f.db.close(); } }); test("failed model batches shrink on retry without consuming or dropping their original nodes", () => { const f = fixture(); try { for(let i=0;i<12;i++) f.append(`event:${i}`, "有效工作事实".repeat(20)); const first=f.claim(), before=f.chain.context().nodes; assert.equal(first.nodes.length,8); f.chain.fail({job_id:first.id,token:first.token,error:"Memory output truncated"},30); const next=f.chain.claim({},30000).job; assert.equal(next.id,first.id); assert.equal(next.nodes.length,4); assert.deepEqual(f.chain.context().nodes,before); f.chain.commit(f.proposal(next),30001); assert.ok(f.chain.context().nodes.some(node=>node.event_id==="event:11")); } finally { f.db.close(); } });