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.
@@ -0,0 +1,173 @@
1
+ function emptyCostAggregate() {
2
+ return { calls: 0, inputChars: 0, outputTokens: 0, reasoningTokens: 0, avgOutputTokens: 0, medianOutputTokens: 0 };
3
+ }
4
+ /** 已排序序列的中位数(偶数个取中间两者平均;空返回 0)。 */
5
+ function medianOf(sorted) {
6
+ const n = sorted.length;
7
+ if (n === 0)
8
+ return 0;
9
+ const mid = Math.floor(n / 2);
10
+ return n % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
11
+ }
12
+ export class CostLedger {
13
+ db = null;
14
+ logger;
15
+ stmtInsert = null;
16
+ stmtDelete = null;
17
+ /** init 是否成功(未就绪 = 宿主库降级,方法全部返回零值不抛错)。 */
18
+ get ready() {
19
+ return this.stmtInsert !== null;
20
+ }
21
+ /** 建表 + 迁移 + 语句缓存(MemoryDb.initSchema 内调用;失败冒泡触发库级降级)。 */
22
+ init(db, logger) {
23
+ this.db = db;
24
+ this.logger = logger;
25
+ db.exec(`
26
+ CREATE TABLE IF NOT EXISTS token_cost (
27
+ ts INTEGER NOT NULL,
28
+ provider TEXT NOT NULL,
29
+ model TEXT NOT NULL,
30
+ layer TEXT NOT NULL,
31
+ input_chars INTEGER NOT NULL DEFAULT 0,
32
+ output_tokens INTEGER NOT NULL DEFAULT 0,
33
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0
34
+ )
35
+ `);
36
+ // 迁移:provider/model 复合键引入前的旧表补 provider 列(历史行回填 unknown)
37
+ const cols = db.prepare('PRAGMA table_info(token_cost)').all();
38
+ if (!cols.some((c) => c.name === 'provider')) {
39
+ db.exec("ALTER TABLE token_cost ADD COLUMN provider TEXT NOT NULL DEFAULT 'unknown'");
40
+ }
41
+ db.exec('CREATE INDEX IF NOT EXISTS idx_token_cost_ts ON token_cost(ts)');
42
+ this.stmtInsert = db.prepare('INSERT INTO token_cost (ts, provider, model, layer, input_chars, output_tokens, reasoning_tokens) VALUES (?, ?, ?, ?, ?, ?, ?)');
43
+ this.stmtDelete = db.prepare('DELETE FROM token_cost WHERE ts < ?');
44
+ }
45
+ /**
46
+ * 记录一次蒸馏调用成本(明细表,写入时按 retentionDays 滚动清理;0 = 永久保留)。
47
+ * 失败/成功都记(token 照烧);记账失败记 warn 但不阻断蒸馏(成本看板是增强能力)。
48
+ */
49
+ insertCostCall(provider, model, layer, inputChars, outputTokens, reasoningTokens, retentionDays) {
50
+ if (!this.db || !this.stmtInsert || !this.stmtDelete)
51
+ return;
52
+ try {
53
+ this.stmtInsert.run(Date.now(), provider, model, layer, Math.max(0, Math.round(inputChars)), Math.max(0, Math.round(outputTokens)), Math.max(0, Math.round(reasoningTokens)));
54
+ if (retentionDays > 0)
55
+ this.stmtDelete.run(Date.now() - retentionDays * 24 * 3600_000);
56
+ }
57
+ catch (err) {
58
+ this.logger?.warn(`[memory][sqlite] token_cost 记账失败: ${err instanceof Error ? err.message : String(err)}`);
59
+ }
60
+ }
61
+ /**
62
+ * 查询 token_cost 单窗口聚合(成本看板用;since 为毫秒起点,0 = 全量)。
63
+ * 输入口径:inputChars 是字符(llm 流拿不到输入 token,沿用 llm-usage 的字符折算口径)。
64
+ * median 需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median 函数)。
65
+ */
66
+ aggregateCost(since) {
67
+ if (!this.ready || !this.db)
68
+ return { total: emptyCostAggregate(), byModel: [] };
69
+ try {
70
+ const total = this.db
71
+ .prepare(`SELECT COUNT(*) AS calls,
72
+ COALESCE(SUM(input_chars), 0) AS inputChars,
73
+ COALESCE(SUM(output_tokens), 0) AS outputTokens,
74
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
75
+ COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
76
+ FROM token_cost WHERE ts >= ?`)
77
+ .get(since);
78
+ const tokenRows = this.db
79
+ .prepare('SELECT output_tokens FROM token_cost WHERE ts >= ? ORDER BY output_tokens')
80
+ .all(since);
81
+ const byModel = this.db
82
+ .prepare(`SELECT provider, model, COUNT(*) AS calls,
83
+ COALESCE(SUM(input_chars), 0) AS inputChars,
84
+ COALESCE(SUM(output_tokens), 0) AS outputTokens,
85
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
86
+ FROM token_cost WHERE ts >= ? GROUP BY provider, model ORDER BY outputTokens DESC`)
87
+ .all(since);
88
+ return {
89
+ total: {
90
+ calls: total.calls,
91
+ inputChars: total.inputChars,
92
+ outputTokens: total.outputTokens,
93
+ reasoningTokens: total.reasoningTokens,
94
+ avgOutputTokens: total.avgOutputTokens,
95
+ medianOutputTokens: medianOf(tokenRows.map((r) => r.output_tokens)),
96
+ },
97
+ byModel,
98
+ };
99
+ }
100
+ catch {
101
+ return { total: emptyCostAggregate(), byModel: [] };
102
+ }
103
+ }
104
+ /**
105
+ * 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
106
+ * 降级/异常返回空数组,不抛错。
107
+ */
108
+ aggregateCostByLayer(since) {
109
+ if (!this.ready || !this.db)
110
+ return [];
111
+ try {
112
+ const rows = this.db
113
+ .prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
114
+ COUNT(*) AS calls,
115
+ COALESCE(SUM(input_chars), 0) AS inputChars,
116
+ COALESCE(SUM(output_tokens), 0) AS outputTokens,
117
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
118
+ COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
119
+ FROM token_cost WHERE ts >= ? GROUP BY layer ORDER BY layer`)
120
+ .all(since);
121
+ // 中位数需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median)
122
+ const tokenRows = this.db
123
+ .prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
124
+ output_tokens
125
+ FROM token_cost WHERE ts >= ? ORDER BY layer, output_tokens`)
126
+ .all(since);
127
+ const medianByLayer = new Map();
128
+ for (const r of tokenRows) {
129
+ const arr = medianByLayer.get(r.layer);
130
+ if (arr)
131
+ arr.push(r.output_tokens);
132
+ else
133
+ medianByLayer.set(r.layer, [r.output_tokens]);
134
+ }
135
+ return rows.map((r) => ({
136
+ ...r,
137
+ medianOutputTokens: medianOf(medianByLayer.get(r.layer) ?? []),
138
+ }));
139
+ }
140
+ catch {
141
+ return [];
142
+ }
143
+ }
144
+ /**
145
+ * 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
146
+ * offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
147
+ * 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
148
+ */
149
+ aggregateByBucket(bucketMs, offsetMs, since, layer) {
150
+ if (!this.ready || !this.db)
151
+ return [];
152
+ try {
153
+ let sql = `SELECT CAST((ts + ?) / ? AS INTEGER) AS bucket, provider, model,
154
+ COUNT(*) AS calls,
155
+ COALESCE(SUM(output_tokens), 0) AS outputTokens,
156
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
157
+ FROM token_cost WHERE ts >= ?`;
158
+ const params = [offsetMs, bucketMs, since];
159
+ if (layer === 'l1') {
160
+ sql += ` AND layer IN ('l1-extract','l1-dedup')`;
161
+ }
162
+ else if (layer) {
163
+ sql += ` AND layer = ?`;
164
+ params.push(layer);
165
+ }
166
+ sql += ` GROUP BY bucket, provider, model ORDER BY bucket, provider, model`;
167
+ return this.db.prepare(sql).all(...params);
168
+ }
169
+ catch {
170
+ return [];
171
+ }
172
+ }
173
+ }
@@ -1,24 +1,7 @@
1
1
  import type { MemoryLogger } from '../types.js';
