dsh-prime-memory 0.26.4 → 0.26.5
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/dist/client.js +36785 -134
- package/dist/clients/drsc.d.ts +93 -0
- package/dist/clients/drsc.js +139 -0
- package/dist/contract.d.ts +74 -0
- package/dist/drs-anchor-backfill.d.ts +88 -0
- package/dist/drs-anchor-backfill.js +150 -0
- package/dist/graph/constraints.d.ts +3 -1
- package/dist/graph/constraints.js +4 -2
- package/dist/graph/preview.d.ts +92 -0
- package/dist/graph/preview.js +186 -0
- package/dist/graph/search.d.ts +22 -5
- package/dist/graph/search.js +69 -7
- package/dist/graph/types.d.ts +26 -1
- package/dist/graph/types.js +26 -1
- package/dist/hooks/capture.d.ts +4 -4
- package/dist/hooks/capture.js +34 -27
- package/dist/hooks/recall.js +8 -1
- package/dist/index.js +58 -20
- package/dist/pipeline/graph.d.ts +9 -0
- package/dist/pipeline/graph.js +29 -2
- package/dist/pipeline/runner.d.ts +17 -4
- package/dist/pipeline/runner.js +33 -6
- package/dist/stats.js +30 -1
- package/dist/store/embedding-source.d.ts +1 -3
- package/dist/store/embedding-source.js +6 -13
- package/dist/store/graph-store.d.ts +40 -3
- package/dist/store/graph-store.js +104 -8
- package/dist/store/l0-watermark.d.ts +46 -0
- package/dist/store/l0-watermark.js +93 -0
- package/dist/store/l0.d.ts +12 -42
- package/dist/store/l0.js +2 -208
- package/dist/store/l1.d.ts +12 -9
- package/dist/store/l1.js +37 -16
- package/dist/store/session-recalled.d.ts +9 -3
- package/dist/store/session-recalled.js +24 -5
- package/dist/store/sqlite.d.ts +3 -0
- package/dist/store/sqlite.js +7 -0
- package/dist/tools/index.d.ts +3 -2
- package/dist/tools/index.js +27 -10
- package/dist/types.d.ts +9 -0
- package/dsh.plugin.json +1 -1
- package/package.json +8 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DrscClient(T8.4 读切,drsc-redact-memory-wiring task_18):memory 读历史
|
|
3
|
+
* 面的 dsh-reference-session-collector HTTP 客户端。
|
|
4
|
+
*
|
|
5
|
+
* 两个读点(R7):
|
|
6
|
+
* - 蒸馏背景窗(recentBySession 等价供给)→ `op=session&tail=N&redact=1`
|
|
7
|
+
* (collector task_11 尾窗升序 + task_12 redact 臂);
|
|
8
|
+
* - 工具面 conversation_search → `op=search&redact=1`(命中基于原文 fts,
|
|
9
|
+
* 返回 snippet 取脱敏锚定文本)。
|
|
10
|
+
*
|
|
11
|
+
* 硬依赖纪律(对齐 T7.3 裁定):collector 缺席/超时/非 2xx/ok=false 一律抛
|
|
12
|
+
* {@link DrscClientError}——**绝不回退本地 l0**(表已退役,无退路);调用方
|
|
13
|
+
* 分口径处理:蒸馏背景窗空窗降级(runner),工具面显式报错(tools)。
|
|
14
|
+
*
|
|
15
|
+
* 同源 Host 解析(steward drs-source 的 resolveDrsBaseUrl 同款):
|
|
16
|
+
* 显式 baseUrl > 首个 trustedHosts 条目(补 http://)> `http://127.0.0.1:8080`
|
|
17
|
+
* (宿主缺省口)。Host 头回显首个 trustedHosts 条目:collector 信任门是精确
|
|
18
|
+
* 匹配,条目省略端口而 URL 带端口会失配。trustedHosts 惰性读取(webStartup
|
|
19
|
+
* 可能晚于本插件就绪),每次请求现算,无缓存陈旧面。
|
|
20
|
+
*/
|
|
21
|
+
import type { ConversationMessage } from '../types.js';
|
|
22
|
+
/** collector API 路径(与其 DRS_API_PATH 字面一致;不跨包 import)。 */
|
|
23
|
+
export declare const DRSC_API_PATH = "/api/dsh-reference-session-collector";
|
|
24
|
+
/** 客户端缺省超时:loopback 正常 <10ms,5s 是给冷启动 GC/断点的天花板。 */
|
|
25
|
+
export declare const DRSC_DEFAULT_TIMEOUT_MS = 5000;
|
|
26
|
+
/** collector 缺席/超时/契约破损。显式报错,不回退本地。 */
|
|
27
|
+
export declare class DrscClientError extends Error {
|
|
28
|
+
constructor(message: string, options?: {
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 基址解析:显式配置 > 首个 trustedHosts 条目(补 http:// 前缀)>
|
|
34
|
+
* `http://127.0.0.1:8080`(宿主默认口;trustedHosts 缺席的老线 collector 门本就放行)。
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveDrscBaseUrl(explicit: string | undefined, trustedHosts?: readonly string[]): string;
|
|
37
|
+
/** `op=session` 的一个 segment 行(redact=1 时 text 为脱敏正文;wire 蛇形直取)。 */
|
|
38
|
+
export interface DrscSessionRow {
|
|
39
|
+
seq: number;
|
|
40
|
+
turn: number;
|
|
41
|
+
step: number;
|
|
42
|
+
kind: string;
|
|
43
|
+
role: string;
|
|
44
|
+
time: number;
|
|
45
|
+
text: string;
|
|
46
|
+
}
|
|
47
|
+
/** `op=search` 的一个命中(redact=1 时 snippet 为脱敏锚定文本)。 */
|
|
48
|
+
export interface DrscSearchHit {
|
|
49
|
+
session_id: string;
|
|
50
|
+
seq: number;
|
|
51
|
+
kind: string;
|
|
52
|
+
rank: number;
|
|
53
|
+
snippet: string;
|
|
54
|
+
time: number;
|
|
55
|
+
on_surface: number;
|
|
56
|
+
turn: number;
|
|
57
|
+
}
|
|
58
|
+
/** kind → L0 角色词表(memory 只收 user/assistant 两类,其余(tool/* 等)不映射)。 */
|
|
59
|
+
export declare function drscRoleOf(kind: string): 'user' | 'assistant' | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* session 行 → ConversationMessage 形状映射(蒸馏背景窗用)。
|
|
62
|
+
* - id:确定性合成 `drsc:<session>:<seq>`(跨次调用稳定,anchor 追溯可解释);
|
|
63
|
+
* - anchor:turn>0 才成立(与 ConversationAnchor 红线一致:没坐标不伪造);
|
|
64
|
+
* - 非 user/assistant 段(tool/call 等)返回 undefined,调用方跳过。
|
|
65
|
+
*/
|
|
66
|
+
export declare function drscRowToConversationMessage(row: DrscSessionRow, sessionId: string): ConversationMessage | undefined;
|
|
67
|
+
export interface DrscClientOptions {
|
|
68
|
+
/** 显式基址(缺省走 trustedHosts → 127.0.0.1:8080,见 resolveDrscBaseUrl)。 */
|
|
69
|
+
baseUrl?: string;
|
|
70
|
+
/** trustedHosts 惰性读取(webStartup 服务可能晚于装配;每次请求现取)。 */
|
|
71
|
+
trustedHosts?: () => readonly string[] | undefined;
|
|
72
|
+
/** 注入面(单测);缺省全局 fetch。 */
|
|
73
|
+
fetchImpl?: typeof fetch;
|
|
74
|
+
/** 单请求超时 ms;缺省 {@link DRSC_DEFAULT_TIMEOUT_MS}。 */
|
|
75
|
+
timeoutMs?: number;
|
|
76
|
+
}
|
|
77
|
+
export declare class DrscClient {
|
|
78
|
+
private readonly options;
|
|
79
|
+
private readonly fetchImpl;
|
|
80
|
+
private readonly timeoutMs;
|
|
81
|
+
constructor(options?: DrscClientOptions);
|
|
82
|
+
/** 每次请求现算基址与 Host 回显(trustedHosts 惰性;无缓存陈旧面)。 */
|
|
83
|
+
private base;
|
|
84
|
+
/** 一个 GET 的 JSON 包络(非 2xx / ok!==true / 超时 / 断网一律抛 DrscClientError)。 */
|
|
85
|
+
private getJson;
|
|
86
|
+
/**
|
|
87
|
+
* 会话尾窗:`op=session&session=<id>&tail=N&redact=1` → 脱敏消息行(升序)。
|
|
88
|
+
* collector 无此会话(session:null)返回空数组(新会话首轮的常态,非错误)。
|
|
89
|
+
*/
|
|
90
|
+
sessionTail(sessionId: string, tail: number): Promise<DrscSessionRow[]>;
|
|
91
|
+
/** 原史检索:`op=search&q=<query>&limit=N&redact=1` → 脱敏 snippet 命中。 */
|
|
92
|
+
search(query: string, limit: number): Promise<DrscSearchHit[]>;
|
|
93
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** collector API 路径(与其 DRS_API_PATH 字面一致;不跨包 import)。 */
|
|
2
|
+
export const DRSC_API_PATH = '/api/dsh-reference-session-collector';
|
|
3
|
+
/** 客户端缺省超时:loopback 正常 <10ms,5s 是给冷启动 GC/断点的天花板。 */
|
|
4
|
+
export const DRSC_DEFAULT_TIMEOUT_MS = 5_000;
|
|
5
|
+
/** collector 缺席/超时/契约破损。显式报错,不回退本地。 */
|
|
6
|
+
export class DrscClientError extends Error {
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = 'DrscClientError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 基址解析:显式配置 > 首个 trustedHosts 条目(补 http:// 前缀)>
|
|
14
|
+
* `http://127.0.0.1:8080`(宿主默认口;trustedHosts 缺席的老线 collector 门本就放行)。
|
|
15
|
+
*/
|
|
16
|
+
export function resolveDrscBaseUrl(explicit, trustedHosts) {
|
|
17
|
+
const trimmed = explicit?.trim() ?? '';
|
|
18
|
+
const pick = trimmed !== '' ? trimmed : trustedHosts?.find((entry) => entry.trim() !== '')?.trim();
|
|
19
|
+
if (pick === undefined || pick === '')
|
|
20
|
+
return 'http://127.0.0.1:8080';
|
|
21
|
+
return /^https?:\/\//u.test(pick) ? pick : `http://${pick}`;
|
|
22
|
+
}
|
|
23
|
+
/** kind → L0 角色词表(memory 只收 user/assistant 两类,其余(tool/* 等)不映射)。 */
|
|
24
|
+
export function drscRoleOf(kind) {
|
|
25
|
+
if (kind === 'user/message')
|
|
26
|
+
return 'user';
|
|
27
|
+
if (kind === 'assistant/message')
|
|
28
|
+
return 'assistant';
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* session 行 → ConversationMessage 形状映射(蒸馏背景窗用)。
|
|
33
|
+
* - id:确定性合成 `drsc:<session>:<seq>`(跨次调用稳定,anchor 追溯可解释);
|
|
34
|
+
* - anchor:turn>0 才成立(与 ConversationAnchor 红线一致:没坐标不伪造);
|
|
35
|
+
* - 非 user/assistant 段(tool/call 等)返回 undefined,调用方跳过。
|
|
36
|
+
*/
|
|
37
|
+
export function drscRowToConversationMessage(row, sessionId) {
|
|
38
|
+
const role = drscRoleOf(row.kind);
|
|
39
|
+
if (role === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const msg = {
|
|
42
|
+
id: `drsc:${sessionId}:${row.seq}`,
|
|
43
|
+
role,
|
|
44
|
+
content: row.text,
|
|
45
|
+
timestamp: row.time,
|
|
46
|
+
};
|
|
47
|
+
if (Number.isFinite(row.turn) && row.turn > 0) {
|
|
48
|
+
msg.anchor = { sessionId, turn: row.turn };
|
|
49
|
+
if (Number.isFinite(row.step) && row.step > 0)
|
|
50
|
+
msg.anchor.step = row.step;
|
|
51
|
+
}
|
|
52
|
+
return msg;
|
|
53
|
+
}
|
|
54
|
+
export class DrscClient {
|
|
55
|
+
options;
|
|
56
|
+
fetchImpl;
|
|
57
|
+
timeoutMs;
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.options = options;
|
|
60
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
61
|
+
this.timeoutMs = options.timeoutMs ?? DRSC_DEFAULT_TIMEOUT_MS;
|
|
62
|
+
}
|
|
63
|
+
/** 每次请求现算基址与 Host 回显(trustedHosts 惰性;无缓存陈旧面)。 */
|
|
64
|
+
base() {
|
|
65
|
+
const trusted = this.options.trustedHosts?.() ?? [];
|
|
66
|
+
const base = resolveDrscBaseUrl(this.options.baseUrl, trusted).replace(/\/+$/u, '');
|
|
67
|
+
const first = trusted.find((entry) => entry.trim() !== '')?.trim();
|
|
68
|
+
return { url: `${base}${DRSC_API_PATH}`, ...(first !== undefined ? { hostHeader: first } : {}) };
|
|
69
|
+
}
|
|
70
|
+
/** 一个 GET 的 JSON 包络(非 2xx / ok!==true / 超时 / 断网一律抛 DrscClientError)。 */
|
|
71
|
+
async getJson(query) {
|
|
72
|
+
const { url, hostHeader } = this.base();
|
|
73
|
+
let response;
|
|
74
|
+
try {
|
|
75
|
+
response = await this.fetchImpl(`${url}${query}`, {
|
|
76
|
+
headers: {
|
|
77
|
+
accept: 'application/json',
|
|
78
|
+
...(hostHeader === undefined ? {} : { host: hostHeader }),
|
|
79
|
+
},
|
|
80
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
throw new DrscClientError(`collector 不可达(${query}): ${err instanceof Error ? err.message : String(err)}`, {
|
|
85
|
+
cause: err,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!response.ok)
|
|
89
|
+
throw new DrscClientError(`collector HTTP ${response.status}(${query})`);
|
|
90
|
+
let body;
|
|
91
|
+
try {
|
|
92
|
+
body = await response.json();
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
throw new DrscClientError(`collector 响应非 JSON(${query})`, { cause: err });
|
|
96
|
+
}
|
|
97
|
+
if (typeof body !== 'object' || body === null || body.ok !== true) {
|
|
98
|
+
const reason = typeof body === 'object' && body !== null
|
|
99
|
+
? String(body.error ?? 'ok=false')
|
|
100
|
+
: '响应非对象';
|
|
101
|
+
throw new DrscClientError(`collector 包络异常(${query}): ${reason}`);
|
|
102
|
+
}
|
|
103
|
+
return body;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 会话尾窗:`op=session&session=<id>&tail=N&redact=1` → 脱敏消息行(升序)。
|
|
107
|
+
* collector 无此会话(session:null)返回空数组(新会话首轮的常态,非错误)。
|
|
108
|
+
*/
|
|
109
|
+
async sessionTail(sessionId, tail) {
|
|
110
|
+
const body = await this.getJson(`?op=session&session=${encodeURIComponent(sessionId)}&tail=${Math.max(0, Math.floor(tail))}&redact=1`);
|
|
111
|
+
if (body['session'] === null || body['session'] === undefined)
|
|
112
|
+
return [];
|
|
113
|
+
const rows = Array.isArray(body['rows']) ? body['rows'] : [];
|
|
114
|
+
return rows.map((r) => ({
|
|
115
|
+
seq: Number(r['seq'] ?? 0),
|
|
116
|
+
turn: Number(r['turn'] ?? 0),
|
|
117
|
+
step: Number(r['step'] ?? 0),
|
|
118
|
+
kind: String(r['kind'] ?? ''),
|
|
119
|
+
role: String(r['role'] ?? ''),
|
|
120
|
+
time: Number(r['time'] ?? 0),
|
|
121
|
+
text: String(r['text'] ?? ''),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
/** 原史检索:`op=search&q=<query>&limit=N&redact=1` → 脱敏 snippet 命中。 */
|
|
125
|
+
async search(query, limit) {
|
|
126
|
+
const body = await this.getJson(`?op=search&q=${encodeURIComponent(query)}&limit=${Math.max(1, Math.floor(limit))}&redact=1`);
|
|
127
|
+
const hits = Array.isArray(body['hits']) ? body['hits'] : [];
|
|
128
|
+
return hits.map((h) => ({
|
|
129
|
+
session_id: String(h['session_id'] ?? ''),
|
|
130
|
+
seq: Number(h['seq'] ?? 0),
|
|
131
|
+
kind: String(h['kind'] ?? ''),
|
|
132
|
+
rank: Number(h['rank'] ?? 0),
|
|
133
|
+
snippet: String(h['snippet'] ?? ''),
|
|
134
|
+
time: Number(h['time'] ?? 0),
|
|
135
|
+
on_surface: Number(h['on_surface'] ?? 1),
|
|
136
|
+
turn: Number(h['turn'] ?? 0),
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
}
|
package/dist/contract.d.ts
CHANGED
|
@@ -927,6 +927,13 @@ export interface UiRecord {
|
|
|
927
927
|
* 只在 `dsh-memory/session-recalled` 这条路径上有意义;缺省 = 无此概念(旧服务端)。
|
|
928
928
|
*/
|
|
929
929
|
superseded?: boolean;
|
|
930
|
+
/**
|
|
931
|
+
* 路归因:hybrid 融合时这条命中来自哪几路(`'fts'|'vec'|'graph'|'recency'`)。
|
|
932
|
+
* 只在 `dsh-memory/session-recalled` 路径上有意义;缺省 = 无从归因
|
|
933
|
+
* (单路检索策略/旧文件条目/旧服务端),展示层不渲染徽标。纯投影字段,
|
|
934
|
+
* 存储侧落 `session-recalled.json` 的 lanes 键(读时拆管道串)。
|
|
935
|
+
*/
|
|
936
|
+
lanes?: string[];
|
|
930
937
|
/**
|
|
931
938
|
* 成熟度(G3 读时派生):priority + 被用次数 + 老化权重合成。
|
|
932
939
|
* 可选字段——旧 client 忽略=向后兼容;不参与存储/快照(纯投影)。
|
|
@@ -1228,6 +1235,69 @@ export interface GraphNodeGetResponse {
|
|
|
1228
1235
|
/** 与该节点相连的 active 边。 */
|
|
1229
1236
|
edges: GraphEdge[];
|
|
1230
1237
|
}
|
|
1238
|
+
/** dsh-memory/graph-preview(面板图谱预览:缺省返回推荐种子榜,seedId 给定展开该节点邻域)。
|
|
1239
|
+
* 候选只含 active|disputed 节点×active 边(与检索同判据);空图/未启用返全零结构不抛。 */
|
|
1240
|
+
export interface GraphPreviewRequest {
|
|
1241
|
+
/** 给定即以该节点为唯一中心(悬挂 id 返空 seeds,不解析不抛——graph-node-get 同款宽容)。 */
|
|
1242
|
+
seedId?: string;
|
|
1243
|
+
/** 'full' = 响应附全图轻量块(mode:'full' 与大图星图共用同端点,一次 loadGraph 全供)。
|
|
1244
|
+
* 非 'full' 一律按缺省——无 mode 响应形状与旧版逐字一致(可选键缺席=零漂移)。 */
|
|
1245
|
+
mode?: 'full';
|
|
1246
|
+
}
|
|
1247
|
+
export interface GraphPreviewResponse {
|
|
1248
|
+
seeds: Array<{
|
|
1249
|
+
node: {
|
|
1250
|
+
id: string;
|
|
1251
|
+
name: string;
|
|
1252
|
+
type: string;
|
|
1253
|
+
degree: number;
|
|
1254
|
+
causalDegree: number;
|
|
1255
|
+
};
|
|
1256
|
+
/** 因果边优先→邻居度数降序→updatedAt 新先;长度 ≤ GRAPH_PREVIEW_NEIGHBORS。 */
|
|
1257
|
+
neighbors: Array<{
|
|
1258
|
+
id: string;
|
|
1259
|
+
name: string;
|
|
1260
|
+
type: string;
|
|
1261
|
+
relation: string;
|
|
1262
|
+
causal: boolean;
|
|
1263
|
+
}>;
|
|
1264
|
+
truncated: boolean;
|
|
1265
|
+
}>;
|
|
1266
|
+
stats: {
|
|
1267
|
+
nodes: number;
|
|
1268
|
+
edges: number;
|
|
1269
|
+
causalEdges: number;
|
|
1270
|
+
/** 未定投影 job 数(pending/running/failed 退避中),>0 → UI 显"构建中 x/y"(x=jobsTotal-pendingJobs)。 */
|
|
1271
|
+
pendingJobs: number;
|
|
1272
|
+
/** 全量 job 数(含各状态),"构建中 x/y"的分母——名字即分母,不拿 done 冒充分子。 */
|
|
1273
|
+
jobsTotal: number;
|
|
1274
|
+
/** 永久失败作业数(dead),可选(零漂移先例);>0 → UI 拆成"完成/失败/排队"三档显示。 */
|
|
1275
|
+
deadJobs?: number;
|
|
1276
|
+
};
|
|
1277
|
+
/** mode==='full' 才出现;全图星图轻量块(可见节点×active 边,截断标记 truncated)。
|
|
1278
|
+
* 不带此键时响应与旧形状逐字节一致——消费方按需读 `res.full?`。 */
|
|
1279
|
+
full?: {
|
|
1280
|
+
nodes: Array<{
|
|
1281
|
+
id: string;
|
|
1282
|
+
name: string;
|
|
1283
|
+
type: string;
|
|
1284
|
+
}>;
|
|
1285
|
+
edges: Array<{
|
|
1286
|
+
from: string;
|
|
1287
|
+
to: string;
|
|
1288
|
+
relation: string;
|
|
1289
|
+
causal: boolean;
|
|
1290
|
+
}>;
|
|
1291
|
+
truncated: boolean;
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
/** dsh-memory/graph-cleanup-dead(清理历史失败作业:删除 status='dead' 的行,不影响已重入队的记录)。 */
|
|
1295
|
+
export interface GraphCleanupDeadRequest {
|
|
1296
|
+
olderThanMs?: number;
|
|
1297
|
+
}
|
|
1298
|
+
export interface GraphCleanupDeadResponse {
|
|
1299
|
+
removed: number;
|
|
1300
|
+
}
|
|
1231
1301
|
/** dsh-memory/scenes(两族拼接的混合视图)。 */
|
|
1232
1302
|
export interface ScenesResponse {
|
|
1233
1303
|
items: Array<{
|
|
@@ -1778,6 +1848,8 @@ export interface DshMemoryRequestMap {
|
|
|
1778
1848
|
'dsh-memory/conflicts-rejected': ConflictRejectedRequest;
|
|
1779
1849
|
'dsh-memory/graph-search': GraphSearchRequest;
|
|
1780
1850
|
'dsh-memory/graph-node-get': GraphNodeGetRequest;
|
|
1851
|
+
'dsh-memory/graph-preview': GraphPreviewRequest;
|
|
1852
|
+
'dsh-memory/graph-cleanup-dead': GraphCleanupDeadRequest;
|
|
1781
1853
|
'dsh-memory/scenes': Record<string, never>;
|
|
1782
1854
|
'dsh-memory/persona': Record<string, never>;
|
|
1783
1855
|
'dsh-memory/log-tail': LogTailRequest;
|
|
@@ -1843,6 +1915,8 @@ export interface DshMemoryResponseMap {
|
|
|
1843
1915
|
'dsh-memory/conflicts-rejected': ConflictRejectedResponse;
|
|
1844
1916
|
'dsh-memory/graph-search': GraphSearchResponse;
|
|
1845
1917
|
'dsh-memory/graph-node-get': GraphNodeGetResponse;
|
|
1918
|
+
'dsh-memory/graph-preview': GraphPreviewResponse;
|
|
1919
|
+
'dsh-memory/graph-cleanup-dead': GraphCleanupDeadResponse;
|
|
1846
1920
|
'dsh-memory/scenes': ScenesResponse;
|
|
1847
1921
|
'dsh-memory/persona': PersonaResponse;
|
|
1848
1922
|
'dsh-memory/log-tail': LogTailResponse;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DRS 历史锚点回填(三期接线 T8.2,一次性任务的纯核心)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:`l0_conversations` 里 `turn IS NULL` 的旧行,按
|
|
5
|
+
* (session_id, role, 时间窗) 与 DRS 投影库 segments(turn/step/role/time)
|
|
6
|
+
* 最近邻匹配,补填 turn/step。
|
|
7
|
+
*
|
|
8
|
+
* 纪律(对齐 store/sqlite.ts R7 迁移注释,「旧行留 NULL ⇒ 无锚点,不伪造」):
|
|
9
|
+
* - 匹配不上留 NULL;同窗内出现**多个不同 turn** 的候选 = 歧义,也留 NULL;
|
|
10
|
+
* - UPDATE 恒带 `AND turn IS NULL`,已锚定行永不覆盖(与 upsert COALESCE 同向);
|
|
11
|
+
* - 行数不变式:回填只填列,绝不增删行;apply 报告给前后 COUNT 对账。
|
|
12
|
+
*
|
|
13
|
+
* DRS 来源契约:projection.db segments 只读直连——与 HTTP `op=session&turn=`
|
|
14
|
+
* 的检索面同字段(session_id/turn/step/role/time);离线一次性任务走只读
|
|
15
|
+
* SQLite,宿主不在线也可跑。role 词表两侧同形:DRS eventRole 只产
|
|
16
|
+
* user/assistant,l0 capture 只收 user/assistant(capture.ts L0Record)。
|
|
17
|
+
*
|
|
18
|
+
* 本模块零 cordis/运行时依赖:入参是结构化 SQLite 面(prepare/all/run),
|
|
19
|
+
* CLI(scripts/drs-anchor-backfill.mjs)喂真身,单测喂临时 DatabaseSync。
|
|
20
|
+
*/
|
|
21
|
+
/** 结构化 SQLite 面(node:sqlite DatabaseSync 的窄投影,方法双向可赋值)。 */
|
|
22
|
+
export interface PreparedStatementLike {
|
|
23
|
+
all(...params: unknown[]): unknown[];
|
|
24
|
+
run(...params: unknown[]): unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface SqliteLike {
|
|
27
|
+
prepare(sql: string): PreparedStatementLike;
|
|
28
|
+
}
|
|
29
|
+
/** 缺省时间窗:|l0.timestamp − segments.time| ≤ 90s 视为同一条消息。 */
|
|
30
|
+
export declare const DEFAULT_WINDOW_MS = 90000;
|
|
31
|
+
export interface AnchorBackfillOptions {
|
|
32
|
+
/** 只回填该会话;缺省全量。 */
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
/** 本次最多处理的候选行数(防跑飞);缺省不限。 */
|
|
35
|
+
limit?: number;
|
|
36
|
+
/** 匹配时间窗(ms);缺省 {@link DEFAULT_WINDOW_MS}。 */
|
|
37
|
+
windowMs?: number;
|
|
38
|
+
}
|
|
39
|
+
/** 一条可回填的匹配(证据齐全:两侧时间与差值)。 */
|
|
40
|
+
export interface AnchorFillRow {
|
|
41
|
+
recordId: string;
|
|
42
|
+
sessionId: string;
|
|
43
|
+
role: string;
|
|
44
|
+
l0Timestamp: number;
|
|
45
|
+
drsTime: number;
|
|
46
|
+
deltaMs: number;
|
|
47
|
+
turn: number;
|
|
48
|
+
step: number;
|
|
49
|
+
}
|
|
50
|
+
/** 一条被保守放弃的候选(宁缺毋伪造)。 */
|
|
51
|
+
export interface AnchorSkipRow {
|
|
52
|
+
recordId: string;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
reason: 'no-drs-session' | 'no-role-match' | 'out-of-window' | 'ambiguous-turn';
|
|
55
|
+
}
|
|
56
|
+
export interface AnchorBackfillPlan {
|
|
57
|
+
windowMs: number;
|
|
58
|
+
/** 扫到的候选数(turn IS NULL)。 */
|
|
59
|
+
candidates: number;
|
|
60
|
+
/** DRS 侧有投影的会话数。 */
|
|
61
|
+
drsSessions: number;
|
|
62
|
+
/** 已锚定(无需处理)行数——信息面,不是候选。 */
|
|
63
|
+
alreadyAnchored: number;
|
|
64
|
+
fills: AnchorFillRow[];
|
|
65
|
+
skips: AnchorSkipRow[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 产出回填计划(纯只读,两个库都不写)。匹配规则:
|
|
69
|
+
* 同会话 + 同 role + |Δt| ≤ windowMs 的 DRS 行里,若候选 turn 唯一 → 取
|
|
70
|
+
* |Δt| 最近一条;turn 不唯一 → 歧义放弃;无行/无窗内行 → 如实分型放弃。
|
|
71
|
+
*/
|
|
72
|
+
export declare function planAnchorBackfill(input: {
|
|
73
|
+
memory: SqliteLike;
|
|
74
|
+
drs: SqliteLike;
|
|
75
|
+
} & AnchorBackfillOptions): AnchorBackfillPlan;
|
|
76
|
+
export interface AnchorApplyReport {
|
|
77
|
+
plan: AnchorBackfillPlan;
|
|
78
|
+
applied: number;
|
|
79
|
+
totalRowsBefore: number;
|
|
80
|
+
totalRowsAfter: number;
|
|
81
|
+
/** 行数不变式:回填只填列,前后 COUNT 必须相等。 */
|
|
82
|
+
invariantHolds: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 应用计划:逐条 UPDATE,恒带 `AND turn IS NULL`(已锚定行永不覆盖;
|
|
86
|
+
* plan→apply 之间若有新写入恰好锚定了同一行,该条自然落空,不报错)。
|
|
87
|
+
*/
|
|
88
|
+
export declare function applyAnchorBackfill(memory: SqliteLike, plan: AnchorBackfillPlan): AnchorApplyReport;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DRS 历史锚点回填(三期接线 T8.2,一次性任务的纯核心)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:`l0_conversations` 里 `turn IS NULL` 的旧行,按
|
|
5
|
+
* (session_id, role, 时间窗) 与 DRS 投影库 segments(turn/step/role/time)
|
|
6
|
+
* 最近邻匹配,补填 turn/step。
|
|
7
|
+
*
|
|
8
|
+
* 纪律(对齐 store/sqlite.ts R7 迁移注释,「旧行留 NULL ⇒ 无锚点,不伪造」):
|
|
9
|
+
* - 匹配不上留 NULL;同窗内出现**多个不同 turn** 的候选 = 歧义,也留 NULL;
|
|
10
|
+
* - UPDATE 恒带 `AND turn IS NULL`,已锚定行永不覆盖(与 upsert COALESCE 同向);
|
|
11
|
+
* - 行数不变式:回填只填列,绝不增删行;apply 报告给前后 COUNT 对账。
|
|
12
|
+
*
|
|
13
|
+
* DRS 来源契约:projection.db segments 只读直连——与 HTTP `op=session&turn=`
|
|
14
|
+
* 的检索面同字段(session_id/turn/step/role/time);离线一次性任务走只读
|
|
15
|
+
* SQLite,宿主不在线也可跑。role 词表两侧同形:DRS eventRole 只产
|
|
16
|
+
* user/assistant,l0 capture 只收 user/assistant(capture.ts L0Record)。
|
|
17
|
+
*
|
|
18
|
+
* 本模块零 cordis/运行时依赖:入参是结构化 SQLite 面(prepare/all/run),
|
|
19
|
+
* CLI(scripts/drs-anchor-backfill.mjs)喂真身,单测喂临时 DatabaseSync。
|
|
20
|
+
*/
|
|
21
|
+
/** 缺省时间窗:|l0.timestamp − segments.time| ≤ 90s 视为同一条消息。 */
|
|
22
|
+
export const DEFAULT_WINDOW_MS = 90_000;
|
|
23
|
+
/** DRS segments → 锚点行(只收 turn>0 且 role∈{user,assistant} 的行)。 */
|
|
24
|
+
function loadDrsAnchors(drs, sessionId) {
|
|
25
|
+
const sql = sessionId === undefined
|
|
26
|
+
? 'SELECT session_id, turn, step, role, time FROM segments WHERE turn > 0 AND role IN (?, ?) ORDER BY session_id, time'
|
|
27
|
+
: 'SELECT session_id, turn, step, role, time FROM segments WHERE turn > 0 AND role IN (?, ?) AND session_id = ? ORDER BY session_id, time';
|
|
28
|
+
const rows = drs.prepare(sql).all('user', 'assistant', ...(sessionId === undefined ? [] : [sessionId]));
|
|
29
|
+
const bySession = new Map();
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
if (typeof row.session_id !== 'string' || typeof row.turn !== 'number' || typeof row.time !== 'number')
|
|
32
|
+
continue;
|
|
33
|
+
const entry = {
|
|
34
|
+
sessionId: row.session_id,
|
|
35
|
+
turn: row.turn,
|
|
36
|
+
step: typeof row.step === 'number' ? row.step : 0,
|
|
37
|
+
role: typeof row.role === 'string' ? row.role : '',
|
|
38
|
+
time: row.time,
|
|
39
|
+
};
|
|
40
|
+
const bucket = bySession.get(entry.sessionId);
|
|
41
|
+
if (bucket === undefined)
|
|
42
|
+
bySession.set(entry.sessionId, [entry]);
|
|
43
|
+
else
|
|
44
|
+
bucket.push(entry);
|
|
45
|
+
}
|
|
46
|
+
return bySession;
|
|
47
|
+
}
|
|
48
|
+
function loadCandidates(memory, sessionId, limit) {
|
|
49
|
+
const sql = sessionId === undefined
|
|
50
|
+
? 'SELECT record_id, session_id, role, timestamp FROM l0_conversations WHERE turn IS NULL AND role IN (?, ?) ORDER BY session_id, timestamp'
|
|
51
|
+
: 'SELECT record_id, session_id, role, timestamp FROM l0_conversations WHERE turn IS NULL AND role IN (?, ?) AND session_id = ? ORDER BY session_id, timestamp';
|
|
52
|
+
const rows = memory.prepare(sql).all('user', 'assistant', ...(sessionId === undefined ? [] : [sessionId]));
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
if (typeof row.record_id !== 'string' || typeof row.session_id !== 'string' || typeof row.timestamp !== 'number')
|
|
56
|
+
continue;
|
|
57
|
+
out.push({
|
|
58
|
+
recordId: row.record_id,
|
|
59
|
+
sessionId: row.session_id,
|
|
60
|
+
role: typeof row.role === 'string' ? row.role : '',
|
|
61
|
+
timestamp: row.timestamp,
|
|
62
|
+
});
|
|
63
|
+
if (limit !== undefined && out.length >= limit)
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 产出回填计划(纯只读,两个库都不写)。匹配规则:
|
|
70
|
+
* 同会话 + 同 role + |Δt| ≤ windowMs 的 DRS 行里,若候选 turn 唯一 → 取
|
|
71
|
+
* |Δt| 最近一条;turn 不唯一 → 歧义放弃;无行/无窗内行 → 如实分型放弃。
|
|
72
|
+
*/
|
|
73
|
+
export function planAnchorBackfill(input) {
|
|
74
|
+
const windowMs = input.windowMs ?? DEFAULT_WINDOW_MS;
|
|
75
|
+
const drsBySession = loadDrsAnchors(input.drs, input.sessionId);
|
|
76
|
+
const candidates = loadCandidates(input.memory, input.sessionId, input.limit);
|
|
77
|
+
const anchored = input.memory.prepare(input.sessionId === undefined
|
|
78
|
+
? 'SELECT COUNT(*) AS n FROM l0_conversations WHERE turn IS NOT NULL'
|
|
79
|
+
: 'SELECT COUNT(*) AS n FROM l0_conversations WHERE turn IS NOT NULL AND session_id = ?').all(...(input.sessionId === undefined ? [] : [input.sessionId]));
|
|
80
|
+
const plan = {
|
|
81
|
+
windowMs,
|
|
82
|
+
candidates: candidates.length,
|
|
83
|
+
drsSessions: drsBySession.size,
|
|
84
|
+
alreadyAnchored: typeof anchored[0]?.n === 'number' ? anchored[0].n : 0,
|
|
85
|
+
fills: [],
|
|
86
|
+
skips: [],
|
|
87
|
+
};
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
const rows = drsBySession.get(candidate.sessionId);
|
|
90
|
+
if (rows === undefined) {
|
|
91
|
+
plan.skips.push({ recordId: candidate.recordId, sessionId: candidate.sessionId, reason: 'no-drs-session' });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const sameRole = rows.filter(row => row.role === candidate.role);
|
|
95
|
+
if (sameRole.length === 0) {
|
|
96
|
+
plan.skips.push({ recordId: candidate.recordId, sessionId: candidate.sessionId, reason: 'no-role-match' });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const inWindow = sameRole
|
|
100
|
+
.map(row => ({ row, delta: Math.abs(row.time - candidate.timestamp) }))
|
|
101
|
+
.filter(entry => entry.delta <= windowMs);
|
|
102
|
+
if (inWindow.length === 0) {
|
|
103
|
+
plan.skips.push({ recordId: candidate.recordId, sessionId: candidate.sessionId, reason: 'out-of-window' });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const turns = new Set(inWindow.map(entry => entry.row.turn));
|
|
107
|
+
if (turns.size > 1) {
|
|
108
|
+
plan.skips.push({ recordId: candidate.recordId, sessionId: candidate.sessionId, reason: 'ambiguous-turn' });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const nearest = inWindow.reduce((best, entry) => (entry.delta < best.delta ? entry : best));
|
|
112
|
+
plan.fills.push({
|
|
113
|
+
recordId: candidate.recordId,
|
|
114
|
+
sessionId: candidate.sessionId,
|
|
115
|
+
role: candidate.role,
|
|
116
|
+
l0Timestamp: candidate.timestamp,
|
|
117
|
+
drsTime: nearest.row.time,
|
|
118
|
+
deltaMs: nearest.delta,
|
|
119
|
+
turn: nearest.row.turn,
|
|
120
|
+
step: nearest.row.step,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return plan;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 应用计划:逐条 UPDATE,恒带 `AND turn IS NULL`(已锚定行永不覆盖;
|
|
127
|
+
* plan→apply 之间若有新写入恰好锚定了同一行,该条自然落空,不报错)。
|
|
128
|
+
*/
|
|
129
|
+
export function applyAnchorBackfill(memory, plan) {
|
|
130
|
+
const countOf = (sql) => {
|
|
131
|
+
const rows = memory.prepare(sql).all();
|
|
132
|
+
return typeof rows[0]?.n === 'number' ? rows[0].n : 0;
|
|
133
|
+
};
|
|
134
|
+
const totalRowsBefore = countOf('SELECT COUNT(*) AS n FROM l0_conversations');
|
|
135
|
+
const update = memory.prepare('UPDATE l0_conversations SET turn = ?, step = ? WHERE record_id = ? AND turn IS NULL');
|
|
136
|
+
let applied = 0;
|
|
137
|
+
for (const fill of plan.fills) {
|
|
138
|
+
const result = update.run(fill.turn, fill.step, fill.recordId);
|
|
139
|
+
if (typeof result?.changes === 'number' && result.changes > 0)
|
|
140
|
+
applied += result.changes;
|
|
141
|
+
}
|
|
142
|
+
const totalRowsAfter = countOf('SELECT COUNT(*) AS n FROM l0_conversations');
|
|
143
|
+
return {
|
|
144
|
+
plan,
|
|
145
|
+
applied,
|
|
146
|
+
totalRowsBefore,
|
|
147
|
+
totalRowsAfter,
|
|
148
|
+
invariantHolds: totalRowsBefore === totalRowsAfter,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -13,8 +13,10 @@ export declare const GRAPH_NODE_TYPE_SET: ReadonlySet<string>;
|
|
|
13
13
|
/** 记录生命周期四态(节点/fact/边通用)。 */
|
|
14
14
|
export declare const GRAPH_STATUSES: readonly GraphRecordStatus[];
|
|
15
15
|
export declare const GRAPH_STATUS_SET: ReadonlySet<string>;
|
|
16
|
+
/** 因果边四词单源(投影提示词枚举、校验与面板预览加权共用——防两处词表漂移)。 */
|
|
17
|
+
export declare const GRAPH_CAUSAL_RELATIONS: readonly ["导致", "因为", "修复了", "取代"];
|
|
16
18
|
/** 规范中文关系词(提示词给枚举;校验不做白名单——关系是开放词表,只做长度钳制)。
|
|
17
|
-
*
|
|
19
|
+
* 尾部即因果集 GRAPH_CAUSAL_RELATIONS——让"前因后果"跨记录成链;仅当 graph.enabled 时
|
|
18
20
|
* 才参与投影(整块图谱默认关,不碰非图谱用户的蒸馏产物)。 */
|
|
19
21
|
export declare const GRAPH_RELATION_WORDS: readonly ["使用", "属于", "创建", "参与", "贡献", "依赖", "位于", "相关", "导致", "因为", "修复了", "取代"];
|
|
20
22
|
/** 提案字段上限(校验侧截断/丢弃与提示词内插共用同一组数字)。 */
|
|
@@ -4,10 +4,12 @@ export const GRAPH_NODE_TYPE_SET = new Set(GRAPH_NODE_TYPES);
|
|
|
4
4
|
/** 记录生命周期四态(节点/fact/边通用)。 */
|
|
5
5
|
export const GRAPH_STATUSES = ['active', 'superseded', 'disputed', 'archived'];
|
|
6
6
|
export const GRAPH_STATUS_SET = new Set(GRAPH_STATUSES);
|
|
7
|
+
/** 因果边四词单源(投影提示词枚举、校验与面板预览加权共用——防两处词表漂移)。 */
|
|
8
|
+
export const GRAPH_CAUSAL_RELATIONS = ['导致', '因为', '修复了', '取代'];
|
|
7
9
|
/** 规范中文关系词(提示词给枚举;校验不做白名单——关系是开放词表,只做长度钳制)。
|
|
8
|
-
*
|
|
10
|
+
* 尾部即因果集 GRAPH_CAUSAL_RELATIONS——让"前因后果"跨记录成链;仅当 graph.enabled 时
|
|
9
11
|
* 才参与投影(整块图谱默认关,不碰非图谱用户的蒸馏产物)。 */
|
|
10
|
-
export const GRAPH_RELATION_WORDS = ['使用', '属于', '创建', '参与', '贡献', '依赖', '位于', '相关',
|
|
12
|
+
export const GRAPH_RELATION_WORDS = ['使用', '属于', '创建', '参与', '贡献', '依赖', '位于', '相关', ...GRAPH_CAUSAL_RELATIONS];
|
|
11
13
|
/** 提案字段上限(校验侧截断/丢弃与提示词内插共用同一组数字)。 */
|
|
12
14
|
export const GRAPH_CAP = {
|
|
13
15
|
/** 提案内临时引用(ref)长度。 */
|