dsh-prime-memory 0.13.1 → 0.14.0-beta.1
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 +35 -1
- package/CHANGELOG.ja.md +35 -1
- package/CHANGELOG.ko.md +35 -1
- package/CHANGELOG.md +39 -0
- package/dist/client.js +21 -2
- package/dist/contract.d.ts +49 -1
- package/dist/hooks/capture.js +45 -7
- package/dist/pipeline/anchors.d.ts +49 -0
- package/dist/pipeline/anchors.js +90 -0
- package/dist/pipeline/l1.d.ts +8 -2
- package/dist/pipeline/l1.js +22 -7
- package/dist/pipeline/rebuild-preserve.d.ts +149 -0
- package/dist/pipeline/rebuild-preserve.js +206 -0
- package/dist/pipeline/rebuild.js +37 -4
- package/dist/pipeline/reconcile-run.d.ts +102 -0
- package/dist/pipeline/reconcile-run.js +206 -0
- package/dist/pipeline/reconcile.d.ts +136 -0
- package/dist/pipeline/reconcile.js +256 -0
- package/dist/pipeline/runner.js +11 -2
- package/dist/settings.js +5 -0
- package/dist/stats.js +20 -1
- package/dist/store/embedding-source.d.ts +28 -1
- package/dist/store/embedding-source.js +60 -0
- package/dist/store/evidence-source.d.ts +161 -0
- package/dist/store/evidence-source.js +322 -0
- package/dist/store/l0.d.ts +4 -0
- package/dist/store/l0.js +24 -8
- package/dist/store/l1-snapshot.d.ts +99 -0
- package/dist/store/l1-snapshot.js +189 -0
- package/dist/store/l1.d.ts +4 -0
- package/dist/store/l1.js +6 -0
- package/dist/store/sqlite.d.ts +14 -0
- package/dist/store/sqlite.js +81 -27
- package/dist/types.d.ts +34 -0
- package/dsh.plugin.json +1 -1
- package/package.json +124 -124
package/CHANGELOG.en.md
CHANGED
|
@@ -5,7 +5,41 @@
|
|
|
5
5
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
6
6
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
7
7
|
|
|
8
|
-
This file covers the **0.
|
|
8
|
+
This file covers the **0.12.0** release notes and the current **unreleased** changes in English. For the full history, see [CHANGELOG.md](./CHANGELOG.md) (Chinese).
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Source anchors (R7) — a memory can now be traced back to a real position in the session.** Until now the traceability chain was broken: L1 carried `source_message_ids`, but those are **L0 message ids** (`msg_<epoch_ms>_<hex>`) while the L0 table had no `turn`/`step` columns, and the id list was never written to the retrieval DB at all (the store kept only `metadata`, silently dropping the field). Result: **no memory could be located in the original conversation**.
|
|
15
|
+
- `l0_conversations` gains `turn`/`step` columns (idempotent `ALTER TABLE`; old rows stay NULL = "no anchor", never back-filled with a guess) plus an `(session_id, turn)` index.
|
|
16
|
+
- The capture hook now folds `step/start`, so `user/message` events — which do not carry `step` in the kernel payload — still get their **same-turn** coordinate. `assistant/message` uses the `{turn, step}` it already carries. Messages before the first `step/start` keep `step` empty on purpose: **a missing coordinate is never invented.**
|
|
17
|
+
- Anchors live under the reserved `metadata_json` key `dsh_source_anchors` (rendered as `t12 s3` in the UI). No new column, no disk-contract change. Both the fresh-store path and the **merge/update** path carry anchors — otherwise a single merge would lose the coordinate.
|
|
18
|
+
- New host API `MemoryDb.l0ByAnchor(sessionId, turn, step?)` fetches L0 messages **by coordinate** instead of by recency; this is the single read entry point for the upcoming evidence reader.
|
|
19
|
+
- **The records panel now shows the source anchor** where it previously always showed a dash.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **`UiRecord.sourceMessageIds` was a dead field.** It read a column `l1_records` never had, so it always resolved to `[]` and the panel's source row **never rendered**. Replaced by `sourceAnchors`, which reads real data.
|
|
24
|
+
|
|
25
|
+
## [0.12.0] — 2026-09-17
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Manual vector-index rebuild (`dsh-memory/embedding-reindex` + the "Vector index" block in settings).** A rebuild previously had exactly two triggers: the `db.init` change-detection chain at startup, and the periodic backfill that fills in missing vectors — **there was no manual entry point**. The settings page showed only "Cancel" (and only while a rebuild was already running): no "Start", no count of what was embedded versus outstanding.
|
|
30
|
+
- **Endpoint surface 31 → 32.** New `EmbeddingReindexStartResponse` (`{accepted:true}`). **Accepting returns immediately and does not carry progress** — the client keeps polling `reindex` on `embedding-state-get`. Two progress vocabularies eventually disagree, so there is deliberately only one. All four places were updated together (`contract.ts` maps / the `MEMORY_ENDPOINTS` allowlist / the dispatcher `case` / the endpoint-count assertion) — **missing any one pins the endpoint at a permanent 404**, and since the client `rpc` catch swallows that silently, the whole panel just vanishes.
|
|
31
|
+
- **`EmbeddingStateView` gained `vectors`**: `embedded` / `total` / `missing` / `skipped` for L1 and L0, which is where "X of Y embedded" comes from. The db layer's **`-1` sentinel passes through verbatim** — "vector capability unavailable" and "nothing embedded yet" must stay two different sentences; collapsing them into one number sends the user to click a button that can never respond.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **Rebuild requests that would be accepted but never actually run are now rejected.** The first line of `L1Store.reindex` / `L0Store.reindex` **silently short-circuits** to `0/0/0` when vector capability is not ready. Ungated, the UI would report "rebuild complete, nothing outstanding" while the rebuild **never started**. That trap was already documented at `src/index.ts:240`, but only for the startup chain; the manual entry point was a new hole. `startReindex()` hoists all five gates, each with an **actionable** message: unloaded / already running / source switch holds the lock / **embedding source off** (`currentInfo` empty) / **service not ready**. ("Turn it on first" and "wait a bit" are different instructions and must not be merged.) This required a `vectorsReady()` accessor on both stores — `helper` is private, so nothing outside could ask.
|
|
36
|
+
- **Incomplete `db` test double in `embedding-subsystem.test.ts`.** It carried only `swapProvider` / `markEmbeddingSynced` and bypassed type checking via `as never`, so the gap never surfaced; once `snapshot()` began including vector counts it crashed (`getVecSkipSet is not a function`). **The double was completed rather than making `vectorCounts` defensive**: the signature declares a full `MemoryDb`, and swallowing missing methods swallows real wiring errors with them.
|
|
37
|
+
|
|
38
|
+
### Tests
|
|
39
|
+
|
|
40
|
+
- 5 new cases: rejected when off / rejected when not ready / accepted and drives L1+L0 with an immediate concurrent rejection / rejected after unload / `snapshot` count semantics. **Every rejection path asserts both "throws" and "downstream never called"** — asserting only the throw would let an implementation that calls downstream *and then* throws pass.
|
|
41
|
+
- **Falsified**: temporarily removing the readiness guard turns `向量能力未就绪 → 拒绝` red (`expected [Function] to throw an error`); restored and green again.
|
|
42
|
+
- Full suite **39 files / 403 cases**; `typecheck` (three tsconfigs), `build` and `smoke` all green.
|
|
9
43
|
|
|
10
44
|
## [Unreleased]
|
|
11
45
|
|
package/CHANGELOG.ja.md
CHANGED
|
@@ -7,7 +7,41 @@
|
|
|
7
7
|
|
|
8
8
|
> **互換性の注意**:本プラグインは日本語ドキュメントを提供しますが、公式 DSH の `LocaleRuntime` が登録する言語は `zh` / `en` のみです。`ja` を選択すると `locale "ja" is not registered` となります。DSH を fork して `LOCALE_IDS` と `LOCALES` ラベルを更新し再ビルドすることで利用可能になります。
|
|
9
9
|
|
|
10
|
-
本ファイルは **0.
|
|
10
|
+
本ファイルは **0.12.0** リリースノートと現在の**未リリース**変更の日本語版です。全履歴は [CHANGELOG.md](./CHANGELOG.md)(中文)を参照してください。
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
### 追加
|
|
15
|
+
|
|
16
|
+
- **ソースアンカー(R7)——記憶をセッション内の実座標まで遡れるようになりました。** これまで追跡チェーンは途切れていました:L1 は `source_message_ids` を持っていますが、それは **L0 メッセージ id**(`msg_<epoch_ms>_<hex>`)であり、L0 テーブルには `turn`/`step` 列がありませんでした。さらに id リストは**検索 DB に一切書き込まれていません**でした(書き込み側は `metadata` のみを保存し、フィールドは黙って破棄)。結果:**どの記憶も原文に位置特定できません**でした。
|
|
17
|
+
- `l0_conversations` に `turn`/`step` 列を追加(冪等な `ALTER TABLE`。旧行は NULL のまま = アンカー無し。**推測での埋め戻しは行いません**)。あわせて `(session_id, turn)` インデックスを追加。
|
|
18
|
+
- キャプチャ側に `step/start` の fold を追加:`user/message` はカーネル payload に `step` を含まないため、同じターンの `step/start` から導出します。`assistant/message` はイベントが持つ `{turn, step}` を使用。**最初の `step/start` より前のメッセージは `step` を空のまま**にします——座標が無いときに座標を捏造しないことが既定です。
|
|
19
|
+
- アンカーは `metadata_json` の予約キー `dsh_source_anchors` に保存(UI では `t12 s3`)。新しい列もディスク契約の変更もありません。**新規保存経路と「マージ/更新」経路の双方**がアンカーを持ちます——片方だけだとマージ一回で座標が失われます。
|
|
20
|
+
- ホスト側の新 API `MemoryDb.l0ByAnchor(sessionId, turn, step?)`:**座標で** L0 メッセージを取得します(時間順ではない)。今後の「証拠リーダー」の唯一の入口です。
|
|
21
|
+
- **記録パネルがソースアンカーを表示**(従来は常に「-」でした)。
|
|
22
|
+
|
|
23
|
+
### 修正
|
|
24
|
+
|
|
25
|
+
- **`UiRecord.sourceMessageIds` は死んだフィールドでした。** `l1_records` に**存在しない列**を読んでいたため常に `[]` になり、パネルの来源行は**一度も描画されていません**でした。実データを読む `sourceAnchors` に置き換えました。
|
|
26
|
+
|
|
27
|
+
## [0.12.0] — 2026-09-17
|
|
28
|
+
|
|
29
|
+
### 追加
|
|
30
|
+
|
|
31
|
+
- **ベクトルインデックスの手動再構築(`dsh-memory/embedding-reindex` エンドポイント + 設定画面の「ベクトルインデックス」ブロック)**。従来、再構築のきっかけは起動時の `db.init` 変更検出チェーンと、欠落ベクトルを補う定期 backfill の二つだけで、**手動の入口が存在しませんでした**。設定画面に見えるのは「キャンセル」のみ(しかも再構築が実行中のときだけ)で、「開始」も、どこまで埋まったか/何が残っているかも分かりませんでした。
|
|
32
|
+
- **エンドポイント面 31 → 32**。`EmbeddingReindexStartResponse`(`{accepted:true}`)を新設。**受理して即座に返し、進捗はここでは返しません**——クライアントは従来どおり `embedding-state-get` の `reindex` をポーリングします。進捗の語彙が二系統あればいずれ食い違うため、意図的に一つに絞っています。4 か所(`contract.ts` のマップ / `MEMORY_ENDPOINTS` 許可リスト / ディスパッチャの `case` / エンドポイント総数のアサーション)を同時に更新しました。**どれか一つでも欠けるとエンドポイントは恒久的に 404** となり、クライアントの `rpc` catch がそれを無言で握り潰すためパネルごと消えます。
|
|
33
|
+
- **`EmbeddingStateView` に `vectors` を追加**:L1 / L0 それぞれの `embedded` / `total` / `missing` / `skipped`。「X / Y 埋め込み済み」はここから来ています。db 層の **`-1` センチネルはそのまま透過**させます——「ベクトル機能が使えない」と「一件も埋め込んでいない」は UI 上で別の文でなければならず、一つの数値に畳むと、決して反応しないボタンをユーザーに押させ続けることになります。
|
|
34
|
+
|
|
35
|
+
### 修正
|
|
36
|
+
|
|
37
|
+
- **「受理したが実際には走らない」再構築要求を拒否**。`L1Store.reindex` / `L0Store.reindex` の先頭行は、ベクトル機能が未準備のとき**無言で `0/0/0` に短絡**します。入口にゲートが無ければ UI は「再構築完了・未処理ゼロ」と表示しますが、実際には**一度も開始していません**。この罠は `src/index.ts:240` に既にコメントがありましたが、それは起動チェーンのみを対象にしており、手動入口は新たな破口でした。`startReindex()` は五つのゲートをすべて前倒しし、それぞれ**行動可能な**文言を与えます:アンロード済み / 再構築実行中 / ソース切替がロック保持中 / **埋め込みソースが無効**(`currentInfo` が空)/ **サービス未準備**(「先に有効化」と「もう少し待つ」は別の指示であり、一つにまとめてはいけません)。このため両ストアに `vectorsReady()` アクセサを追加しました——`helper` は private で、外部から問い合わせる手段が無かったためです。
|
|
38
|
+
- **`embedding-subsystem.test.ts` の `db` スタブが不完全**。`swapProvider` / `markEmbeddingSynced` の二つしか持たず、`as never` で型検査を迂回していたため欠落が露見していませんでした。`snapshot()` がベクトル件数を含めるようになった時点で即座に落ちます(`getVecSkipSet is not a function`)。**`vectorCounts` を防御的にするのではなく、スタブを補完**しました:型シグネチャは完全な `MemoryDb` を宣言しており、欠落メソッドを握り潰すことは実際の配線ミスをも一緒に隠すことだからです。
|
|
39
|
+
|
|
40
|
+
### テスト
|
|
41
|
+
|
|
42
|
+
- 新規 5 件:無効時の拒否 / 未準備時の拒否 / 受理して L1+L0 を駆動しかつ並行の二回目が即座に拒否される / アンロード後の拒否 / `snapshot` の件数口径。**すべての拒否経路で「例外を投げる」と「下流が一度も呼ばれない」の両方をアサート**します——例外だけをアサートすると、「先に下流を呼んでから投げる」実装でも通ってしまいます。
|
|
43
|
+
- **反証**:準備完了ガードを一時的に外すと `向量能力未就绪 → 拒绝` が確かに赤くなり(`expected [Function] to throw an error`)、戻すと再び緑になります。
|
|
44
|
+
- 全量 **39 ファイル / 403 ケース**が通過。`typecheck`(3 つの tsconfig)、`build`、`smoke` すべて緑。
|
|
11
45
|
|
|
12
46
|
## [未リリース]
|
|
13
47
|
|
package/CHANGELOG.ko.md
CHANGED
|
@@ -7,7 +7,41 @@
|
|
|
7
7
|
|
|
8
8
|
> **호환성 참고**: 본 플러그인은 한국어 문서를 제공하지만, 공식 DSH의 `LocaleRuntime`이 등록하는 언어는 `zh` / `en`뿐입니다. `ko`를 선택하면 `locale "ko" is not registered` 오류가 납니다. DSH를 fork하여 `LOCALE_IDS`와 `LOCALES` 라벨을 갱신하고 재빌드하면 사용 가능해집니다.
|
|
9
9
|
|
|
10
|
-
이 파일은 **0.
|
|
10
|
+
이 파일은 **0.12.0** 릴리스 노트와 현재 **미출시** 변경 사항의 한국어판입니다. 전체 이력은 [CHANGELOG.md](./CHANGELOG.md)(中文)를 참조하세요.
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
### 추가
|
|
15
|
+
|
|
16
|
+
- **출처 앵커(R7) — 이제 기억을 세션 안의 실제 좌표까지 추적할 수 있습니다.** 지금까지 추적 체인은 끊겨 있었습니다: L1은 `source_message_ids`를 가지고 있지만 그것은 **L0 메시지 id**(`msg_<epoch_ms>_<hex>`)이고, L0 테이블에는 `turn`/`step` 컬럼이 없었습니다. 게다가 그 id 목록은 **검색 DB에 전혀 기록되지 않았습니다**(쓰기 측이 `metadata`만 저장하고 해당 필드는 조용히 버려졌습니다). 결과: **어떤 기억도 원문에서 위치를 찾을 수 없었습니다.**
|
|
17
|
+
- `l0_conversations`에 `turn`/`step` 컬럼 추가(멱등 `ALTER TABLE`; 기존 행은 NULL 유지 = 앵커 없음, **추측으로 채우지 않습니다**)와 `(session_id, turn)` 인덱스.
|
|
18
|
+
- 캡처 측에 `step/start` fold 추가: `user/message`는 커널 payload에 `step`이 없으므로 같은 턴의 `step/start`에서 유도합니다. `assistant/message`는 이벤트가 가진 `{turn, step}`을 사용합니다. **첫 `step/start` 이전 메시지는 `step`을 비워 둡니다** — 좌표가 없을 때 좌표를 만들어내지 않는 것이 기본입니다.
|
|
19
|
+
- 앵커는 `metadata_json`의 예약 키 `dsh_source_anchors`에 저장됩니다(UI에는 `t12 s3`). 새 컬럼도, 디스크 계약 변경도 없습니다. **신규 저장 경로와 "병합/업데이트" 경로 모두** 앵커를 가집니다 — 한쪽만 있으면 병합 한 번에 좌표가 사라집니다.
|
|
20
|
+
- 호스트 신규 API `MemoryDb.l0ByAnchor(sessionId, turn, step?)`: **좌표로**(시간순이 아니라) L0 메시지를 가져옵니다. 향후 "증거 리더"의 유일한 진입점입니다.
|
|
21
|
+
- **기록 패널이 출처 앵커를 표시**합니다(이전에는 항상 "-"였습니다).
|
|
22
|
+
|
|
23
|
+
### 수정
|
|
24
|
+
|
|
25
|
+
- **`UiRecord.sourceMessageIds`는 죽은 필드였습니다.** `l1_records`에 **존재하지 않는 컬럼**을 읽어 항상 `[]`가 되었고, 패널의 출처 행은 **한 번도 렌더링되지 않았습니다**. 실제 데이터를 읽는 `sourceAnchors`로 교체했습니다.
|
|
26
|
+
|
|
27
|
+
## [0.12.0] — 2026-09-17
|
|
28
|
+
|
|
29
|
+
### 추가
|
|
30
|
+
|
|
31
|
+
- **벡터 인덱스 수동 재구축(`dsh-memory/embedding-reindex` 엔드포인트 + 설정 화면의 "벡터 인덱스" 블록)**. 지금까지 재구축 계기는 시작 시의 `db.init` 변경 감지 체인과, 누락 벡터를 채우는 주기적 backfill 두 가지뿐이었고 **수동 진입점이 없었습니다**. 설정 화면에 보이는 것은 "취소"뿐(그것도 재구축이 실행 중일 때만)이었고, "시작"도, 얼마나 임베딩되었고 얼마나 남았는지도 알 수 없었습니다.
|
|
32
|
+
- **엔드포인트 면 31 → 32**. `EmbeddingReindexStartResponse`(`{accepted:true}`) 신설. **접수 후 즉시 반환하며 진행률은 여기서 반환하지 않습니다** — 클라이언트는 기존대로 `embedding-state-get`의 `reindex` 필드를 폴링합니다. 진행률 어휘가 두 벌이면 언젠가 서로 어긋나므로 의도적으로 하나만 유지합니다. 네 곳(`contract.ts` 매핑 / `MEMORY_ENDPOINTS` 허용 목록 / 디스패처 `case` / 엔드포인트 총수 단언)을 함께 갱신했습니다. **하나라도 빠지면 해당 엔드포인트는 영구 404**가 되고, 클라이언트 `rpc`의 catch가 이를 조용히 삼키므로 패널 전체가 사라집니다.
|
|
33
|
+
- **`EmbeddingStateView`에 `vectors` 추가**: L1 / L0 각각의 `embedded` / `total` / `missing` / `skipped`. "X / Y 임베딩됨"이 여기서 나옵니다. db 계층의 **`-1` 센티넬은 그대로 통과**시킵니다 — "벡터 기능을 쓸 수 없음"과 "하나도 임베딩하지 않음"은 UI에서 서로 다른 문장이어야 하며, 하나의 숫자로 합치면 절대 반응하지 않는 버튼을 사용자에게 계속 누르게 만듭니다.
|
|
34
|
+
|
|
35
|
+
### 수정
|
|
36
|
+
|
|
37
|
+
- **"접수했지만 실제로는 돌지 않는" 재구축 요청을 거부**. `L1Store.reindex` / `L0Store.reindex`의 첫 줄은 벡터 기능이 준비되지 않았을 때 **조용히 `0/0/0`으로 단락**됩니다. 진입점에 게이트가 없으면 UI는 "재구축 완료, 미처리 0건"을 표시하지만 실제로는 **한 번도 시작하지 않았습니다**. 이 함정은 `src/index.ts:240`에 이미 주석이 있었으나 시작 체인만을 대상으로 했고, 수동 진입점은 새로운 구멍이었습니다. `startReindex()`는 다섯 개의 게이트를 모두 앞당기고 각각 **행동 가능한** 문구를 부여합니다: 언로드됨 / 재구축 진행 중 / 소스 전환이 잠금 보유 / **임베딩 소스 꺼짐**(`currentInfo` 비어 있음) / **서비스 미준비**. ("먼저 활성화하세요"와 "조금 기다리세요"는 서로 다른 지시이며 하나로 합쳐서는 안 됩니다.) 이를 위해 두 스토어에 `vectorsReady()` 접근자를 추가했습니다 — `helper`가 private이라 외부에서 물어볼 방법이 없었기 때문입니다.
|
|
38
|
+
- **`embedding-subsystem.test.ts`의 `db` 스텁이 불완전**. `swapProvider` / `markEmbeddingSynced` 두 개만 가지고 있었고 `as never`로 타입 검사를 우회했기에 누락이 드러나지 않았습니다. `snapshot()`이 벡터 개수를 포함하기 시작하자 즉시 죽습니다(`getVecSkipSet is not a function`). **`vectorCounts`를 방어적으로 만드는 대신 스텁을 보완**했습니다: 타입 시그니처는 완전한 `MemoryDb`를 선언하고 있으며, 누락된 메서드를 삼키는 것은 실제 배선 오류까지 함께 숨기는 일이기 때문입니다.
|
|
39
|
+
|
|
40
|
+
### 테스트
|
|
41
|
+
|
|
42
|
+
- 신규 5건: 꺼짐 상태 거부 / 미준비 거부 / 접수 후 L1+L0 구동 및 동시 두 번째 즉시 거부 / 언로드 후 거부 / `snapshot` 개수 기준. **모든 거부 경로에서 "예외를 던진다"와 "하위가 한 번도 호출되지 않는다"를 함께 단언**합니다 — 예외만 단언하면 "먼저 하위를 호출하고 나서 던지는" 구현도 통과합니다.
|
|
43
|
+
- **반증**: 준비 가드를 일시적으로 제거하면 `向量能力未就绪 → 拒绝`가 실제로 빨간색이 되고(`expected [Function] to throw an error`), 되돌리면 다시 초록색이 됩니다.
|
|
44
|
+
- 전체 **39 파일 / 403 케이스** 통과. `typecheck`(tsconfig 3종), `build`, `smoke` 모두 초록.
|
|
11
45
|
|
|
12
46
|
## [미출시]
|
|
13
47
|
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@
|
|
|
6
6
|
> **UI 截图约定**:带界面变化的条目在 `assets/changelog/<版本号>/<两位编号>-<简述>.png`
|
|
7
7
|
> 存真机截图,并在条目内以相对路径引用,读者可在更新日志里直接看到新版本 UI 的样子。
|
|
8
8
|
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- **来源锚点(R7)——记忆现在能追回会话里的**真实位置**。** 在此之前溯源链是断的:L1 带着 `source_message_ids`,但那些是 **L0 消息 id**(`msg_<epoch_ms>_<hex>`),而 L0 表没有 `turn`/`step` 列;且该 id 列表**根本没写进检索库**(写入侧只取 `metadata`,字段被静默丢弃)。结果是**任何一条记忆都无法定位到原文**。
|
|
14
|
+
- `l0_conversations` 补 `turn`/`step` 两列(幂等 `ALTER TABLE`;旧行保持 NULL = 无锚点,**绝不猜测回填**),并新增 `(session_id, turn)` 索引。
|
|
15
|
+
- 捕获侧新增 `step/start` fold:`user/message` 在内核负载里**不带** `step`,靠同轮 `step/start` 推出;`assistant/message` 用事件自带的 `{turn, step}`。**首个 `step/start` 之前的消息 step 留空**——缺坐标时不编坐标,这是红线。
|
|
16
|
+
- 锚点存在 `metadata_json` 的保留键 `dsh_source_anchors`(UI 显示为 `t12 s3`)。不加列、不动磁盘契约。**新建与「合并/更新」两条写入路径都带锚点**——否则合并一次就丢坐标,而合并是长会话里最常发生的动作。
|
|
17
|
+
- 新增宿主取数接口 `MemoryDb.l0ByAnchor(sessionId, turn, step?)`:**按坐标**(而非按时间)取 L0 消息,是后续「证据读取器」的唯一入口。
|
|
18
|
+
- **记录面板显示来源锚点**(此前该行永远是「-」)。
|
|
19
|
+
- **证据读取器(R1)——把锚点还原成会话原文。** 取原文走**内核 `ctx.sessionQuery`** 直连(`readSession` / `listEvents`),不再等外部索引插件的 HTTP 端点:本插件是宿主插件,手里就有 `ctx`,少一层进程边界与失败点。本轮落地**纯函数层**(装配接线与真机实调见后续条目)。
|
|
20
|
+
- 会话 id **两种形态都试**:实测索引里 `session_id` 有带 `session-` 前缀与纯 uuid 两种,只试一种会**静默漏掉 124 个会话**(不报错,只是永远查不到)。
|
|
21
|
+
- `foldEventAnchors` 在读取侧复用**与捕获侧同一条 fold 规则**,保证写入的坐标与读回的坐标是同一套语义。
|
|
22
|
+
- **忠实投影**:不 `stripCodeBlocks`、不按长度截断、不做「值不值得记」筛选——**捕获可以为省 token 丢东西,取证不行**。唯一保留的过滤是「插件注入的上下文不算用户发言」。
|
|
23
|
+
- **失败可分类**(这是本节的重点):`no-service` / `no-anchor` / `session-unreadable` / `anchor-not-found` / `timeout` / `error`。前四类的区分是必需的——把「读不到」当成「没谈过」会让已归档会话的记忆被系统性误判。
|
|
24
|
+
|
|
25
|
+
### 修复
|
|
26
|
+
|
|
27
|
+
- **`UiRecord.sourceMessageIds` 是死字段。** 它读的是 `l1_records` **从不存在的列**,永远回退 `[]`,于是记录面板的来源行**从未渲染过**。已替换为读真实数据的 `sourceAnchors`。
|
|
28
|
+
|
|
29
|
+
## [0.12.0] — 2026-09-17
|
|
30
|
+
|
|
31
|
+
### 新增
|
|
32
|
+
|
|
33
|
+
- **手动重建向量索引(端点 `dsh-memory/embedding-reindex` + 设置页「向量索引」区块)**。此前重建只有两条路:启动时的 `db.init` 变更检测链,与周期 backfill 的缺失补齐——**用户没有任何手动入口**。设置页里能看到的只有「取消」(且只在重建进行中才出现),既看不到「开始」,也看不到当前嵌了多少、还缺多少。现在补齐:
|
|
34
|
+
- **端点面 31 → 32**。新增 `EmbeddingReindexStartResponse`(`{accepted:true}`),**受理即返回,进度不在此回传**——客户端照旧轮询 `embedding-state-get` 的 `reindex` 字段。两套进度语义各说各话是迟早要出事的,所以刻意只留一套。三处清单(`contract.ts` 映射表 / `stats.ts` 的 `MEMORY_ENDPOINTS` 白名单 / 分发 `case`)与端点总数断言同步更新;这四处**少改任何一处都会让端点恒返 404**,而客户端 `rpc` 的 catch 会静默吞掉异常、面板整块消失。
|
|
35
|
+
- **`EmbeddingStateView` 新增 `vectors`**:L1 / L0 各自的 `embedded` / `total` / `missing` / `skipped`。「已嵌入 X / 总 Y」由此而来。db 层的 **`-1` 哨兵原样透传**——「向量能力不可用」与「一条都没嵌」在界面上必须是两句不同的话;折叠成一个数字,用户就会去点一个永远没反应的按钮。
|
|
36
|
+
|
|
37
|
+
### 修复
|
|
38
|
+
|
|
39
|
+
- **拒绝「受理了但根本不会跑」的重建请求**。`L1Store.reindex` / `L0Store.reindex` 首行在向量能力未就绪时**静默短路**成 `0/0/0`。入口若不设门槛,UI 会显示「重建完成、零条待补」——而真相是它**根本没开始**。该陷阱在 `src/index.ts:240` 早有注释,但那只覆盖启动链,手动入口是新开的破口。`startReindex()` 现将五道门槛全部前置,且各自给出**可行动的**文案:已卸载 / 重建已在进行中 / 嵌入源切换占用 / **嵌入源已关闭**(`currentInfo` 为空)/ **嵌入服务未就绪**(「先启用」与「再等等」是两句不同的话,不能合并成一句)。为此给两个 store 补了 `vectorsReady()` 访问器——`helper` 是私有的,外部无从询问。
|
|
40
|
+
- **`embedding-subsystem.test.ts` 的 `db` 桩件不完整**。它只有 `swapProvider` / `markEmbeddingSynced` 两个方法,靠 `as never` 绕过类型检查,因此从未被检出;`snapshot()` 开始附带向量计数后即崩(`getVecSkipSet is not a function`)。**补桩件,而不是把 `vectorCounts` 改成防御式**:类型签名声明的是完整 `MemoryDb`,把缺失方法吞掉,等于把真实接线错误一并藏起来。
|
|
41
|
+
|
|
42
|
+
### 测试
|
|
43
|
+
|
|
44
|
+
- 新增 5 个用例:关闭态拒绝 / 未就绪拒绝 / 受理并驱动 L1+L0 且并发第二次立即被拒 / 卸载后拒绝 / `snapshot` 计数口径。**每条拒绝路径都同时断言「抛错」与「下游一次都没被调用」**——只断言抛错的话,一个「先调用下游、再抛错」的实现照样能过。
|
|
45
|
+
- **反证**:临时摘除就绪守卫后,`向量能力未就绪 → 拒绝,不谎报「已受理」` 确实变红(`expected [Function] to throw an error`),还原后复绿。
|
|
46
|
+
- 全量 **39 文件 / 403 用例**通过;`typecheck`(三份 tsconfig)、`build`、`smoke` 均绿。
|
|
47
|
+
|
|
9
48
|
## [0.11.0] — 2026-09-13
|
|
10
49
|
|
|
11
50
|
### 兼容性(按 DSH 插件框架文档适配)
|
package/dist/client.js
CHANGED
|
@@ -2500,6 +2500,12 @@ var __defProp = Object.defineProperty;
|
|
|
2500
2500
|
m.id
|
|
2501
2501
|
);
|
|
2502
2502
|
});
|
|
2503
|
+
const vec = st.vectors;
|
|
2504
|
+
const vecOk = !!vec && vec.l1.embedded >= 0 && vec.l0.embedded >= 0;
|
|
2505
|
+
const vecNote = !vec ? "向量索引:宿主版本较旧,未上报计数" : !vecOk ? "向量索引不可用(sqlite-vec 扩展缺失或检索库降级),重建无从谈起" : "已嵌入 L1 " + vec.l1.embedded + "/" + vec.l1.total + " · L0 " + vec.l0.embedded + "/" + vec.l0.total + (vec.l1.missing + vec.l0.missing > 0 ? "(待补 " + (vec.l1.missing + vec.l0.missing) + " 条)" : "") + (vec.l1.skipped + vec.l0.skipped > 0 ? "(" + (vec.l1.skipped + vec.l0.skipped) + " 条内容不可嵌入,已跳过)" : "");
|
|
2506
|
+
const reindexRunning = !!(st.reindex && st.reindex.running);
|
|
2507
|
+
const canRebuild = vecOk && st.source !== "off" && !reindexRunning && !st.apply.busy;
|
|
2508
|
+
const rebuildHint = !vecOk ? "向量能力不可用" : st.source === "off" ? "嵌入源已关闭,请先启用" : reindexRunning ? "重建已在进行中" : st.apply.busy ? "嵌入源切换进行中" : "只补缺失向量;已有向量不动。零向量内容会被跳过(重试无意义)";
|
|
2503
2509
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "dsh-mem-rb-card", children: [
|
|
2504
2510
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: S.flexRow, children: [
|
|
2505
2511
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { fontWeight: 600, whiteSpace: "nowrap" }, children: "语义检索(嵌入源)" }),
|
|
@@ -2613,7 +2619,20 @@ var __defProp = Object.defineProperty;
|
|
|
2613
2619
|
] })
|
|
2614
2620
|
] }),
|
|
2615
2621
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: S.panelLabel, children: "本地模型目录(下载后离线可用,不随插件分发)" }),
|
|
2616
|
-
modelCards
|
|
2622
|
+
modelCards,
|
|
2623
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: RSTY.block, children: [
|
|
2624
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: RSTY.title, children: "向量索引" }),
|
|
2625
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: RSTY.note, children: vecNote }),
|
|
2626
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: RSTY.row, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
2627
|
+
NButton,
|
|
2628
|
+
{
|
|
2629
|
+
disabled: !canRebuild,
|
|
2630
|
+
title: rebuildHint,
|
|
2631
|
+
onClick: () => call("dsh-memory/embedding-reindex", {}),
|
|
2632
|
+
children: "重建索引"
|
|
2633
|
+
}
|
|
2634
|
+
) })
|
|
2635
|
+
] })
|
|
2617
2636
|
] });
|
|
2618
2637
|
}
|
|
2619
2638
|
|
|
@@ -3367,7 +3386,7 @@ var __defProp = Object.defineProperty;
|
|
|
3367
3386
|
) : null
|
|
3368
3387
|
] }),
|
|
3369
3388
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.content, children: m.content }),
|
|
3370
|
-
open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.detail, children: "id: " + m.id + "\n情境: " + (m.scene || "-") + "\n版本: v" + m.version + "(去重合并次数 " + m.version + ")\n创建: " + fmtTime(m.createdAt) + "\n活跃时间: " + (m.timestamps && m.timestamps.length > 0 ? m.timestamps.map(fmtTime).join(" → ") : "-") + "\n" + (m.
|
|
3389
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.detail, children: "id: " + m.id + "\n情境: " + (m.scene || "-") + "\n版本: v" + m.version + "(去重合并次数 " + m.version + ")\n创建: " + fmtTime(m.createdAt) + "\n活跃时间: " + (m.timestamps && m.timestamps.length > 0 ? m.timestamps.map(fmtTime).join(" → ") : "-") + "\n" + (m.sourceAnchors && m.sourceAnchors.length > 0 ? "来源锚点: " + m.sourceAnchors.join(", ") : "来源锚点: -") }) : null
|
|
3371
3390
|
]
|
|
3372
3391
|
},
|
|
3373
3392
|
m.id
|
package/dist/contract.d.ts
CHANGED
|
@@ -93,6 +93,9 @@ export interface MemoryLiveSettings {
|
|
|
93
93
|
/** 记忆写删权限门:true 才允许写删记忆工具(memory_add/memory_delete)与面板高权限删除
|
|
94
94
|
* (records-delete)。默认 false(模型写删风险高,须显式在面板开启高权限模式)。 */
|
|
95
95
|
memoryMutate: boolean;
|
|
96
|
+
/** §C 人工冲突裁决总开关:true = 去重判定"两边都像对的"时冻结冲突对,停放到待人工裁决区;
|
|
97
|
+
* false = 默认,冲突按 LLM 的 winner/loser 自动了结。运行时覆盖静态 config 的 conflictFreeze.enabled。 */
|
|
98
|
+
conflictFreeze: boolean;
|
|
96
99
|
}
|
|
97
100
|
/** 召回停用原因(session-stats recall.enabled=false 时带出;短路序第一个为假的因子)。 */
|
|
98
101
|
export type RecallDisabledReason = 'deploy' | 'global' | 'session' | 'mode';
|
|
@@ -143,6 +146,14 @@ export interface RebuildStatus {
|
|
|
143
146
|
error: string | null;
|
|
144
147
|
/** 归档产物名(提示用户可手工找回)。 */
|
|
145
148
|
archiveNote: string | null;
|
|
149
|
+
/**
|
|
150
|
+
* 保留集说明(无 L0 来源、清空前被保全的记忆;task_8c)。
|
|
151
|
+
*
|
|
152
|
+
* 与 `archiveNote` 并列暴露,是因为"重建后导入记忆还在不在"必须**可观测** ——
|
|
153
|
+
* 只写日志的话,用户看到"重建完成"根本无从得知那些外部记忆是被保住了还是被清掉了。
|
|
154
|
+
* null 表示尚未进入准备阶段。
|
|
155
|
+
*/
|
|
156
|
+
preserveNote: string | null;
|
|
146
157
|
}
|
|
147
158
|
/** 反刍阶段。 */
|
|
148
159
|
export type RuminatePhase = 'idle' | 'refreshing' | 'distilling' | 'consolidating' | 'updating' | 'done' | 'cancelled' | 'failed';
|
|
@@ -374,8 +385,27 @@ export interface EmbeddingStateView {
|
|
|
374
385
|
error: string | null;
|
|
375
386
|
} | null;
|
|
376
387
|
reindex: ReindexProgressState;
|
|
388
|
+
/** 向量索引概况("已嵌入 X / 总 Y"的数据源)。 */
|
|
389
|
+
vectors: VectorIndexView;
|
|
377
390
|
activeNote?: string;
|
|
378
391
|
}
|
|
392
|
+
/** 单层(L1 / L0)的向量索引计数。
|
|
393
|
+
* 不可用哨兵统一为 **-1**(沿用 db 层 `countL1Vec` / `countL1VecMissing` 的约定),
|
|
394
|
+
* 与"真的是 0 条"区分——`嵌入能力挂掉` 和 `一条都没嵌` 在 UI 上是两句不同的话。 */
|
|
395
|
+
export interface VectorCountView {
|
|
396
|
+
/** 向量表行数(已嵌入)。 */
|
|
397
|
+
embedded: number;
|
|
398
|
+
/** 元数据行数(嵌入分母)。 */
|
|
399
|
+
total: number;
|
|
400
|
+
/** 缺向量且可补齐的条数(已排除 skip 集)。 */
|
|
401
|
+
missing: number;
|
|
402
|
+
/** 内容不可嵌入、已进 skip 集的条数(重试无意义)。 */
|
|
403
|
+
skipped: number;
|
|
404
|
+
}
|
|
405
|
+
export interface VectorIndexView {
|
|
406
|
+
l1: VectorCountView;
|
|
407
|
+
l0: VectorCountView;
|
|
408
|
+
}
|
|
379
409
|
/** dsh-memory/stats */
|
|
380
410
|
export interface StatsResponse extends MemoryStats {
|
|
381
411
|
}
|
|
@@ -525,6 +555,8 @@ export interface SettingsSetRequest {
|
|
|
525
555
|
embedRemoteApiKey?: string;
|
|
526
556
|
/** 记忆写删权限门(true = 允许写删工具与面板高权限删除)。 */
|
|
527
557
|
memoryMutate?: boolean;
|
|
558
|
+
/** §C 人工冲突裁决总开关(true = 冻结冲突对,停放到待人工裁决区)。 */
|
|
559
|
+
conflictFreeze?: boolean;
|
|
528
560
|
}
|
|
529
561
|
export interface SettingsSetResponse {
|
|
530
562
|
ok: true;
|
|
@@ -556,7 +588,15 @@ export interface UiRecord {
|
|
|
556
588
|
createdAt: string | null;
|
|
557
589
|
updatedAt: string | null;
|
|
558
590
|
version: number;
|
|
559
|
-
|
|
591
|
+
/**
|
|
592
|
+
* 来源锚点(R7):形如 `t12 s3`(无 step 时为 `t12`),来自
|
|
593
|
+
* `metadata.dsh_source_anchors`。**空数组 = 该记忆无锚点**(老数据 / 捕获侧
|
|
594
|
+
* 未打戳 / 解析不到坐标),不是"没有来源"。
|
|
595
|
+
*
|
|
596
|
+
* 2026-09-17 替换原 `sourceMessageIds`:`l1_records` 从不存该列,原字段
|
|
597
|
+
* 永远是 `[]`(死字段,UI 因此从未显示过来源行)。锚点是同一意图的**活实现**。
|
|
598
|
+
*/
|
|
599
|
+
sourceAnchors: string[];
|
|
560
600
|
/** 检索相关度(列表路径无 score → null)。 */
|
|
561
601
|
score: number | null;
|
|
562
602
|
}
|
|
@@ -746,6 +786,12 @@ export interface EmbeddingDownloadStartResponse {
|
|
|
746
786
|
export interface EmbeddingCancelResponse {
|
|
747
787
|
cancelled: boolean;
|
|
748
788
|
}
|
|
789
|
+
/** dsh-memory/embedding-reindex(手动触发重建)。
|
|
790
|
+
* 受理即返回,**不在此回传进度**——客户端照旧轮询 embedding-state-get 的 reindex 字段,
|
|
791
|
+
* 否则"受理响应"与"进度快照"会各有一套进度语义,两边迟早对不上。 */
|
|
792
|
+
export interface EmbeddingReindexStartResponse {
|
|
793
|
+
accepted: true;
|
|
794
|
+
}
|
|
749
795
|
/** dsh-memory/embedding-model-delete */
|
|
750
796
|
export interface EmbeddingModelDeleteRequest {
|
|
751
797
|
modelId: string;
|
|
@@ -855,6 +901,7 @@ export interface DshMemoryRequestMap {
|
|
|
855
901
|
'dsh-memory/embedding-download-cancel': Record<string, never>;
|
|
856
902
|
'dsh-memory/embedding-model-delete': EmbeddingModelDeleteRequest;
|
|
857
903
|
'dsh-memory/embedding-runtime-cancel': Record<string, never>;
|
|
904
|
+
'dsh-memory/embedding-reindex': Record<string, never>;
|
|
858
905
|
'dsh-memory/embedding-reindex-cancel': Record<string, never>;
|
|
859
906
|
}
|
|
860
907
|
export interface DshMemoryResponseMap {
|
|
@@ -889,6 +936,7 @@ export interface DshMemoryResponseMap {
|
|
|
889
936
|
'dsh-memory/embedding-download-cancel': EmbeddingCancelResponse;
|
|
890
937
|
'dsh-memory/embedding-model-delete': EmbeddingModelDeleteResponse;
|
|
891
938
|
'dsh-memory/embedding-runtime-cancel': EmbeddingCancelResponse;
|
|
939
|
+
'dsh-memory/embedding-reindex': EmbeddingReindexStartResponse;
|
|
892
940
|
'dsh-memory/embedding-reindex-cancel': EmbeddingCancelResponse;
|
|
893
941
|
}
|
|
894
942
|
/** 全部端点名(client 调用与 host case 表的共用字面量来源)。 */
|
package/dist/hooks/capture.js
CHANGED
|
@@ -11,8 +11,18 @@ import { sanitizeText, shouldCaptureL0, stripCodeBlocks } from '../util/sanitize
|
|
|
11
11
|
* 需要进缓冲的事件类型。流式 chunk(text-delta/reasoning 等)一秒钟可达数百条,
|
|
12
12
|
* 缓冲它们会把 MAX_BUFFER 撑爆、把轮次头部(turn/start + user 消息)裁掉——
|
|
13
13
|
* 2026-08-16 真实事故:长回复轮次丢失 user 消息。
|
|
14
|
+
*
|
|
15
|
+
* `step/start` (2026-09-17 加入,R7):只为 **fold 出 step 坐标** 而缓冲,自身不落盘。
|
|
16
|
+
* 实测占比仅 **0.68%**(`memory-evidence-reconcile/findings.md` 容量表),相对
|
|
17
|
+
* 47% 的 streaming delta 可忽略;换来的是 `user/message` 也能拿到同轮 step。
|
|
14
18
|
*/
|
|
15
|
-
const RELEVANT_TYPES = new Set([
|
|
19
|
+
const RELEVANT_TYPES = new Set([
|
|
20
|
+
'user/message',
|
|
21
|
+
'assistant/message',
|
|
22
|
+
'turn/start',
|
|
23
|
+
'turn/end',
|
|
24
|
+
'step/start',
|
|
25
|
+
]);
|
|
16
26
|
export function isCaptureRelevant(type) {
|
|
17
27
|
return RELEVANT_TYPES.has(type);
|
|
18
28
|
}
|
|
@@ -85,7 +95,7 @@ export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
|
85
95
|
if (event.type === 'turn/end') {
|
|
86
96
|
const turn = event.data.turn;
|
|
87
97
|
const turnEvents = buffers.takeTurn(sid, turn);
|
|
88
|
-
const messages = turnEventsToMessages(turnEvents, cfg, logger);
|
|
98
|
+
const messages = turnEventsToMessages(turnEvents, cfg, logger, sid, turn);
|
|
89
99
|
if (messages.length > 0) {
|
|
90
100
|
const roles = messages.reduce((acc, m) => {
|
|
91
101
|
acc[m.role] = (acc[m.role] ?? 0) + 1;
|
|
@@ -154,9 +164,25 @@ function findTurnStart(buf, turn) {
|
|
|
154
164
|
return -1;
|
|
155
165
|
}
|
|
156
166
|
/** 把轮次事件转成 L0 消息(仅真实 user 消息 + assistant 消息,清洗过滤)。 */
|
|
157
|
-
function turnEventsToMessages(events, cfg, logger) {
|
|
167
|
+
function turnEventsToMessages(events, cfg, logger, sessionId, turn) {
|
|
158
168
|
const out = [];
|
|
169
|
+
/**
|
|
170
|
+
* step fold(R7):`assistant/message` 与 `tool/result` 自带 `{turn, step}`,
|
|
171
|
+
* 但 `user/message` **不带**(内核 `types.d.ts:274` 对 `:291-324`)。按 seq 序
|
|
172
|
+
* 推进当前 step,让轮内的 user 消息也能拿到**同轮**坐标。
|
|
173
|
+
*
|
|
174
|
+
* 红线:`step/start` 之前出现的 user 消息**留空 step**,不拿上一轮的 step 顶替
|
|
175
|
+
* ——"轮内第一个 step 尚未开始"是真的没有坐标,编一个比留空更糟。
|
|
176
|
+
*/
|
|
177
|
+
let currentStep;
|
|
159
178
|
for (const event of events) {
|
|
179
|
+
// step 边界推进 fold 游标(不作为消息落盘,故不参与 out)
|
|
180
|
+
if (event.type === 'step/start') {
|
|
181
|
+
const s = event.data.step;
|
|
182
|
+
if (typeof s === 'number' && Number.isFinite(s))
|
|
183
|
+
currentStep = s;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
160
186
|
if (event.type === 'user/message') {
|
|
161
187
|
const msg = event.data;
|
|
162
188
|
// 只捕获真实用户输入(source.kind === 'user'),跳过插件注入上下文
|
|
@@ -166,7 +192,10 @@ function turnEventsToMessages(events, cfg, logger) {
|
|
|
166
192
|
}
|
|
167
193
|
const content = sanitizeText(blocksToText(msg.content));
|
|
168
194
|
if (shouldCaptureL0(content)) {
|
|
169
|
-
|
|
195
|
+
const anchor = { sessionId, turn };
|
|
196
|
+
if (currentStep !== undefined)
|
|
197
|
+
anchor.step = currentStep;
|
|
198
|
+
out.push(makeMessage('user', content, event.time, cfg.capture.maxMessageChars, anchor));
|
|
170
199
|
}
|
|
171
200
|
}
|
|
172
201
|
else if (event.type === 'assistant/message') {
|
|
@@ -175,7 +204,13 @@ function turnEventsToMessages(events, cfg, logger) {
|
|
|
175
204
|
if (cfg.capture.stripCodeBlocks)
|
|
176
205
|
content = stripCodeBlocks(content);
|
|
177
206
|
if (shouldCaptureL0(content)) {
|
|
178
|
-
|
|
207
|
+
// 事件自带 turn/step 优先(fold 只服务于不带该字段的事件类型)
|
|
208
|
+
const evTurn = typeof data.turn === 'number' && Number.isFinite(data.turn) ? data.turn : turn;
|
|
209
|
+
const evStep = typeof data.step === 'number' && Number.isFinite(data.step) ? data.step : currentStep;
|
|
210
|
+
const anchor = { sessionId, turn: evTurn };
|
|
211
|
+
if (evStep !== undefined)
|
|
212
|
+
anchor.step = evStep;
|
|
213
|
+
out.push(makeMessage('assistant', content, event.time, cfg.capture.maxMessageChars, anchor));
|
|
179
214
|
}
|
|
180
215
|
}
|
|
181
216
|
}
|
|
@@ -184,11 +219,14 @@ function turnEventsToMessages(events, cfg, logger) {
|
|
|
184
219
|
}
|
|
185
220
|
return out;
|
|
186
221
|
}
|
|
187
|
-
function makeMessage(role, content, timestamp, maxChars) {
|
|
188
|
-
|
|
222
|
+
function makeMessage(role, content, timestamp, maxChars, anchor) {
|
|
223
|
+
const msg = {
|
|
189
224
|
id: `msg_${Date.now()}_${randomBytes(3).toString('hex')}`,
|
|
190
225
|
role,
|
|
191
226
|
content: content.slice(0, maxChars),
|
|
192
227
|
timestamp,
|
|
193
228
|
};
|
|
229
|
+
if (anchor !== undefined)
|
|
230
|
+
msg.anchor = anchor;
|
|
231
|
+
return msg;
|
|
194
232
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话位置锚点(R7)——纯函数层。
|
|
3
|
+
*
|
|
4
|
+
* **为什么单独一层**:锚点的两个动作(批量带入 L1 记录、从库里读回)都是
|
|
5
|
+
* "输入 → 输出"的纯变换,且是 R7 的**正确性核心**。放在 pipeline 里就要靠
|
|
6
|
+
* 真跑 LLM 才能验;抽出来可以直接单测,包括 fold 正确性与越界降级。
|
|
7
|
+
*
|
|
8
|
+
* **红线(不得违反)**:
|
|
9
|
+
* 1. 锚点只能来自内核事件负载或捕获侧 fold 出的值,**永不推算**。
|
|
10
|
+
* 2. `step` 缺失就留空,**不得**用相邻事件的 step 补齐。
|
|
11
|
+
* 3. `source_message_ids` 里映射不到的消息 id **直接丢弃**,不得猜测坐标。
|
|
12
|
+
*/
|
|
13
|
+
import type { ConversationAnchor, ConversationMessage } from '../types.js';
|
|
14
|
+
/**
|
|
15
|
+
* `l1_records.metadata_json` 里承载锚点集合的**保留键**。
|
|
16
|
+
*
|
|
17
|
+
* 加前缀 `dsh_` 是为了与 LLM 产出的 metadata 键(`hall` / `activity_start_time` 等)
|
|
18
|
+
* 在命名空间上隔开——写库时两者会被合并进同一个 JSON 对象(见 `withSourceAnchors`)。
|
|
19
|
+
*/
|
|
20
|
+
export declare const ANCHOR_METADATA_KEY = "dsh_source_anchors";
|
|
21
|
+
/**
|
|
22
|
+
* 建立 `L0 消息 id → 锚点` 映射。
|
|
23
|
+
*
|
|
24
|
+
* 只收录**带锚点**的消息:没有内核坐标的消息(老数据、无 turn 的事件)不进映射,
|
|
25
|
+
* 于是它在 `resolveSourceAnchors` 里自然落进"未命中"分支,而不是被伪造一个坐标。
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildAnchorMap(messages: ConversationMessage[]): Map<string, ConversationAnchor>;
|
|
28
|
+
/**
|
|
29
|
+
* 把一条记忆的 `source_message_ids` 解析成去重、有序的锚点集合。
|
|
30
|
+
*
|
|
31
|
+
* - 命中映射 → 收下该锚点
|
|
32
|
+
* - 未命中(LLM 引用了背景消息、或该消息本就没有坐标) → **丢弃**
|
|
33
|
+
* - 结果为空 → 返回 `undefined`,由调用方按"无锚点"处理(不是空数组)
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveSourceAnchors(sourceMessageIds: readonly string[] | undefined, anchors: ReadonlyMap<string, ConversationAnchor>): ConversationAnchor[] | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* 把锚点集合写进 metadata(返回**新对象**,不改入参——不可变更新)。
|
|
38
|
+
*
|
|
39
|
+
* 无锚点时不写键:老记录与"解析不到坐标"的记录保持与改动前**逐字一致的**
|
|
40
|
+
* metadata,这样既有导出/比对/测试不会被一个空数组搅动。
|
|
41
|
+
*/
|
|
42
|
+
export declare function withSourceAnchors(metadata: Record<string, unknown> | undefined, anchors: ConversationAnchor[] | undefined): Record<string, unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* 从 metadata 读回锚点集合(读侧唯一入口)。
|
|
45
|
+
*
|
|
46
|
+
* 形状校验从严:任何一项缺 `turn` 或类型不对 → 该项丢弃;全丢 → `undefined`。
|
|
47
|
+
* 宁可报告"无锚点",也不把半截坐标喂给下游的证据读取器。
|
|
48
|
+
*/
|
|
49
|
+
export declare function readSourceAnchors(metadata: unknown): ConversationAnchor[] | undefined;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `l1_records.metadata_json` 里承载锚点集合的**保留键**。
|
|
3
|
+
*
|
|
4
|
+
* 加前缀 `dsh_` 是为了与 LLM 产出的 metadata 键(`hall` / `activity_start_time` 等)
|
|
5
|
+
* 在命名空间上隔开——写库时两者会被合并进同一个 JSON 对象(见 `withSourceAnchors`)。
|
|
6
|
+
*/
|
|
7
|
+
export const ANCHOR_METADATA_KEY = 'dsh_source_anchors';
|
|
8
|
+
/** 锚点的规范性比较键:`turn` 升序,`step` 缺失排在同 turn 的最前。 */
|
|
9
|
+
function anchorOrderKey(a) {
|
|
10
|
+
const step = typeof a.step === 'number' ? String(a.step).padStart(6, '0') : '000000';
|
|
11
|
+
return `${a.sessionId}\u0000${String(a.turn).padStart(10, '0')}\u0000${step}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 建立 `L0 消息 id → 锚点` 映射。
|
|
15
|
+
*
|
|
16
|
+
* 只收录**带锚点**的消息:没有内核坐标的消息(老数据、无 turn 的事件)不进映射,
|
|
17
|
+
* 于是它在 `resolveSourceAnchors` 里自然落进"未命中"分支,而不是被伪造一个坐标。
|
|
18
|
+
*/
|
|
19
|
+
export function buildAnchorMap(messages) {
|
|
20
|
+
const map = new Map();
|
|
21
|
+
for (const m of messages) {
|
|
22
|
+
if (m.anchor !== undefined && Number.isFinite(m.anchor.turn)) {
|
|
23
|
+
map.set(m.id, m.anchor);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 把一条记忆的 `source_message_ids` 解析成去重、有序的锚点集合。
|
|
30
|
+
*
|
|
31
|
+
* - 命中映射 → 收下该锚点
|
|
32
|
+
* - 未命中(LLM 引用了背景消息、或该消息本就没有坐标) → **丢弃**
|
|
33
|
+
* - 结果为空 → 返回 `undefined`,由调用方按"无锚点"处理(不是空数组)
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSourceAnchors(sourceMessageIds, anchors) {
|
|
36
|
+
if (sourceMessageIds === undefined || sourceMessageIds.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
const byKey = new Map();
|
|
39
|
+
for (const id of sourceMessageIds) {
|
|
40
|
+
const a = anchors.get(id);
|
|
41
|
+
if (a === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
const key = anchorOrderKey(a);
|
|
44
|
+
if (!byKey.has(key))
|
|
45
|
+
byKey.set(key, a);
|
|
46
|
+
}
|
|
47
|
+
if (byKey.size === 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
return [...byKey.entries()].sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0)).map(([, a]) => a);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 把锚点集合写进 metadata(返回**新对象**,不改入参——不可变更新)。
|
|
53
|
+
*
|
|
54
|
+
* 无锚点时不写键:老记录与"解析不到坐标"的记录保持与改动前**逐字一致的**
|
|
55
|
+
* metadata,这样既有导出/比对/测试不会被一个空数组搅动。
|
|
56
|
+
*/
|
|
57
|
+
export function withSourceAnchors(metadata, anchors) {
|
|
58
|
+
const base = metadata ?? {};
|
|
59
|
+
if (anchors === undefined || anchors.length === 0)
|
|
60
|
+
return base;
|
|
61
|
+
return { ...base, [ANCHOR_METADATA_KEY]: anchors };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 从 metadata 读回锚点集合(读侧唯一入口)。
|
|
65
|
+
*
|
|
66
|
+
* 形状校验从严:任何一项缺 `turn` 或类型不对 → 该项丢弃;全丢 → `undefined`。
|
|
67
|
+
* 宁可报告"无锚点",也不把半截坐标喂给下游的证据读取器。
|
|
68
|
+
*/
|
|
69
|
+
export function readSourceAnchors(metadata) {
|
|
70
|
+
if (metadata === null || typeof metadata !== 'object')
|
|
71
|
+
return undefined;
|
|
72
|
+
const raw = metadata[ANCHOR_METADATA_KEY];
|
|
73
|
+
if (!Array.isArray(raw))
|
|
74
|
+
return undefined;
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const item of raw) {
|
|
77
|
+
if (item === null || typeof item !== 'object')
|
|
78
|
+
continue;
|
|
79
|
+
const o = item;
|
|
80
|
+
const sessionId = o.sessionId;
|
|
81
|
+
const turn = o.turn;
|
|
82
|
+
if (typeof sessionId !== 'string' || typeof turn !== 'number' || !Number.isFinite(turn))
|
|
83
|
+
continue;
|
|
84
|
+
const anchor = { sessionId, turn };
|
|
85
|
+
if (typeof o.step === 'number' && Number.isFinite(o.step))
|
|
86
|
+
anchor.step = o.step;
|
|
87
|
+
out.push(anchor);
|
|
88
|
+
}
|
|
89
|
+
return out.length > 0 ? out : undefined;
|
|
90
|
+
}
|
package/dist/pipeline/l1.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import type { MemoryConfig } from '../config.js';
|
|
3
3
|
import type { L1Store } from '../store/l1.js';
|
|
4
4
|
import type { MemoryState } from '../store/state.js';
|
|
5
|
-
import type { ConversationMessage, ExtractMode, MemoryFamily, MemoryLogger, MemoryRecord } from '../types.js';
|
|
5
|
+
import type { ConversationAnchor, ConversationMessage, ExtractMode, MemoryFamily, MemoryLogger, MemoryRecord } from '../types.js';
|
|
6
6
|
export interface ExtractionResult {
|
|
7
7
|
stored: number;
|
|
8
8
|
skipped: boolean;
|
|
@@ -22,4 +22,10 @@ export declare function runExtraction(ctx: Context, cfg: MemoryConfig, store: L1
|
|
|
22
22
|
* 传 undefined 时行为与改动前**逐字一致**——`cfg.scope='global'` 的既有部署
|
|
23
23
|
* 永远走这条分支,这是零漂移的构造性保证。
|
|
24
24
|
*/
|
|
25
|
-
workspaceId?: string
|
|
25
|
+
workspaceId?: string,
|
|
26
|
+
/**
|
|
27
|
+
* R7 锚点映射(`L0 消息 id → 会话坐标`),由调用方经 `buildAnchorMap` 构造。
|
|
28
|
+
* **缺省时行为与改动前逐字一致**——传入的 `pending` 消息若不带锚点(老数据、
|
|
29
|
+
* 未启用捕获侧打戳),`resolveSourceAnchors` 一律返回 undefined,不写 metadata 键。
|
|
30
|
+
*/
|
|
31
|
+
anchorMap?: ReadonlyMap<string, ConversationAnchor>): Promise<ExtractionResult>;
|