dsh-prime-memory 0.16.1 → 0.16.4
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/CHANGELOG.md +44 -0
- package/cordis.patch.yml +6 -0
- package/dist/client.js +1333 -168
- package/dist/config.d.ts +37 -0
- package/dist/config.js +7 -0
- package/dist/contract.d.ts +155 -1
- package/dist/hooks/capture-recovery.d.ts +64 -0
- package/dist/hooks/capture-recovery.js +105 -0
- package/dist/hooks/capture.js +123 -1
- package/dist/hooks/recall-ack.d.ts +48 -0
- package/dist/hooks/recall-ack.js +79 -0
- package/dist/hooks/recall.d.ts +23 -1
- package/dist/hooks/recall.js +121 -6
- package/dist/index.d.ts +22 -0
- package/dist/index.js +18 -4
- package/dist/metadata-validators.d.ts +5 -0
- package/dist/metadata-validators.js +11 -1
- package/dist/pipeline/l1.d.ts +4 -0
- package/dist/pipeline/l1.js +9 -2
- package/dist/pipeline/reconcile.js +4 -0
- package/dist/pipeline/ruminate.d.ts +11 -1
- package/dist/pipeline/ruminate.js +18 -1
- package/dist/pipeline/runner.js +62 -0
- package/dist/prompts/boundary.d.ts +17 -0
- package/dist/prompts/boundary.js +20 -0
- package/dist/prompts/graph-projection.js +4 -1
- package/dist/prompts/l1-dedup.d.ts +3 -3
- package/dist/prompts/l1-dedup.js +9 -1
- package/dist/prompts/l1-extraction.d.ts +3 -3
- package/dist/prompts/l1-extraction.js +12 -0
- package/dist/prompts/persona.js +6 -0
- package/dist/prompts/scene.js +7 -1
- package/dist/room-review.d.ts +196 -0
- package/dist/room-review.js +281 -0
- package/dist/stats.d.ts +26 -2
- package/dist/stats.js +270 -13
- package/dist/store/l0.d.ts +4 -0
- package/dist/store/l0.js +8 -0
- package/dist/store/l1.d.ts +34 -0
- package/dist/store/l1.js +68 -1
- package/dist/store/recall-usage.d.ts +43 -0
- package/dist/store/recall-usage.js +149 -0
- package/dist/store/rooms-registry.d.ts +66 -0
- package/dist/store/rooms-registry.js +250 -0
- package/dist/store/sqlite.d.ts +16 -0
- package/dist/store/sqlite.js +57 -0
- package/dist/store/trace.d.ts +39 -0
- package/dist/store/trace.js +149 -0
- package/dist/tools/index.d.ts +4 -1
- package/dist/tools/index.js +249 -2
- package/dist/types.d.ts +4 -0
- package/dist/util/redact.d.ts +27 -0
- package/dist/util/redact.js +228 -0
- package/dsh.plugin.json +1 -1
- package/package.json +10 -10
- package/dist/hall-backfill.d.ts +0 -33
- package/dist/hall-backfill.js +0 -95
- package/dist/hall-migrate.d.ts +0 -32
- package/dist/hall-migrate.js +0 -88
package/dist/config.d.ts
CHANGED
|
@@ -28,6 +28,21 @@ export interface MemoryConfig {
|
|
|
28
28
|
stripCodeBlocks: boolean;
|
|
29
29
|
/** 单条消息内容最大字符数。 */
|
|
30
30
|
maxMessageChars: number;
|
|
31
|
+
/**
|
|
32
|
+
* §C 载荷脱敏(memorax-absorb):捕获单点把 8 类密钥(PEM/Bearer/JWT/Cookie/
|
|
33
|
+
* 厂商 API key/邮箱/长号/高熵 ID)替换为 `[REDACTED:<KIND>]` 占位符,覆盖
|
|
34
|
+
* L0 JSONL/SQLite/蒸馏输入与手动 memory_add/import。**默认开启**——密钥被
|
|
35
|
+
* 逐轮召回注入的危害大于偶发误脱敏;注意开启后 L0 原文自此改变(重建不可得原文)。
|
|
36
|
+
* 决策记录见计划 spec §C;`false` 一键回到明文行为。 */
|
|
37
|
+
redactSecrets?: boolean;
|
|
38
|
+
};
|
|
39
|
+
/** §F 结构化追踪(memorax-absorb):recall/distill 事件按天 JSONL。 */
|
|
40
|
+
trace: {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
/** 事件文件保留天数(写入时切日清理;0 = 永久)。 */
|
|
43
|
+
retentionDays: number;
|
|
44
|
+
/** 默认 metadata-only(chars+sha);true 才存 query 原文(≤200 字符)。 */
|
|
45
|
+
captureContent: boolean;
|
|
31
46
|
};
|
|
32
47
|
extract: {
|
|
33
48
|
enabled: boolean;
|
|
@@ -196,10 +211,21 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
196
211
|
enabled: Schema<boolean, boolean>;
|
|
197
212
|
stripCodeBlocks: Schema<boolean, boolean>;
|
|
198
213
|
maxMessageChars: Schema<number, number>;
|
|
214
|
+
redactSecrets: Schema<boolean, boolean>;
|
|
199
215
|
}>, Schemastery.ObjectT<{
|
|
200
216
|
enabled: Schema<boolean, boolean>;
|
|
201
217
|
stripCodeBlocks: Schema<boolean, boolean>;
|
|
202
218
|
maxMessageChars: Schema<number, number>;
|
|
219
|
+
redactSecrets: Schema<boolean, boolean>;
|
|
220
|
+
}>>;
|
|
221
|
+
trace: Schema<Schemastery.ObjectS<{
|
|
222
|
+
enabled: Schema<boolean, boolean>;
|
|
223
|
+
retentionDays: Schema<number, number>;
|
|
224
|
+
captureContent: Schema<boolean, boolean>;
|
|
225
|
+
}>, Schemastery.ObjectT<{
|
|
226
|
+
enabled: Schema<boolean, boolean>;
|
|
227
|
+
retentionDays: Schema<number, number>;
|
|
228
|
+
captureContent: Schema<boolean, boolean>;
|
|
203
229
|
}>>;
|
|
204
230
|
extract: Schema<Schemastery.ObjectS<{
|
|
205
231
|
enabled: Schema<boolean, boolean>;
|
|
@@ -480,10 +506,21 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
480
506
|
enabled: Schema<boolean, boolean>;
|
|
481
507
|
stripCodeBlocks: Schema<boolean, boolean>;
|
|
482
508
|
maxMessageChars: Schema<number, number>;
|
|
509
|
+
redactSecrets: Schema<boolean, boolean>;
|
|
483
510
|
}>, Schemastery.ObjectT<{
|
|
484
511
|
enabled: Schema<boolean, boolean>;
|
|
485
512
|
stripCodeBlocks: Schema<boolean, boolean>;
|
|
486
513
|
maxMessageChars: Schema<number, number>;
|
|
514
|
+
redactSecrets: Schema<boolean, boolean>;
|
|
515
|
+
}>>;
|
|
516
|
+
trace: Schema<Schemastery.ObjectS<{
|
|
517
|
+
enabled: Schema<boolean, boolean>;
|
|
518
|
+
retentionDays: Schema<number, number>;
|
|
519
|
+
captureContent: Schema<boolean, boolean>;
|
|
520
|
+
}>, Schemastery.ObjectT<{
|
|
521
|
+
enabled: Schema<boolean, boolean>;
|
|
522
|
+
retentionDays: Schema<number, number>;
|
|
523
|
+
captureContent: Schema<boolean, boolean>;
|
|
487
524
|
}>>;
|
|
488
525
|
extract: Schema<Schemastery.ObjectS<{
|
|
489
526
|
enabled: Schema<boolean, boolean>;
|
package/dist/config.js
CHANGED
|
@@ -26,6 +26,13 @@ export const memorySchema = Schema.object({
|
|
|
26
26
|
enabled: Schema.boolean().default(true),
|
|
27
27
|
stripCodeBlocks: Schema.boolean().default(true),
|
|
28
28
|
maxMessageChars: Schema.number().min(200).max(200_000).default(4000),
|
|
29
|
+
// §C 默认开启:本地存储也不落明文密钥(决策记录见计划 spec §C)
|
|
30
|
+
redactSecrets: Schema.boolean().default(true),
|
|
31
|
+
}),
|
|
32
|
+
trace: Schema.object({
|
|
33
|
+
enabled: Schema.boolean().default(true),
|
|
34
|
+
retentionDays: Schema.number().min(0).max(365).default(14),
|
|
35
|
+
captureContent: Schema.boolean().default(false),
|
|
29
36
|
}),
|
|
30
37
|
extract: Schema.object({
|
|
31
38
|
enabled: Schema.boolean().default(true),
|
package/dist/contract.d.ts
CHANGED
|
@@ -507,10 +507,91 @@ export interface WingOverviewResponse {
|
|
|
507
507
|
* 降级/无 tags 时 `rooms` 为空数组(面板显示"暂无"),不报错。
|
|
508
508
|
*/
|
|
509
509
|
export interface RoomsGetResponse {
|
|
510
|
-
/** Room 列表(
|
|
510
|
+
/** Room 列表(按记录数降序,同数按名升序;注册表条目 count=0 并带 source/label)。
|
|
511
|
+
* 条目可选字段 source/label 由端点层合并注册表后附带(客户端可选读,零断裂)。 */
|
|
511
512
|
rooms: RoomCount[];
|
|
512
513
|
/** Room 总数(= rooms.length,冗余只为前端少写一次 .length)。 */
|
|
513
514
|
total: number;
|
|
515
|
+
/** 孤儿记忆数:没有任何 Room(tags 为空/缺失)的记录条数(与 rooms 同口径含已退场)。 */
|
|
516
|
+
orphanCount: number;
|
|
517
|
+
/** 注册表条目(全量含 retired;分类管理面板用;未装配 = undefined)。 */
|
|
518
|
+
registry?: Array<{
|
|
519
|
+
slug: string;
|
|
520
|
+
label?: string;
|
|
521
|
+
description?: string;
|
|
522
|
+
source: 'pre-registered' | 'grown';
|
|
523
|
+
aliases?: string[];
|
|
524
|
+
status: 'active' | 'retired';
|
|
525
|
+
}>;
|
|
526
|
+
}
|
|
527
|
+
/** dsh-memory/room-register(预注册 Room;高权限:面板 memoryMutate 开启才生效)。 */
|
|
528
|
+
export interface RoomRegisterRequest {
|
|
529
|
+
slug: string;
|
|
530
|
+
label?: string;
|
|
531
|
+
description?: string;
|
|
532
|
+
/** 收编自生长 slug 时传 grown(缺省 pre-registered;已存在条目忽略此字段)。 */
|
|
533
|
+
source?: 'pre-registered' | 'grown';
|
|
534
|
+
}
|
|
535
|
+
export interface RoomRegisterResponse {
|
|
536
|
+
slug: string;
|
|
537
|
+
created: boolean;
|
|
538
|
+
/** 幂等补全时的提示(如「已存在,只补全缺失字段」)。 */
|
|
539
|
+
notice?: string;
|
|
540
|
+
}
|
|
541
|
+
/** dsh-memory/rooms-export(分类数据导出,CSV 直出)。 */
|
|
542
|
+
export interface RoomsExportRequest {
|
|
543
|
+
/** rooms = 词表+计数;orphans = 无 Room 绑定的记录清单;records = 按 Room 的记录清单(tag 必传)。 */
|
|
544
|
+
kind: 'rooms' | 'orphans' | 'records';
|
|
545
|
+
/** 各 kind 通用上限(1..10000,默认 2000)。 */
|
|
546
|
+
limit?: number;
|
|
547
|
+
/** kind=records 的目标 Room slug。 */
|
|
548
|
+
tag?: string;
|
|
549
|
+
}
|
|
550
|
+
export interface RoomsExportResponse {
|
|
551
|
+
kind: 'rooms' | 'orphans' | 'records';
|
|
552
|
+
/** CSV 文本(首行表头;
|
|
553
|
+
行尾;RFC 4180 转义)。 */
|
|
554
|
+
csv: string;
|
|
555
|
+
/** 数据行数(不含表头)。 */
|
|
556
|
+
total: number;
|
|
557
|
+
/** truncated = 达到 limit 上限,结果可能不完整。 */
|
|
558
|
+
truncated: boolean;
|
|
559
|
+
}
|
|
560
|
+
/** dsh-memory/room-admin(Room 破坏性面管理;高权限:面板 memoryMutate 开启才生效)。 */
|
|
561
|
+
export interface RoomAdminRequest {
|
|
562
|
+
/** merge/rename:from→to(mergeRoom/renameRoom 编排:预览→实跑+自动备份+入队场景重算);retire:slug 退役/恢复;update:改写 label/description。 */
|
|
563
|
+
action: 'merge' | 'rename' | 'retire' | 'update';
|
|
564
|
+
/** update 的显示名(可中文;非空才改写)。 */
|
|
565
|
+
label?: string;
|
|
566
|
+
/** update 的归类说明(喂标注器;非空才改写)。 */
|
|
567
|
+
description?: string;
|
|
568
|
+
/** merge/rename 的源 Room slug。 */
|
|
569
|
+
from?: string;
|
|
570
|
+
/** merge/rename 的目标 Room slug。 */
|
|
571
|
+
to?: string;
|
|
572
|
+
/** retire 的目标 Room slug。 */
|
|
573
|
+
slug?: string;
|
|
574
|
+
/** retire:true=恢复 active;缺省/false=退役。 */
|
|
575
|
+
active?: boolean;
|
|
576
|
+
/** retire:同时把该 room 下全部**活跃**记录退场(软删,可在「已退场」区恢复)。 */
|
|
577
|
+
withRecords?: boolean;
|
|
578
|
+
/** merge/rename:缺省 true 只返预览;实跑需显式 false。 */
|
|
579
|
+
dryRun?: boolean;
|
|
580
|
+
}
|
|
581
|
+
export interface RoomAdminResponse {
|
|
582
|
+
notice: string;
|
|
583
|
+
/** merge/rename 本次是否 dryRun(预览)。 */
|
|
584
|
+
dryRun?: boolean;
|
|
585
|
+
/** merge/rename 影响的记录条数(实跑 = 已改写条数)。 */
|
|
586
|
+
affected?: number;
|
|
587
|
+
/** 预览样例(受影响记录 id 的前几条)。 */
|
|
588
|
+
preview?: string[];
|
|
589
|
+
/** true = 超过单次上限,需续跑。 */
|
|
590
|
+
hasMore?: boolean;
|
|
591
|
+
/** 实跑时改写前备份文件名(rooms-merge-backups/)。 */
|
|
592
|
+
backupFile?: string;
|
|
593
|
+
/** retire withRecords 实际退场的记录条数(软删,可恢复)。 */
|
|
594
|
+
retiredRecords?: number;
|
|
514
595
|
}
|
|
515
596
|
/** dsh-memory/wing-backfill(一键回填,后台任务;端点立即返回,进度以 wing-overview 轮询)。 */
|
|
516
597
|
export interface WingBackfillResponse {
|
|
@@ -650,6 +731,11 @@ export interface ListRecordsRequest {
|
|
|
650
731
|
* Room 由 tags 派生(自生长),故这里传的是**具体 tag 名**而非枚举 id。
|
|
651
732
|
*/
|
|
652
733
|
tag?: string;
|
|
734
|
+
/**
|
|
735
|
+
* 孤儿记忆筛选:true = 只列**没有任何 Room(tags 为空/缺失)**的记录。
|
|
736
|
+
* 与 `tag` 互斥;同传时 `tag` 优先(客户端保证不同传)。
|
|
737
|
+
*/
|
|
738
|
+
untagged?: boolean;
|
|
653
739
|
/**
|
|
654
740
|
* 退场(软删)筛查:三态。省略 = 全部(活跃+已退场混排,靠 `UiRecord.retired`
|
|
655
741
|
* 徽标区分);`false` = 仅活跃;`true` = 仅已退场。
|
|
@@ -686,6 +772,15 @@ export interface UiRecord {
|
|
|
686
772
|
sourceAnchors: string[];
|
|
687
773
|
/** 检索相关度(列表路径无 score → null)。 */
|
|
688
774
|
score: number | null;
|
|
775
|
+
/** 老化权重:读路径衰减系数 max(0.5, 0.5^(Δ天/半衰期))(Δ=updatedAt;半衰期 0=关 → null)。 */
|
|
776
|
+
agingWeight?: number | null;
|
|
777
|
+
/** 召回使用统计:**进 topN(被注入)才算 used**;参与尝试未进 = 仅 attempts。缺省 = 未装配。 */
|
|
778
|
+
usage?: {
|
|
779
|
+
attempts: number;
|
|
780
|
+
used: number;
|
|
781
|
+
lastAttemptAt: number | null;
|
|
782
|
+
lastUsedAt: number | null;
|
|
783
|
+
} | null;
|
|
689
784
|
/**
|
|
690
785
|
* 是否已退场(软删)。`true` 时该记录在活动列表里仍以「已退场」态呈现(置灰 + 徽标),
|
|
691
786
|
* 可被 `records-restore` 找回——这正是设计要的「不静默消失、可恢复」,而非真删。
|
|
@@ -704,6 +799,13 @@ export interface ListRecordsResponse {
|
|
|
704
799
|
truncated: boolean;
|
|
705
800
|
/** 场景筛选下拉选项(仅 offset===0 时附带)。 */
|
|
706
801
|
scenes?: string[];
|
|
802
|
+
/** 召回使用汇总(老化权重效果口径:进 topN 才算 used)。未装配 = undefined。 */
|
|
803
|
+
usageSummary?: {
|
|
804
|
+
tracked: number;
|
|
805
|
+
attempted: number;
|
|
806
|
+
used: number;
|
|
807
|
+
rate: number;
|
|
808
|
+
} | null;
|
|
707
809
|
/**
|
|
708
810
|
* Wing 词表(R8 单一事实源:服务端随 list-records 下发,client 不再手抄;仅 offset===0 时附带)。
|
|
709
811
|
* 含 8 角 + `general`(跨域兜底,存量大 reserved 值仍可筛)。缺省(旧服务端)时 client
|
|
@@ -913,6 +1015,50 @@ export interface LogTailRequest {
|
|
|
913
1015
|
export interface LogTailResponse {
|
|
914
1016
|
lines: string[];
|
|
915
1017
|
}
|
|
1018
|
+
/** dsh-memory/trace-tail(§F 追踪事件流,memorax-absorb Wave 4) */
|
|
1019
|
+
export interface RecallTraceEvent {
|
|
1020
|
+
kind: 'recall_turn';
|
|
1021
|
+
ts: number;
|
|
1022
|
+
sessionId: string;
|
|
1023
|
+
turn?: number;
|
|
1024
|
+
/** metadata-only 默认:query 长度 + sha256 前 16 位;captureContent 才存原文(≤200)。 */
|
|
1025
|
+
queryChars: number;
|
|
1026
|
+
querySha?: string;
|
|
1027
|
+
queryText?: string;
|
|
1028
|
+
hitIds: string[];
|
|
1029
|
+
hitScores: number[];
|
|
1030
|
+
injectedIds: string[];
|
|
1031
|
+
suppressedCount: number;
|
|
1032
|
+
durationMs: number;
|
|
1033
|
+
outcome: 'injected' | 'suppressed' | 'timeout' | 'off';
|
|
1034
|
+
}
|
|
1035
|
+
export interface DistillTraceEvent {
|
|
1036
|
+
kind: 'distill_run';
|
|
1037
|
+
ts: number;
|
|
1038
|
+
layer: 'l1' | 'l2' | 'l3';
|
|
1039
|
+
sessionId: string;
|
|
1040
|
+
/** 仅 l1 有(与 l1_receipts 同一 runId,可交叉审计)。 */
|
|
1041
|
+
runId?: string;
|
|
1042
|
+
inputChars: number;
|
|
1043
|
+
/** 仅 l1:决策六值词表聚合(store/update/merge/skip/conflict/skip_missing)。 */
|
|
1044
|
+
byKind?: Record<string, number>;
|
|
1045
|
+
newRecords?: number;
|
|
1046
|
+
durationMs: number;
|
|
1047
|
+
provider?: string;
|
|
1048
|
+
model?: string;
|
|
1049
|
+
ok: boolean;
|
|
1050
|
+
errorKind?: string;
|
|
1051
|
+
}
|
|
1052
|
+
export type TraceEvent = RecallTraceEvent | DistillTraceEvent;
|
|
1053
|
+
export interface TraceTailRequest {
|
|
1054
|
+
/** 1~500,默认 100。 */
|
|
1055
|
+
lines?: number;
|
|
1056
|
+
/** 缺省 = 两类都取(按时间混合)。 */
|
|
1057
|
+
kind?: 'recall_turn' | 'distill_run';
|
|
1058
|
+
}
|
|
1059
|
+
export interface TraceTailResponse {
|
|
1060
|
+
events: TraceEvent[];
|
|
1061
|
+
}
|
|
916
1062
|
/** dsh-memory/rebuild-* */
|
|
917
1063
|
export type RebuildStatusResponse = {
|
|
918
1064
|
supported: false;
|
|
@@ -1157,6 +1303,9 @@ export interface DshMemoryRequestMap {
|
|
|
1157
1303
|
'dsh-memory/session-mode-set': SessionModeSetRequest;
|
|
1158
1304
|
'dsh-memory/wing-overview': Record<string, never>;
|
|
1159
1305
|
'dsh-memory/rooms-get': Record<string, never>;
|
|
1306
|
+
'dsh-memory/rooms-export': RoomsExportRequest;
|
|
1307
|
+
'dsh-memory/room-register': RoomRegisterRequest;
|
|
1308
|
+
'dsh-memory/room-admin': RoomAdminRequest;
|
|
1160
1309
|
'dsh-memory/wing-backfill': Record<string, never>;
|
|
1161
1310
|
'dsh-memory/session-stats': SessionStatsRequest;
|
|
1162
1311
|
'dsh-memory/settings-get': Record<string, never>;
|
|
@@ -1172,6 +1321,7 @@ export interface DshMemoryRequestMap {
|
|
|
1172
1321
|
'dsh-memory/scenes': Record<string, never>;
|
|
1173
1322
|
'dsh-memory/persona': Record<string, never>;
|
|
1174
1323
|
'dsh-memory/log-tail': LogTailRequest;
|
|
1324
|
+
'dsh-memory/trace-tail': TraceTailRequest;
|
|
1175
1325
|
'dsh-memory/rebuild-status': Record<string, never>;
|
|
1176
1326
|
'dsh-memory/rebuild-start': Record<string, never>;
|
|
1177
1327
|
'dsh-memory/rebuild-cancel': Record<string, never>;
|
|
@@ -1201,6 +1351,9 @@ export interface DshMemoryResponseMap {
|
|
|
1201
1351
|
'dsh-memory/session-mode-set': SessionModeSetResponse;
|
|
1202
1352
|
'dsh-memory/wing-overview': WingOverviewResponse;
|
|
1203
1353
|
'dsh-memory/rooms-get': RoomsGetResponse;
|
|
1354
|
+
'dsh-memory/rooms-export': RoomsExportResponse;
|
|
1355
|
+
'dsh-memory/room-register': RoomRegisterResponse;
|
|
1356
|
+
'dsh-memory/room-admin': RoomAdminResponse;
|
|
1204
1357
|
'dsh-memory/wing-backfill': WingBackfillResponse;
|
|
1205
1358
|
'dsh-memory/session-stats': SessionStatsResponse;
|
|
1206
1359
|
'dsh-memory/settings-get': SettingsGetResponse;
|
|
@@ -1216,6 +1369,7 @@ export interface DshMemoryResponseMap {
|
|
|
1216
1369
|
'dsh-memory/scenes': ScenesResponse;
|
|
1217
1370
|
'dsh-memory/persona': PersonaResponse;
|
|
1218
1371
|
'dsh-memory/log-tail': LogTailResponse;
|
|
1372
|
+
'dsh-memory/trace-tail': TraceTailResponse;
|
|
1219
1373
|
'dsh-memory/rebuild-status': RebuildStatusResponse;
|
|
1220
1374
|
'dsh-memory/rebuild-start': RebuildStatus;
|
|
1221
1375
|
'dsh-memory/rebuild-cancel': RebuildStatus;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §A 崩溃恢复 · 折叠纯函数(memorax-absorb Wave 1 / task_6)。
|
|
3
|
+
*
|
|
4
|
+
* 宿主在 turn 中途崩溃时,插件的内存缓冲丢失、该轮永不落 L0——但事件日志就在
|
|
5
|
+
* 宿主持久化层里,且 persistence 会在 reload 时给崩溃孤儿轮补写
|
|
6
|
+
* `turn/end{reason:'interrupted'}` 闭合标记(运行期实证:本机 98 处,
|
|
7
|
+
* 见计划 evidence/probe-runtime.json)。resume 时对账 `(水位线, 末尾]` 内
|
|
8
|
+
* bracket 完整的 turn,经 capture 的正常路径落 L0 + 蒸馏。
|
|
9
|
+
*
|
|
10
|
+
* 本模块只做**纯折叠**:events in → bracket 列表 out,零 I/O、零时钟、零随机。
|
|
11
|
+
* 幂等(水位线 + 逐 turn 存在性检查)与 off 档排除在调用方(capture.ts 接线层)。
|
|
12
|
+
*
|
|
13
|
+
* 切片语义与 CaptureBuffers.takeTurn 对齐:含 turn/end、不含 turn/start
|
|
14
|
+
* (turn/start 与 turn/end 本身不产消息,与既有转换器的类型过滤一致)。
|
|
15
|
+
*/
|
|
16
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
17
|
+
/** 一个可恢复的完整轮次括号;events 为 (startSeq, endSeq] 切片。 */
|
|
18
|
+
export interface RecoveredTurn {
|
|
19
|
+
turn: number;
|
|
20
|
+
startSeq: number;
|
|
21
|
+
endSeq: number;
|
|
22
|
+
events: SessionEvent[];
|
|
23
|
+
}
|
|
24
|
+
export interface FoldOptions {
|
|
25
|
+
/** 最多恢复的轮数(取最靠尾的 N 个);缺省 2——只补尾轮,非历史倾倒。 */
|
|
26
|
+
maxTurns?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 从持久化事件日志折叠出可恢复轮次。
|
|
30
|
+
*
|
|
31
|
+
* - **bracket 完整性**:只有 `turn/start(T) … turn/end(T)` 成对(任意 reason——
|
|
32
|
+
* interrupted 与 completed 同等对待,插件停机期间宿主正常完成的轮次同样缺 L0)才入选;
|
|
33
|
+
* 未闭合 tail 跳过(宿主 reload 补闭合后,下次 resume 自然纳入)。
|
|
34
|
+
* - **水位线窗口**:只取 `turn > watermark`(watermark=undefined 视为会话 L0 全空,
|
|
35
|
+
* 全部入选)。
|
|
36
|
+
* - **上限**:按 turn 取最靠尾的 `maxTurns`(缺省 2)个,返回按 startSeq 升序
|
|
37
|
+
* (调用方按序落 L0,保持时序)。
|
|
38
|
+
* - **防御**:输入乱序时按 seq 稳定排序;同 turn 重复 bracket(代际文件重叠等畸形)
|
|
39
|
+
* 保留 endSeq 更晚的一个;`data.turn` 非有限数字的括号一律不折叠。
|
|
40
|
+
*/
|
|
41
|
+
export declare function foldRecoverableTurns(events: readonly SessionEvent[], watermark: number | undefined, opts?: FoldOptions): RecoveredTurn[];
|
|
42
|
+
/**
|
|
43
|
+
* 宿主读日志服务的最小结构形状。真实类型随宿主版本漂移(compat 0.1.1-rc.2 与
|
|
44
|
+
* 检出主线已是两代 API),故只做**结构探针**——方法存在才调,逐级 try/catch。
|
|
45
|
+
*/
|
|
46
|
+
export interface HostRecoveryServices {
|
|
47
|
+
sessionQuery?: unknown;
|
|
48
|
+
sessionPersistence?: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* task_8 降级链:`agent.session.events` 不可得时,依次尝试
|
|
52
|
+
* ① `ctx.sessionQuery.readSession(id)`(live 或 persisted 均可,返回完整原始日志);
|
|
53
|
+
* ② `ctx.sessionPersistence.readFrom(id, 0)`(按 seq 范围读,0 = 从头取全量——
|
|
54
|
+
* 水位线以 turn 计,无 seq 对应,全量交由 fold + 上限 2 收敛)。
|
|
55
|
+
*
|
|
56
|
+
* 0.1.5 适配:readSession/readFrom 在宿主 0.1.5 上是 **async** 且解析为
|
|
57
|
+
* `{ session: { id }, events }`(见 src/store/evidence-source.ts 的
|
|
58
|
+
* SessionQueryLike);0.1.7+ 为同步。本函数统一 await,`accepts` 同时认
|
|
59
|
+
* 顶层 `events` 与 `session.events` 两种放置。
|
|
60
|
+
*
|
|
61
|
+
* 返回 undefined = 两级均不可用/均失败(调用方走一次性提示 + 维持现状)。
|
|
62
|
+
* 守卫纪律与 recall.ts 的 loadStored 探针一致:不假设方法存在,失败静默降级。
|
|
63
|
+
*/
|
|
64
|
+
export declare function readPersistedEventsViaServices(svcs: HostRecoveryServices, sessionId: string): Promise<SessionEvent[] | undefined>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
function turnOf(event) {
|
|
2
|
+
const t = event.data?.turn;
|
|
3
|
+
return typeof t === 'number' && Number.isFinite(t) ? t : undefined;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 从持久化事件日志折叠出可恢复轮次。
|
|
7
|
+
*
|
|
8
|
+
* - **bracket 完整性**:只有 `turn/start(T) … turn/end(T)` 成对(任意 reason——
|
|
9
|
+
* interrupted 与 completed 同等对待,插件停机期间宿主正常完成的轮次同样缺 L0)才入选;
|
|
10
|
+
* 未闭合 tail 跳过(宿主 reload 补闭合后,下次 resume 自然纳入)。
|
|
11
|
+
* - **水位线窗口**:只取 `turn > watermark`(watermark=undefined 视为会话 L0 全空,
|
|
12
|
+
* 全部入选)。
|
|
13
|
+
* - **上限**:按 turn 取最靠尾的 `maxTurns`(缺省 2)个,返回按 startSeq 升序
|
|
14
|
+
* (调用方按序落 L0,保持时序)。
|
|
15
|
+
* - **防御**:输入乱序时按 seq 稳定排序;同 turn 重复 bracket(代际文件重叠等畸形)
|
|
16
|
+
* 保留 endSeq 更晚的一个;`data.turn` 非有限数字的括号一律不折叠。
|
|
17
|
+
*/
|
|
18
|
+
export function foldRecoverableTurns(events, watermark, opts) {
|
|
19
|
+
const maxTurns = opts?.maxTurns ?? 2;
|
|
20
|
+
const sorted = [...events].sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
21
|
+
const open = new Map();
|
|
22
|
+
const brackets = [];
|
|
23
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
24
|
+
const event = sorted[i];
|
|
25
|
+
if (event.type === 'turn/start') {
|
|
26
|
+
const turn = turnOf(event);
|
|
27
|
+
if (turn !== undefined)
|
|
28
|
+
open.set(turn, { startSeq: event.seq, startIdx: i });
|
|
29
|
+
}
|
|
30
|
+
else if (event.type === 'turn/end') {
|
|
31
|
+
const turn = turnOf(event);
|
|
32
|
+
const start = turn !== undefined ? open.get(turn) : undefined;
|
|
33
|
+
if (turn === undefined || start === undefined)
|
|
34
|
+
continue;
|
|
35
|
+
open.delete(turn);
|
|
36
|
+
brackets.push({
|
|
37
|
+
turn,
|
|
38
|
+
startSeq: start.startSeq,
|
|
39
|
+
endSeq: event.seq,
|
|
40
|
+
events: sorted.slice(start.startIdx + 1, i + 1),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const eligible = brackets.filter((b) => watermark === undefined || b.turn > watermark);
|
|
45
|
+
const byTurn = new Map();
|
|
46
|
+
for (const b of eligible) {
|
|
47
|
+
const prev = byTurn.get(b.turn);
|
|
48
|
+
if (prev === undefined || b.endSeq >= prev.endSeq)
|
|
49
|
+
byTurn.set(b.turn, b);
|
|
50
|
+
}
|
|
51
|
+
return [...byTurn.values()].sort((a, b) => a.turn - b.turn).slice(-maxTurns).sort((a, b) => a.startSeq - b.startSeq);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* task_8 降级链:`agent.session.events` 不可得时,依次尝试
|
|
55
|
+
* ① `ctx.sessionQuery.readSession(id)`(live 或 persisted 均可,返回完整原始日志);
|
|
56
|
+
* ② `ctx.sessionPersistence.readFrom(id, 0)`(按 seq 范围读,0 = 从头取全量——
|
|
57
|
+
* 水位线以 turn 计,无 seq 对应,全量交由 fold + 上限 2 收敛)。
|
|
58
|
+
*
|
|
59
|
+
* 0.1.5 适配:readSession/readFrom 在宿主 0.1.5 上是 **async** 且解析为
|
|
60
|
+
* `{ session: { id }, events }`(见 src/store/evidence-source.ts 的
|
|
61
|
+
* SessionQueryLike);0.1.7+ 为同步。本函数统一 await,`accepts` 同时认
|
|
62
|
+
* 顶层 `events` 与 `session.events` 两种放置。
|
|
63
|
+
*
|
|
64
|
+
* 返回 undefined = 两级均不可用/均失败(调用方走一次性提示 + 维持现状)。
|
|
65
|
+
* 守卫纪律与 recall.ts 的 loadStored 探针一致:不假设方法存在,失败静默降级。
|
|
66
|
+
*/
|
|
67
|
+
export async function readPersistedEventsViaServices(svcs, sessionId) {
|
|
68
|
+
const unwrap = (r) => {
|
|
69
|
+
if (r === null || typeof r !== 'object')
|
|
70
|
+
return undefined;
|
|
71
|
+
const o = r;
|
|
72
|
+
if (Array.isArray(o.events) && o.events.length > 0)
|
|
73
|
+
return o.events;
|
|
74
|
+
const inner = o.session;
|
|
75
|
+
if (inner !== null && typeof inner === 'object' && Array.isArray(inner.events) && inner.events.length > 0) {
|
|
76
|
+
return inner.events;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
80
|
+
const q = svcs.sessionQuery;
|
|
81
|
+
if (q !== null && typeof q === 'object' && typeof q.readSession === 'function') {
|
|
82
|
+
try {
|
|
83
|
+
const result = await q.readSession(sessionId);
|
|
84
|
+
const events = unwrap(result);
|
|
85
|
+
if (events)
|
|
86
|
+
return events;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* 落到下一级 */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const p = svcs.sessionPersistence;
|
|
93
|
+
if (p !== null && typeof p === 'object' && typeof p.readFrom === 'function') {
|
|
94
|
+
try {
|
|
95
|
+
const result = await p.readFrom(sessionId, 0);
|
|
96
|
+
const events = unwrap(result);
|
|
97
|
+
if (events)
|
|
98
|
+
return events;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* 均不可用 */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
package/dist/hooks/capture.js
CHANGED
|
@@ -6,7 +6,32 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomBytes } from 'node:crypto';
|
|
8
8
|
import { blocksToText } from '../util/text.js';
|
|
9
|
+
import { redactSecrets } from '../util/redact.js';
|
|
9
10
|
import { sanitizeText, shouldCaptureL0, stripCodeBlocks } from '../util/sanitize.js';
|
|
11
|
+
import { foldRecoverableTurns, readPersistedEventsViaServices } from './capture-recovery.js';
|
|
12
|
+
import { confirmInjectionByMessageId } from './recall-ack.js';
|
|
13
|
+
/** §C 一次性告警标记:脱敏自身异常时放行原文,只提示一次(不阻断捕获)。 */
|
|
14
|
+
let redactWarnedOnce = false;
|
|
15
|
+
/**
|
|
16
|
+
* §C 捕获单点脱敏(memorax-absorb):`capture.redactSecrets !== false` 时把
|
|
17
|
+
* 8 类密钥替换为 `[REDACTED:<KIND>]`。位置在 sanitize 之后、L0/蒸馏分发之前——
|
|
18
|
+
* 同一份 messages 覆盖 L0 JSONL/L0 SQLite/蒸馏输入三条下游,无分裂态。
|
|
19
|
+
* 红线:脱敏失败放行原文 + 一次性 warn(不阻断捕获)。
|
|
20
|
+
*/
|
|
21
|
+
function redactContent(content, cfg, logger) {
|
|
22
|
+
if (cfg.capture?.redactSecrets === false)
|
|
23
|
+
return content;
|
|
24
|
+
try {
|
|
25
|
+
return redactSecrets(content).text;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (!redactWarnedOnce) {
|
|
29
|
+
redactWarnedOnce = true;
|
|
30
|
+
logger.warn(`[memory] 脱敏失败,本轮放行原文(一次性提示): ${err instanceof Error ? err.message : String(err)}`);
|
|
31
|
+
}
|
|
32
|
+
return content;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
10
35
|
/**
|
|
11
36
|
* 需要进缓冲的事件类型。流式 chunk(text-delta/reasoning 等)一秒钟可达数百条,
|
|
12
37
|
* 缓冲它们会把 MAX_BUFFER 撑爆、把轮次头部(turn/start + user 消息)裁掉——
|
|
@@ -70,6 +95,8 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
70
95
|
return;
|
|
71
96
|
const startFloor = Date.now();
|
|
72
97
|
const buffers = new CaptureBuffers();
|
|
98
|
+
// task_8 一次性提示标记:宿主读日志服务不可得时只提示一次(防每次 resume 刷屏)
|
|
99
|
+
let warnedRecoveryUnavailable = false;
|
|
73
100
|
// L0 即时落盘链:turn/end 立刻写,不被蒸馏队列(慢 LLM 调用)阻塞;
|
|
74
101
|
// 串行化保证同轮次顺序与单次写入(进程退出时排队中的 L0 不再依赖蒸馏完成)
|
|
75
102
|
let l0Queue = Promise.resolve();
|
|
@@ -79,6 +106,15 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
79
106
|
if (!s.enabled || !s.capture)
|
|
80
107
|
return;
|
|
81
108
|
const sid = String(session.id ?? session);
|
|
109
|
+
// §D 日志回执(memorax-absorb):注入消息以原样 user/message 入日志时在此确认,
|
|
110
|
+
// 才真正 dedupe.mark。**必须置于 source.kind 过滤之前**——过滤在
|
|
111
|
+
// turnEventsToMessages 内且只放行 kind='user',注入消息(kind='plugin',本线 v3 署名)
|
|
112
|
+
// 走不到那里;也置于 off 档返回之前(确认与档位无关,且代价是一次 Map 查找)。
|
|
113
|
+
if (event.type === 'user/message') {
|
|
114
|
+
const mid = event.data.id;
|
|
115
|
+
if (typeof mid === 'string' && mid)
|
|
116
|
+
confirmInjectionByMessageId(mid);
|
|
117
|
+
}
|
|
82
118
|
// off 档:本会话对记忆系统完全隐身(不缓冲、不写 L0、不蒸馏)
|
|
83
119
|
if (modes.get(sid) === 'off')
|
|
84
120
|
return;
|
|
@@ -124,6 +160,91 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
124
160
|
logger.warn(`[memory] session/event 处理失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
161
|
}
|
|
126
162
|
});
|
|
163
|
+
// §A 恢复共用水位线对账段(同步主路径与异步降级延续共用;mode 由调用方传入,
|
|
164
|
+
// 调用点已被 off 早退收窄,故直接取 enqueue 的形参类型)
|
|
165
|
+
const recoverFromEvents = (sid, mode, events) => {
|
|
166
|
+
const watermark = l0.maxCapturedTurn(sid);
|
|
167
|
+
const brackets = foldRecoverableTurns(events, watermark);
|
|
168
|
+
if (brackets.length === 0)
|
|
169
|
+
return;
|
|
170
|
+
let recovered = 0;
|
|
171
|
+
let skipped = 0;
|
|
172
|
+
let total = 0;
|
|
173
|
+
for (const bracket of brackets) {
|
|
174
|
+
if (l0.hasAnyL0Message(sid, bracket.turn)) {
|
|
175
|
+
// 部分写残行:水位线只判"整轮无",存在性检查兜底——整轮跳过,宁少收不重复
|
|
176
|
+
skipped += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const messages = turnEventsToMessages(bracket.events, cfg, logger, sid, bracket.turn);
|
|
180
|
+
if (messages.length === 0)
|
|
181
|
+
continue;
|
|
182
|
+
recovered += 1;
|
|
183
|
+
total += messages.length;
|
|
184
|
+
const n = messages.length;
|
|
185
|
+
l0Queue = l0Queue
|
|
186
|
+
.then(() => l0.append(sid, messages))
|
|
187
|
+
.then(() => logger.info(`[memory] L0 恢复落盘 ${n} 条`))
|
|
188
|
+
.catch((err) => logger.warn(`[memory] L0 恢复落盘失败: ${err instanceof Error ? err.message : String(err)}`));
|
|
189
|
+
runner.enqueue(sid, messages, mode);
|
|
190
|
+
}
|
|
191
|
+
if (recovered > 0 || skipped > 0) {
|
|
192
|
+
logger.info(`[memory] resume 对账:水位线 ${watermark ?? '无'} → 恢复 ${recovered} 轮 ${total} 条,幂等跳过 ${skipped} 轮(session=${sid})`);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
// ── §A 崩溃恢复(memorax-absorb Wave 1 / task_7):resume 时对账宿主持久化日志,
|
|
196
|
+
// 补收 (L0 水位线, 末尾] 内 bracket 完整的尾轮(≤2 轮)——崩溃轮在缓冲丢失后
|
|
197
|
+
// 仍可从持久化层找回(persistence 会给孤儿轮补 interrupted 闭合,探针② 实证 98 处)。
|
|
198
|
+
// fail-open 纪律:任何异常只 warn,不阻塞会话启动;幂等 = 水位线 + 逐 turn 存在性检查。
|
|
199
|
+
ctx.on('agent/session-start', (payload) => {
|
|
200
|
+
try {
|
|
201
|
+
if (payload.source !== 'resume')
|
|
202
|
+
return;
|
|
203
|
+
const s = live.get();
|
|
204
|
+
if (!s.enabled || !s.capture)
|
|
205
|
+
return;
|
|
206
|
+
const session = payload.agent?.session;
|
|
207
|
+
const sid = session?.id !== undefined && session.id !== null ? String(session.id) : '';
|
|
208
|
+
if (!sid)
|
|
209
|
+
return;
|
|
210
|
+
const mode = modes.get(sid);
|
|
211
|
+
if (mode === 'off')
|
|
212
|
+
return; // off 档对记忆系统完全隐身,对齐现行为
|
|
213
|
+
const primary = session?.events;
|
|
214
|
+
if (primary && primary.length > 0) {
|
|
215
|
+
recoverFromEvents(sid, mode, primary);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// task_8 降级链:events 不可得(宿主版本偏差)时依次试
|
|
219
|
+
// ctx.sessionQuery.readSession / ctx.sessionPersistence.readFrom(守卫探针式)。
|
|
220
|
+
// 0.1.5 适配:两级服务都是 async,故走异步延续;listener 保持同步
|
|
221
|
+
// (0.1.5 无 serial 契约),延续内部自捕获异常,fail-open 纪律不变。
|
|
222
|
+
const get = ctx.get;
|
|
223
|
+
const svcs = typeof get === 'function'
|
|
224
|
+
? {
|
|
225
|
+
sessionQuery: get.call(ctx, 'sessionQuery'),
|
|
226
|
+
sessionPersistence: get.call(ctx, 'sessionPersistence'),
|
|
227
|
+
}
|
|
228
|
+
: {};
|
|
229
|
+
void readPersistedEventsViaServices(svcs, sid)
|
|
230
|
+
.then((degraded) => {
|
|
231
|
+
if (degraded === undefined) {
|
|
232
|
+
if (!warnedRecoveryUnavailable) {
|
|
233
|
+
warnedRecoveryUnavailable = true;
|
|
234
|
+
logger.info('[memory] resume 恢复:宿主读日志服务不可得,跳过对账(一次性提示)');
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
recoverFromEvents(sid, mode, degraded);
|
|
239
|
+
})
|
|
240
|
+
.catch((err) => {
|
|
241
|
+
logger.warn(`[memory] resume 恢复失败(不阻塞会话启动): ${err instanceof Error ? err.message : String(err)}`);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
logger.warn(`[memory] resume 恢复失败(不阻塞会话启动): ${err instanceof Error ? err.message : String(err)}`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
127
248
|
// 冲刷 = 等待串行链排空(链上每环自带 catch,永不 reject)。
|
|
128
249
|
// 注:极小概率在 await 期间又入队的新消息会落到链尾——其 JSONL 事实源照写、
|
|
129
250
|
// DB 写入由 upsert 内部兜底(关库后 warn),下次重建自愈。
|
|
@@ -190,7 +311,7 @@ function turnEventsToMessages(events, cfg, logger, sessionId, turn) {
|
|
|
190
311
|
logger.info(`[memory] L0 跳过非用户来源消息(source.kind=${msg.source?.kind ?? 'none'})`);
|
|
191
312
|
continue;
|
|
192
313
|
}
|
|
193
|
-
const content = sanitizeText(blocksToText(msg.content));
|
|
314
|
+
const content = redactContent(sanitizeText(blocksToText(msg.content)), cfg, logger);
|
|
194
315
|
if (shouldCaptureL0(content)) {
|
|
195
316
|
const anchor = { sessionId, turn };
|
|
196
317
|
if (currentStep !== undefined)
|
|
@@ -203,6 +324,7 @@ function turnEventsToMessages(events, cfg, logger, sessionId, turn) {
|
|
|
203
324
|
let content = sanitizeText(blocksToText(data.message?.content));
|
|
204
325
|
if (cfg.capture.stripCodeBlocks)
|
|
205
326
|
content = stripCodeBlocks(content);
|
|
327
|
+
content = redactContent(content, cfg, logger);
|
|
206
328
|
if (shouldCaptureL0(content)) {
|
|
207
329
|
// 事件自带 turn/step 优先(fold 只服务于不带该字段的事件类型)
|
|
208
330
|
const evTurn = typeof data.turn === 'number' && Number.isFinite(data.turn) ? data.turn : turn;
|