dsh-tdai-memory 0.3.2 → 0.3.3

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/LICENSE +21 -21
  2. package/README.md +9 -1
  3. package/README.zh.md +8 -1
  4. package/THIRD-PARTY-NOTICES.md +20 -20
  5. package/cordis.patch.yml +8 -8
  6. package/package.json +16 -4
  7. package/recall-inject.js +86 -86
  8. package/vendor/tdai/adapters/index.js +15 -15
  9. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  10. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  11. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  12. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  13. package/vendor/tdai/adapters/standalone/index.js +5 -5
  14. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  15. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  16. package/vendor/tdai/core/hooks/auto-recall.js +640 -640
  17. package/vendor/tdai/core/index.js +8 -8
  18. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  19. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  20. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  21. package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
  22. package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
  23. package/vendor/tdai/core/prompts/persona-generation.js +35 -35
  24. package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
  25. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  26. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  27. package/vendor/tdai/core/record/l1-reader.js +178 -178
  28. package/vendor/tdai/core/record/l1-writer.js +141 -141
  29. package/vendor/tdai/core/report/reporter.js +86 -86
  30. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  31. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  32. package/vendor/tdai/core/scene/scene-format.js +53 -53
  33. package/vendor/tdai/core/scene/scene-index.js +82 -82
  34. package/vendor/tdai/core/scene/scene-navigation.js +57 -57
  35. package/vendor/tdai/core/seed/input.js +404 -404
  36. package/vendor/tdai/core/seed/types.js +9 -9
  37. package/vendor/tdai/core/store/bm25-client.js +117 -117
  38. package/vendor/tdai/core/store/bm25-local.js +68 -68
  39. package/vendor/tdai/core/store/embedding.js +632 -632
  40. package/vendor/tdai/core/store/factory.js +95 -95
  41. package/vendor/tdai/core/store/search-utils.js +54 -54
  42. package/vendor/tdai/core/store/sqlite.js +1632 -1632
  43. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  44. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  45. package/vendor/tdai/core/store/types.js +17 -17
  46. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  47. package/vendor/tdai/core/tools/memory-search.js +207 -207
  48. package/vendor/tdai/core/types.js +12 -12
  49. package/vendor/tdai/utils/backup.js +189 -189
  50. package/vendor/tdai/utils/checkpoint.js +360 -360
  51. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  52. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  53. package/vendor/tdai/utils/env.js +13 -13
  54. package/vendor/tdai/utils/managed-timer.js +127 -127
  55. package/vendor/tdai/utils/manifest.js +91 -91
  56. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  57. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  58. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  59. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  60. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  61. package/vendor/tdai/utils/sanitize.js +360 -360
  62. package/vendor/tdai/utils/serial-queue.js +103 -103
  63. package/vendor/tdai/utils/session-filter.js +88 -88
  64. package/vendor/tdai/utils/text-utils.js +29 -29
  65. package/vendor/tdai/utils/time.js +252 -252
