dsh-prime-memory 0.18.2 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.en.md +14 -0
  2. package/CHANGELOG.ja.md +14 -0
  3. package/CHANGELOG.ko.md +14 -0
  4. package/CHANGELOG.md +42 -0
  5. package/README.en.md +4 -0
  6. package/README.ja.md +4 -0
  7. package/README.ko.md +4 -0
  8. package/README.md +4 -0
  9. package/cordis.patch.yml +6 -0
  10. package/dist/client.js +1333 -168
  11. package/dist/config.d.ts +37 -0
  12. package/dist/config.js +7 -0
  13. package/dist/contract.d.ts +155 -1
  14. package/dist/hooks/capture-recovery.d.ts +59 -0
  15. package/dist/hooks/capture-recovery.js +88 -0
  16. package/dist/hooks/capture.js +114 -1
  17. package/dist/hooks/recall-ack.d.ts +48 -0
  18. package/dist/hooks/recall-ack.js +79 -0
  19. package/dist/hooks/recall.d.ts +23 -1
  20. package/dist/hooks/recall.js +124 -8
  21. package/dist/index.d.ts +22 -0
  22. package/dist/index.js +18 -4
  23. package/dist/metadata-validators.d.ts +5 -0
  24. package/dist/metadata-validators.js +11 -1
  25. package/dist/pipeline/l1.d.ts +4 -0
  26. package/dist/pipeline/l1.js +9 -2
  27. package/dist/pipeline/reconcile.js +4 -0
  28. package/dist/pipeline/ruminate.d.ts +11 -1
  29. package/dist/pipeline/ruminate.js +18 -1
  30. package/dist/pipeline/runner.js +62 -0
  31. package/dist/prompts/boundary.d.ts +17 -0
  32. package/dist/prompts/boundary.js +20 -0
  33. package/dist/prompts/graph-projection.js +4 -1
  34. package/dist/prompts/l1-dedup.d.ts +3 -3
  35. package/dist/prompts/l1-dedup.js +9 -1
  36. package/dist/prompts/l1-extraction.d.ts +3 -3
  37. package/dist/prompts/l1-extraction.js +12 -0
  38. package/dist/prompts/persona.js +6 -0
  39. package/dist/prompts/scene.js +7 -1
  40. package/dist/room-review.d.ts +196 -0
  41. package/dist/room-review.js +281 -0
  42. package/dist/stats.d.ts +26 -2
  43. package/dist/stats.js +270 -13
  44. package/dist/store/l0.d.ts +4 -0
  45. package/dist/store/l0.js +8 -0
  46. package/dist/store/l1.d.ts +34 -0
  47. package/dist/store/l1.js +68 -1
  48. package/dist/store/recall-usage.d.ts +43 -0
  49. package/dist/store/recall-usage.js +149 -0
  50. package/dist/store/rooms-registry.d.ts +66 -0
  51. package/dist/store/rooms-registry.js +250 -0
  52. package/dist/store/sqlite.d.ts +16 -0
  53. package/dist/store/sqlite.js +57 -0
  54. package/dist/store/trace.d.ts +39 -0
  55. package/dist/store/trace.js +149 -0
  56. package/dist/tools/index.d.ts +4 -1
  57. package/dist/tools/index.js +249 -2
  58. package/dist/types.d.ts +4 -0
  59. package/dist/util/redact.d.ts +27 -0
  60. package/dist/util/redact.js +228 -0
  61. package/package.json +13 -12
  62. package/dist/hall-backfill.d.ts +0 -33
  63. package/dist/hall-backfill.js +0 -95
  64. package/dist/hall-migrate.d.ts +0 -32
  65. package/dist/hall-migrate.js +0 -88
