dsh-prime-memory 0.18.2 → 0.18.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.en.md +14 -0
- package/CHANGELOG.ja.md +14 -0
- package/CHANGELOG.ko.md +14 -0
- package/CHANGELOG.md +14 -0
- package/README.en.md +4 -0
- package/README.ja.md +4 -0
- package/README.ko.md +4 -0
- package/README.md +4 -0
- package/cordis.patch.yml +6 -0
- package/dist/client.js +36 -9
- package/dist/config.d.ts +37 -0
- package/dist/config.js +7 -0
- package/dist/contract.d.ts +46 -0
- package/dist/hooks/capture-recovery.d.ts +59 -0
- package/dist/hooks/capture-recovery.js +88 -0
- package/dist/hooks/capture.js +110 -1
- package/dist/hooks/recall-ack.d.ts +48 -0
- package/dist/hooks/recall-ack.js +79 -0
- package/dist/hooks/recall.d.ts +19 -0
- package/dist/hooks/recall.js +114 -5
- package/dist/index.d.ts +22 -0
- package/dist/index.js +4 -0
- 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/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/stats.js +9 -0
- package/dist/store/l0.d.ts +4 -0
- package/dist/store/l0.js +8 -0
- package/dist/store/sqlite.d.ts +12 -0
- package/dist/store/sqlite.js +37 -0
- package/dist/store/trace.d.ts +39 -0
- package/dist/store/trace.js +149 -0
- package/dist/tools/index.js +25 -1
- package/dist/util/redact.d.ts +27 -0
- package/dist/util/redact.js +228 -0
- package/package.json +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -22,6 +22,20 @@ This file covers the **0.12.0** release notes and the current **unreleased** cha
|
|
|
22
22
|
- **Silent first-write loss regression**: lock files are created before the write; when the target's parent directory did not exist yet, `open('wx')` failed with ENOENT and stores swallowed it as a warn. `rmwJson` now `ensureDir`s before acquiring the lock (included from the main line).
|
|
23
23
|
- Also includes everything from the main line: file-layer hardening (atomic writes / read-side classification / version fail-closed / file locking / path safety — see [Unreleased] entries).
|
|
24
24
|
|
|
25
|
+
## [0.18.4] — 2026-09-28
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Crash recovery (§A)**: a turn interrupted by a host crash was previously lost forever with the in-memory capture buffer (the resume cold-start floor discarded all history). On `agent/session-start(resume)` the plugin now reconciles against the host's persisted event log: with the max L0-persisted turn as watermark it recovers complete trailing turns (cap 2; crash-orphaned turns are closed by the host with `turn/end{reason:'interrupted'}` — 98 instances found on this machine). Idempotency = watermark + per-turn existence check; when host read services are unavailable it degrades stepwise (`sessionQuery.readSession` → `sessionPersistence.readFrom` → current behavior + one-time notice), always fail-open and never blocking session startup.
|
|
30
|
+
- **Payload redaction (§C, `capture.redactSecrets`, default on)**: the capture boundary replaces 8 classes of secrets (PEM keys / Authorization headers / Bearer & JWT / Cookies / vendor API key shapes / emails / ≥9-digit numbers with dates excluded / high-entropy IDs) with typed `[REDACTED:<KIND>]` placeholders — a single choke point covering L0 JSONL, L0 SQLite, distill input and manual `memory_add`/`memory_import`. Safe values (`example` / `$VAR` / `${{…}}`) are kept; placeholders preserve the category so keyword search still matches. Note: once enabled the original L0 text is changed (rebuild cannot recover it); `false` restores plaintext.
|
|
31
|
+
- **Distill prompt anti-injection (§B)**: all six distill prompt files (L1 extract ×3 / L1 dedup ×3 / L2 scene ×2 / L3 persona ×2 / graph projection / reconcile) now carry a "content boundary (anti-injection)" declaration plus data-delimiter notes — conversation text, existing memory pools and scene contents embedded in prompts are data, not instructions, so instruction-like text in conversations can no longer hijack distillation, dedup or adjudication. Decision vocabularies and output contracts are unchanged.
|
|
32
|
+
|
|
33
|
+
### Improved
|
|
34
|
+
|
|
35
|
+
- **Injection acknowledgment (§D)**: recall injection no longer marks the dedupe store on return; it is now a two-phase "pending → log receipt" — the mark happens only when the host actually writes the injected message into the session log (`user/message` with the `plugin:memory` signature and stable id). Injections overwritten or cancelled no longer suppress memories (they can be re-injected next turn); after 5 minutes without a receipt the mark is applied anyway (guards against an ack-chain failure disabling dedupe), unconfirmed-expired entries are dropped.
|
|
36
|
+
- **Compaction-aware re-injection (§E)**: after `compaction/end` (no error), the session's next turn performs one targeted enhanced recall — dedupe suppression is skipped (compaction evicted prior injections from context), the persona section is re-delivered immediately, and the occupancy ledger resets; consumed on use. The existing compact/clear full reset remains as a fallback; both paths are idempotent.
|
|
37
|
+
- **Structured trace (§F, `trace.*`)**: recall and distill events are persisted as daily JSONL under `<dataDir>/trace/` (14-day retention by default; 5 MB per-day cap with a marker). Recall events carry a query fingerprint (metadata-only by default: length + sha256; `trace.captureContent` stores the text ≤200 chars), hit/injected ids, scores, latency and four outcomes; distill events carry runId, the six-value decision aggregation (same source as `l1_receipts`, cross-auditable), latency and success. New `dsh-memory/trace-tail` endpoint; the Log tab gains a three-source switch (system log / recall trace / distill trace).
|
|
38
|
+
|
|
25
39
|
## [0.18.2] — 2026-09-27
|
|
26
40
|
|
|
27
41
|
### Fixed
|
package/CHANGELOG.ja.md
CHANGED
|
@@ -9,6 +9,20 @@
|
|
|
9
9
|
|
|
10
10
|
本ファイルは **0.12.0** リリースノートと現在の**未リリース**変更の日本語版です。全履歴は [CHANGELOG.md](./CHANGELOG.md)(中文)を参照してください。
|
|
11
11
|
|
|
12
|
+
## [0.18.4] — 2026-09-28
|
|
13
|
+
|
|
14
|
+
### 追加
|
|
15
|
+
|
|
16
|
+
- **クラッシュ復元(§A)**:ホストがターン途中でクラッシュすると、それまで当該ターンの会話はメモリバッファとともに永久失われていました(resume 時のコールドスタート保護が全履歴を破棄するため)。現在は resume 時にホストの永続化イベントログと突き合わせます:L0 記録済み最大 turn をウォーターマークとして、その後の完全なターンを最大 2 件まで補収(クラッシュ孤児ターンはホストが `turn/end{reason:'interrupted'}` で閉じます——実機で 98 件確認)。冪等性 = ウォーターマーク + ターン単位の存在チェック。ホストの読み取りサービスが利用不可の場合は段階的に縮退(`sessionQuery.readSession` → `sessionPersistence.readFrom` → 従来動作 + 一回限りの通知)し、常に fail-open でセッション起動を阻塞しません。
|
|
17
|
+
- **ペイロード赤字化(§C、`capture.redactSecrets`、デフォルト有効)**:捕获書き込み境界で 8 種の秘密(PEM 秘密鍵 / Authorization ヘッダー / Bearer・JWT / Cookie / ベンダー API キー形式 / メールアドレス / 9 桁以上の数値(日付は除外)/ 高エントロピー ID)を `[REDACTED:<KIND>]` 型付きプレースホルダーに置換——L0 JSONL・L0 SQLite・蒸留入力・手動 `memory_add`/`memory_import` を単一点でカバー。安全値(`example` / `$VAR` / `${{…}}` 等)は対象外。プレースホルダーはカテゴリ意味を保持し検索可能。注意:有効化後は L0 原文が変わります(再構築でも原文は取得不可)。`false` で平文に戻せます。
|
|
18
|
+
- **蒸留プロンプト耐注入(§B)**:6 つの蒸留プロンプトファイル(L1 抽出 ×3 / L1 重複排除 ×3 / L2 シーン ×2 / L3 ペルソナ ×2 / グラフ投影 / 突き合わせ器)すべてに「内容境界(耐注入)」宣言とデータスロット界定を追加——会話原文・既存記憶プール・シーン全文等の埋め込み内容はデータであり指令ではない。判定語彙と出力契約は不変。
|
|
19
|
+
|
|
20
|
+
### 改善
|
|
21
|
+
|
|
22
|
+
- **注入確認(§D)**:リコール注入は返却時に即マークしなくなりました。「pending → ログ受領」の二段式——ホストが注入メッセージを実際にセッションログへ書き込んだ(`plugin:memory` 署名と安定 id を持つ `user/message`)時点で `dedupe.mark` します。上書き・取消された注入は記憶を誤抑制しなくなり(次ターンで再注入可能)、5 分受領なしで通常マークに縮退(確認経路故障による重複排除無効化を防ぐ)、未確認の期限切れは破棄。
|
|
23
|
+
- **圧縮後の精密再注入(§E)**:`compaction/end`(error なし)の後、当該セッションの次ターンで一回の拡張リコールを実施——重複排除抑制をスキップ(圧縮が旧注入をコンテキストから追い出したため)、ペルソナを即時再注入、占有台帳をリセット。使用時に消費。既存の compact/clear 全量リセットはフォールバックとして保持し、両経路は冪等。
|
|
24
|
+
- **構造化トレース(§F、`trace.*`)**:リコールと蒸留のイベントを日次 JSONL に永続化(`<dataDir>/trace/`、既定保持 14 日、日次 5MB 上限で停止 + マーカー)。リコールイベントは query 指紋(既定は metadata-only:長さ + sha256、`trace.captureContent` で原文 ≤200 文字)、ヒット/注入 id、スコア、所要時間と 4 結果を記録。蒸留イベントは runId・6 値の決定集計(`l1_receipts` と同源で交差監査可能)・所要時間と成否。新エンドポイント `dsh-memory/trace-tail`、設定ページの「ログ」Tab に 3 データソース切替(システムログ / リコールトレース / 蒸留トレース)。
|
|
25
|
+
|
|
12
26
|
## [0.18.2] — 2026-09-27
|
|
13
27
|
|
|
14
28
|
### 修正
|
package/CHANGELOG.ko.md
CHANGED
|
@@ -9,6 +9,20 @@
|
|
|
9
9
|
|
|
10
10
|
이 파일은 **0.12.0** 릴리스 노트와 현재 **미출시** 변경 사항의 한국어판입니다. 전체 이력은 [CHANGELOG.md](./CHANGELOG.md)(中文)를 참조하세요.
|
|
11
11
|
|
|
12
|
+
## [0.18.4] — 2026-09-28
|
|
13
|
+
|
|
14
|
+
### 추가
|
|
15
|
+
|
|
16
|
+
- **크래시 복구(§A)**: 호스트가 턴 도중 크래시하면 해당 턴 대화는 메모리 버퍼와 함께 영구 유실되었습니다(resume 시 콜드스타트 보호가 전체 이력을 폐기). 이제 resume 시 호스트의 영속 이벤트 로그와 대조합니다: L0 기록 최대 turn을 워터마크로, 그 이후의 완전한 턴을 최대 2개까지 보수(크래시 고아 턴은 호스트가 `turn/end{reason:'interrupted'}`로 닫습니다 — 실기에서 98건 확인). 멱등성 = 워터마크 + 턴 단위 존재 검사. 호스트 읽기 서비스를 사용할 수 없으면 단계적 축소(`sessionQuery.readSession` → `sessionPersistence.readFrom` → 기존 동작 + 일회성 안내), 항상 fail-open으로 세션 시작을 막지 않습니다.
|
|
17
|
+
- **페이로드 마스킹(§C, `capture.redactSecrets`, 기본 활성)**: 캡처 쓰기 경계에서 8종의 시크릿(PEM 개인키 / Authorization 헤더 / Bearer·JWT / Cookie / 벤더 API 키 형태 / 이메일 / 9자리 이상 숫자(날짜 제외) / 고엔트로피 ID)을 `[REDACTED:<KIND>]` 타입 플레이스홀더로 치환 — L0 JSONL·L0 SQLite·증류 입력·수동 `memory_add`/`memory_import`를 단일 지점에서 커버. 안전값(`example` / `$VAR` / `${{…}}` 등)은 제외. 플레이스홀더는 카테고리 의미를 유지해 검색 가능. 주의: 활성화 후 L0 원문은 변경됩니다(재구축으로도 원문 복원 불가). `false`로 평문 복귀.
|
|
18
|
+
- **증류 프롬프트 내성 주입(§B)**: 6개 증류 프롬프트 파일(L1 추출 ×3 / L1 중복제거 ×3 / L2 씬 ×2 / L3 페르소나 ×2 / 그래프 투영 / 대조기) 모두에 "콘텐츠 경계(내성 주입)" 선언과 데이터 슬롯 경계를 추가 — 대화 원문·기존 기억 풀·씬 전문 등 임베디드 콘텐츠는 데이터이지 지시가 아닙니다. 판정 어휘와 출력 계약은 불변.
|
|
19
|
+
|
|
20
|
+
### 개선
|
|
21
|
+
|
|
22
|
+
- **주입 확인(§D)**: 리콜 주입은 반환 즉시 마크하지 않습니다. "pending → 로그 수신" 2단 방식 — 호스트가 주입 메시지를 실제로 세션 로그에 기록했을 때(`plugin:memory` 서명과 안정 id를 가진 `user/message`) `dedupe.mark` 합니다. 덮어쓰기·취소된 주입은 더 이상 기억을 오억제하지 않고(다음 턴 재주입 가능), 5분 수신 없으면 일반 마크로 축소(확인 경로 장애로 인한 중복제거 무효화 방지), 미확인 만료는 폐기.
|
|
23
|
+
- **압축 후 정밀 재주입(§E)**: `compaction/end`(error 없음) 후 해당 세션의 다음 턴에 한 번의 확장 리콜 실행 — 중복제거 억제를 건너뛰고(압축이 기존 주입을 컨텍스트에서 축출), 페르소나 즉시 재주입, 점유 원장 리셋. 사용 시 소비. 기존 compact/clear 전량 리셋은 폴백으로 유지되며 양 경로는 멱등.
|
|
24
|
+
- **구조화 트레이스(§F, `trace.*`)**: 리콜과 증류 이벤트를 일별 JSONL로 영속화(`<dataDir>/trace/`, 기본 보존 14일, 일별 5MB 상한에서 정지 + 마커). 리콜 이벤트는 query 지문(기본 metadata-only: 길이 + sha256, `trace.captureContent` 시 원문 ≤200자), 히트/주입 id, 스코어, 소요시간과 4가지 결과를 기록. 증류 이벤트는 runId·6값 결정 집계(`l1_receipts`와 동원으로 교차 감사 가능)·소요시간과 성패. 신규 엔드포인트 `dsh-memory/trace-tail`, 설정 페이지 "로그" 탭에 3개 데이터 소스 전환(시스템 로그 / 리콜 트레이스 / 증류 트레이스).
|
|
25
|
+
|
|
12
26
|
## [0.18.2] — 2026-09-27
|
|
13
27
|
|
|
14
28
|
### 수정
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@
|
|
|
6
6
|
> **UI 截图约定**:带界面变化的条目在 `assets/changelog/<版本号>/<两位编号>-<简述>.png`
|
|
7
7
|
> 存真机截图,并在条目内以相对路径引用,读者可在更新日志里直接看到新版本 UI 的样子。
|
|
8
8
|
|
|
9
|
+
## [0.18.4] — 2026-09-28
|
|
10
|
+
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- **崩溃恢复(§A)**:宿主在轮次中途崩溃时,该轮对话此前会随内存缓冲永久丢失(resume 的冷启动地板丢弃全部历史)。现在 resume 时从宿主持久化事件日志对账:以 L0 已落盘的最大 turn 为水位线,补收其后的完整轮次(上限 2 轮;崩溃孤儿轮由宿主补写 `turn/end{reason:'interrupted'}` 闭合,本机实测 98 处)。幂等 = 水位线 + 逐轮存在性检查;宿主读日志服务不可用时逐级降级(`sessionQuery.readSession` → `sessionPersistence.readFrom` → 维持现状 + 一次性提示),全程 fail-open 不阻塞会话启动。
|
|
14
|
+
- **载荷脱敏(§C,`capture.redactSecrets`,默认开启)**:捕获写入边界把 8 类密钥(PEM 私钥 / Authorization 头 / Bearer 与 JWT / Cookie / 厂商 API key 形态 / 邮箱 / ≥9 位长号(排除日期)/ 高熵 ID)替换为 `[REDACTED:<KIND>]` 类型化占位符——单点覆盖 L0 JSONL、L0 SQLite、蒸馏输入与手动 `memory_add`/`memory_import`(两条 L1 写入边界同词表)。安全值(`example` / `$VAR` / `${{…}}` 等)不脱;占位符保留类别语义、可被检索命中。注意:开启后 L0 原文自此改变(重建不可得原文),`false` 一键回明文。
|
|
15
|
+
- **蒸馏提示词防注入(§B)**:六处蒸馏 prompt(L1 抽取 ×3 / L1 去重 ×3 / L2 场景 ×2 / L3 画像 ×2 / 图谱投影 / 核对器)全部加入"内容边界(防注入)"总声明与数据插槽界定——对话原文、既有记忆池、场景全文等嵌入内容一律是数据不是指令,防止对话携带的指令性文字劫持蒸馏、去重与裁决。判定词表与输出契约零变化。
|
|
16
|
+
|
|
17
|
+
### 改进
|
|
18
|
+
|
|
19
|
+
- **注入确认(§D)**:召回注入不再在返回瞬间标记去重,改为"pending → 日志回执"两段式——宿主把注入消息真实写入会话日志(`user/message` 带 `plugin:memory` 署名与稳定 id)时才 `dedupe.mark`。被覆盖/取消的注入不再误压制记忆(下轮可重注);5 分钟未回执降级为照常标记(防确认链路故障导致去重失效),未确认即弃。
|
|
20
|
+
- **压缩精确再注入(§E)**:`compaction/end`(无 error)后,该会话下一轮做一次定向增强召回——跳过去重压制(压缩已把旧注入逐出上下文)、画像立即重注入、占用账本归零;消费即清。原有 compact/clear 会话级全量重置保留为回退路径,双路径幂等。
|
|
21
|
+
- **结构化追踪(§F,`trace.*`)**:召回与蒸馏事件按天 JSONL 落盘(`<dataDir>/trace/`,保留期默认 14 天,单日 5MB 停写 + marker)。召回事件含 query 指纹(默认 metadata-only:长度 + sha256,`trace.captureContent` 才存原文 ≤200 字符)、命中/注入 id、分数、耗时与四种结局(injected/suppressed/timeout/off);蒸馏事件含 runId、六值决策聚合(与 `l1_receipts` 同源可交叉审计)、耗时与成败。新增 `dsh-memory/trace-tail` 端点;设置页"日志"Tab 新增 系统日志 / 召回追踪 / 蒸馏追踪 三数据源切换。
|
|
22
|
+
|
|
9
23
|
## [0.18.2] — 2026-09-27
|
|
10
24
|
|
|
11
25
|
### 修复
|
package/README.en.md
CHANGED
|
@@ -283,6 +283,10 @@ the bundle layer appends and causes `duplicate loader entry id` startup failure)
|
|
|
283
283
|
| `capture.enabled` | `true` | L0 capture |
|
|
284
284
|
| `capture.stripCodeBlocks` | `true` | Strip code blocks from assistant messages |
|
|
285
285
|
| `capture.maxMessageChars` | `4000` | Max characters per message |
|
|
286
|
+
| `capture.redactSecrets` | `true` | Payload redaction: 8 secret classes (PEM/Bearer/JWT/Cookie/vendor API keys/emails/long numbers/high-entropy IDs) become `[REDACTED:<KIND>]` placeholders at capture time — covers L0, distill input and manual writes; `false` restores plaintext (irreversible once enabled) |
|
|
287
|
+
| `trace.enabled` | `true` | Structured trace: recall/distill events as daily JSONL under `<dataDir>/trace/`; switch sources in the Log tab |
|
|
288
|
+
| `trace.retentionDays` | `14` | Trace retention in days (`0` = forever) |
|
|
289
|
+
| `trace.captureContent` | `false` | `true` stores recall query text (default: length + sha256 only) |
|
|
286
290
|
| `extract.enabled` | `true` | L1 extraction |
|
|
287
291
|
| `extract.minMessages` | `6` | Steady-state trigger threshold: run L1 extraction once a session accumulates N new messages. The effective threshold ramps up 1→2→4→…→N (first turn yields memories immediately, then batches to save calls) |
|
|
288
292
|
| `extract.idleSeconds` | `300` | Idle flush: distill a session's pending slice after N seconds of silence (catches "user left before reaching the threshold"); `0` disables |
|
package/README.ja.md
CHANGED
|
@@ -227,6 +227,10 @@ node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600
|
|
|
227
227
|
| `capture.enabled` | `true` | L0 捕捉 |
|
|
228
228
|
| `capture.stripCodeBlocks` | `true` | アシスタントメッセージからコードブロックを除去 |
|
|
229
229
|
| `capture.maxMessageChars` | `4000` | 単メッセージ最大文字数 |
|
|
230
|
+
| `capture.redactSecrets` | `true` | ペイロード赤字化:捕获書き込み時に 8 種の秘密(PEM/Bearer/JWT/Cookie/ベンダー API キー/メール/長数字/高エントロピー ID)を `[REDACTED:<KIND>]` に置換——L0・蒸留入力・手動書き込みをカバー。`false` で平文に戻す(有効化後は原文不可逆) |
|
|
231
|
+
| `trace.enabled` | `true` | 構造化トレース:リコール/蒸留イベントを日次 JSONL で保存(`<dataDir>/trace/`)、設定ページの「ログ」Tab で切替表示 |
|
|
232
|
+
| `trace.retentionDays` | `14` | トレース保持日数(`0` = 無期限) |
|
|
233
|
+
| `trace.captureContent` | `false` | `true` でリコール query 原文を保存(既定は長さ + sha256 のみ) |
|
|
230
234
|
| `extract.enabled` | `true` | L1 抽出 |
|
|
231
235
|
| `extract.minMessages` | `6` | 定常トリガ閾値:単セッションが N 件新メッセージを溜めて L1 抽出を1回実行。立ち上がりは 1→2→4→…→N と倍増 |
|
|
232
236
|
| `extract.idleSeconds` | `300` | アイドル兜底:セッションが N 秒無言で未蒸留スライスを落とす。`0` で無効 |
|
package/README.ko.md
CHANGED
|
@@ -227,6 +227,10 @@ node bench/harness/retrieval-metrics.mjs <runDir> --flood 200,600
|
|
|
227
227
|
| `capture.enabled` | `true` | L0 포착 |
|
|
228
228
|
| `capture.stripCodeBlocks` | `true` | 어시스턴트 메시지에서 코드 블록 제거 |
|
|
229
229
|
| `capture.maxMessageChars` | `4000` | 단메시지 최대 문자 수 |
|
|
230
|
+
| `capture.redactSecrets` | `true` | 페이로드 마스킹:캡처 쓰기 시 8종 시크릿(PEM/Bearer/JWT/Cookie/벤더 API 키/이메일/긴 숫자/고엔트로피 ID)을 `[REDACTED:<KIND>]`로 치환 — L0·증류 입력·수동 쓰기 커버. `false`로 평문 복귀(활성화 후 원문 비가역) |
|
|
231
|
+
| `trace.enabled` | `true` | 구조화 트레이스:리콜/증류 이벤트를 일별 JSONL로 저장(`<dataDir>/trace/`), 설정 페이지 "로그" 탭에서 전환 조회 |
|
|
232
|
+
| `trace.retentionDays` | `14` | 트레이스 보존 일수(`0` = 무기한) |
|
|
233
|
+
| `trace.captureContent` | `false` | `true` 시 리콜 query 원문 저장(기본은 길이 + sha256만) |
|
|
230
234
|
| `extract.enabled` | `true` | L1 추출 |
|
|
231
235
|
| `extract.minMessages` | `6` | 정상 트리거 임계값:단세션이 N건 새 메시지 누적 시 L1 추출 1회. 시작 단계는 1→2→4→…→N 배증 |
|
|
232
236
|
| `extract.idleSeconds` | `300` | 유휴兜底:세션이 N초 무음이면 미증류 슬라이스 투하. `0` 비활성 |
|
package/README.md
CHANGED
|
@@ -295,6 +295,10 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
|
|
|
295
295
|
| `capture.enabled` | `true` | L0 捕获 |
|
|
296
296
|
| `capture.stripCodeBlocks` | `true` | 助手消息剥离代码块 |
|
|
297
297
|
| `capture.maxMessageChars` | `4000` | 单条消息最大字符数 |
|
|
298
|
+
| `capture.redactSecrets` | `true` | 载荷脱敏:捕获写入即把 8 类密钥(PEM/Bearer/JWT/Cookie/厂商 API key/邮箱/长号/高熵 ID)替换为 `[REDACTED:<KIND>]` 占位符,覆盖 L0、蒸馏输入与手动写入;`false` 回明文(开启后 L0 原文不再可逆) |
|
|
299
|
+
| `trace.enabled` | `true` | 结构化追踪:召回/蒸馏事件按天 JSONL(`<dataDir>/trace/`),设置页"日志"Tab 可切换查看 |
|
|
300
|
+
| `trace.retentionDays` | `14` | 追踪事件保留天数(`0` 永久) |
|
|
301
|
+
| `trace.captureContent` | `false` | `true` 才存召回 query 原文(默认只存长度 + sha256) |
|
|
298
302
|
| `extract.enabled` | `true` | L1 抽取 |
|
|
299
303
|
| `extract.minMessages` | `6` | 稳态触发阈值:单会话攒够 N 条新消息跑一次 L1 抽取。起步阶段生效阈值从 1 翻倍爬坡到此值(首轮即出记忆,随后自动攒批省调用) |
|
|
300
304
|
| `extract.idleSeconds` | `300` | 闲置兜底:会话静默 N 秒后把未蒸馏切片落袋(接住"没攒够阈值用户就离开");`0` 关闭 |
|
package/cordis.patch.yml
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
# config: # 全部字段有默认值,按需覆盖:
|
|
12
12
|
# family: auto # 新会话默认档:auto | chat | work
|
|
13
13
|
# dataDir: '' # 默认 $DSH_HOME/memory
|
|
14
|
+
# capture:
|
|
15
|
+
# redactSecrets: true # §C 脱敏:8 类密钥→[REDACTED:KIND] 占位符(false 回明文)
|
|
16
|
+
# trace:
|
|
17
|
+
# enabled: true # §F 追踪:recall/distill 事件按天 JSONL
|
|
18
|
+
# retentionDays: 14 # 事件保留天数(0 = 永久)
|
|
19
|
+
# captureContent: false # true 才存 query 原文(默认只存 chars+sha)
|
|
14
20
|
# llm:
|
|
15
21
|
# provider: '' # 蒸馏模型路由(默认跟随当前默认模型)
|
|
16
22
|
# model: ''
|
package/dist/client.js
CHANGED
|
@@ -1178,20 +1178,46 @@ var __defProp = Object.defineProperty;
|
|
|
1178
1178
|
// client/src/tabs/LogTab.tsx
|
|
1179
1179
|
var import_react4 = require("react");
|
|
1180
1180
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1181
|
+
var SOURCES = [
|
|
1182
|
+
{ id: "", label: "系统日志" },
|
|
1183
|
+
{ id: "recall_turn", label: "召回追踪" },
|
|
1184
|
+
{ id: "distill_run", label: "蒸馏追踪" }
|
|
1185
|
+
];
|
|
1186
|
+
function fmtEvent(ev) {
|
|
1187
|
+
if (ev.kind === "recall_turn") {
|
|
1188
|
+
const outcome = String(ev.outcome ?? "?");
|
|
1189
|
+
const hits = ev.hitIds?.length ?? 0;
|
|
1190
|
+
const injected = ev.injectedIds?.length ?? 0;
|
|
1191
|
+
const q = ev.queryText ? ` query="${String(ev.queryText).slice(0, 40)}"` : ` sha=${String(ev.querySha ?? "")}`;
|
|
1192
|
+
return `[${new Date(Number(ev.ts)).toLocaleTimeString()}] recall ${outcome}${q} hits=${hits} injected=${injected} suppressed=${String(ev.suppressedCount ?? 0)} ${String(ev.durationMs ?? 0)}ms`;
|
|
1193
|
+
}
|
|
1194
|
+
if (ev.kind === "distill_run") {
|
|
1195
|
+
const by = ev.byKind ? " byKind=" + JSON.stringify(ev.byKind) : "";
|
|
1196
|
+
const err = ev.errorKind ? " error=" + String(ev.errorKind) : "";
|
|
1197
|
+
return `[${new Date(Number(ev.ts)).toLocaleTimeString()}] distill ${String(ev.layer)}${ev.runId ? " run=" + String(ev.runId) : ""} ok=${String(ev.ok)} in=${String(ev.inputChars ?? 0)}ch new=${String(ev.newRecords ?? 0)} ${String(ev.durationMs ?? 0)}ms${by}${err}`;
|
|
1198
|
+
}
|
|
1199
|
+
return JSON.stringify(ev);
|
|
1200
|
+
}
|
|
1181
1201
|
function LogTab(props) {
|
|
1182
1202
|
const rpc = props.rpc;
|
|
1203
|
+
const [source, setSource] = (0, import_react4.useState)("");
|
|
1183
1204
|
const [lines, setLines] = (0, import_react4.useState)(null);
|
|
1184
1205
|
const [error, setError] = (0, import_react4.useState)(null);
|
|
1185
1206
|
const preRef = (0, import_react4.useRef)(null);
|
|
1186
1207
|
const load = (0, import_react4.useCallback)(() => {
|
|
1187
1208
|
setError(null);
|
|
1188
|
-
|
|
1189
|
-
|
|
1209
|
+
if (source === "") {
|
|
1210
|
+
rpc("dsh-memory/log-tail", { lines: 200 }).then((r) => {
|
|
1211
|
+
if (r && r.ok) setLines(r.value.lines);
|
|
1212
|
+
else setError(r && r.error ? r.error.message : "RPC error");
|
|
1213
|
+
}).catch((e) => setError(String(e && e.message || e)));
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
rpc("dsh-memory/trace-tail", { lines: 200, kind: source }).then((r) => {
|
|
1217
|
+
if (r && r.ok) setLines(r.value.events.map(fmtEvent));
|
|
1190
1218
|
else setError(r && r.error ? r.error.message : "RPC error");
|
|
1191
|
-
}).catch((e) =>
|
|
1192
|
-
|
|
1193
|
-
});
|
|
1194
|
-
}, [rpc]);
|
|
1219
|
+
}).catch((e) => setError(String(e && e.message || e)));
|
|
1220
|
+
}, [rpc, source]);
|
|
1195
1221
|
(0, import_react4.useEffect)(() => {
|
|
1196
1222
|
load();
|
|
1197
1223
|
}, [load]);
|
|
@@ -1199,12 +1225,13 @@ var __defProp = Object.defineProperty;
|
|
|
1199
1225
|
if (lines && preRef.current) preRef.current.scrollTop = preRef.current.scrollHeight;
|
|
1200
1226
|
}, [lines]);
|
|
1201
1227
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
|
|
1202
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
|
|
1203
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1228
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10, gap: 8 }, children: [
|
|
1229
|
+
SOURCES.map((s) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NButton, { onClick: () => setSource(s.id), children: source === s.id ? "● " + s.label : s.label }, s.id)),
|
|
1204
1230
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: S.grow }),
|
|
1205
1231
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(NButton, { onClick: load, children: "刷新" })
|
|
1206
1232
|
] }),
|
|
1207
|
-
|
|
1233
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { ...S.muted, marginBottom: 10 }, children: error ? "加载失败" : lines === null ? "加载中…" : `最近 ${lines.length} 条(${SOURCES.find((s) => s.id === source)?.label})` }),
|
|
1234
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { ...S.error, marginBottom: 10 }, children: "读取失败:" + error + "(点右上“刷新”重试)" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("pre", { style: S.pre, ref: preRef, children: (lines || []).join("\n") || "(暂无记录)" })
|
|
1208
1235
|
] });
|
|
1209
1236
|
}
|
|
1210
1237
|
|
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),
|
package/dist/contract.d.ts
CHANGED
|
@@ -913,6 +913,50 @@ export interface LogTailRequest {
|
|
|
913
913
|
export interface LogTailResponse {
|
|
914
914
|
lines: string[];
|
|
915
915
|
}
|
|
916
|
+
/** dsh-memory/trace-tail(§F 追踪事件流,memorax-absorb Wave 4) */
|
|
917
|
+
export interface RecallTraceEvent {
|
|
918
|
+
kind: 'recall_turn';
|
|
919
|
+
ts: number;
|
|
920
|
+
sessionId: string;
|
|
921
|
+
turn?: number;
|
|
922
|
+
/** metadata-only 默认:query 长度 + sha256 前 16 位;captureContent 才存原文(≤200)。 */
|
|
923
|
+
queryChars: number;
|
|
924
|
+
querySha?: string;
|
|
925
|
+
queryText?: string;
|
|
926
|
+
hitIds: string[];
|
|
927
|
+
hitScores: number[];
|
|
928
|
+
injectedIds: string[];
|
|
929
|
+
suppressedCount: number;
|
|
930
|
+
durationMs: number;
|
|
931
|
+
outcome: 'injected' | 'suppressed' | 'timeout' | 'off';
|
|
932
|
+
}
|
|
933
|
+
export interface DistillTraceEvent {
|
|
934
|
+
kind: 'distill_run';
|
|
935
|
+
ts: number;
|
|
936
|
+
layer: 'l1' | 'l2' | 'l3';
|
|
937
|
+
sessionId: string;
|
|
938
|
+
/** 仅 l1 有(与 l1_receipts 同一 runId,可交叉审计)。 */
|
|
939
|
+
runId?: string;
|
|
940
|
+
inputChars: number;
|
|
941
|
+
/** 仅 l1:决策六值词表聚合(store/update/merge/skip/conflict/skip_missing)。 */
|
|
942
|
+
byKind?: Record<string, number>;
|
|
943
|
+
newRecords?: number;
|
|
944
|
+
durationMs: number;
|
|
945
|
+
provider?: string;
|
|
946
|
+
model?: string;
|
|
947
|
+
ok: boolean;
|
|
948
|
+
errorKind?: string;
|
|
949
|
+
}
|
|
950
|
+
export type TraceEvent = RecallTraceEvent | DistillTraceEvent;
|
|
951
|
+
export interface TraceTailRequest {
|
|
952
|
+
/** 1~500,默认 100。 */
|
|
953
|
+
lines?: number;
|
|
954
|
+
/** 缺省 = 两类都取(按时间混合)。 */
|
|
955
|
+
kind?: 'recall_turn' | 'distill_run';
|
|
956
|
+
}
|
|
957
|
+
export interface TraceTailResponse {
|
|
958
|
+
events: TraceEvent[];
|
|
959
|
+
}
|
|
916
960
|
/** dsh-memory/rebuild-* */
|
|
917
961
|
export type RebuildStatusResponse = {
|
|
918
962
|
supported: false;
|
|
@@ -1172,6 +1216,7 @@ export interface DshMemoryRequestMap {
|
|
|
1172
1216
|
'dsh-memory/scenes': Record<string, never>;
|
|
1173
1217
|
'dsh-memory/persona': Record<string, never>;
|
|
1174
1218
|
'dsh-memory/log-tail': LogTailRequest;
|
|
1219
|
+
'dsh-memory/trace-tail': TraceTailRequest;
|
|
1175
1220
|
'dsh-memory/rebuild-status': Record<string, never>;
|
|
1176
1221
|
'dsh-memory/rebuild-start': Record<string, never>;
|
|
1177
1222
|
'dsh-memory/rebuild-cancel': Record<string, never>;
|
|
@@ -1216,6 +1261,7 @@ export interface DshMemoryResponseMap {
|
|
|
1216
1261
|
'dsh-memory/scenes': ScenesResponse;
|
|
1217
1262
|
'dsh-memory/persona': PersonaResponse;
|
|
1218
1263
|
'dsh-memory/log-tail': LogTailResponse;
|
|
1264
|
+
'dsh-memory/trace-tail': TraceTailResponse;
|
|
1219
1265
|
'dsh-memory/rebuild-status': RebuildStatusResponse;
|
|
1220
1266
|
'dsh-memory/rebuild-start': RebuildStatus;
|
|
1221
1267
|
'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
|
+
}
|