dsh-layered-memory 0.8.7 → 0.8.9
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/README.en.md +47 -4
- package/README.md +51 -4
- package/assets/changelog/0.8.9/01-panel.png +0 -0
- package/assets/changelog/0.8.9/02-halo.png +0 -0
- package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
- package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
- package/dist/client.js +3542 -3293
- package/dist/config.d.ts +286 -4
- package/dist/config.js +27 -0
- package/dist/contract.d.ts +652 -0
- package/dist/contract.js +1 -0
- package/dist/hooks/recall.d.ts +13 -18
- package/dist/hooks/recall.js +143 -20
- package/dist/index.d.ts +268 -4
- package/dist/index.js +9 -3
- package/dist/llm-usage.d.ts +2 -1
- package/dist/llm.d.ts +63 -11
- package/dist/llm.js +165 -15
- package/dist/pipeline/rebuild.d.ts +2 -21
- package/dist/pipeline/runner.d.ts +3 -3
- package/dist/pipeline/runner.js +52 -10
- package/dist/settings.d.ts +25 -29
- package/dist/settings.js +107 -1
- package/dist/stats.d.ts +9 -26
- package/dist/stats.js +149 -22
- package/dist/store/cost-ledger.d.ts +78 -0
- package/dist/store/cost-ledger.js +173 -0
- package/dist/store/download-queue.d.ts +2 -20
- package/dist/store/embedding-source.d.ts +2 -52
- package/dist/store/occupancy.d.ts +30 -0
- package/dist/store/occupancy.js +134 -0
- package/dist/store/runtime-installer.d.ts +2 -13
- package/dist/store/sqlite.d.ts +11 -61
- package/dist/store/sqlite.js +15 -150
- package/dist/token-cost.d.ts +3 -72
- package/dist/util/context-occupancy.d.ts +68 -0
- package/dist/util/context-occupancy.js +92 -0
- package/package.json +8 -4
package/dist/store/sqlite.js
CHANGED
|
@@ -22,6 +22,9 @@ import { bm25RankToScore, buildFtsQuery, tokenizeForFts } from './search-utils.j
|
|
|
22
22
|
import { describeTokenizer, ensureTokenizer, tokenizerStamp } from '../util/tokenizer.js';
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
24
|
const TAG = '[memory][sqlite]';
|
|
25
|
+
// 成本账本(token_cost 表族:类型 + 实现)已整体迁出到 ./cost-ledger.ts——与检索引擎
|
|
26
|
+
// 零关系的独立职责;此处 re-export 不断裂既有引用,MemoryDb 四个公开方法改为一行委托。
|
|
27
|
+
import { CostLedger } from './cost-ledger.js';
|
|
25
28
|
/** vec0 KNN 对遗留零向量的补偿缓冲(官方同款)。 */
|
|
26
29
|
const ZERO_VEC_BUFFER = 10;
|
|
27
30
|
/** IN 查询/删除的分块大小(保守避开 SQLite 变量数上限:现代构建 32766,老版 999)。 */
|
|
@@ -39,13 +42,6 @@ function emptyCostAggregate() {
|
|
|
39
42
|
return { calls: 0, inputChars: 0, outputTokens: 0, reasoningTokens: 0, avgOutputTokens: 0, medianOutputTokens: 0 };
|
|
40
43
|
}
|
|
41
44
|
/** 已排序序列的中位数(偶数个取中间两者平均;空返回 0)。 */
|
|
42
|
-
function medianOf(sorted) {
|
|
43
|
-
const n = sorted.length;
|
|
44
|
-
if (n === 0)
|
|
45
|
-
return 0;
|
|
46
|
-
const mid = Math.floor(n / 2);
|
|
47
|
-
return n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
48
|
-
}
|
|
49
45
|
export class MemoryDb {
|
|
50
46
|
db;
|
|
51
47
|
degraded = false;
|
|
@@ -67,9 +63,8 @@ export class MemoryDb {
|
|
|
67
63
|
stmtL1FtsDelete;
|
|
68
64
|
stmtL1FtsSearch;
|
|
69
65
|
stmtL1FtsSearchFamily;
|
|
70
|
-
/** token_cost
|
|
71
|
-
|
|
72
|
-
stmtDeleteCost;
|
|
66
|
+
/** 成本账本(token_cost 表族;init 内初始化,未就绪时方法返回零值)。 */
|
|
67
|
+
costLedger = new CostLedger();
|
|
73
68
|
stmtUpsertL0;
|
|
74
69
|
stmtGetL0;
|
|
75
70
|
/** 主表存在性点查(同 L1:防御性 FTS 删除的前置判断)。 */
|
|
@@ -339,25 +334,8 @@ export class MemoryDb {
|
|
|
339
334
|
this.stmtGetL0 = this.db.prepare('SELECT session_id, role, message_text, recorded_at, timestamp FROM l0_conversations WHERE record_id = ?');
|
|
340
335
|
this.stmtL0Exists = this.db.prepare('SELECT 1 FROM l0_conversations WHERE record_id = ?');
|
|
341
336
|
this.prepareL0VecStatements();
|
|
342
|
-
// ── token_cost
|
|
343
|
-
this.db.
|
|
344
|
-
CREATE TABLE IF NOT EXISTS token_cost (
|
|
345
|
-
ts INTEGER NOT NULL,
|
|
346
|
-
provider TEXT NOT NULL,
|
|
347
|
-
model TEXT NOT NULL,
|
|
348
|
-
layer TEXT NOT NULL,
|
|
349
|
-
input_chars INTEGER NOT NULL DEFAULT 0,
|
|
350
|
-
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
351
|
-
reasoning_tokens INTEGER NOT NULL DEFAULT 0
|
|
352
|
-
)
|
|
353
|
-
`);
|
|
354
|
-
// 迁移:provider/model 复合键引入前的旧表补 provider 列(历史行回填 unknown)
|
|
355
|
-
if (this.tableExists('token_cost') && !this.hasColumn('token_cost', 'provider')) {
|
|
356
|
-
this.db.exec("ALTER TABLE token_cost ADD COLUMN provider TEXT NOT NULL DEFAULT 'unknown'");
|
|
357
|
-
}
|
|
358
|
-
this.db.exec('CREATE INDEX IF NOT EXISTS idx_token_cost_ts ON token_cost(ts)');
|
|
359
|
-
this.stmtInsertCost = this.db.prepare('INSERT INTO token_cost (ts, provider, model, layer, input_chars, output_tokens, reasoning_tokens) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
360
|
-
this.stmtDeleteCost = this.db.prepare('DELETE FROM token_cost WHERE ts < ?');
|
|
337
|
+
// ── token_cost:蒸馏成本明细表(成本账本自治;见 cost-ledger.ts) ──
|
|
338
|
+
this.costLedger.init(this.db, this.logger);
|
|
361
339
|
// ── FTS5 全文索引(建表失败仅停用 FTS,不降级整个库) ──
|
|
362
340
|
try {
|
|
363
341
|
// 索引重建判据(FTS5 无法 ALTER,只能 drop 后从源表全量回灌):
|
|
@@ -1011,134 +989,21 @@ export class MemoryDb {
|
|
|
1011
989
|
return false;
|
|
1012
990
|
}
|
|
1013
991
|
}
|
|
1014
|
-
/**
|
|
1015
|
-
* 记录一次蒸馏调用成本(明细表,写入时按 retentionDays 滚动清理;0 = 永久保留)。
|
|
1016
|
-
* 失败/成功都记(token 照烧);记账失败记 warn 但不阻断蒸馏(成本看板是增强能力)。
|
|
1017
|
-
*/
|
|
992
|
+
/** 记录一次蒸馏调用成本(委托 cost-ledger;语义见 CostLedger.insertCostCall)。 */
|
|
1018
993
|
insertCostCall(provider, model, layer, inputChars, outputTokens, reasoningTokens, retentionDays) {
|
|
1019
|
-
|
|
1020
|
-
return;
|
|
1021
|
-
try {
|
|
1022
|
-
this.stmtInsertCost.run(Date.now(), provider, model, layer, Math.max(0, Math.round(inputChars)), Math.max(0, Math.round(outputTokens)), Math.max(0, Math.round(reasoningTokens)));
|
|
1023
|
-
if (retentionDays > 0)
|
|
1024
|
-
this.stmtDeleteCost.run(Date.now() - retentionDays * 24 * 3600_000);
|
|
1025
|
-
}
|
|
1026
|
-
catch (err) {
|
|
1027
|
-
this.logger?.warn(`${TAG} token_cost 记账失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
1028
|
-
}
|
|
994
|
+
this.costLedger.insertCostCall(provider, model, layer, inputChars, outputTokens, reasoningTokens, retentionDays);
|
|
1029
995
|
}
|
|
1030
|
-
/**
|
|
1031
|
-
* 查询 token_cost 单窗口聚合(成本看板用;since 为毫秒起点,0 = 全量)。
|
|
1032
|
-
* 输入口径:inputChars 是字符(llm 流拿不到输入 token,沿用 llm-usage 的字符折算口径)。
|
|
1033
|
-
* 成本看板是增强能力:降级态/查询异常一律返回零值,不向上抛错。
|
|
1034
|
-
* median 需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median 函数)。
|
|
1035
|
-
*/
|
|
996
|
+
/** 查询 token_cost 单窗口聚合(委托 cost-ledger;降级/异常返回零值)。 */
|
|
1036
997
|
aggregateCost(since) {
|
|
1037
|
-
|
|
1038
|
-
return { total: emptyCostAggregate(), byModel: [] };
|
|
1039
|
-
try {
|
|
1040
|
-
const total = this.db
|
|
1041
|
-
.prepare(`SELECT COUNT(*) AS calls,
|
|
1042
|
-
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
1043
|
-
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
1044
|
-
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
1045
|
-
COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
|
|
1046
|
-
FROM token_cost WHERE ts >= ?`)
|
|
1047
|
-
.get(since);
|
|
1048
|
-
const tokenRows = this.db
|
|
1049
|
-
.prepare('SELECT output_tokens FROM token_cost WHERE ts >= ? ORDER BY output_tokens')
|
|
1050
|
-
.all(since);
|
|
1051
|
-
const byModel = this.db
|
|
1052
|
-
.prepare(`SELECT provider, model, COUNT(*) AS calls,
|
|
1053
|
-
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
1054
|
-
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
1055
|
-
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
|
|
1056
|
-
FROM token_cost WHERE ts >= ? GROUP BY provider, model ORDER BY outputTokens DESC`)
|
|
1057
|
-
.all(since);
|
|
1058
|
-
return {
|
|
1059
|
-
total: {
|
|
1060
|
-
calls: total.calls,
|
|
1061
|
-
inputChars: total.inputChars,
|
|
1062
|
-
outputTokens: total.outputTokens,
|
|
1063
|
-
reasoningTokens: total.reasoningTokens,
|
|
1064
|
-
avgOutputTokens: total.avgOutputTokens,
|
|
1065
|
-
medianOutputTokens: medianOf(tokenRows.map((r) => r.output_tokens)),
|
|
1066
|
-
},
|
|
1067
|
-
byModel,
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
catch {
|
|
1071
|
-
return { total: emptyCostAggregate(), byModel: [] };
|
|
1072
|
-
}
|
|
998
|
+
return this.costLedger.aggregateCost(since);
|
|
1073
999
|
}
|
|
1074
|
-
/**
|
|
1075
|
-
* 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
|
|
1076
|
-
* 降级/异常返回空数组,不抛错。
|
|
1077
|
-
*/
|
|
1000
|
+
/** 按层级归并聚合(委托 cost-ledger;降级/异常返回空数组)。 */
|
|
1078
1001
|
aggregateCostByLayer(since) {
|
|
1079
|
-
|
|
1080
|
-
return [];
|
|
1081
|
-
try {
|
|
1082
|
-
const rows = this.db
|
|
1083
|
-
.prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
|
|
1084
|
-
COUNT(*) AS calls,
|
|
1085
|
-
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
1086
|
-
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
1087
|
-
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
1088
|
-
COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
|
|
1089
|
-
FROM token_cost WHERE ts >= ? GROUP BY layer ORDER BY layer`)
|
|
1090
|
-
.all(since);
|
|
1091
|
-
// 中位数需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median)
|
|
1092
|
-
const tokenRows = this.db
|
|
1093
|
-
.prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
|
|
1094
|
-
output_tokens
|
|
1095
|
-
FROM token_cost WHERE ts >= ? ORDER BY layer, output_tokens`)
|
|
1096
|
-
.all(since);
|
|
1097
|
-
const medianByLayer = new Map();
|
|
1098
|
-
for (const r of tokenRows) {
|
|
1099
|
-
const arr = medianByLayer.get(r.layer);
|
|
1100
|
-
if (arr)
|
|
1101
|
-
arr.push(r.output_tokens);
|
|
1102
|
-
else
|
|
1103
|
-
medianByLayer.set(r.layer, [r.output_tokens]);
|
|
1104
|
-
}
|
|
1105
|
-
return rows.map((r) => ({
|
|
1106
|
-
...r,
|
|
1107
|
-
medianOutputTokens: medianOf(medianByLayer.get(r.layer) ?? []),
|
|
1108
|
-
}));
|
|
1109
|
-
}
|
|
1110
|
-
catch {
|
|
1111
|
-
return [];
|
|
1112
|
-
}
|
|
1002
|
+
return this.costLedger.aggregateCostByLayer(since);
|
|
1113
1003
|
}
|
|
1114
|
-
/**
|
|
1115
|
-
* 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
|
|
1116
|
-
* offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
|
|
1117
|
-
* 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
|
|
1118
|
-
*/
|
|
1004
|
+
/** 按时间桶 + model 聚合(委托 cost-ledger;趋势图与日均/周均/月均共用)。 */
|
|
1119
1005
|
aggregateByBucket(bucketMs, offsetMs, since, layer) {
|
|
1120
|
-
|
|
1121
|
-
return [];
|
|
1122
|
-
try {
|
|
1123
|
-
let sql = `SELECT CAST((ts + ?) / ? AS INTEGER) AS bucket, provider, model,
|
|
1124
|
-
COUNT(*) AS calls,
|
|
1125
|
-
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
1126
|
-
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
|
|
1127
|
-
FROM token_cost WHERE ts >= ?`;
|
|
1128
|
-
const params = [offsetMs, bucketMs, since];
|
|
1129
|
-
if (layer === 'l1') {
|
|
1130
|
-
sql += ` AND layer IN ('l1-extract','l1-dedup')`;
|
|
1131
|
-
}
|
|
1132
|
-
else if (layer) {
|
|
1133
|
-
sql += ` AND layer = ?`;
|
|
1134
|
-
params.push(layer);
|
|
1135
|
-
}
|
|
1136
|
-
sql += ` GROUP BY bucket, provider, model ORDER BY bucket, provider, model`;
|
|
1137
|
-
return this.db.prepare(sql).all(...params);
|
|
1138
|
-
}
|
|
1139
|
-
catch {
|
|
1140
|
-
return [];
|
|
1141
|
-
}
|
|
1006
|
+
return this.costLedger.aggregateByBucket(bucketMs, offsetMs, since, layer);
|
|
1142
1007
|
}
|
|
1143
1008
|
countL0() {
|
|
1144
1009
|
if (this.degraded)
|
package/dist/token-cost.d.ts
CHANGED
|
@@ -7,84 +7,15 @@
|
|
|
7
7
|
* 与作者 llm-usage.ts 的关系:作者是"按 layer 累计的纯内存计数器"(给 bench 用);
|
|
8
8
|
* 本模块补上三个缺口——按 model 分组、持久化(保留期可配置,默认 365 天)、面向 UI 成本看板。
|
|
9
9
|
*/
|
|
10
|
+
import type { CostSnapshot, Granularity } from './contract.js';
|
|
10
11
|
import type { DistillLayer } from './llm-usage.js';
|
|
11
|
-
import type {
|
|
12
|
+
import type { MemoryDb } from './store/sqlite.js';
|
|
13
|
+
export type { CostWindow, Granularity, ModelMetrics, LayerMetrics, LayerWindow, LayerCost, TrendBucket, TrendSnapshot, CostSnapshot, } from './contract.js';
|
|
12
14
|
/** 插件启动时注入 db 与明细保留期(index.ts 调用;retentionDays 0 = 永久保留)。 */
|
|
13
15
|
export declare function initTokenCost(d: MemoryDb, retention: number): void;
|
|
14
16
|
/** 插件卸载时清空 db 引用(index.ts 的 ctx.effect 清理里调用,防悬空引用)。 */
|
|
15
17
|
export declare function resetTokenCost(): void;
|
|
16
18
|
/** 记录一次蒸馏调用成本(callLLM 出口调用;provider/model 由调用方传入)。 */
|
|
17
19
|
export declare function recordCostCall(provider: string, model: string, layer: DistillLayer, inputChars: number, outputTokens: number, reasoningTokens: number): void;
|
|
18
|
-
/** 成本看板单个时间窗口(day/week/month/all)。 */
|
|
19
|
-
export interface CostWindow {
|
|
20
|
-
range: 'day' | 'week' | 'month' | 'all';
|
|
21
|
-
/** 窗口起点(毫秒;all 为 0)。 */
|
|
22
|
-
since: number;
|
|
23
|
-
calls: number;
|
|
24
|
-
inputChars: number;
|
|
25
|
-
outputTokens: number;
|
|
26
|
-
reasoningTokens: number;
|
|
27
|
-
avgOutputTokens: number;
|
|
28
|
-
medianOutputTokens: number;
|
|
29
|
-
}
|
|
30
|
-
/** 成本看板时间粒度(趋势图 + 统计口径共用)。 */
|
|
31
|
-
export type Granularity = 'day' | 'week' | 'month';
|
|
32
|
-
/** 每模型统计指标(层级表格行;均值/中位数按 since=0 全量历史的活跃桶口径,非「最近窗口」)。 */
|
|
33
|
-
export interface ModelMetrics {
|
|
34
|
-
model: string;
|
|
35
|
-
dayCalls: number;
|
|
36
|
-
weekCalls: number;
|
|
37
|
-
monthCalls: number;
|
|
38
|
-
dayOutput: number;
|
|
39
|
-
dayMedian: number;
|
|
40
|
-
weekOutput: number;
|
|
41
|
-
weekMedian: number;
|
|
42
|
-
monthOutput: number;
|
|
43
|
-
monthMedian: number;
|
|
44
|
-
}
|
|
45
|
-
/** 每层级(l1/l2/l3)的模型统计(层级表格)。 */
|
|
46
|
-
export interface LayerMetrics {
|
|
47
|
-
layer: 'l1' | 'l2' | 'l3';
|
|
48
|
-
models: ModelMetrics[];
|
|
49
|
-
}
|
|
50
|
-
/** 层级 × 窗口矩阵里的单个窗口格子。 */
|
|
51
|
-
export interface LayerWindow {
|
|
52
|
-
range: 'day' | 'week' | 'month' | 'all';
|
|
53
|
-
calls: number;
|
|
54
|
-
inputChars: number;
|
|
55
|
-
outputTokens: number;
|
|
56
|
-
reasoningTokens: number;
|
|
57
|
-
avgOutputTokens: number;
|
|
58
|
-
medianOutputTokens: number;
|
|
59
|
-
}
|
|
60
|
-
/** 单个层级在四个窗口的聚合(层级×窗口表格行)。 */
|
|
61
|
-
export interface LayerCost {
|
|
62
|
-
layer: 'l1' | 'l2' | 'l3';
|
|
63
|
-
windows: LayerWindow[];
|
|
64
|
-
}
|
|
65
|
-
/** 趋势单桶。 */
|
|
66
|
-
export interface TrendBucket {
|
|
67
|
-
ts: number;
|
|
68
|
-
total: number;
|
|
69
|
-
byModel: Record<string, number>;
|
|
70
|
-
}
|
|
71
|
-
/** 趋势快照:三个层级各一份连续桶序列。 */
|
|
72
|
-
export interface TrendSnapshot {
|
|
73
|
-
granularity: Granularity;
|
|
74
|
-
byLayer: Record<'l1' | 'l2' | 'l3', TrendBucket[]>;
|
|
75
|
-
}
|
|
76
|
-
/** 成本看板快照(dsh-memory/token-cost 端点返回值)。 */
|
|
77
|
-
export interface CostSnapshot {
|
|
78
|
-
/** day/week/month/all 四窗口聚合。 */
|
|
79
|
-
windows: CostWindow[];
|
|
80
|
-
/** 全量窗口(all)按 model 分组。 */
|
|
81
|
-
byModel: CostByModel[];
|
|
82
|
-
/** 按层级(l1/l2/l3 归并)在四个窗口的聚合(层级×窗口表格)。 */
|
|
83
|
-
byLayer: LayerCost[];
|
|
84
|
-
/** 每层级的模型统计指标(层级表格)。 */
|
|
85
|
-
byLayerStats: LayerMetrics[];
|
|
86
|
-
/** 趋势图数据。 */
|
|
87
|
-
trend: TrendSnapshot;
|
|
88
|
-
}
|
|
89
20
|
/** 读成本看板快照(db 未注入/降级时返回全零结构,不抛错;rangeDays>0 = 趋势展示近 N 天)。 */
|
|
90
21
|
export declare function snapshotTokenCost(granularity: Granularity, rangeDays: number): CostSnapshot;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文占用账本:插件注入内容在当前会话上下文窗口中的启发式 token 存量。
|
|
3
|
+
*
|
|
4
|
+
* 本模块是全部占用数字的**唯一算术来源**(host 记账与 client 渲染共用,esbuild
|
|
5
|
+
* 将其内联进 client bundle),任何一侧不得另写算法。口径与官方
|
|
6
|
+
* @deepseek-ai/dsh-token-meter/estimate 的固定密度启发式同式:
|
|
7
|
+
*
|
|
8
|
+
* - text 块 = ceil(chars / 4) + 4(块开销);
|
|
9
|
+
* - 整条 user 消息(召回注入体的形态)再叠加 role overhead 4;
|
|
10
|
+
* - 系统提示里的稳定区**子片**计入时不加任何 overhead——那 +8 是官方对整段
|
|
11
|
+
* 消息/整体系统提示的结构成本,切片重复计会与官方总数对不上;
|
|
12
|
+
* - 字符数一律 JS `.length`(UTF-16 单元),与官方产出严格可比。
|
|
13
|
+
*/
|
|
14
|
+
/** 固定文本密度:每 4 字符记 1 token(官方 CHARS_PER_TOKEN 同值)。 */
|
|
15
|
+
export declare const CHARS_PER_TOKEN = 4;
|
|
16
|
+
/** 召回注入体(单 text 块 user message)的启发式 token 数。 */
|
|
17
|
+
export declare function estimateInjectedMessageTokens(chars: number): number;
|
|
18
|
+
/** 稳定区子片的启发式 token 数(不加结构开销,见模块头注释)。 */
|
|
19
|
+
export declare function estimateStableSectionTokens(chars: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* 单会话占用账本(权威账本只存在于 host 侧;client 仅消费展示)。
|
|
22
|
+
* stockTokens = recallTokens + profileTokens 恒等式由三个迁移函数共同维护。
|
|
23
|
+
*/
|
|
24
|
+
export interface OccupancyLedger {
|
|
25
|
+
/** 当前估算仍在上下文窗口内的记忆存量(token)。 */
|
|
26
|
+
stockTokens: number;
|
|
27
|
+
/** 累计召回注入存留份额。 */
|
|
28
|
+
recallTokens: number;
|
|
29
|
+
/** 系统提示稳定区当前份额(OFF 使 text() 返回空时即时清零)。 */
|
|
30
|
+
profileTokens: number;
|
|
31
|
+
/** 最近一轮召回注入的增量(悬浮卡/面板"最近一次"语义)。 */
|
|
32
|
+
lastInjectTokens: number;
|
|
33
|
+
updatedAt: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function emptyOccupancyLedger(now?: number): OccupancyLedger;
|
|
36
|
+
/** 召回注入成功后的入账(失败跳过的轮次不调用——账目零扰动)。 */
|
|
37
|
+
export declare function recordRecallInjection(l: OccupancyLedger, chars: number, now?: number): void;
|
|
38
|
+
/**
|
|
39
|
+
* 稳定区份额随每次实际组进的文本同步(含切回非 OFF 的重新计入):
|
|
40
|
+
* 增量记账保证重复调用不双算,清零后重设即可净额回补。
|
|
41
|
+
*/
|
|
42
|
+
export declare function recordProfileShare(l: OccupancyLedger, chars: number, now?: number): void;
|
|
43
|
+
/** OFF 边界:稳定区随下一次请求组装物理离场,账目同边界清零(宁低勿高)。 */
|
|
44
|
+
export declare function clearProfileShare(l: OccupancyLedger, now?: number): void;
|
|
45
|
+
/**
|
|
46
|
+
* compaction 复位:v1 采用"清零重新累积"的低估近似——被压缩掉的注入以整数轮粒度
|
|
47
|
+
* 退出账本,幸存的摘要行不重复认领(已知限制,方向宁低勿高;事件级精确对齐留待 v2)。
|
|
48
|
+
*/
|
|
49
|
+
export declare function resetForCompaction(l: OccupancyLedger, now?: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* 官方上下文环周长:viewBox 14 内 r=5.5 圆(2π×5.5 ≈ 34.55751918948772),
|
|
52
|
+
* 与真机反推的官方 fill dasharray 分母逐字一致。
|
|
53
|
+
*/
|
|
54
|
+
export declare const CONTEXT_METER_CIRCUMFERENCE = 34.55751918948772;
|
|
55
|
+
/**
|
|
56
|
+
* 光晕弧 stroke-dasharray:形状沿用官方 fill(len + 全周长 gap)。
|
|
57
|
+
* @param occupancyRatio 占窗口比 0..1(越界钳制)。
|
|
58
|
+
* @param minLen 最小可见弧长(单位=-viewBox 坐标;14px 渲染下 <1 单位即亚像素不可见,
|
|
59
|
+
* 真实占比常低至 0.6%≈0.2 单位——指示灯语义需要"存在即可见",精确数字归面板)。
|
|
60
|
+
*/
|
|
61
|
+
export declare function haloDashArray(occupancyRatio: number, circumference?: number, minLen?: number): string;
|
|
62
|
+
/** 官方环触发按钮的结构签名(locale 无关定位依据;见组件设计 C1 锚点节)。 */
|
|
63
|
+
export interface ContextMeterAnchorSignature {
|
|
64
|
+
ariaHasPopup?: string | null;
|
|
65
|
+
viewBox?: string | null;
|
|
66
|
+
circleRadii?: ReadonlyArray<number>;
|
|
67
|
+
}
|
|
68
|
+
export declare function isContextMeterAnchor(sig: ContextMeterAnchorSignature): boolean;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文占用账本:插件注入内容在当前会话上下文窗口中的启发式 token 存量。
|
|
3
|
+
*
|
|
4
|
+
* 本模块是全部占用数字的**唯一算术来源**(host 记账与 client 渲染共用,esbuild
|
|
5
|
+
* 将其内联进 client bundle),任何一侧不得另写算法。口径与官方
|
|
6
|
+
* @deepseek-ai/dsh-token-meter/estimate 的固定密度启发式同式:
|
|
7
|
+
*
|
|
8
|
+
* - text 块 = ceil(chars / 4) + 4(块开销);
|
|
9
|
+
* - 整条 user 消息(召回注入体的形态)再叠加 role overhead 4;
|
|
10
|
+
* - 系统提示里的稳定区**子片**计入时不加任何 overhead——那 +8 是官方对整段
|
|
11
|
+
* 消息/整体系统提示的结构成本,切片重复计会与官方总数对不上;
|
|
12
|
+
* - 字符数一律 JS `.length`(UTF-16 单元),与官方产出严格可比。
|
|
13
|
+
*/
|
|
14
|
+
/** 固定文本密度:每 4 字符记 1 token(官方 CHARS_PER_TOKEN 同值)。 */
|
|
15
|
+
export const CHARS_PER_TOKEN = 4;
|
|
16
|
+
const BLOCK_OVERHEAD = 4;
|
|
17
|
+
const ROLE_OVERHEAD = 4;
|
|
18
|
+
/** 召回注入体(单 text 块 user message)的启发式 token 数。 */
|
|
19
|
+
export function estimateInjectedMessageTokens(chars) {
|
|
20
|
+
return Math.ceil(chars / CHARS_PER_TOKEN) + BLOCK_OVERHEAD + ROLE_OVERHEAD;
|
|
21
|
+
}
|
|
22
|
+
/** 稳定区子片的启发式 token 数(不加结构开销,见模块头注释)。 */
|
|
23
|
+
export function estimateStableSectionTokens(chars) {
|
|
24
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
25
|
+
}
|
|
26
|
+
export function emptyOccupancyLedger(now = Date.now()) {
|
|
27
|
+
return { stockTokens: 0, recallTokens: 0, profileTokens: 0, lastInjectTokens: 0, updatedAt: now };
|
|
28
|
+
}
|
|
29
|
+
/** 召回注入成功后的入账(失败跳过的轮次不调用——账目零扰动)。 */
|
|
30
|
+
export function recordRecallInjection(l, chars, now = Date.now()) {
|
|
31
|
+
const tokens = estimateInjectedMessageTokens(chars);
|
|
32
|
+
l.recallTokens += tokens;
|
|
33
|
+
l.stockTokens += tokens;
|
|
34
|
+
l.lastInjectTokens = tokens;
|
|
35
|
+
l.updatedAt = now;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 稳定区份额随每次实际组进的文本同步(含切回非 OFF 的重新计入):
|
|
39
|
+
* 增量记账保证重复调用不双算,清零后重设即可净额回补。
|
|
40
|
+
*/
|
|
41
|
+
export function recordProfileShare(l, chars, now = Date.now()) {
|
|
42
|
+
const tokens = estimateStableSectionTokens(chars);
|
|
43
|
+
l.stockTokens += tokens - l.profileTokens;
|
|
44
|
+
l.profileTokens = tokens;
|
|
45
|
+
l.updatedAt = now;
|
|
46
|
+
}
|
|
47
|
+
/** OFF 边界:稳定区随下一次请求组装物理离场,账目同边界清零(宁低勿高)。 */
|
|
48
|
+
export function clearProfileShare(l, now = Date.now()) {
|
|
49
|
+
l.stockTokens -= l.profileTokens;
|
|
50
|
+
l.profileTokens = 0;
|
|
51
|
+
l.updatedAt = now;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* compaction 复位:v1 采用"清零重新累积"的低估近似——被压缩掉的注入以整数轮粒度
|
|
55
|
+
* 退出账本,幸存的摘要行不重复认领(已知限制,方向宁低勿高;事件级精确对齐留待 v2)。
|
|
56
|
+
*/
|
|
57
|
+
export function resetForCompaction(l, now = Date.now()) {
|
|
58
|
+
l.stockTokens = 0;
|
|
59
|
+
l.recallTokens = 0;
|
|
60
|
+
l.profileTokens = 0;
|
|
61
|
+
l.lastInjectTokens = 0;
|
|
62
|
+
l.updatedAt = now;
|
|
63
|
+
}
|
|
64
|
+
/* ── 渲染侧纯函数(client 寄生组件用;宿主 smoke 一并断言,数字唯一来源在此) ── */
|
|
65
|
+
/**
|
|
66
|
+
* 官方上下文环周长:viewBox 14 内 r=5.5 圆(2π×5.5 ≈ 34.55751918948772),
|
|
67
|
+
* 与真机反推的官方 fill dasharray 分母逐字一致。
|
|
68
|
+
*/
|
|
69
|
+
export const CONTEXT_METER_CIRCUMFERENCE = 34.55751918948772;
|
|
70
|
+
/**
|
|
71
|
+
* 光晕弧 stroke-dasharray:形状沿用官方 fill(len + 全周长 gap)。
|
|
72
|
+
* @param occupancyRatio 占窗口比 0..1(越界钳制)。
|
|
73
|
+
* @param minLen 最小可见弧长(单位=-viewBox 坐标;14px 渲染下 <1 单位即亚像素不可见,
|
|
74
|
+
* 真实占比常低至 0.6%≈0.2 单位——指示灯语义需要"存在即可见",精确数字归面板)。
|
|
75
|
+
*/
|
|
76
|
+
export function haloDashArray(occupancyRatio, circumference = CONTEXT_METER_CIRCUMFERENCE, minLen = 0) {
|
|
77
|
+
const clamped = Number.isFinite(occupancyRatio) ? Math.min(1, Math.max(0, occupancyRatio)) : 0;
|
|
78
|
+
const len = Math.max(clamped * circumference, clamped > 0 ? minLen : 0);
|
|
79
|
+
return `${len} ${circumference}`;
|
|
80
|
+
}
|
|
81
|
+
/** 半径比对容差:吞掉渲染库对属性值的无害浮点噪声。 */
|
|
82
|
+
const RADIUS_EPSILON = 1e-6;
|
|
83
|
+
export function isContextMeterAnchor(sig) {
|
|
84
|
+
if (sig.ariaHasPopup !== 'dialog')
|
|
85
|
+
return false;
|
|
86
|
+
if (sig.viewBox !== '0 0 14 14')
|
|
87
|
+
return false;
|
|
88
|
+
const radii = sig.circleRadii;
|
|
89
|
+
if (!Array.isArray(radii) || radii.length !== 2)
|
|
90
|
+
return false;
|
|
91
|
+
return radii.every((r) => Math.abs(r - 5.5) < RADIUS_EPSILON);
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-layered-memory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
4
4
|
"description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
"cordis.patch.yml"
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
|
-
"build": "tsc -p tsconfig.json && node scripts/copy-client.mjs",
|
|
35
|
-
"smoke": "node dist-smoke/smoke.js",
|
|
36
|
-
"verify-catalog": "npm run build && node scripts/verify-catalog.mjs"
|
|
34
|
+
"build": "tsc -p tsconfig.json && node scripts/build-client.mjs && node scripts/copy-client.mjs",
|
|
35
|
+
"smoke": "npm run build:smoke && node dist-smoke/smoke.js",
|
|
36
|
+
"verify-catalog": "npm run build && node scripts/verify-catalog.mjs",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
38
|
+
"build:smoke": "tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution nodenext --target es2022 --strict --skipLibCheck --esModuleInterop"
|
|
37
39
|
},
|
|
38
40
|
"engines": {
|
|
39
41
|
"node": ">=22.16.0"
|
|
@@ -78,6 +80,8 @@
|
|
|
78
80
|
"@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
|
|
79
81
|
"@deepseek-ai/dsh-tools": "0.1.1-rc.2",
|
|
80
82
|
"@types/node": "^22.0.0",
|
|
83
|
+
"@types/react": "^19.2.18",
|
|
84
|
+
"esbuild": "^0.28.2",
|
|
81
85
|
"typescript": "^5.6.0"
|
|
82
86
|
}
|
|
83
87
|
}
|