package/dist/config.d.ts CHANGED
@@ -29,6 +29,21 @@ export interface MemoryConfig {
29
29
  stripCodeBlocks: boolean;
30
30
  /** 单条消息内容最大字符数。 */
31
31
  maxMessageChars: number;
32
+ /**
33
+ * §C 载荷脱敏(memorax-absorb):捕获单点把 8 类密钥(PEM/Bearer/JWT/Cookie/
34
+ * 厂商 API key/邮箱/长号/高熵 ID)替换为 `[REDACTED:<KIND>]` 占位符,覆盖
35
+ * L0 JSONL/SQLite/蒸馏输入与手动 memory_add/import。**默认开启**——密钥被
36
+ * 逐轮召回注入的危害大于偶发误脱敏;注意开启后 L0 原文自此改变(重建不可得原文)。
37
+ * 决策记录见计划 spec §C;`false` 一键回到明文行为。 */
38
+ redactSecrets?: boolean;
39
+ };
40
+ /** §F 结构化追踪(memorax-absorb):recall/distill 事件按天 JSONL。 */
41
+ trace: {
42
+ enabled: boolean;
43
+ /** 事件文件保留天数(写入时切日清理;0 = 永久)。 */
44
+ retentionDays: number;
45
+ /** 默认 metadata-only(chars+sha);true 才存 query 原文(≤200 字符)。 */
46
+ captureContent: boolean;
32
47
  };