@@ -1,17 +1,17 @@
1
- /**
2
- * Memory Store Abstraction Layer — Core Types & Interfaces.
3
- *
4
- * This module defines the storage contracts that all backend implementations
5
- * (SQLite local, Tencent Cloud VectorDB, etc.) must satisfy.
6
- *
7
- * Design principles:
8
- * 1. **Backend-agnostic**: Upper-layer modules (hooks, tools, pipeline, record)
9
- * depend only on these interfaces — never on concrete implementations.
10
- * 2. **Capability-based**: Features like vector search, FTS, and hybrid search
11
- * are expressed as capability flags so callers can gracefully degrade.
12
- * 3. **Fault-tolerant**: All methods return empty results or `false` on
13
- * failure rather than throwing, unless explicitly documented otherwise.
14
- * 4. **Sync-first**: Matches current SQLite DatabaseSync usage. TCVDB backend
15
- * adapts internally without changing these signatures.
16
- */
17
- export {};
1
+ /**
2
+ * Memory Store Abstraction Layer — Core Types & Interfaces.
3
+ *
4
+ * This module defines the storage contracts that all backend implementations
5
+ * (SQLite local, Tencent Cloud VectorDB, etc.) must satisfy.
6
+ *
7
+ * Design principles:
8
+ * 1. **Backend-agnostic**: Upper-layer modules (hooks, tools, pipeline, record)
9
+ * depend only on these interfaces — never on concrete implementations.
10
+ * 2. **Capability-based**: Features like vector search, FTS, and hybrid search
11
+ * are expressed as capability flags so callers can gracefully degrade.
12
+ * 3. **Fault-tolerant**: All methods return empty results or `false` on
13
+ * failure rather than throwing, unless explicitly documented otherwise.
14
+ * 4. **Sync-first**: Matches current SQLite DatabaseSync usage. TCVDB backend
15
+ * adapts internally without changing these signatures.
16
+ */
17
+ export {};
@@ -1,199 +1,199 @@
1
- /**
2
- * conversation_search tool: Agent-callable tool for searching L0 conversation records.
3
- *
4
- * Supports three search strategies with automatic degradation:
5
- * 1. **hybrid** (default) — FTS5 keyword + vector embedding in parallel,
6
- * merged via Reciprocal Rank Fusion (RRF).
7
- * 2. **embedding** — pure vector similarity (when FTS5 is unavailable).
8
- * 3. **fts** — pure FTS5 keyword search (when embedding is unavailable).
9
- *
10
- * The tool is registered via `api.registerTool()` in index.ts.
11
- */
12
- import { buildFtsQuery } from "../store/sqlite.js";
13
- const TAG = "[memory-tdai][tdai_conversation_search]";
14
- // ============================
15
- // RRF (Reciprocal Rank Fusion)
16
- // ============================
17
- /** Standard RRF constant from the original RRF paper. */
18
- const RRF_K = 60;
19
- /**
20
- * Merge multiple ranked lists of `ConversationSearchResultItem` via Reciprocal
21
- * Rank Fusion. Items appearing in multiple lists get their RRF scores summed.
22
- *
23
- * Returns items sorted by descending RRF score. The `score` field of each
24
- * returned item is replaced by the RRF score for consistent ranking semantics.
25
- */
26
- function rrfMergeL0(...lists) {
27
- const map = new Map();
28
- for (const list of lists) {
29
- for (let rank = 0; rank < list.length; rank++) {
30
- const item = list[rank];
31
- const score = 1 / (RRF_K + rank + 1);
32
- const existing = map.get(item.id);
33
- if (existing) {
34
- existing.rrfScore += score;
35
- }
36
- else {
37
- map.set(item.id, { item, rrfScore: score });
38
- }
39
- }
40
- }
41
- return [...map.values()]
42
- .sort((a, b) => b.rrfScore - a.rrfScore)
43
- .map(({ item, rrfScore }) => ({ ...item, score: rrfScore }));
44
- }
45
- // ============================
46
- // Search implementation
47
- // ============================
48
- export async function executeConversationSearch(params) {
49
- const { query, limit, sessionKey: sessionFilter, vectorStore, embeddingService, logger, } = params;
50
- logger?.debug?.(`${TAG} CALLED: query="${query.slice(0, 100)}", limit=${limit}, ` +
51
- `sessionFilter=${sessionFilter ?? "(none)"}, ` +
52
- `vectorStore=${vectorStore ? "available" : "UNAVAILABLE"}, ` +
53
- `embeddingService=${embeddingService ? "available" : "UNAVAILABLE"}`);
54
- if (!query || query.trim().length === 0) {
55
- logger?.debug?.(`${TAG} Empty query, returning empty`);
56
- return { results: [], total: 0, strategy: "none" };
57
- }
58
- if (!vectorStore) {
59
- logger?.warn?.(`${TAG} VectorStore not available`);
60
- return { results: [], total: 0, strategy: "none" };
61
- }
62
- // ── Determine available capabilities ──
63
- const hasEmbedding = !!embeddingService;
64
- const hasFts = vectorStore.isFtsAvailable();
65
- if (!hasEmbedding && !hasFts) {
66
- logger?.warn?.(`${TAG} Neither EmbeddingService nor FTS5 available — cannot search`);
67
- return {
68
- results: [],
69
- total: 0,
70
- strategy: "none",
71
- message: "Embedding service is not configured and FTS is not available. " +
72
- "Conversation search requires an embedding provider or FTS5 support. " +
73
- "Please configure an embedding provider in the embedding.provider setting (e.g. openai_compatible).",
74
- };
75
- }
76
- // ── Over-retrieve for later filtering and RRF merging ──
77
- const candidateK = sessionFilter ? limit * 4 : limit * 3;
78
- // ── Run available search strategies in parallel ──
79
- const [ftsItems, vecItems] = await Promise.all([
80
- // FTS5 keyword search on L0
81
- (async () => {
82
- if (!hasFts)
83
- return [];
84
- try {
85
- const ftsQuery = buildFtsQuery(query);
86
- if (!ftsQuery) {
87
- logger?.debug?.(`${TAG} [hybrid-fts] No usable FTS tokens from query`);
88
- return [];
89
- }
90
- logger?.debug?.(`${TAG} [hybrid-fts] FTS5 query: "${ftsQuery}"`);
91
- const ftsResults = await vectorStore.searchL0Fts(ftsQuery, candidateK);
92
- logger?.debug?.(`${TAG} [hybrid-fts] FTS5 returned ${ftsResults.length} candidates`);
93
- return ftsResults.map((r) => ({
94
- id: r.record_id,
95
- session_key: r.session_key,
96
- role: r.role,
97
- content: r.message_text,
98
- score: r.score,
99
- recorded_at: r.recorded_at,
100
- }));
101
- }
102
- catch (err) {
103
- logger?.warn?.(`${TAG} [hybrid-fts] FTS5 search failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
104
- return [];
105
- }
106
- })(),
107
- // Vector embedding search on L0
108
- (async () => {
109
- if (!hasEmbedding)
110
- return [];
111
- try {
112
- logger?.debug?.(`${TAG} [hybrid-vec] Generating query embedding...`);
113
- const queryEmbedding = await embeddingService.embed(query);
114
- logger?.debug?.(`${TAG} [hybrid-vec] Embedding OK, dims=${queryEmbedding.length}, searching top-${candidateK}...`);
115
- const vecResults = await vectorStore.searchL0Vector(queryEmbedding, candidateK, query);
116
- logger?.debug?.(`${TAG} [hybrid-vec] Vector search returned ${vecResults.length} candidates`);
117
- return vecResults.map((r) => ({
118
- id: r.record_id,
119
- session_key: r.session_key,
120
- role: r.role,
121
- content: r.message_text,
122
- score: r.score,
123
- recorded_at: r.recorded_at,
124
- }));
125
- }
126
- catch (err) {
127
- logger?.warn?.(`${TAG} [hybrid-vec] Embedding search failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
128
- return [];
129
- }
130
- })(),
131
- ]);
132
- // ── Determine effective strategy ──
133
- const ftsOk = ftsItems.length > 0;
134
- const vecOk = vecItems.length > 0;
135
- let strategy;
136
- if (ftsOk && vecOk) {
137
- strategy = "hybrid";
138
- }
139
- else if (vecOk) {
140
- strategy = "embedding";
141
- }
142
- else if (ftsOk) {
143
- strategy = "fts";
144
- }
145
- else {
146
- logger?.debug?.(`${TAG} Both search paths returned 0 results`);
147
- return { results: [], total: 0, strategy: hasEmbedding ? "embedding" : "fts" };
148
- }
149
- // ── Merge results ──
150
- let results;
151
- if (strategy === "hybrid") {
152
- results = rrfMergeL0(ftsItems, vecItems);
153
- logger?.debug?.(`${TAG} [hybrid] RRF merged: fts=${ftsItems.length}, vec=${vecItems.length} → ${results.length} unique`);
154
- }
155
- else {
156
- // Single-source: use whichever list has results (already sorted by score)
157
- results = ftsOk ? ftsItems : vecItems;
158
- }
159
- // ── Apply session key filter ──
160
- if (sessionFilter) {
161
- const preFilterCount = results.length;
162
- results = results.filter((r) => r.session_key === sessionFilter);
163
- logger?.debug?.(`${TAG} After session filter "${sessionFilter}": ${results.length}/${preFilterCount}`);
164
- }
165
- // ── Trim to requested limit ──
166
- const trimmed = results.slice(0, limit);
167
- logger?.debug?.(`${TAG} RESULT (strategy=${strategy}): returning ${trimmed.length} messages ` +
168
- `(scores: [${trimmed.map((r) => r.score.toFixed(3)).join(", ")}])`);
169
- return {
170
- results: trimmed,
171
- total: trimmed.length,
172
- strategy,
173
- };
174
- }
175
- // ============================
176
- // Tool response formatter
177
- // ============================
178
- export function formatConversationSearchResponse(result) {
179
- if (result.message) {
180
- return result.message;
181
- }
182
- if (result.results.length === 0) {
183
- return "No matching conversation messages found.";
184
- }
185
- const lines = [
186
- `Found ${result.total} matching message(s):`,
187
- "",
188
- ];
189
- for (const item of result.results) {
190
- const scoreStr = typeof item.score === "number" ? ` (score: ${item.score.toFixed(3)})` : "";
191
- const dateStr = item.recorded_at ? ` [${item.recorded_at}]` : "";
192
- lines.push(`---`);
193
- lines.push(`**[${item.role}]** Session: ${item.session_key}${dateStr}${scoreStr}`);
194
- lines.push("");
195
- lines.push(item.content);
196
- lines.push("");
197
- }
198
- return lines.join("\n");
199
- }
1
+ /**
2
+ * conversation_search tool: Agent-callable tool for searching L0 conversation records.
3
+ *
4
+ * Supports three search strategies with automatic degradation:
5
+ * 1. **hybrid** (default) — FTS5 keyword + vector embedding in parallel,
6
+ * merged via Reciprocal Rank Fusion (RRF).
7
+ * 2. **embedding** — pure vector similarity (when FTS5 is unavailable).
8
+ * 3. **fts** — pure FTS5 keyword search (when embedding is unavailable).
9
+ *
10
+ * The tool is registered via `api.registerTool()` in index.ts.
11
+ */
12
+ import { buildFtsQuery } from "../store/sqlite.js";
13
+ const TAG = "[memory-tdai][tdai_conversation_search]";
14
+ // ============================
15
+ // RRF (Reciprocal Rank Fusion)
16
+ // ============================
17
+ /** Standard RRF constant from the original RRF paper. */
18
+ const RRF_K = 60;
19
+ /**
20
+ * Merge multiple ranked lists of `ConversationSearchResultItem` via Reciprocal
21
+ * Rank Fusion. Items appearing in multiple lists get their RRF scores summed.
22
+ *
23
+ * Returns items sorted by descending RRF score. The `score` field of each
24
+ * returned item is replaced by the RRF score for consistent ranking semantics.
25
+ */
26
+ function rrfMergeL0(...lists) {
27
+ const map = new Map();
28
+ for (const list of lists) {
29
+ for (let rank = 0; rank < list.length; rank++) {
30
+ const item = list[rank];
31
+ const score = 1 / (RRF_K + rank + 1);
32
+ const existing = map.get(item.id);
33
+ if (existing) {
34
+ existing.rrfScore += score;
35
+ }
36
+ else {
37
+ map.set(item.id, { item, rrfScore: score });
38
+ }
39
+ }
40
+ }
41
+ return [...map.values()]
42
+ .sort((a, b) => b.rrfScore - a.rrfScore)
43
+ .map(({ item, rrfScore }) => ({ ...item, score: rrfScore }));
44
+ }
45
+ // ============================
46
+ // Search implementation
47
+ // ============================
48
+ export async function executeConversationSearch(params) {
49
+ const { query, limit, sessionKey: sessionFilter, vectorStore, embeddingService, logger, } = params;
50
+ logger?.debug?.(`${TAG} CALLED: query="${query.slice(0, 100)}", limit=${limit}, ` +
51
+ `sessionFilter=${sessionFilter ?? "(none)"}, ` +
52
+ `vectorStore=${vectorStore ? "available" : "UNAVAILABLE"}, ` +
53
+ `embeddingService=${embeddingService ? "available" : "UNAVAILABLE"}`);
54
+ if (!query || query.trim().length === 0) {
55
+ logger?.debug?.(`${TAG} Empty query, returning empty`);
56
+ return { results: [], total: 0, strategy: "none" };
57
+ }
58
+ if (!vectorStore) {
59
+ logger?.warn?.(`${TAG} VectorStore not available`);
60
+ return { results: [], total: 0, strategy: "none" };
61
+ }
62
+ // ── Determine available capabilities ──
63
+ const hasEmbedding = !!embeddingService;
64
+ const hasFts = vectorStore.isFtsAvailable();
65
+ if (!hasEmbedding && !hasFts) {
66
+ logger?.warn?.(`${TAG} Neither EmbeddingService nor FTS5 available — cannot search`);
67
+ return {
68
+ results: [],
69
+ total: 0,
70
+ strategy: "none",
71
+ message: "Embedding service is not configured and FTS is not available. " +
72
+ "Conversation search requires an embedding provider or FTS5 support. " +
73
+ "Please configure an embedding provider in the embedding.provider setting (e.g. openai_compatible).",
74
+ };
75
+ }
76
+ // ── Over-retrieve for later filtering and RRF merging ──
77
+ const candidateK = sessionFilter ? limit * 4 : limit * 3;
78
+ // ── Run available search strategies in parallel ──
79
+ const [ftsItems, vecItems] = await Promise.all([
80
+ // FTS5 keyword search on L0
81
+ (async () => {
82
+ if (!hasFts)
83
+ return [];
84
+ try {
85
+ const ftsQuery = buildFtsQuery(query);
86
+ if (!ftsQuery) {
87
+ logger?.debug?.(`${TAG} [hybrid-fts] No usable FTS tokens from query`);
88
+ return [];
89
+ }
90
+ logger?.debug?.(`${TAG} [hybrid-fts] FTS5 query: "${ftsQuery}"`);
91
+ const ftsResults = await vectorStore.searchL0Fts(ftsQuery, candidateK);
92
+ logger?.debug?.(`${TAG} [hybrid-fts] FTS5 returned ${ftsResults.length} candidates`);
93
+ return ftsResults.map((r) => ({
94
+ id: r.record_id,
95
+ session_key: r.session_key,
96
+ role: r.role,
97
+ content: r.message_text,
98
+ score: r.score,
99
+ recorded_at: r.recorded_at,
100
+ }));
101
+ }
102
+ catch (err) {
103
+ logger?.warn?.(`${TAG} [hybrid-fts] FTS5 search failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
104
+ return [];
105
+ }
106
+ })(),
107
+ // Vector embedding search on L0
108
+ (async () => {
109
+ if (!hasEmbedding)
110
+ return [];
111
+ try {
112
+ logger?.debug?.(`${TAG} [hybrid-vec] Generating query embedding...`);
113
+ const queryEmbedding = await embeddingService.embed(query);
114
+ logger?.debug?.(`${TAG} [hybrid-vec] Embedding OK, dims=${queryEmbedding.length}, searching top-${candidateK}...`);
115
+ const vecResults = await vectorStore.searchL0Vector(queryEmbedding, candidateK, query);
116
+ logger?.debug?.(`${TAG} [hybrid-vec] Vector search returned ${vecResults.length} candidates`);
117
+ return vecResults.map((r) => ({
118
+ id: r.record_id,
119
+ session_key: r.session_key,
120
+ role: r.role,
121
+ content: r.message_text,
122
+ score: r.score,
123
+ recorded_at: r.recorded_at,
124
+ }));
125
+ }
126
+ catch (err) {
127
+ logger?.warn?.(`${TAG} [hybrid-vec] Embedding search failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
128
+ return [];
129
+ }
130
+ })(),
131
+ ]);
132
+ // ── Determine effective strategy ──
133
+ const ftsOk = ftsItems.length > 0;
134
+ const vecOk = vecItems.length > 0;
135
+ let strategy;
136
+ if (ftsOk && vecOk) {
137
+ strategy = "hybrid";
138
+ }
139
+ else if (vecOk) {
140
+ strategy = "embedding";
141
+ }
142
+ else if (ftsOk) {
143
+ strategy = "fts";
144
+ }
145
+ else {
146
+ logger?.debug?.(`${TAG} Both search paths returned 0 results`);
147
+ return { results: [], total: 0, strategy: hasEmbedding ? "embedding" : "fts" };
148
+ }
149
+ // ── Merge results ──
150
+ let results;
151
+ if (strategy === "hybrid") {
152
+ results = rrfMergeL0(ftsItems, vecItems);
153
+ logger?.debug?.(`${TAG} [hybrid] RRF merged: fts=${ftsItems.length}, vec=${vecItems.length} → ${results.length} unique`);
154
+ }
155
+ else {
156
+ // Single-source: use whichever list has results (already sorted by score)
157
+ results = ftsOk ? ftsItems : vecItems;
158
+ }
159
+ // ── Apply session key filter ──
160
+ if (sessionFilter) {
161
+ const preFilterCount = results.length;
162
+ results = results.filter((r) => r.session_key === sessionFilter);
163
+ logger?.debug?.(`${TAG} After session filter "${sessionFilter}": ${results.length}/${preFilterCount}`);
164
+ }
165
+ // ── Trim to requested limit ──
166
+ const trimmed = results.slice(0, limit);
167
+ logger?.debug?.(`${TAG} RESULT (strategy=${strategy}): returning ${trimmed.length} messages ` +
168
+ `(scores: [${trimmed.map((r) => r.score.toFixed(3)).join(", ")}])`);
169
+ return {
170
+ results: trimmed,
171
+ total: trimmed.length,
172
+ strategy,
173
+ };
174
+ }
175
+ // ============================
176
+ // Tool response formatter
177
+ // ============================
178
+ export function formatConversationSearchResponse(result) {
179
+ if (result.message) {
180
+ return result.message;
181
+ }
182
+ if (result.results.length === 0) {
183
+ return "No matching conversation messages found.";
184
+ }
185
+ const lines = [
186
+ `Found ${result.total} matching message(s):`,
187
+ "",
188
+ ];
189
+ for (const item of result.results) {
190
+ const scoreStr = typeof item.score === "number" ? ` (score: ${item.score.toFixed(3)})` : "";
191
+ const dateStr = item.recorded_at ? ` [${item.recorded_at}]` : "";
192
+ lines.push(`---`);
193
+ lines.push(`**[${item.role}]** Session: ${item.session_key}${dateStr}${scoreStr}`);
194
+ lines.push("");
195
+ lines.push(item.content);
196
+ lines.push("");
197
+ }
198
+ return lines.join("\n");
199
+ }