Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions dist/dsh.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { createHash, randomUUID } from "node:crypto";
import { openDb } from "./src/store/db.js";
import { allActiveNodes, findByName, getBySession, getStats, getVectorStats, getUnextracted, markExtracted, saveMessageOnce, updateNode, upsertEdge, upsertNode, } from "./src/store/store.js";
import { allActiveNodes, findByName, getBySession, getStats, getVectorStats, getUnextracted, markExtracted, saveMessageOnce, updateNode, upsertEdge, upsertNode, replaceNodeSources, } from "./src/store/store.js";
import { Extractor } from "./src/extractor/extract.js";
import { Recaller } from "./src/recaller/recall.js";
import { assembleContext } from "./src/format/assemble.js";
Expand Down Expand Up @@ -230,11 +230,15 @@ export function apply(ctx, input = {}) {
name: stableName,
description: "Consolidated checkpoint for an older span of one DSH conversation",
content: summary,
}, sid, sources);
}, sid);
const node = updateNode(db, result.node.name, {
description: "Consolidated checkpoint for an older span of one DSH conversation",
content: summary,
}) ?? result.node;
// Replace, never append: a fresh summary supersedes the previously
// shadowed span. Keeping the node permanently pinned to every old
// message grows gm_node_sources without bound for long sessions.
replaceNodeSources(db, node.id, sid, sources);
void recaller.syncEmbed(node);
invalidateGraphCache();
}
Expand Down
10 changes: 10 additions & 0 deletions dist/src/store/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ export function saveNodeSources(db, nodeId, sessionId, sources) {
insert.run(nodeId, sessionId, source.messageId);
}
}
/**
* Replace (not append) the durable message sources of one graph node.
* Used for compaction capsule nodes: a fresh summary supersedes the
* previously shadowed span, so the node must reference only the most
* recent checkpoint instead of accumulating every old span forever.
*/
export function replaceNodeSources(db, nodeId, sessionId, sources) {
db.prepare("DELETE FROM gm_node_sources WHERE node_id=?").run(nodeId);
saveNodeSources(db, nodeId, sessionId, sources);
}
/** 按 name 精确更新 description / content;找不到返回 null(调用方决定报错语义) */
export function updateNode(db, name, patch) {
const ex = findByName(db, name);
Expand Down
7 changes: 6 additions & 1 deletion dsh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
updateNode,
upsertEdge,
upsertNode,
replaceNodeSources,
} from "./src/store/store.ts";
import { Extractor } from "./src/extractor/extract.ts";
import { Recaller } from "./src/recaller/recall.ts";
Expand Down Expand Up @@ -312,11 +313,15 @@ export function apply(ctx: DshContext, input: Config = {}): void {
name: stableName,
description: "Consolidated checkpoint for an older span of one DSH conversation",
content: summary,
}, sid, sources);
}, sid);
const node = updateNode(db, result.node.name, {
description: "Consolidated checkpoint for an older span of one DSH conversation",
content: summary,
}) ?? result.node;
// Replace, never append: a fresh summary supersedes the previously
// shadowed span. Keeping the node permanently pinned to every old
// message grows gm_node_sources without bound for long sessions.
replaceNodeSources(db, node.id, sid, sources);
void recaller.syncEmbed(node);
invalidateGraphCache();
}
Expand Down
16 changes: 16 additions & 0 deletions src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ export function saveNodeSources(
}
}

/**
* Replace (not append) the durable message sources of one graph node.
* Used for compaction capsule nodes: a fresh summary supersedes the
* previously shadowed span, so the node must reference only the most
* recent checkpoint instead of accumulating every old span forever.
*/
export function replaceNodeSources(
db: DatabaseSyncInstance,
nodeId: string,
sessionId: string,
sources: Array<{ messageId: string; turnIndex: number }>,
): void {
db.prepare("DELETE FROM gm_node_sources WHERE node_id=?").run(nodeId);
saveNodeSources(db, nodeId, sessionId, sources);
}

/** 按 name 精确更新 description / content;找不到返回 null(调用方决定报错语义) */
export function updateNode(
db: DatabaseSyncInstance,
Expand Down
36 changes: 35 additions & 1 deletion test/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
mergeNodes, edgesFrom, edgesTo, allActiveNodes, allEdges,
searchNodes, topNodes, graphWalk, getBySession,
saveMessage, saveMessageOnce, getMessages, getUnextracted, markExtracted, getEpisodicMessages,
getNodeSourceMessages,
getNodeSourceMessages, replaceNodeSources,
saveSignal, pendingSignals, markSignalsDone,
getStats, saveVector, vectorSearch, getAllVectors, upsertCommunitySummary,
vectorSearchWithScore,
Expand Down Expand Up @@ -73,6 +73,40 @@ describe("host event messages", () => {
});
});

describe("compaction capsule sources", () => {
it("replaceNodeSources 替换而非追加:新 span 覆盖旧 span", () => {
saveMessageOnce(db, "dsh:s1:100", "dsh:s1", 100, "user", {
role: "user", content: [{ type: "text", text: "old span" }],
});
saveMessageOnce(db, "dsh:s1:200", "dsh:s1", 200, "user", {
role: "user", content: [{ type: "text", text: "new span" }],
});
const { node } = upsertNode(db, {
type: "EVENT", name: "session-memory-capsule", description: "", content: "summary v1",
}, "dsh:s1", [{ messageId: "dsh:s1:100", turnIndex: 100 }]);

// 第二次压缩:替换而非追加
replaceNodeSources(db, node.id, "dsh:s1", [{ messageId: "dsh:s1:200", turnIndex: 200 }]);

const texts = getNodeSourceMessages(db, node.id, 1000).map(message => message.text);
expect(texts).toEqual(["new span"]);
expect(texts).not.toContain("old span");
});

it("replaceNodeSources 空 sources 清空节点溯源", () => {
saveMessageOnce(db, "dsh:s1:100", "dsh:s1", 100, "user", {
role: "user", content: [{ type: "text", text: "old span" }],
});
const { node } = upsertNode(db, {
type: "EVENT", name: "session-memory-capsule-empty", description: "", content: "summary",
}, "dsh:s1", [{ messageId: "dsh:s1:100", turnIndex: 100 }]);

replaceNodeSources(db, node.id, "dsh:s1", []);

expect(getNodeSourceMessages(db, node.id, 1000)).toEqual([]);
});
});

// ═══════════════════════════════════════════════════════════════
// 节点 CRUD
// ═══════════════════════════════════════════════════════════════
Expand Down