33
48
  extract: {
34
49
  enabled: boolean;
@@ -407,10 +422,21 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<NoInfer<{
407
422
  enabled: Schema<boolean, boolean, "defined">;
408
423
  stripCodeBlocks: Schema<boolean, boolean, "defined">;
409
424
  maxMessageChars: Schema<number, number, "defined">;
425
+ redactSecrets: Schema<boolean, boolean, "defined">;
410
426
  }>>, Schemastery.ObjectT<NoInfer<{
411
427
  enabled: Schema<boolean, boolean, "defined">;
412
428
  stripCodeBlocks: Schema<boolean, boolean, "defined">;
413
429
  maxMessageChars: Schema<number, number, "defined">;
430
+ redactSecrets: Schema<boolean, boolean, "defined">;
431
+ }>>, "plain">;
432
+ trace: Schema<Schemastery.ObjectS<NoInfer<{
433
+ enabled: Schema<boolean, boolean, "defined">;
434
+ retentionDays: Schema<number, number, "defined">;
435
+ captureContent: Schema<boolean, boolean, "defined">;
436
+ }>>, Schemastery.ObjectT<NoInfer<{
437
+ enabled: Schema<boolean, boolean, "defined">;
438
+ retentionDays: Schema<number, number, "defined">;
439
+ captureContent: Schema<boolean, boolean, "defined">;
414
440
  }>>, "plain">;
415
441
  extract: Schema<Schemastery.ObjectS<NoInfer<{
416
442
  enabled: Schema<boolean, boolean, "defined">;
@@ -886,10 +912,21 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<NoInfer<{
886
912
  enabled: Schema<boolean, boolean, "defined">;
887
913
  stripCodeBlocks: Schema<boolean, boolean, "defined">;
888
914
  maxMessageChars: Schema<number, number, "defined">;
915
+ redactSecrets: Schema<boolean, boolean, "defined">;
889
916
  }>>, Schemastery.ObjectT<NoInfer<{
890
917
  enabled: Schema<boolean, boolean, "defined">;
891
918
  stripCodeBlocks: Schema<boolean, boolean, "defined">;
892
919
  maxMessageChars: Schema<number, number, "defined">;
920
+ redactSecrets: Schema<boolean, boolean, "defined">;
921
+ }>>, "plain">;
922
+ trace: Schema<Schemastery.ObjectS<NoInfer<{
923
+ enabled: Schema<boolean, boolean, "defined">;
924
+ retentionDays: Schema<number, number, "defined">;
925
+ captureContent: Schema<boolean, boolean, "defined">;
926
+ }>>, Schemastery.ObjectT<NoInfer<{
927
+ enabled: Schema<boolean, boolean, "defined">;
928
+ retentionDays: Schema<number, number, "defined">;
929
+ captureContent: Schema<boolean, boolean, "defined">;
893
930
  }>>, "plain">;
894
931
  extract: Schema<Schemastery.ObjectS<NoInfer<{
895
932
  enabled: Schema<boolean, boolean, "defined">;
package/dist/config.js CHANGED
@@ -84,6 +84,13 @@ export const memorySchema = Schema.object({
84
84
  enabled: Schema.boolean().default(true),
85
85
  stripCodeBlocks: Schema.boolean().default(true),
86
86
  maxMessageChars: Schema.number().min(200).max(200_000).default(4000),
87
+ // §C 默认开启:本地存储也不落明文密钥(决策记录见计划 spec §C)
88
+ redactSecrets: Schema.boolean().default(true),
89
+ }),
90
+ trace: Schema.object({
91
+ enabled: Schema.boolean().default(true),
92
+ retentionDays: Schema.number().min(0).max(365).default(14),
93
+ captureContent: Schema.boolean().default(false),
87
94
  }),
88
95
  extract: Schema.object({
89
96
  enabled: Schema.boolean().default(true),
@@ -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,59 @@
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
+ * 返回 undefined = 两级均不可用/均失败(调用方走一次性提示 + 维持现状)。
57
+ * 守卫纪律与 recall.ts 的 loadStored 探针一致:不假设方法存在,失败静默降级。
58
+ */
59
+ export declare function readPersistedEventsViaServices(svcs: HostRecoveryServices, sessionId: string): SessionEvent[] | undefined;
@@ -0,0 +1,88 @@
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
+ * 返回 undefined = 两级均不可用/均失败(调用方走一次性提示 + 维持现状)。
60
+ * 守卫纪律与 recall.ts 的 loadStored 探针一致:不假设方法存在,失败静默降级。
61
+ */
62
+ export function readPersistedEventsViaServices(svcs, sessionId) {
63
+ const accepts = (r) => r !== null && typeof r === 'object' && Array.isArray(r.events) &&
64
+ r.events.length > 0;
65
+ const q = svcs.sessionQuery;
66
+ if (q !== null && typeof q === 'object' && typeof q.readSession === 'function') {
67
+ try {
68
+ const result = q.readSession(sessionId);
69
+ if (accepts(result))
70
+ return result.events;
71
+ }
72
+ catch {
73
+ /* 落到下一级 */
74
+ }
75
+ }
76
+ const p = svcs.sessionPersistence;
77
+ if (p !== null && typeof p === 'object' && typeof p.readFrom === 'function') {
78
+ try {
79
+ const result = p.readFrom(sessionId, 0);
80
+ if (accepts(result))
81
+ return result.events;
82
+ }
83
+ catch {
84
+ /* 均不可用 */
85
+ }
86
+ }
87
+ return undefined;
88
+ }
@@ -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:memory')
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,82 @@ 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 崩溃恢复(memorax-absorb Wave 1 / task_7):resume 时对账宿主持久化日志,
164
+ // 补收 (L0 水位线, 末尾] 内 bracket 完整的尾轮(≤2 轮)——崩溃轮在缓冲丢失后
165
+ // 仍可从持久化层找回(persistence 会给孤儿轮补 interrupted 闭合,探针② 实证 98 处)。
166
+ // fail-open 纪律:任何异常只 warn,不阻塞会话启动;幂等 = 水位线 + 逐 turn 存在性检查。
167
+ // 0.1.7-rc.2 起 agent/session-start 并入 agent/created(serial:监听器需 async,
168
+ // 同 main 线 0.19.0 的 0.2.0 适配)。仍监听旧事件名的实现在 rc.2 宿主上永不触发。
169
+ ctx.on('agent/created', async (payload) => {
170
+ try {
171
+ if (payload.source !== 'resume')
172
+ return;
173
+ const s = live.get();
174
+ if (!s.enabled || !s.capture)
175
+ return;
176
+ const session = payload.agent?.session;
177
+ const sid = session?.id !== undefined && session.id !== null ? String(session.id) : '';
178
+ if (!sid)
179
+ return;
180
+ const mode = modes.get(sid);
181
+ if (mode === 'off')
182
+ return; // off 档对记忆系统完全隐身,对齐现行为
183
+ // 0.1.7-rc.2 起 Session 弃用同步全量 .events,改 snapshotEvents()(同 main 线
184
+ // 0.19.0 的 0.2.0 适配);探针式读取,旧宿主形状走下方降级链。
185
+ let events = typeof session?.snapshotEvents === 'function' ? session.snapshotEvents() : undefined;
186
+ if (!events || events.length === 0) {
187
+ // task_8 降级链:events 不可得(宿主版本偏差)时依次试
188
+ // ctx.sessionQuery.readSession / ctx.sessionPersistence.readFrom(守卫探针式)
189
+ const get = ctx.get;
190
+ const svcs = typeof get === 'function'
191
+ ? {
192
+ sessionQuery: get.call(ctx, 'sessionQuery'),
193
+ sessionPersistence: get.call(ctx, 'sessionPersistence'),
194
+ }
195
+ : {};
196
+ const degraded = readPersistedEventsViaServices(svcs, sid);
197
+ if (degraded === undefined) {
198
+ if (!warnedRecoveryUnavailable) {
199
+ warnedRecoveryUnavailable = true;
200
+ logger.info('[memory] resume 恢复:宿主读日志服务不可得,跳过对账(一次性提示)');
201
+ }
202
+ return;
203
+ }
204
+ events = degraded;
205
+ }
206
+ const watermark = l0.maxCapturedTurn(sid);
207
+ const brackets = foldRecoverableTurns(events, watermark);
208
+ if (brackets.length === 0)
209
+ return;
210
+ let recovered = 0;
211
+ let skipped = 0;
212
+ let total = 0;
213
+ for (const bracket of brackets) {
214
+ if (l0.hasAnyL0Message(sid, bracket.turn)) {
215
+ // 部分写残行:水位线只判"整轮无",存在性检查兜底——整轮跳过,宁少收不重复
216
+ skipped += 1;
217
+ continue;
218
+ }
219
+ const messages = turnEventsToMessages(bracket.events, cfg, logger, sid, bracket.turn);
220
+ if (messages.length === 0)
221
+ continue;
222
+ recovered += 1;
223
+ total += messages.length;
224
+ const n = messages.length;
225
+ l0Queue = l0Queue
226
+ .then(() => l0.append(sid, messages))
227
+ .then(() => logger.info(`[memory] L0 恢复落盘 ${n} 条`))
228
+ .catch((err) => logger.warn(`[memory] L0 恢复落盘失败: ${err instanceof Error ? err.message : String(err)}`));
229
+ runner.enqueue(sid, messages, mode);
230
+ }
231
+ if (recovered > 0 || skipped > 0) {
232
+ logger.info(`[memory] resume 对账:水位线 ${watermark ?? '无'} → 恢复 ${recovered} 轮 ${total} 条,幂等跳过 ${skipped} 轮(session=${sid})`);
233
+ }
234
+ }
235
+ catch (err) {
236
+ logger.warn(`[memory] resume 恢复失败(不阻塞会话启动): ${err instanceof Error ? err.message : String(err)}`);
237
+ }
238
+ });
127
239
  // 冲刷 = 等待串行链排空(链上每环自带 catch,永不 reject)。
128
240
  // 注:极小概率在 await 期间又入队的新消息会落到链尾——其 JSONL 事实源照写、
129
241
  // DB 写入由 upsert 内部兜底(关库后 warn),下次重建自愈。
@@ -190,7 +302,7 @@ function turnEventsToMessages(events, cfg, logger, sessionId, turn) {
190
302
  logger.info(`[memory] L0 跳过非用户来源消息(source.kind=${msg.source?.kind ?? 'none'})`);
191
303
  continue;
192
304
  }
193
- const content = sanitizeText(blocksToText(msg.content));
305
+ const content = redactContent(sanitizeText(blocksToText(msg.content)), cfg, logger);
194
306
  if (shouldCaptureL0(content)) {
195
307
  const anchor = { sessionId, turn };
196
308
  if (currentStep !== undefined)
@@ -203,6 +315,7 @@ function turnEventsToMessages(events, cfg, logger, sessionId, turn) {
203
315
  let content = sanitizeText(blocksToText(data.message?.content));
204
316
  if (cfg.capture.stripCodeBlocks)
205
317
  content = stripCodeBlocks(content);
318
+ content = redactContent(content, cfg, logger);
206
319
  if (shouldCaptureL0(content)) {
207
320
  // 事件自带 turn/step 优先(fold 只服务于不带该字段的事件类型)
208
321
  const evTurn = typeof data.turn === 'number' && Number.isFinite(data.turn) ? data.turn : turn;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * §D 注入确认——日志回执协议(memorax-absorb Wave 3 / task_20-22)。
3
+ *
4
+ * 问题:`dedupe.mark` 原在 pre-step 返回注入的瞬间执行,但宿主是否真的把该
5
+ * 注入消息收进会话历史,此时并未确认(pre-step 被其他插件覆盖/步骤取消时,
6
+ * 模型根本没看到)——误标记会把这些记忆压制整个会话。
7
+ *
8
+ * 协议(两段式):
9
+ * ① recall.ts 构造注入消息后只登记 pending(messageId → 记录 id 集),不标记;
10
+ * ② capture.ts 在 `session/event` 观察到该 `user/message` 真正入日志时确认,
11
+ * 此时才 `dedupe.mark`(与 §D 红线一致:确认匹配在 source.kind 过滤之前——
12
+ * 注入消息的 source.kind 是 `plugin:memory`,放过滤后永远到不了匹配)。
13
+ *
14
+ * 超时降级(task_22):pending 超 5 分钟未确认 → 照旧标记 + 一次性提示。
15
+ * 防确认链路自身故障导致 dedupe 永不生效、记忆每轮重注的回归;正常确认时延
16
+ * <2s(同进程事件循环)。未确认且未超时的 pending 过期即弃(不标记)——
17
+ * 模型没看到的东西不应被抑制,允许下轮重注。
18
+ *
19
+ * 进程内 volatile,容量 64 条 LRU(注入速率低,正常情况秒级确认,积压即异常)。
20
+ */
21
+ export interface PendingInjection {
22
+ agentId: string;
23
+ messageId: string;
24
+ recordIds: string[];
25
+ at: number;
26
+ }
27
+ /** 确认后执行的实际标记动作(recall.ts 注入 = dedupe.mark 的绑定)。 */
28
+ type InjectionMarker = (agentId: string, recordIds: string[]) => void;
29
+ export declare const PENDING_TIMEOUT_MS: number;
30
+ /** recall.ts 注册实际标记动作(dedupe.mark 绑定);重复注册以最后一次为准。 */
31
+ export declare function setInjectionMarker(fn: InjectionMarker): void;
32
+ /** ① 登记待确认注入(超容量淘汰最旧——积压到 64 条说明确认链路已异常)。 */
33
+ export declare function registerPendingInjection(agentId: string, messageId: string, recordIds: string[], now?: number): void;
34
+ /**
35
+ * ② 日志回执确认:该注入消息已真实进入会话历史。执行标记并移除 pending。
36
+ * @returns 是否命中一条 pending(未命中 = 与本插件注入无关的事件,静默)。
37
+ */
38
+ export declare function confirmInjectionByMessageId(messageId: string): boolean;
39
+ /**
40
+ * 超时清扫:超时的 pending 照旧标记(降级保连续,防 dedupe 失效回归),并移除。
41
+ * 未超时的不动。@returns 本次降级标记的条数(recall 侧据此前瞻性提示一次)。
42
+ */
43
+ export declare function expirePendingInjections(now?: number, timeoutMs?: number): number;
44
+ /** 诊断/测试:当前 pending 条数。 */
45
+ export declare function pendingCount(): number;
46
+ /** 测试隔离:清空 pending 与 marker(模块级状态,用例间互扰)。 */
47
+ export declare function resetAckForTests(): void;
48
+ export {};