2
2
  import { type CatalogEntry } from './model-catalog.js';
3
- export type DownloadPhase = 'downloading' | 'verifying' | 'done' | 'cancelled' | 'error';
4
- export interface DownloadProgress {
5
- modelId: string;
6
- phase: DownloadPhase;
7
- /** 当前文件序号(1-based)。 */
8
- fileIndex: number;
9
- fileCount: number;
10
- /** 当前文件已收字节(含续传基线)。 */
11
- fileReceived: number;
12
- fileTotal: number;
13
- /** 整模型累计字节(含已完成文件;分母 = 目录总大小)。 */
14
- overallReceived: number;
15
- overallTotal: number;
16
- /** EMA 平滑速度(字节/秒)。 */
17
- speedBps: number;
18
- startedAt: number;
19
- /** phase=error 时的原因。 */
20
- error?: string;
21
- }
3
+ import type { DownloadProgress } from '../contract.js';
4
+ export type { DownloadPhase, DownloadProgress } from '../contract.js';
22
5
  export interface ModelStatus {
23
6
  id: string;
24
7
  /** none=未下载;partial=有断点/不完整;downloaded=全部文件就位且尺寸吻合。 */
@@ -104,4 +87,3 @@ export declare function resolveProxyUrl(setting: string | undefined, host: strin
104
87
  /** 代理 URL 日志脱敏:剥掉 userinfo(内网代理常带 user:pass 凭据),只留 scheme//host;
105
88
  * 解析失败的串原样也可能是凭据形态,返回占位符。 */
106
89
  export declare function maskProxyUrl(proxy: string): string;
107
- export {};
@@ -7,7 +7,8 @@ import { LocalEmbeddingService } from './local-embedding.js';
7
7
  import { ModelDownloadQueue } from './download-queue.js';
8
8
  import { RuntimeInstaller } from './runtime-installer.js';
9
9
  import type { MemoryDb } from './sqlite.js';
10
- export type EmbeddingSourceKind = 'remote' | 'local' | 'off';
10
+ import type { EmbeddingSourceKind, EmbeddingStateView } from '../contract.js';
11
+ export type { ApplyPhase, EmbeddingSourceKind, EmbeddingStateView, ReindexProgressState } from '../contract.js';
11
12
  export interface EmbeddingSourceState {
12
13
  source: EmbeddingSourceKind;
13
14
  /** source=local 时启用的目录模型 id。 */
@@ -38,17 +39,6 @@ export declare function resolveInitialEmbedding(cfg: MemoryConfig, sourceStore:
38
39
  /** 本地服务构造工厂(index.ts 的初始解析与 Manager 共用一份实现,防漂移)。
39
40
  * 推理在 worker 线程(见 local-embedding.ts);此处只传 runtime 目录与模型目录。 */
40
41
  export declare function makeLocalServiceFactory(installer: RuntimeInstaller, downloader: ModelDownloadQueue, logger?: MemoryLogger, maxInputChars?: number): (modelId: string) => LocalEmbeddingService | null;
41
- export type ApplyPhase = 'idle' | 'installing-runtime' | 'warming' | 'switching' | 'reindexing' | 'done' | 'error';
42
- export interface ReindexProgressState {
43
- running: boolean;
44
- l1Done: number;
45
- l1Total: number;
46
- l0Done: number;
47
- l0Total: number;
48
- startedAt: number;
49
- cancelled: boolean;
50
- error?: string;
51
- }
52
42
  export interface EmbeddingManagerDeps {
53
43
  dataDir: string;
54
44
  cfg: MemoryConfig;
@@ -119,43 +109,3 @@ export declare class EmbeddingManager {
119
109
  /** RPC 快照(设置页嵌入区块数据源;client 忙时 1s 轮询)。 */
120
110
  snapshot(): Promise<EmbeddingStateView>;
121
111
  }
122
- export interface EmbeddingStateView {
123
- source: EmbeddingSourceKind;
124
- activeModel: string | null;
125
- ceilings: {
126
- remote: boolean;
127
- local: boolean;
128
- };
129
- runtime: {
130
- targetVersion: string;
131
- phase: 'idle' | 'installing' | 'ready' | 'error' | 'cancelled';
132
- installedVersion: string | null;
133
- elapsedMs: number;
134
- lastLines: string[];
135
- error?: string;
136
- };
137
- models: Array<{
138
- id: string;
139
- name: string;
140
- dims: number;
141
- contextTokens: number;
142
- tags: string[];
143
- description: string;
144
- totalBytes: number;
145
- bytesOnDisk: number;
146
- state: 'none' | 'partial' | 'downloaded';
147
- }>;
148
- download: ReturnType<ModelDownloadQueue['getProgress']>;
149
- apply: {
150
- phase: ApplyPhase;
151
- message: string;
152
- startedAt: number;
153
- busy: boolean;
154
- };
155
- local: {
156
- state: 'idle' | 'loading' | 'ready' | 'failed' | 'terminated';
157
- error: string | null;
158
- } | null;
159
- reindex: ReindexProgressState;
160
- activeNote?: string;
161
- }
@@ -0,0 +1,30 @@
1
+ import type { MemoryLogger } from '../types.js';
2
+ import type { OccupancyLedger } from '../util/context-occupancy.js';
3
+ /** 会话条目上限(按 updatedAt 淘汰最旧;防文件无限增长)。 */
4
+ export declare const OCCUPANCY_SESSION_CAP = 200;
5
+ export declare class OccupancyStore {
6
+ private readonly logger?;
7
+ private readonly file;
8
+ private readonly entries;
9
+ private persistFailed;
10
+ /** 串行化持久化写(避免并发原子写撞临时文件名);init 链最前(先载入再落盘,防丢更新)。 */
11
+ private writeChain;
12
+ constructor(dataDir: string, logger?: MemoryLogger | undefined);
13
+ /** 载入持久化账目(合并进内存——构造与载入之间发生的 save 不丢);失败降级内存态。 */
14
+ private init;
15
+ /**
16
+ * 该会话的持久化账目(热路径同步读;从未注入/已复位返回 null)。
17
+ * 返回**浅拷贝**——调用方(ledgerFor 复生)会在其上做迁移;若交出内部引用,
18
+ * 原地修改会让 save() 的 prev/ledger 恒等比较误判“数值未变”而跳过写穿(实测踩坑)。
19
+ */
20
+ load(sessionId: string): OccupancyLedger | null;
21
+ /**
22
+ * 迁移后写穿。数值未变只刷新内存时间戳不落盘(profile 稳定区每次请求组装都
23
+ * 触发迁移,但内容不变时不应产生文件写);stock 归零即删除条目。
24
+ */
25
+ save(sessionId: string, ledger: OccupancyLedger): void;
26
+ /** 等待在途持久化写完成(测试/停机用)。 */
27
+ flush(): Promise<void>;
28
+ private persist;
29
+ private serialize;
30
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 记忆占用流水存储:sessionId → OccupancyLedger 的持久化映射。
3
+ *
4
+ * 动机(2026-08-27 票07):占用账本是进程内存态,dsh 重启后历史会话"窗口里明明
5
+ * 有记忆、账面却为零",指示器对重启前注入的存量失明。本存储让账目跨进程复生:
6
+ * - 每次账本迁移后写穿(recall-dedupe 同款:串行化原子写 + 失败降级内存态);
7
+ * - 查询热路径仍是同步内存读——文件只在迁移时刻与启动载入时触碰;
8
+ * - stock 归零的条目不落盘(compaction 复位即天然删除);
9
+ * - agent 销毁不删记录(持久化语义与召回去重一致:会话恢复后账目继续);
10
+ * - 90 天未更新过期清理 + 会话条目上限(session-modes/dedupe 同款量级)。
11
+ */
12
+ import * as path from 'node:path';
13
+ import { errDetail } from '../util/filelog.js';
14
+ import { atomicWriteJson, ensureDir, readJsonIfExists } from './io.js';
15
+ /** 会话条目上限(按 updatedAt 淘汰最旧;防文件无限增长)。 */
16
+ export const OCCUPANCY_SESSION_CAP = 200;
17
+ /** 条目过期清理(90 天未更新即丢弃,与 session-modes/dedupe 同款量级)。 */
18
+ const PRUNE_MS = 90 * 24 * 3600_000;
19
+ export class OccupancyStore {
20
+ logger;
21
+ file;
22
+ entries = new Map();
23
+ persistFailed = false;
24
+ /** 串行化持久化写(避免并发原子写撞临时文件名);init 链最前(先载入再落盘,防丢更新)。 */
25
+ writeChain;
26
+ constructor(dataDir, logger) {
27
+ this.logger = logger;
28
+ this.file = path.join(dataDir, 'occupancy.json');
29
+ this.writeChain = this.init();
30
+ }
31
+ /** 载入持久化账目(合并进内存——构造与载入之间发生的 save 不丢);失败降级内存态。 */
32
+ async init() {
33
+ const data = await readJsonIfExists(this.file);
34
+ if (!data?.sessions || typeof data.sessions !== 'object')
35
+ return;
36
+ const now = Date.now();
37
+ let count = 0;
38
+ for (const [sid, entry] of Object.entries(data.sessions)) {
39
+ if (typeof entry?.stockTokens !== 'number')
40
+ continue;
41
+ if (now - (entry.updatedAt ?? 0) > PRUNE_MS)
42
+ continue;
43
+ const existing = this.entries.get(sid);
44
+ if (existing) {
45
+ if (entry.updatedAt > existing.updatedAt)
46
+ this.entries.set(sid, { ...entry });
47
+ }
48
+ else {
49
+ this.entries.set(sid, { ...entry });
50
+ }
51
+ count++;
52
+ }
53
+ if (count > 0)
54
+ this.logger?.info(`[memory] 记忆占用账目载入 ${count} 个会话`);
55
+ }
56
+ /**
57
+ * 该会话的持久化账目(热路径同步读;从未注入/已复位返回 null)。
58
+ * 返回**浅拷贝**——调用方(ledgerFor 复生)会在其上做迁移;若交出内部引用,
59
+ * 原地修改会让 save() 的 prev/ledger 恒等比较误判“数值未变”而跳过写穿(实测踩坑)。
60
+ */
61
+ load(sessionId) {
62
+ const e = this.entries.get(sessionId);
63
+ return e ? { ...e } : null;
64
+ }
65
+ /**
66
+ * 迁移后写穿。数值未变只刷新内存时间戳不落盘(profile 稳定区每次请求组装都
67
+ * 触发迁移,但内容不变时不应产生文件写);stock 归零即删除条目。
68
+ */
69
+ save(sessionId, ledger) {
70
+ const prev = this.entries.get(sessionId);
71
+ const sameNumbers = prev !== undefined &&
72
+ prev.stockTokens === ledger.stockTokens &&
73
+ prev.recallTokens === ledger.recallTokens &&
74
+ prev.profileTokens === ledger.profileTokens &&
75
+ prev.lastInjectTokens === ledger.lastInjectTokens;
76
+ if (sameNumbers) {
77
+ prev.updatedAt = ledger.updatedAt;
78
+ return;
79
+ }
80
+ if (ledger.stockTokens <= 0) {
81
+ if (prev === undefined)
82
+ return;
83
+ this.entries.delete(sessionId);
84
+ }
85
+ else {
86
+ this.entries.set(sessionId, { ...ledger });
87
+ }
88
+ this.writeChain = this.writeChain.then(() => this.persist());
89
+ }
90
+ /** 等待在途持久化写完成(测试/停机用)。 */
91
+ flush() {
92
+ return this.writeChain;
93
+ }
94
+ async persist() {
95
+ try {
96
+ await ensureDir(path.dirname(this.file));
97
+ await atomicWriteJson(this.file, this.serialize());
98
+ this.persistFailed = false;
99
+ }
100
+ catch (err) {
101
+ if (!this.persistFailed) {
102
+ this.persistFailed = true;
103
+ this.logger?.warn(`[memory] 记忆占用持久化失败(降级内存态): ${errDetail(err)}`);
104
+ }
105
+ }
106
+ }
107
+ serialize() {
108
+ const now = Date.now();
109
+ for (const [sid, e] of this.entries) {
110
+ if (now - e.updatedAt > PRUNE_MS && e.updatedAt > 0)
111
+ this.entries.delete(sid);
112
+ }
113
+ while (this.entries.size > OCCUPANCY_SESSION_CAP) {
114
+ let oldest;
115
+ let oldestAt = Infinity;
116
+ for (const [sid, e] of this.entries) {
117
+ if (e.updatedAt > 0 && e.updatedAt < oldestAt) {
118
+ oldest = sid;
119
+ oldestAt = e.updatedAt;
120
+ }
121
+ }
122
+ if (oldest === undefined)
123
+ break;
124
+ this.entries.delete(oldest);
125
+ }
126
+ const sessions = {};
127
+ for (const [sid, e] of this.entries) {
128
+ if (e.stockTokens <= 0)
129
+ continue; // 归零条目不落盘
130
+ sessions[sid] = { ...e };
131
+ }
132
+ return { version: 1, sessions };
133
+ }
134
+ }
@@ -1,19 +1,8 @@
1
1
  import type { MemoryLogger } from '../types.js';
2
2
  /** 钉死的 transformers.js 版本(D8:精确版本,升级插件时在此变更并重装运行时)。 */
3
3
  export declare const PINNED_TRANSFORMERS_VERSION = "4.2.0";
4
- export interface RuntimeProgress {
5
- phase: 'idle' | 'installing' | 'ready' | 'error' | 'cancelled';
6
- /** 插件钉死的目标版本。 */
7
- targetVersion: string;
8
- /** runtime 里实际就位的版本(未安装为 null)。 */
9
- installedVersion: string | null;
10
- startedAt: number;
11
- /** 已运行毫秒(读时计算)。 */
12
- elapsedMs: number;
13
- /** npm 输出尾行(最近 5 行,让用户看到 npm 在干什么)。 */
14
- lastLines: string[];
15
- error?: string;
16
- }
4
+ import type { RuntimeProgress } from '../contract.js';
5
+ export type { RuntimeProgress } from '../contract.js';
17
6
  /** 子进程抽象(测试注入缝):注册输出行回调 + 终止 + 终态。 */
18
7
  export interface SpawnedProcess {
19
8
  onStdout(cb: (line: string) => void): void;
@@ -9,45 +9,10 @@ export interface StoreCapabilities {
9
9
  ftsSearch: boolean;
10
10
  vectorSearch: boolean;
11
11
  }
12
- /** token_cost 单窗口成本聚合(成本看板用)。 */
13
- export interface CostAggregate {
14
- calls: number;
15
- inputChars: number;
16
- outputTokens: number;
17
- reasoningTokens: number;
18
- /** 单次调用输出 token 均值(无数据为 0)。 */
19
- avgOutputTokens: number;
20
- /** 单次调用输出 token 中位数(无数据为 0)。 */
21
- medianOutputTokens: number;
22
- }
23
- /** 按 model 分组的成本行(成本看板用)。 */
24
- export interface CostByModel {
25
- provider: string;
26
- model: string;
27
- calls: number;
28
- inputChars: number;
29
- outputTokens: number;
30
- reasoningTokens: number;
31
- }
32
- /** 按层级(l1/l2/l3 归并)分组的成本行。 */
33
- export interface CostByLayer {
34
- layer: string;
35
- calls: number;
36
- inputChars: number;
37
- outputTokens: number;
38
- reasoningTokens: number;
39
- avgOutputTokens: number;
40
- medianOutputTokens: number;
41
- }
42
- /** 按时间桶 + provider/model 聚合的扁平行(趋势图与日均/周均/月均 + 中位数统计共用)。 */
43
- export interface BucketRow {
44
- bucket: number;
45
- provider: string;
46
- model: string;
47
- calls: number;
48
- outputTokens: number;
49
- reasoningTokens: number;
50
- }
12
+ import { CostLedger } from './cost-ledger.js';
13
+ import type { BucketRow, CostAggregate, CostByLayer } from './cost-ledger.js';
14
+ export type { BucketRow, CostAggregate, CostByLayer } from './cost-ledger.js';
15
+ import type { CostByModel } from '../contract.js';
51
16
  /** L1 检索命中(含 BM25/余弦归一分数)。 */
52
17
  export interface L1SearchHit {
53
18
  id: string;
@@ -62,6 +27,7 @@ export interface L1SearchHit {
62
27
  export interface L0SearchHit extends L0MessageRecord {
63
28
  score: number;
64
29
  }
30
+ /** 已排序序列的中位数(偶数个取中间两者平均;空返回 0)。 */
65
31
  export declare class MemoryDb {
66
32
  private db;
67
33
  private degraded;
@@ -83,9 +49,8 @@ export declare class MemoryDb {
83
49
  private stmtL1FtsDelete;
84
50
  private stmtL1FtsSearch;
85
51
  private stmtL1FtsSearchFamily;
86
- /** token_cost 明细写入 / 滚动清理语句(构造期 prepare 缓存)。 */
87
- private stmtInsertCost;
88
- private stmtDeleteCost;
52
+ /** 成本账本(token_cost 表族;init 内初始化,未就绪时方法返回零值)。 */
53
+ readonly costLedger: CostLedger;
89
54
  private stmtUpsertL0;
90
55
  private stmtGetL0;
91
56
  /** 主表存在性点查(同 L1:防御性 FTS 删除的前置判断)。 */
@@ -187,31 +152,16 @@ export declare class MemoryDb {
187
152
  searchL1Vector(embedding: Float32Array, topK: number, family?: string): L1SearchHit[];
188
153
  /** 批量 upsert L0 消息(元数据 + FTS;embeddings 与 records 等长,可省略)。 */
189
154
  upsertL0Batch(records: L0MessageRecord[], embeddings?: Array<Float32Array | undefined>): boolean;
190
- /**
191
- * 记录一次蒸馏调用成本(明细表,写入时按 retentionDays 滚动清理;0 = 永久保留)。
192
- * 失败/成功都记(token 照烧);记账失败记 warn 但不阻断蒸馏(成本看板是增强能力)。
193
- */
155
+ /** 记录一次蒸馏调用成本(委托 cost-ledger;语义见 CostLedger.insertCostCall)。 */
194
156
  insertCostCall(provider: string, model: string, layer: string, inputChars: number, outputTokens: number, reasoningTokens: number, retentionDays: number): void;
195
- /**
196
- * 查询 token_cost 单窗口聚合(成本看板用;since 为毫秒起点,0 = 全量)。
197
- * 输入口径:inputChars 是字符(llm 流拿不到输入 token,沿用 llm-usage 的字符折算口径)。
198
- * 成本看板是增强能力:降级态/查询异常一律返回零值,不向上抛错。
199
- * median 需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median 函数)。
200
- */
157
+ /** 查询 token_cost 单窗口聚合(委托 cost-ledger;降级/异常返回零值)。 */
201
158
  aggregateCost(since: number): {
202
159
  total: CostAggregate;
203
160
  byModel: CostByModel[];
204
161
  };
205
- /**
206
- * 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
207
- * 降级/异常返回空数组,不抛错。
208
- */
162
+ /** 按层级归并聚合(委托 cost-ledger;降级/异常返回空数组)。 */
209
163
  aggregateCostByLayer(since: number): CostByLayer[];
210
- /**
211
- * 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
212
- * offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
213
- * 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
214
- */
164
+ /** 按时间桶 + model 聚合(委托 cost-ledger;趋势图与日均/周均/月均共用)。 */
215
165
  aggregateByBucket(bucketMs: number, offsetMs: number, since: number, layer: string): BucketRow[];
216
166
  countL0(): number;
217
167
  /** 统计 recorded_at >= iso 的消息数(状态面板"今日捕获"用)。 */