dsh-reference-session-collector 0.0.0-stage → 0.1.0

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/lib/index.d.ts ADDED
@@ -0,0 +1,668 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ import { IncomingMessage, ServerResponse } from "node:http";
4
+ //#region src/session-file-decode.d.ts
5
+ /** 日志的物理编码(对齐宿主的 JsonlCompression 概念)。 */
6
+ type LogCompression = 'zstd' | 'none';
7
+ /** 一份日志产物在磁盘上的事实。 */
8
+ interface LogArtifact {
9
+ /** 文件名(不含目录)。 */
10
+ name: string;
11
+ /** 绝对路径。 */
12
+ path: string;
13
+ bytes: number;
14
+ /** mtime(epoch ms)。 */
15
+ mtimeMs: number;
16
+ }
17
+ /** 一份规范代次产物。 */
18
+ interface GenerationArtifact extends LogArtifact {
19
+ /** 该文件承载的格式代次;0 为无版本号的历史代。 */
20
+ version: number;
21
+ /** 该文件的物理编码。 */
22
+ compression: LogCompression;
23
+ }
24
+ /** 一个会话目录的代次事实。 */
25
+ interface SessionGenerations {
26
+ /** 目录内全部规范产物,按代次升序(同代次再按 mtime 升序)。 */
27
+ canonical: GenerationArtifact[];
28
+ /** 当前代 = 规范产物中代次最高的一份;目录内没有规范产物时为 undefined。 */
29
+ current?: GenerationArtifact;
30
+ /** 历史代(代次 0);宿主按契约保留已提交的旧代次,不删不改。 */
31
+ legacy?: GenerationArtifact;
32
+ /** 迁移暂存残留(发布源)。 */
33
+ staging: LogArtifact[];
34
+ /** 只有历史代、尚无更高代次。 */
35
+ legacyOnly: boolean;
36
+ }
37
+ /**
38
+ * 某代次的规范日志文件名。
39
+ *
40
+ * 注意:历史代的名字必须**拼接构造**(`'.'` 归入分支串),源码文本中不得
41
+ * 出现检查器正则的连续字面量(见文件头 D7 纪律)。
42
+ * @param version - 非负安全整数的格式代次。
43
+ * @param compression - 物理编码(缺省 zstd)。
44
+ * @returns 该代次在会话目录内的文件名。
45
+ */
46
+ declare function generationLogFilename(version: number, compression?: LogCompression): string;
47
+ /**
48
+ * 读取一个会话目录的代次事实(只读,不碰任何文件内容)。
49
+ * 目录在枚举与读取之间消失(宿主正在删会话)时如实返回「没有产物」,不抛。
50
+ * @param dir - 会话目录的绝对路径。
51
+ * @returns 规范产物(按代次升序)、当前代、历史代与暂存残留。
52
+ */
53
+ declare function readSessionGenerations(dir: string): SessionGenerations;
54
+ /** 解码后的事件(磁盘存储记录的结构子集;与宿主 SessionEvent 形状兼容但不 import 其类型,保持叶子零依赖)。 */
55
+ interface DecodedEvent {
56
+ type: string;
57
+ seq: number;
58
+ time: number;
59
+ data?: Record<string, unknown>;
60
+ surfaceOp?: 'append' | {
61
+ op: 'replace';
62
+ start: number;
63
+ end: number;
64
+ };
65
+ sourceEventSeqs?: number[];
66
+ }
67
+ /** 一次读取的统计与问题清单。 */
68
+ interface SessionLogRead {
69
+ events: DecodedEvent[];
70
+ /** 完整帧数。 */
71
+ frames: number;
72
+ /** 撕裂尾帧起始偏移(无撕裂为 undefined)。 */
73
+ tornStart?: number;
74
+ /** 从撕裂尾帧抢救出的明文字节数。 */
75
+ recoveredFromTorn: number;
76
+ /** 读取问题(解码失败/seq 缺口),按遇到顺序。 */
77
+ issues: {
78
+ line: number;
79
+ why: string;
80
+ }[];
81
+ /** 实际使用的解码器来源。 */
82
+ decoder: 'official' | 'local';
83
+ /** 尾部是否仍有未收尾的 open step。 */
84
+ openStep?: {
85
+ turn: number;
86
+ step: number;
87
+ };
88
+ /** 日志首行的 header(`type: 'session'` 记录);首行不是 JSON 时为 undefined。 */
89
+ header?: Record<string, unknown>;
90
+ }
91
+ /** 官方解码器面(软加载所得)。 */
92
+ interface OfficialDecoders {
93
+ decodeStorageRecord(value: unknown): DecodedEvent[];
94
+ decodeSeqRanges(value: unknown, seq: number): number[];
95
+ }
96
+ /**
97
+ * 解码一个会话日志(zstd 帧扫描 + 撕裂尾帧抢救 + 解包 + seq 连续性)。
98
+ * @param bytes - 文件内容(调用方读取,便于测试注入与 mtime/size 只读断言)。
99
+ * @param decoders - 可选官方解码器(缺省用本地等价实现;差异记入 decoder 字段,禁止静默回退)。
100
+ * @returns 事件、统计与问题清单。
101
+ */
102
+ declare function decodeSessionLogBytes(bytes: Buffer, decoders?: OfficialDecoders): SessionLogRead;
103
+ /** 从文件读取并解码(只读)。 */
104
+ declare function decodeSessionLogFile(file: string, decoders?: OfficialDecoders): SessionLogRead;
105
+ /** header 代次分类结果:`known` 可回填;其余调用方必须跳过该会话(fail-soft)。 */
106
+ type SessionHeaderVerdict = {
107
+ readonly state: 'known';
108
+ readonly version: number;
109
+ } | {
110
+ readonly state: 'missing-header';
111
+ } | {
112
+ readonly state: 'not-session';
113
+ } | {
114
+ readonly state: 'unknown-version';
115
+ readonly version: unknown;
116
+ };
117
+ /**
118
+ * 从解码结果的 header 判定格式代次是否可回填。
119
+ *
120
+ * 本模块只分类不告警——「跳过该会话 + 恰一条 warn」的裁决属于调用方
121
+ * (stats-backfill)的责任面,保证叶子模块无副作用。
122
+ * @param header - decodeSessionLog 结果里的 header(首行不是 JSON 时 undefined)。
123
+ * @param known - 已知代次集合(缺省 {@link KNOWN_SESSION_FORMAT_GENERATIONS})。
124
+ */
125
+ declare function classifySessionHeader(header: Record<string, unknown> | undefined, known?: ReadonlySet<number>): SessionHeaderVerdict;
126
+ //#endregion
127
+ //#region src/projection.d.ts
128
+ /** 精确计数器(tokenizer.countText 的数值臂);缺席时 chars/4 上界。 */
129
+ type ExactCounter = (text: string) => number;
130
+ /** segments 表一行(snake_case 列名映射在 schema.ts 完成)。 */
131
+ interface ProjectionSegment {
132
+ seq: number;
133
+ turn: number;
134
+ step: number;
135
+ kind: string;
136
+ role: string;
137
+ /** 事件 epoch ms。 */
138
+ time: number;
139
+ /** 模型可见文本(CCI 视界)。 */
140
+ text: string;
141
+ tokens: number;
142
+ /** 检索视界(steward 抽取超集)。 */
143
+ searchText: string;
144
+ /** 1=当前表面;0=被某 replace 区间覆盖。 */
145
+ onSurface: 0 | 1;
146
+ /** 1=本行被替换对覆盖(compressed 标记,R3)。 */
147
+ compressed: 0 | 1;
148
+ /** 当前表面上是什么(链解析后的终替文本);未压缩=null。 */
149
+ surfaceText: string | null;
150
+ surfaceTokens: number | null;
151
+ /** 被谁替换(链解析后的终替事件 seq);未压缩=null。 */
152
+ replacedBy: number | null;
153
+ }
154
+ /** replaces 表一行(R4 替换对指针)。 */
155
+ interface ProjectionReplace {
156
+ seq: number;
157
+ startSeq: number;
158
+ endSeq: number;
159
+ aTokens: number;
160
+ bTokens: number;
161
+ kind: 'compress' | 'recall';
162
+ turn: number;
163
+ step: number;
164
+ /**
165
+ * 1=替身行仍在面(delta 生效);0=替身已被后续 checkpoint/replace 再覆盖,
166
+ * delta 清除不入 gross(用户定标 2026-10-20:checkpoint 只影响其后的积分,
167
+ * 被它吸收的先前对不再重复计费——与 CCI projectSavingsCore 「只算仍在
168
+ * surface 中的替换节点」同语义)。per-pair a/b 保持原始区间事实不回改。
169
+ */
170
+ live: 0 | 1;
171
+ }
172
+ /** sessions 表一行的投影侧字段(watermark/archived 由 sync 侧补写)。 */
173
+ interface SessionSummary {
174
+ sessionId: string;
175
+ dirId: string;
176
+ title: string;
177
+ cwd: string;
178
+ eventCount: number;
179
+ maxSeq: number;
180
+ turnCount: number;
181
+ segmentTokens: number;
182
+ surfaceTokens: number;
183
+ grossSaved: number;
184
+ recallTokens: number;
185
+ compressPairs: number;
186
+ recallPairs: number;
187
+ basis: 'ds-token' | 'chars/4';
188
+ lastTimeMs: number;
189
+ /** 文件头自声明格式代次(0..4 放行;-1=无 header 版本,理论上不至)。 */
190
+ formatGeneration: number;
191
+ }
192
+ /** 一次完整投影结果。 */
193
+ interface SessionProjection {
194
+ summary: SessionSummary;
195
+ segments: ProjectionSegment[];
196
+ replaces: ProjectionReplace[];
197
+ }
198
+ /** projectSessionEvents 选项。 */
199
+ interface ProjectionOptions {
200
+ dirId: string;
201
+ /** 缺省=dirId(宿主目录名即会话 id 的代次布局)。 */
202
+ sessionId?: string;
203
+ /** 日志首行 header(decodeSessionLogBytes 产出),取 cwd/title 元数据。 */
204
+ header?: Record<string, unknown>;
205
+ /** 精确计数器;缺省 chars/4 降级并如实标注 basis。 */
206
+ exactCounter?: ExactCounter;
207
+ /** 预置标题(steward 式从事件流抽出的最后一帧标题)。 */
208
+ title?: string;
209
+ }
210
+ /**
211
+ * 检索视界——steward `extractSessionEventText`(extract.ts:161-199)逐字同构。
212
+ * @param event - 解码后的一个会话事件。
213
+ * @returns 换行拼接的检索文本;不可检索事件返回空串。
214
+ */
215
+ declare function extractSearchEventText(event: DecodedEvent): string;
216
+ /**
217
+ * 从事件流抽最后一帧标题——steward `extractTitleFromEvents`(extract.ts:28-43)
218
+ * 同构(形状三兼容:`{title}`、`{title:{title}}`、`{title:{val}}`)。
219
+ */
220
+ declare function extractTitleFromEvents(events: readonly DecodedEvent[]): string;
221
+ /**
222
+ * 投影一个会话的全部解码事件(两遍扫描)。
223
+ * @param events - decodeSessionLogBytes 产出的连续事件流(含 header 后的全部行)。
224
+ * @param options - 见 {@link ProjectionOptions}。
225
+ * @returns summary + segments(text/search_text 任一非空才收)+ replaces(R4)。
226
+ */
227
+ declare function projectSessionEvents(events: readonly DecodedEvent[], options: ProjectionOptions): SessionProjection;
228
+ //#endregion
229
+ //#region src/schema.d.ts
230
+ /** schema 版本(meta 表;断裂式迁移的比对基线)。
231
+ * v2=加 sessions.format_generation;
232
+ * v3=search_text 视界变更(剥离宿主注入样板段,strip-injected horizon v2);
233
+ * v4=账目双修(真机取证 2026-10-20):① text/tokens 视界补 data.content 双臂
234
+ * (checkpoint 替身/用户消息旧视界全计 0);② replaces 加 live 列——替身行
235
+ * 被后续区间再覆盖时 delta 清除不入 gross(用户定标:checkpoint 只影响其后
236
+ * 的积分)。旧行两项口径均错,整库清空重建(投影可重建,丢行无损)。 */
237
+ declare const PROJECTION_SCHEMA_VERSION = "4";
238
+ /** sessions 表原始行(snake_case 与 DDL 一致)。 */
239
+ interface SessionRow {
240
+ session_id: string;
241
+ dir_id: string;
242
+ title: string;
243
+ cwd: string;
244
+ event_count: number;
245
+ max_seq: number;
246
+ turn_count: number;
247
+ segment_tokens: number;
248
+ surface_tokens: number;
249
+ gross_saved: number;
250
+ recall_tokens: number;
251
+ compress_pairs: number;
252
+ recall_pairs: number;
253
+ basis: string;
254
+ artifact_bytes: number;
255
+ artifact_mtime_ms: number;
256
+ last_time_ms: number;
257
+ format_generation: number;
258
+ archived: number;
259
+ synced_at: string;
260
+ }
261
+ /** replaceSession 的产物 watermark 附加项。 */
262
+ interface ReplaceSessionExtra {
263
+ /** 归档集标志(sync 侧按 workspace.json 只读跟随)。 */
264
+ archived?: 0 | 1;
265
+ /** 预置标题(steward 切料同款,缺省用 projection.summary.title)。 */
266
+ title?: string;
267
+ }
268
+ /** 投影库句柄。 */
269
+ interface ProjectionStore {
270
+ db: DatabaseSync;
271
+ /** 缓存层 2:sessions 全表常驻(~百行级),status/sessions 列表 0 SQL。 */
272
+ summaries: Map<string, SessionRow>;
273
+ /** 事务式全量重写一个会话;提交后增量更新常驻行。 */
274
+ replaceSession(projection: SessionProjection, artifactBytes: number, artifactMtimeMs: number, extra?: ReplaceSessionExtra): void;
275
+ /** 翻转归档旗标(不动投影内容)。 */
276
+ setArchived(sessionId: string, archived: 0 | 1): void;
277
+ /**
278
+ * 级联清除一个会话的全部投影(删除剪枝专用,2026-10-20 用户定标):
279
+ * WAL 事务内删 segments/segments_fts/replaces/sessions 四表,提交后摘
280
+ * 常驻 Map;会话不存在返回 false(幂等,不抛)。投影可重建,删行无损。
281
+ */
282
+ deleteSession(sessionId: string): boolean;
283
+ close(): void;
284
+ }
285
+ /**
286
+ * 打开(或创建)投影库。
287
+ * @param dir - 库目录(缺省布局 ~/.dsh-reference-session-collector;env 覆盖在 sync 侧解析)。
288
+ * @returns {@link ProjectionStore},已载入 sessions 常驻 Map。
289
+ */
290
+ declare function openProjectionStore(dir: string): ProjectionStore;
291
+ /** 重新打开时同步常驻 Map(内部工具;openProjectionStore 已在构造时载入)。 */
292
+ declare function reloadSummaries(store: ProjectionStore): void;
293
+ //#endregion
294
+ //#region src/surface.d.ts
295
+ /** 台账 LRU 容量(缓存层 3)。 */
296
+ declare const LEDGER_LRU_CAP = 32;
297
+ /** per-session 台账 LRU:键含 synced_at 代际;get 命中重插尾部。 */
298
+ declare class LedgerCache<T> {
299
+ private readonly map;
300
+ hits: number;
301
+ misses: number;
302
+ get(key: string): T | undefined;
303
+ set(key: string, value: T): void;
304
+ get size(): number;
305
+ stats(): {
306
+ entries: number;
307
+ hits: number;
308
+ misses: number;
309
+ };
310
+ }
311
+ /** 一个 store 的查询上下文(持有两类台账 LRU 与计数)。 */
312
+ interface QueryContext {
313
+ store: ProjectionStore;
314
+ surfaceLru: LedgerCache<unknown>;
315
+ recallLru: LedgerCache<unknown>;
316
+ }
317
+ declare function createQueryContext(store: ProjectionStore): QueryContext;
318
+ /** status:库自检 + 缓存统计(spec §HTTP 面)。 */
319
+ declare function status(ctx: QueryContext, lastSync?: unknown): Record<string, unknown>;
320
+ /** sessions 汇总表分页(0 SQL,走常驻 Map;按 last_time_ms 降序)。 */
321
+ declare function listSessions(ctx: QueryContext, offset?: number, limit?: number): {
322
+ total: number;
323
+ rows: SessionRow[];
324
+ };
325
+ /**
326
+ * segments 明细。
327
+ * @param turn - `turn=` 区间过滤(`a-b` 闭区间或单值)。
328
+ * @param fields - `'search'` 返检索面字段(time/search_text/on_surface)。
329
+ */
330
+ declare function sessionDetail(ctx: QueryContext, sessionId: string, opts?: {
331
+ turn?: string;
332
+ fields?: string;
333
+ }): {
334
+ session: SessionRow | undefined;
335
+ rows: unknown[];
336
+ };
337
+ /** surface 台账(R3):当前表面行 + 被替换指针视图(LRU)。 */
338
+ declare function surfaceLedger(ctx: QueryContext, sessionId: string): Record<string, unknown>;
339
+ /**
340
+ * 召回对账(R4):替换对分类 + Σb(回吐)+ Σ(a−b)(毛节省)(LRU)。
341
+ * 毛节省只计 live=1 的对——替身被后续 checkpoint 再覆盖时 delta 清除(用户定标)。
342
+ */
343
+ declare function recallReport(ctx: QueryContext, sessionId: string): Record<string, unknown>;
344
+ /** FTS5 BM25 内容搜索(中英同构切词,走 sanitizeFtsQuery)。 */
345
+ declare function search(ctx: QueryContext, query: string, opts?: {
346
+ session?: string;
347
+ limit?: number;
348
+ }): {
349
+ query: string;
350
+ match: string;
351
+ hits: Record<string, unknown>[];
352
+ };
353
+ /**
354
+ * 锤定查询词首次出现的节选(steward buildSnippet 逐字同构移植,
355
+ * 2026-10-20):短文本直返;命中词居中开窗;无锚点退化为头部截断。
356
+ */
357
+ declare function buildSearchSnippet(text: string, query: string, max?: number): string;
358
+ /** 分组搜索请求(与 steward engine.search 入参同形;kinds 为原事件 type 列表)。 */
359
+ interface GroupedSearchRequest {
360
+ query: string;
361
+ /** 允许的原事件 type(已由调用方展开粗类);缺省= user+assistant(steward resolveTypes 同标)。 */
362
+ kinds?: readonly string[];
363
+ /** 检索域:'active' 仅活跃、'archived' 仅归档、缺省 'all'。 */
364
+ archived?: 'active' | 'archived' | 'all';
365
+ /** 会话末事件时间下界(epoch ms,含)。 */
366
+ from?: number;
367
+ /** 上界(排他)。 */
368
+ to?: number;
369
+ /** true(缺省)=仅当前表面行(on_surface=1,steward surface='current' 同款)。 */
370
+ surfaceOnly?: boolean;
371
+ /** 排序:缺省 relevance;'time'=会话 updated_at 降序、分数平手。 */
372
+ sortBy?: 'relevance' | 'time';
373
+ limit?: number;
374
+ }
375
+ /** 分组命中(wire 蛇形,会话级最强命中)。 */
376
+ interface GroupedSearchHit {
377
+ session_id: string;
378
+ title: string;
379
+ seq: number;
380
+ type: string;
381
+ time: number;
382
+ updated_at: number;
383
+ snippet: string;
384
+ }
385
+ /**
386
+ * 会话分组全文搜索(op=search&group=session):BM25 候选窗 → 按会话取
387
+ * 加权最强命中 → 排序分页。数学/视界与 steward engine.search 逐字同构:
388
+ * score = −rank × weight(type),时间排序键是会话 updated_at(非命中 time)。
389
+ */
390
+ declare function searchGrouped(ctx: QueryContext, request: GroupedSearchRequest): {
391
+ query: string;
392
+ hits: GroupedSearchHit[];
393
+ };
394
+ //#endregion
395
+ //#region src/segment.d.ts
396
+ /**
397
+ * ⚠ 同构移植(回搬件)——检索切分视界源自 dsh-session-steward
398
+ * `src/host/index/extract.ts:62-95`(SEGMENTER/segmentForIndex/segmentQueryTerm)
399
+ * 与 `src/host/index/engine.ts:83-95`(sanitizeFtsQuery),回搬时点 2026-10-19。
400
+ * 回搬纪律:**逐字同构,禁止分叉**;索引侧与查询侧必须共用本文件的同一
401
+ * 切分函数(spec 决策「CJK 分词=DRS 自带 Intl.Segmenter 切分」)。
402
+ * 唯一非授权差异:导出面去掉 steward 内部类型耦合(行为零差异)。
403
+ */
404
+ /**
405
+ * Space-separate word boundaries so the FTS5 unicode61 tokenizer indexes
406
+ * words instead of whole CJK runs: the index and query sides must apply the
407
+ * exact same segmentation for a token to meet its match.
408
+ * @param text - raw extracted text (or a query term).
409
+ * @returns text with a single space at every word boundary.
410
+ */
411
+ declare function segmentForIndex(text: string): string;
412
+ /**
413
+ * Segment one whitespace-delimited query term into FTS5 phrase tokens.
414
+ * @returns word-like segments, or the raw term when segmentation is unavailable.
415
+ */
416
+ declare function segmentQueryTerm(term: string): string[];
417
+ /**
418
+ * Sanitize free text into a safe FTS5 query over the segmented index: each
419
+ * whitespace term is segmented into word tokens, quoted as an adjacent
420
+ * phrase, and the last token carries a prefix `*` so partial input matches
421
+ * ("正在搜" hits 正在搜索). Terms AND together.
422
+ */
423
+ declare function sanitizeFtsQuery(query: string): string;
424
+ //#endregion
425
+ //#region src/strip-injected.d.ts
426
+ /**
427
+ * 宿主注入样板段剥离(检索视界专用,2026-10-20 真机取证定标)。
428
+ *
429
+ * 背景:user/message 原文头部含宿主注入的成对标记段(记忆召回/人格/工具指南/
430
+ * 场景导航/系统提醒/技能指令)。真机统计(46 会话/3100 条用户消息):1210 条
431
+ * (39%)含注入段,标记全集七类——这些段落随原文进了 search_text 后成为检索
432
+ * 污染主因:关键词命中在用户没写过的注入样板里,节选与正文对不上。
433
+ *
434
+ * 纪律:**只作用于 search_text(检索面),绝不作用于 text/tokens**——后者是
435
+ * CCI verify 同口径视界(savings-verify 对 message.content 全文计数),
436
+ * 剥离会破坏对账数学(spec R5)。
437
+ * 只剥**完整成对**标记段;未闭合(撕裂尾行)原样保留,不伪造丢失。
438
+ */
439
+ /** 注入标记全集(真机 tagscan 实测,按出现频降序)。 */
440
+ declare const INJECTED_BLOCK_TAGS: readonly string[];
441
+ /**
442
+ * 剥离一段文本里的全部注入样板块。
443
+ * @param text - 原始检索文本(任意事件视界产出均可过)。
444
+ * @returns 剥离并重整空白后的文本;无注入块时与原串仅可能差在首尾空白。
445
+ */
446
+ declare function stripInjectedBlocks(text: string): string;
447
+ //#endregion
448
+ //#region src/token-count.d.ts
449
+ /**
450
+ * ⚠ VENDORED(回搬件)——源自 dsh-context-compression-improved
451
+ * `packages/selector/src/runtime/token-count.ts`,回搬时点 2026-10-19。
452
+ * 回搬纪律:**文件级拷贝,禁止分叉**;与源文件逐字节一致,无授权 diff。
453
+ */
454
+ /** Token counts owned by the standalone compression runtime. */
455
+ /** Exact count of one canonical value under a pinned tokenizer artifact. */
456
+ interface ExactTokenizerTokenCount {
457
+ readonly kind: 'exact-tokenizer';
458
+ readonly tokens: number;
459
+ readonly tokenizerId: string;
460
+ readonly tokenizerRevision: string;
461
+ }
462
+ /** A value that the bundled tokenizer cannot safely count. */
463
+ interface UnavailableTokenCount {
464
+ readonly kind: 'unavailable';
465
+ readonly reason: string;
466
+ }
467
+ /** Best-effort estimate carrying a conservative upper value when available. */
468
+ interface TokenizerEstimateTokenCount {
469
+ readonly kind: 'tokenizer-estimate';
470
+ readonly tokens: number;
471
+ readonly upperBoundTokens: number;
472
+ readonly estimatorId: string;
473
+ readonly estimatorRevision: string;
474
+ readonly calibration?: Readonly<{
475
+ readonly sampleCount: number;
476
+ readonly conservativeMarginTokens: number;
477
+ }>;
478
+ }
479
+ /** Exact, conservative request estimate, or an explicit refusal. */
480
+ type TokenCount = ExactTokenizerTokenCount | TokenizerEstimateTokenCount | UnavailableTokenCount;
481
+ /** Bound exact text counter for one durable provider/model request target. */
482
+ type CanonicalTextTokenCounter = (text: string) => TokenCount;
483
+ //#endregion
484
+ //#region src/sync.d.ts
485
+ /** 一个会话目录的发现事实:目录名 + 当前代产物。 */
486
+ interface DiscoveredSessionLog {
487
+ dirSessionId: string;
488
+ dir: string;
489
+ artifact: GenerationArtifact;
490
+ }
491
+ /** 单轮发现的目录上限(walk 兜底,防异常目录树拖死主线程)。 */
492
+ declare const DISCOVERY_LIMIT = 5000;
493
+ /**
494
+ * 走 `~/.dsh/sessions` 发现会话目录(与 CCI discoverBackfillSessionLogs 同构):
495
+ * 目录内无任何代次产物与暂存残留 → 视为路径中间层继续下钻;有当前代产物 →
496
+ * 收录。上限截断,不抛。
497
+ */
498
+ declare function discoverSessionLogs(dshHome: string, limit?: number): DiscoveredSessionLog[];
499
+ /** DSH home 解析:显式入参 > DSH_HOME 环境变量 > ~/.dsh。 */
500
+ declare function resolveDshHome(explicit?: string, env?: NodeJS.ProcessEnv): string;
501
+ /** 投影库目录解析:显式入参 > DSH_REFERENCE_SESSION_COLLECTOR_DIR > ~/.dsh-reference-session-collector。 */
502
+ declare function resolveIndexDir(explicit?: string, env?: NodeJS.ProcessEnv): string;
503
+ /**
504
+ * 读归档会话 id 集:~/.dsh/storages/workspace.json 的
505
+ * `global.archivedSessionIds`。任何形状不符/文件不可读 → 空集(无归档),
506
+ * 绝不抛——归档只是检索域标志,不配阻塞同步。
507
+ */
508
+ declare function readArchivedSessionIds(dshHome: string): ReadonlySet<string>;
509
+ /** 投影核心调用选项(与 L1 projectSessionEvents 第二参逐字对齐)。 */
510
+ interface ProjectionCallOptions {
511
+ dirId: string;
512
+ header?: Record<string, unknown>;
513
+ exactCounter?: CanonicalTextTokenCounter;
514
+ }
515
+ /**
516
+ * 投影端口:同步引擎把解码事件交给投影核心,产物对 sync 不透明
517
+ * (由 store 端口解释)。L1 落地后由 index.ts 绑定 projectSessionEvents。
518
+ */
519
+ type ProjectionPort = (events: readonly DecodedEvent[], options: ProjectionCallOptions) => unknown;
520
+ /** sync 关心的 store 水位行(真身 schema 的窄投影)。 */
521
+ interface SyncWatermark {
522
+ sessionId: string;
523
+ artifactBytes: number;
524
+ artifactMtimeMs: number;
525
+ archived: number;
526
+ }
527
+ /** replaceSession 附带元数据:水位 + 本轮归档态。 */
528
+ interface SyncReplaceMeta {
529
+ artifactBytes: number;
530
+ artifactMtimeMs: number;
531
+ archived: number;
532
+ }
533
+ /**
534
+ * 投影库端口。真身 = schema.ts 的 ProjectionStore(L1);单测注入内存 mock。
535
+ * 五个方法恰好是 sync 需要的全部写/读面——真身实现不得要求更多。
536
+ */
537
+ interface SyncStorePort {
538
+ /** 目录水位预查(stat 命中免解码的关键);无记录返回 undefined。 */
539
+ getSessionByDir(dirId: string): SyncWatermark | undefined;
540
+ /** 全会话重写式落库(事务内 DELETE+INSERT,真身职责);projection 不透明。 */
541
+ replaceSession(projection: unknown, meta: SyncReplaceMeta): void;
542
+ /** 仅翻归档旗标(stat 命中的会话随归档集变更走这里,免解码)。 */
543
+ setSessionArchived(sessionId: string, archived: number): void;
544
+ /** 已投影名册枚举(删除剪枝对照面):dirId=发现键,sessionId=删除键。 */
545
+ listProjectedDirs(): {
546
+ dirId: string;
547
+ sessionId: string;
548
+ }[];
549
+ /** 级联清除一个会话的投影(WAL 事务内 segments/fts/replaces/sessions 四表+常驻 Map)。 */
550
+ deleteSession(sessionId: string): void;
551
+ }
552
+ type SyncLog = (level: 'info' | 'warn', message: string, ...args: unknown[]) => void;
553
+ /** DS 精确计数器探测:flash → pro 逐个试;全缺席 undefined(chars/4 降级)。 */
554
+ declare function resolveExactCounter(): CanonicalTextTokenCounter | undefined;
555
+ /** 长度口径:ds-token=精确词表;chars/4=如实降级;unprobed=本轮未探测(busy 空转)。 */
556
+ type SyncBasis = 'ds-token' | 'chars/4' | 'unprobed';
557
+ interface SyncFailure {
558
+ sessionId: string;
559
+ error: string;
560
+ }
561
+ interface SyncSoftSkip {
562
+ sessionId: string;
563
+ state: string;
564
+ }
565
+ interface SyncResult {
566
+ ok: boolean;
567
+ /** true=本轮撞上 syncing 旗标,直接返回(不计入进度)。 */
568
+ busy: boolean;
569
+ totalDiscovered: number;
570
+ updated: number;
571
+ skipped: number;
572
+ softSkips: SyncSoftSkip[];
573
+ failures: SyncFailure[];
574
+ /**
575
+ * 本轮被删除剪枝的 sessionId 列表:宿主删会话=磁盘目录消失,投影绝不
576
+ * 留陈旧行(真身 deleteSession 走 WAL 事务级联清四表)。守护条件见
577
+ * syncAll 剪枝段——sessions 根缺失或发现截断时**不动手**。
578
+ */
579
+ pruned: string[];
580
+ basis: SyncBasis;
581
+ durationMs: number;
582
+ }
583
+ interface ProjectionSyncOptions {
584
+ store: SyncStorePort;
585
+ project: ProjectionPort;
586
+ /** 缺省 console.warn 单臂(info 静默——宿主半另有 logger 接线面)。 */
587
+ log?: SyncLog;
588
+ /** 'auto'(缺省)=探测 DS 精确计数器;'off'=强制 chars/4(单测/省资产加载)。 */
589
+ counter?: 'auto' | 'off';
590
+ }
591
+ declare class ProjectionSync {
592
+ private readonly store;
593
+ private readonly project;
594
+ private readonly log;
595
+ private readonly counterMode;
596
+ private syncing;
597
+ private lastResult;
598
+ /** 软跳 warn 一次性:sessionId+state 为键,重复轮不再刷屏。 */
599
+ private readonly warnedSoftSkips;
600
+ constructor(options: ProjectionSyncOptions);
601
+ get isSyncing(): boolean;
602
+ /** 最近一轮完整结果(busy 空转轮不覆盖它);POST sync 响应体用它。 */
603
+ get lastSyncResult(): SyncResult | undefined;
604
+ /**
605
+ * 一轮增量同步:发现 → 逐会话(水位预查/软跳/投影/落库/让路)。
606
+ * 重入直接返回 busy(不排队——下一轮 interval 自然补上)。
607
+ */
608
+ syncAll(input?: {
609
+ dshHome?: string;
610
+ limit?: number;
611
+ }): Promise<SyncResult>;
612
+ private syncOne;
613
+ }
614
+ /** interval 轮缺省周期:steward 二期新鲜度(≤1 分钟)靠它,不靠手动 POST。 */
615
+ declare const DEFAULT_SYNC_INTERVAL_MS = 30000;
616
+ declare const SYNC_INTERVAL_ENV = "DSH_REFERENCE_SESSION_COLLECTOR_SYNC_INTERVAL_MS";
617
+ /** 解析轮询周期:env 缺省 30s;0=关闭(纯手动);非法值回落缺省。 */
618
+ declare function resolveSyncIntervalMs(env?: NodeJS.ProcessEnv): number;
619
+ /**
620
+ * interval 的目标面:结构最小型(单测可注返回部分结果的 mock;真身
621
+ * ProjectionSync 的 syncAll 结构兼容)。轮内只调用、不解读结果。
622
+ */
623
+ interface IntervalSyncTarget {
624
+ syncAll: (input?: {
625
+ dshHome?: string;
626
+ limit?: number;
627
+ }) => Promise<unknown>;
628
+ }
629
+ /**
630
+ * 启动 interval 同步,返回 dispose(清定时器)。
631
+ * 轮与启动首轮/syncing 旗标不并发:syncAll 撞旗标返回 busy,无害。
632
+ * 周期 0(或负/非法)→ 不建定时器,dispose 为空操作。
633
+ */
634
+ declare function startIntervalSync(target: IntervalSyncTarget, options?: {
635
+ intervalMs?: number;
636
+ }): () => void;
637
+ //#endregion
638
+ //#region src/index.d.ts
639
+ /** 本插件声明的宿主服务(唯一声明;webStartup 走 ctx.get 回退臂)。 */
640
+ declare const inject: string[];
641
+ /** API 路由前缀(fenced 单条)。 */
642
+ declare const DRS_API_PATH = "/api/dsh-reference-session-collector";
643
+ /** 启动首轮同步延迟(ms)——让路给宿主冷启动。 */
644
+ declare const BOOT_SYNC_DELAY_MS = 2500;
645
+ /** sync 端口 → ProjectionStore 真身适配(dir 水位经常驻 Map 扫描)。 */
646
+ declare function createStorePort(store: ProjectionStore): SyncStorePort;
647
+ /** 请求处理依赖(导出面便于路由单测直调)。 */
648
+ interface DrsHandlerDeps {
649
+ query: QueryContext;
650
+ sync: {
651
+ syncAll: () => Promise<unknown>;
652
+ lastSyncResult: unknown;
653
+ isSyncing: boolean;
654
+ };
655
+ trustedHosts?: readonly string[];
656
+ }
657
+ /**
658
+ * op 分发:GET `?op=…`;POST `{op:'sync'}` 触发后台同步。
659
+ * 未知 op / 缺参 → 400(不留空壳)。
660
+ */
661
+ declare function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessage, res: ServerResponse): Promise<void>;
662
+ /**
663
+ * 插件主体:开库 → 装配 sync(启动延迟首轮 + interval 轮)→ 双通道注册路由。
664
+ * @param ctx - host 插件上下文(webServer 声明注入;webStartup/webRuntime 回退臂)。
665
+ */
666
+ declare function apply(ctx: Context): void;
667
+ //#endregion
668
+ export { BOOT_SYNC_DELAY_MS, DEFAULT_SYNC_INTERVAL_MS, DISCOVERY_LIMIT, DRS_API_PATH, type DecodedEvent, DrsHandlerDeps, type ExactCounter, type GroupedSearchHit, type GroupedSearchRequest, INJECTED_BLOCK_TAGS, LEDGER_LRU_CAP, LedgerCache, PROJECTION_SCHEMA_VERSION, type ProjectionOptions, type ProjectionPort, type ProjectionReplace, type ProjectionSegment, type ProjectionStore, ProjectionSync, type QueryContext, SYNC_INTERVAL_ENV, type SessionLogRead, type SessionProjection, type SessionRow, type SessionSummary, type SyncResult, type SyncStorePort, apply, buildSearchSnippet, classifySessionHeader, createQueryContext, createStorePort, decodeSessionLogBytes, decodeSessionLogFile, discoverSessionLogs, extractSearchEventText, extractTitleFromEvents, generationLogFilename, handleDrsRequest, inject, listSessions, openProjectionStore, projectSessionEvents, readArchivedSessionIds, readSessionGenerations, recallReport, reloadSummaries, resolveDshHome, resolveExactCounter, resolveIndexDir, resolveSyncIntervalMs, sanitizeFtsQuery, search, searchGrouped, segmentForIndex, segmentQueryTerm, sessionDetail, startIntervalSync, status, stripInjectedBlocks, surfaceLedger };