memory-lancedb-pro 1.1.0-beta.7 → 1.1.0-beta.9
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/.claude/settings.local.json +9 -0
- package/.github/workflows/auto-assign.yml +1 -1
- package/.github/workflows/claude-code-review.yml +4 -0
- package/README.md +25 -2
- package/README_CN.md +27 -3
- package/index.ts +9 -3
- package/openclaw.plugin.json +7 -5
- package/package.json +3 -2
- package/src/extraction-prompts.ts +5 -3
- package/src/memory-categories.ts +15 -1
- package/src/reflection-slices.ts +65 -11
- package/src/reflection-store.ts +13 -10
- package/src/retriever.ts +95 -25
- package/src/smart-extractor.ts +161 -13
- package/src/smart-metadata.ts +133 -6
- package/src/store.ts +50 -26
- package/src/tools.ts +93 -2
- package/test/memory-reflection.test.mjs +375 -0
- package/test/memory-update-supersede.test.mjs +312 -0
- package/test/plugin-manifest-regression.mjs +9 -0
- package/test/retriever-rerank-regression.mjs +186 -0
- package/test/self-improvement.test.mjs +27 -0
- package/test/temporal-facts.test.mjs +286 -0
- package/test/workflow-fork-guards.test.mjs +26 -0
|
@@ -12,6 +12,10 @@ on:
|
|
|
12
12
|
|
|
13
13
|
jobs:
|
|
14
14
|
claude-review:
|
|
15
|
+
# Skip fork PRs: Claude Code review needs secrets/OIDC that are not available
|
|
16
|
+
# on fork-based pull_request events. Same-repo PRs still run normally.
|
|
17
|
+
if: ${{ github.event.pull_request.head.repo.fork == false }}
|
|
18
|
+
|
|
15
19
|
# Optional: Filter by PR author
|
|
16
20
|
# if: |
|
|
17
21
|
# github.event.pull_request.user.login == 'external-contributor' ||
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Most AI agents have amnesia. They forget everything the moment you start a new c
|
|
|
39
39
|
| Vector search | ✅ | ✅ |
|
|
40
40
|
| BM25 full-text search | ❌ | ✅ |
|
|
41
41
|
| Hybrid fusion (Vector + BM25) | ❌ | ✅ |
|
|
42
|
-
| Cross-encoder rerank (
|
|
42
|
+
| Cross-encoder rerank (multi-provider) | ❌ | ✅ |
|
|
43
43
|
| Recency boost & time decay | ❌ | ✅ |
|
|
44
44
|
| Length normalization | ❌ | ✅ |
|
|
45
45
|
| MMR diversity | ❌ | ✅ |
|
|
@@ -290,7 +290,7 @@ Query → BM25 FTS ─────┘
|
|
|
290
290
|
|
|
291
291
|
### Cross-Encoder Reranking
|
|
292
292
|
|
|
293
|
-
- Supports **Jina**, **SiliconFlow**, **Voyage AI**, **Pinecone**,
|
|
293
|
+
- Supports **Jina-compatible endpoints** plus dedicated adapters for **TEI**, **SiliconFlow**, **Voyage AI**, **Pinecone**, and **DashScope**
|
|
294
294
|
- Hybrid scoring: 60% cross-encoder + 40% original fused score
|
|
295
295
|
- Graceful degradation: falls back to cosine similarity on API failure
|
|
296
296
|
|
|
@@ -441,10 +441,32 @@ Cross-encoder reranking supports multiple providers via `rerankProvider`:
|
|
|
441
441
|
| Provider | `rerankProvider` | Endpoint | Example Model |
|
|
442
442
|
| --- | --- | --- | --- |
|
|
443
443
|
| **Jina** (default) | `jina` | `https://api.jina.ai/v1/rerank` | `jina-reranker-v3` |
|
|
444
|
+
| **Hugging Face TEI** | `tei` | `http://host:8081/rerank` | `BAAI/bge-reranker-v2-m3` |
|
|
444
445
|
| **SiliconFlow** (free tier available) | `siliconflow` | `https://api.siliconflow.com/v1/rerank` | `BAAI/bge-reranker-v2-m3` |
|
|
445
446
|
| **Voyage AI** | `voyage` | `https://api.voyageai.com/v1/rerank` | `rerank-2.5` |
|
|
446
447
|
| **Pinecone** | `pinecone` | `https://api.pinecone.io/rerank` | `bge-reranker-v2-m3` |
|
|
447
448
|
|
|
449
|
+
<details>
|
|
450
|
+
<summary>TEI config example</summary>
|
|
451
|
+
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"retrieval": {
|
|
455
|
+
"rerank": "cross-encoder",
|
|
456
|
+
"rerankProvider": "tei",
|
|
457
|
+
"rerankEndpoint": "http://host:8081/rerank",
|
|
458
|
+
"rerankApiKey": "tei-local"
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Notes:
|
|
464
|
+
- `tei` sends `{ query, texts }` and parses the top-level `[{ index, score }]` response used by Hugging Face Text Embeddings Inference.
|
|
465
|
+
- `rerankModel` is ignored by the TEI adapter, so you do not need to set it for TEI container endpoints.
|
|
466
|
+
- If your local TEI endpoint does not require auth, a placeholder `rerankApiKey` still enables the cross-encoder rerank path.
|
|
467
|
+
|
|
468
|
+
</details>
|
|
469
|
+
|
|
448
470
|
<details>
|
|
449
471
|
<summary>SiliconFlow config example</summary>
|
|
450
472
|
|
|
@@ -497,6 +519,7 @@ Cross-encoder reranking supports multiple providers via `rerankProvider`:
|
|
|
497
519
|
</details>
|
|
498
520
|
|
|
499
521
|
Notes:
|
|
522
|
+
- TEI request/response shape is documented in the TEI config example above.
|
|
500
523
|
- `voyage` sends `{ model, query, documents }` without `top_n`. Responses are parsed from `data[].relevance_score`.
|
|
501
524
|
|
|
502
525
|
</details>
|
package/README_CN.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
| 向量搜索 | ✅ | ✅ |
|
|
40
40
|
| BM25 全文检索 | ❌ | ✅ |
|
|
41
41
|
| 混合融合(Vector + BM25) | ❌ | ✅ |
|
|
42
|
-
| 跨编码器 Rerank
|
|
42
|
+
| 跨编码器 Rerank(多 Provider) | ❌ | ✅ |
|
|
43
43
|
| 时效性加成 & 时间衰减 | ❌ | ✅ |
|
|
44
44
|
| 长度归一化 | ❌ | ✅ |
|
|
45
45
|
| MMR 多样性去重 | ❌ | ✅ |
|
|
@@ -290,7 +290,7 @@ Query → BM25 FTS ─────┘
|
|
|
290
290
|
|
|
291
291
|
### 跨编码器 Rerank
|
|
292
292
|
|
|
293
|
-
- 支持 **Jina**、**SiliconFlow**、**Voyage AI**、**Pinecone**
|
|
293
|
+
- 支持 **Jina 兼容端点**,以及 **TEI**、**SiliconFlow**、**Voyage AI**、**Pinecone**、**DashScope** 的专用适配
|
|
294
294
|
- 混合评分:60% cross-encoder + 40% 原始融合分
|
|
295
295
|
- 降级策略:API 失败时回退到 cosine similarity rerank
|
|
296
296
|
|
|
@@ -441,10 +441,32 @@ OpenClaw 默认行为:
|
|
|
441
441
|
| 提供商 | `rerankProvider` | Endpoint | 示例模型 |
|
|
442
442
|
| --- | --- | --- | --- |
|
|
443
443
|
| **Jina**(默认) | `jina` | `https://api.jina.ai/v1/rerank` | `jina-reranker-v3` |
|
|
444
|
+
| **Hugging Face TEI** | `tei` | `http://host:8081/rerank` | `BAAI/bge-reranker-v2-m3` |
|
|
444
445
|
| **SiliconFlow**(有免费额度) | `siliconflow` | `https://api.siliconflow.com/v1/rerank` | `BAAI/bge-reranker-v2-m3` |
|
|
445
446
|
| **Voyage AI** | `voyage` | `https://api.voyageai.com/v1/rerank` | `rerank-2.5` |
|
|
446
447
|
| **Pinecone** | `pinecone` | `https://api.pinecone.io/rerank` | `bge-reranker-v2-m3` |
|
|
447
448
|
|
|
449
|
+
<details>
|
|
450
|
+
<summary>TEI 配置示例</summary>
|
|
451
|
+
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"retrieval": {
|
|
455
|
+
"rerank": "cross-encoder",
|
|
456
|
+
"rerankProvider": "tei",
|
|
457
|
+
"rerankEndpoint": "http://host:8081/rerank",
|
|
458
|
+
"rerankApiKey": "tei-local"
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
说明:
|
|
464
|
+
- `tei` 发送 `{ query, texts }`,并解析 Hugging Face Text Embeddings Inference 使用的顶层 `[{ index, score }]` 响应。
|
|
465
|
+
- TEI 适配器会忽略 `rerankModel`,所以对 TEI 容器端点无需单独配置该字段。
|
|
466
|
+
- 如果你的本地 TEI 端点不需要鉴权,仍建议填一个占位 `rerankApiKey`,这样插件才会启用 cross-encoder rerank 路径。
|
|
467
|
+
|
|
468
|
+
</details>
|
|
469
|
+
|
|
448
470
|
<details>
|
|
449
471
|
<summary>SiliconFlow 配置示例</summary>
|
|
450
472
|
|
|
@@ -496,7 +518,9 @@ OpenClaw 默认行为:
|
|
|
496
518
|
|
|
497
519
|
</details>
|
|
498
520
|
|
|
499
|
-
|
|
521
|
+
说明:
|
|
522
|
+
- TEI 的请求/响应格式见上方 TEI 配置示例。
|
|
523
|
+
- `voyage` 发送 `{ model, query, documents }` 格式(不含 `top_n`),响应从 `data[].relevance_score` 解析。
|
|
500
524
|
|
|
501
525
|
</details>
|
|
502
526
|
|
package/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "./src/reflection-store.js";
|
|
33
33
|
import {
|
|
34
34
|
extractReflectionLearningGovernanceCandidates,
|
|
35
|
-
|
|
35
|
+
extractInjectableReflectionMappedMemoryItems,
|
|
36
36
|
} from "./src/reflection-slices.js";
|
|
37
37
|
import { createReflectionEventId } from "./src/reflection-event-store.js";
|
|
38
38
|
import { buildReflectionMappedMetadata } from "./src/reflection-mapped-metadata.js";
|
|
@@ -85,7 +85,13 @@ interface PluginConfig {
|
|
|
85
85
|
rerankApiKey?: string;
|
|
86
86
|
rerankModel?: string;
|
|
87
87
|
rerankEndpoint?: string;
|
|
88
|
-
rerankProvider?:
|
|
88
|
+
rerankProvider?:
|
|
89
|
+
| "jina"
|
|
90
|
+
| "siliconflow"
|
|
91
|
+
| "voyage"
|
|
92
|
+
| "pinecone"
|
|
93
|
+
| "dashscope"
|
|
94
|
+
| "tei";
|
|
89
95
|
recencyHalfLifeDays?: number;
|
|
90
96
|
recencyWeight?: number;
|
|
91
97
|
filterNoise?: boolean;
|
|
@@ -2768,7 +2774,7 @@ const memoryLanceDBProPlugin = {
|
|
|
2768
2774
|
command: String(event.action || "unknown"),
|
|
2769
2775
|
});
|
|
2770
2776
|
|
|
2771
|
-
const mappedReflectionMemories =
|
|
2777
|
+
const mappedReflectionMemories = extractInjectableReflectionMappedMemoryItems(reflectionText);
|
|
2772
2778
|
for (const mapped of mappedReflectionMemories) {
|
|
2773
2779
|
const vector = await embedder.embedPassage(mapped.text);
|
|
2774
2780
|
let existing: Awaited<ReturnType<typeof store.vectorSearch>> = [];
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "memory-lancedb-pro",
|
|
3
3
|
"name": "Memory (LanceDB Pro)",
|
|
4
4
|
"description": "Enhanced LanceDB-backed long-term memory with hybrid retrieval, multi-scope isolation, long-context chunking, and management CLI",
|
|
5
|
-
"version": "1.1.0-beta.
|
|
5
|
+
"version": "1.1.0-beta.9",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
"rerankEndpoint": {
|
|
179
179
|
"type": "string",
|
|
180
180
|
"default": "https://api.jina.ai/v1/rerank",
|
|
181
|
-
"description": "Reranker API endpoint URL. Compatible with Jina, SiliconFlow, Pinecone,
|
|
181
|
+
"description": "Reranker API endpoint URL. Compatible with Jina-compatible endpoints and dedicated adapters such as TEI, SiliconFlow, Voyage, Pinecone, and DashScope."
|
|
182
182
|
},
|
|
183
183
|
"rerankProvider": {
|
|
184
184
|
"type": "string",
|
|
@@ -186,10 +186,12 @@
|
|
|
186
186
|
"jina",
|
|
187
187
|
"siliconflow",
|
|
188
188
|
"voyage",
|
|
189
|
-
"pinecone"
|
|
189
|
+
"pinecone",
|
|
190
|
+
"dashscope",
|
|
191
|
+
"tei"
|
|
190
192
|
],
|
|
191
193
|
"default": "jina",
|
|
192
|
-
"description": "Reranker provider format. Determines request/response shape and auth header."
|
|
194
|
+
"description": "Reranker provider format. Determines request/response shape and auth header. Use tei for Hugging Face Text Embeddings Inference /rerank endpoints. DashScope uses gte-rerank-v2 with endpoint https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank."
|
|
193
195
|
},
|
|
194
196
|
"candidatePoolSize": {
|
|
195
197
|
"type": "integer",
|
|
@@ -608,7 +610,7 @@
|
|
|
608
610
|
},
|
|
609
611
|
"retrieval.rerankProvider": {
|
|
610
612
|
"label": "Reranker Provider",
|
|
611
|
-
"help": "Provider format: jina (default), siliconflow, voyage, or
|
|
613
|
+
"help": "Provider format: jina (default), siliconflow, voyage, pinecone, dashscope, or tei",
|
|
612
614
|
"advanced": true
|
|
613
615
|
},
|
|
614
616
|
"retrieval.candidatePoolSize": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-lancedb-pro",
|
|
3
|
-
"version": "1.1.0-beta.
|
|
3
|
+
"version": "1.1.0-beta.9",
|
|
4
4
|
"description": "OpenClaw enhanced LanceDB memory plugin with hybrid retrieval (Vector + BM25), cross-encoder rerank, multi-scope isolation, long-context chunking, and management CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@lancedb/lancedb": "^0.26.2",
|
|
29
29
|
"@sinclair/typebox": "0.34.48",
|
|
30
|
+
"apache-arrow": "18.1.0",
|
|
30
31
|
"openai": "^6.21.0"
|
|
31
32
|
},
|
|
32
33
|
"openclaw": {
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
]
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"test": "node test/embedder-error-hints.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node test/plugin-manifest-regression.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs",
|
|
39
|
+
"test": "node test/embedder-error-hints.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node test/plugin-manifest-regression.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node --test test/workflow-fork-guards.test.mjs",
|
|
39
40
|
"test:openclaw-host": "node test/openclaw-host-functional.mjs",
|
|
40
41
|
"version": "node scripts/sync-plugin-version.mjs openclaw.plugin.json package.json && git add openclaw.plugin.json"
|
|
41
42
|
},
|
|
@@ -149,25 +149,27 @@ Please decide:
|
|
|
149
149
|
- SKIP: Candidate memory duplicates existing memories, no need to save. Also SKIP if the candidate contains LESS information than an existing memory on the same topic (information degradation — e.g., candidate says "programming language preference" but existing memory already says "programming language preference: Python, TypeScript")
|
|
150
150
|
- CREATE: This is completely new information not covered by any existing memory, should be created
|
|
151
151
|
- MERGE: Candidate memory adds genuinely NEW details to an existing memory and should be merged
|
|
152
|
+
- SUPERSEDE: Candidate states that the same mutable fact has changed over time. Keep the old memory as historical but no longer current, and create a new current memory.
|
|
152
153
|
- SUPPORT: Candidate reinforces/confirms an existing memory in a specific context (e.g. "still prefers tea in the evening")
|
|
153
154
|
- CONTEXTUALIZE: Candidate adds a situational nuance to an existing memory (e.g. existing: "likes coffee", candidate: "prefers tea at night" — different context, same topic)
|
|
154
155
|
- CONTRADICT: Candidate directly contradicts an existing memory in a specific context (e.g. existing: "runs on weekends", candidate: "stopped running on weekends")
|
|
155
156
|
|
|
156
157
|
IMPORTANT:
|
|
157
|
-
- "events" and "cases" categories are independent records — they do NOT support MERGE/SUPPORT/CONTEXTUALIZE/CONTRADICT. For these categories, only use SKIP or CREATE.
|
|
158
|
+
- "events" and "cases" categories are independent records — they do NOT support MERGE/SUPERSEDE/SUPPORT/CONTEXTUALIZE/CONTRADICT. For these categories, only use SKIP or CREATE.
|
|
158
159
|
- If the candidate appears to be derived from a recall question (e.g., "Do you remember X?" / "你记得X吗?") and an existing memory already covers topic X with equal or more detail, you MUST choose SKIP.
|
|
159
160
|
- A candidate with less information than an existing memory on the same topic should NEVER be CREATED or MERGED — always SKIP.
|
|
161
|
+
- For "preferences" and "entities", use SUPERSEDE when the candidate replaces the current truth instead of adding detail or context. Example: existing "Preferred editor: VS Code", candidate "Preferred editor: Zed".
|
|
160
162
|
- For SUPPORT/CONTEXTUALIZE/CONTRADICT, you MUST provide a context_label from this vocabulary: general, morning, evening, night, weekday, weekend, work, leisure, summer, winter, travel.
|
|
161
163
|
|
|
162
164
|
Return JSON format:
|
|
163
165
|
{
|
|
164
|
-
"decision": "skip|create|merge|support|contextualize|contradict",
|
|
166
|
+
"decision": "skip|create|merge|supersede|support|contextualize|contradict",
|
|
165
167
|
"match_index": 1,
|
|
166
168
|
"reason": "Decision reason",
|
|
167
169
|
"context_label": "evening"
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
- If decision is "merge"/"support"/"contextualize"/"contradict", set "match_index" to the number of the existing memory (1-based).
|
|
172
|
+
- If decision is "merge"/"supersede"/"support"/"contextualize"/"contradict", set "match_index" to the number of the existing memory (1-based).
|
|
171
173
|
- Only include "context_label" for support/contextualize/contradict decisions.`;
|
|
172
174
|
}
|
|
173
175
|
|
package/src/memory-categories.ts
CHANGED
|
@@ -26,6 +26,12 @@ export const MERGE_SUPPORTED_CATEGORIES = new Set<MemoryCategory>([
|
|
|
26
26
|
"patterns",
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
+
/** Categories whose facts can be replaced over time without deleting history. */
|
|
30
|
+
export const TEMPORAL_VERSIONED_CATEGORIES = new Set<MemoryCategory>([
|
|
31
|
+
"preferences",
|
|
32
|
+
"entities",
|
|
33
|
+
]);
|
|
34
|
+
|
|
29
35
|
/** Categories that are append-only (CREATE or SKIP only, no MERGE). */
|
|
30
36
|
export const APPEND_ONLY_CATEGORIES = new Set<MemoryCategory>([
|
|
31
37
|
"events",
|
|
@@ -44,7 +50,14 @@ export type CandidateMemory = {
|
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
/** Dedup decision from LLM. */
|
|
47
|
-
export type DedupDecision =
|
|
53
|
+
export type DedupDecision =
|
|
54
|
+
| "create"
|
|
55
|
+
| "merge"
|
|
56
|
+
| "skip"
|
|
57
|
+
| "support"
|
|
58
|
+
| "contextualize"
|
|
59
|
+
| "contradict"
|
|
60
|
+
| "supersede";
|
|
48
61
|
|
|
49
62
|
export type DedupResult = {
|
|
50
63
|
decision: DedupDecision;
|
|
@@ -58,6 +71,7 @@ export type ExtractionStats = {
|
|
|
58
71
|
merged: number;
|
|
59
72
|
skipped: number;
|
|
60
73
|
supported?: number; // context-aware support count
|
|
74
|
+
superseded?: number; // temporal fact replacements
|
|
61
75
|
};
|
|
62
76
|
|
|
63
77
|
/** Validate and normalize a category string. */
|
package/src/reflection-slices.ts
CHANGED
|
@@ -90,6 +90,27 @@ export function sanitizeReflectionSliceLines(lines: string[]): string[] {
|
|
|
90
90
|
.filter((line) => !isPlaceholderReflectionSliceLine(line));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const INJECTABLE_REFLECTION_BLOCK_PATTERNS: RegExp[] = [
|
|
94
|
+
/^\s*(?:(?:next|this)\s+run\s+)?(?:ignore|disregard|forget|override|bypass)\b[\s\S]{0,80}\b(?:instructions?|guardrails?|policy|developer|system)\b/i,
|
|
95
|
+
/\b(?:reveal|print|dump|show|output)\b[\s\S]{0,80}\b(?:system prompt|developer prompt|hidden prompt|hidden instructions?|full prompt|prompt verbatim|secrets?|keys?|tokens?)\b/i,
|
|
96
|
+
/<\s*\/?\s*(?:system|assistant|user|tool|developer|inherited-rules|derived-focus)\b[^>]*>/i,
|
|
97
|
+
/^(?:system|assistant|user|developer|tool)\s*:/i,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
export function isUnsafeInjectableReflectionLine(line: string): boolean {
|
|
101
|
+
const normalized = normalizeReflectionSliceLine(line);
|
|
102
|
+
if (!normalized) return true;
|
|
103
|
+
return INJECTABLE_REFLECTION_BLOCK_PATTERNS.some((pattern) =>
|
|
104
|
+
pattern.test(normalized),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function sanitizeInjectableReflectionLines(lines: string[]): string[] {
|
|
109
|
+
return sanitizeReflectionSliceLines(lines).filter(
|
|
110
|
+
(line) => !isUnsafeInjectableReflectionLine(line),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
function isInvariantRuleLike(line: string): boolean {
|
|
94
115
|
return /^(always|never|when\b|if\b|before\b|after\b|prefer\b|avoid\b|require\b|only\b|do not\b|must\b|should\b)/i.test(line) ||
|
|
95
116
|
/\b(must|should|never|always|prefer|avoid|required?)\b/i.test(line);
|
|
@@ -172,7 +193,10 @@ export function extractReflectionMappedMemories(reflectionText: string): Reflect
|
|
|
172
193
|
return extractReflectionMappedMemoryItems(reflectionText).map(({ text, category, heading }) => ({ text, category, heading }));
|
|
173
194
|
}
|
|
174
195
|
|
|
175
|
-
|
|
196
|
+
function extractReflectionMappedMemoryItemsWithSanitizer(
|
|
197
|
+
reflectionText: string,
|
|
198
|
+
sanitizeLines: (lines: string[]) => string[],
|
|
199
|
+
): ReflectionMappedMemoryItem[] {
|
|
176
200
|
const mappedSections: Array<{
|
|
177
201
|
heading: string;
|
|
178
202
|
category: "preference" | "fact" | "decision";
|
|
@@ -201,30 +225,45 @@ export function extractReflectionMappedMemoryItems(reflectionText: string): Refl
|
|
|
201
225
|
];
|
|
202
226
|
|
|
203
227
|
return mappedSections.flatMap(({ heading, category, mappedKind }) => {
|
|
204
|
-
const lines =
|
|
228
|
+
const lines = sanitizeLines(parseSectionBullets(reflectionText, heading));
|
|
205
229
|
const groupSize = lines.length;
|
|
206
230
|
return lines.map((text, ordinal) => ({ text, category, heading, mappedKind, ordinal, groupSize }));
|
|
207
231
|
});
|
|
208
232
|
}
|
|
209
233
|
|
|
210
|
-
export function
|
|
234
|
+
export function extractReflectionMappedMemoryItems(reflectionText: string): ReflectionMappedMemoryItem[] {
|
|
235
|
+
return extractReflectionMappedMemoryItemsWithSanitizer(reflectionText, sanitizeReflectionSliceLines);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function extractInjectableReflectionMappedMemoryItems(reflectionText: string): ReflectionMappedMemoryItem[] {
|
|
239
|
+
return extractReflectionMappedMemoryItemsWithSanitizer(reflectionText, sanitizeInjectableReflectionLines);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function extractInjectableReflectionMappedMemories(reflectionText: string): ReflectionMappedMemory[] {
|
|
243
|
+
return extractInjectableReflectionMappedMemoryItems(reflectionText).map(({ text, category, heading }) => ({ text, category, heading }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function extractReflectionSlicesWithSanitizer(
|
|
247
|
+
reflectionText: string,
|
|
248
|
+
sanitizeLines: (lines: string[]) => string[],
|
|
249
|
+
): ReflectionSlices {
|
|
211
250
|
const invariantSection = parseSectionBullets(reflectionText, "Invariants");
|
|
212
251
|
const derivedSection = parseSectionBullets(reflectionText, "Derived");
|
|
213
252
|
const mergedSection = parseSectionBullets(reflectionText, "Invariants & Reflections");
|
|
214
253
|
|
|
215
|
-
const invariantsPrimary =
|
|
216
|
-
const derivedPrimary =
|
|
254
|
+
const invariantsPrimary = sanitizeLines(invariantSection).filter(isInvariantRuleLike);
|
|
255
|
+
const derivedPrimary = sanitizeLines(derivedSection).filter(isDerivedDeltaLike);
|
|
217
256
|
|
|
218
|
-
const invariantLinesLegacy =
|
|
257
|
+
const invariantLinesLegacy = sanitizeLines(
|
|
219
258
|
mergedSection.filter((line) => /invariant|stable|policy|rule/i.test(line))
|
|
220
259
|
).filter(isInvariantRuleLike);
|
|
221
|
-
const reflectionLinesLegacy =
|
|
260
|
+
const reflectionLinesLegacy = sanitizeLines(
|
|
222
261
|
mergedSection.filter((line) => /reflect|inherit|derive|change|apply/i.test(line))
|
|
223
262
|
).filter(isDerivedDeltaLike);
|
|
224
|
-
const openLoopLines =
|
|
263
|
+
const openLoopLines = sanitizeLines(parseSectionBullets(reflectionText, "Open loops / next actions"))
|
|
225
264
|
.filter(isOpenLoopAction)
|
|
226
265
|
.filter(isDerivedDeltaLike);
|
|
227
|
-
const durableDecisionLines =
|
|
266
|
+
const durableDecisionLines = sanitizeLines(parseSectionBullets(reflectionText, "Decisions (durable)"))
|
|
228
267
|
.filter(isInvariantRuleLike);
|
|
229
268
|
|
|
230
269
|
const invariants = invariantsPrimary.length > 0
|
|
@@ -240,8 +279,15 @@ export function extractReflectionSlices(reflectionText: string): ReflectionSlice
|
|
|
240
279
|
};
|
|
241
280
|
}
|
|
242
281
|
|
|
243
|
-
export function
|
|
244
|
-
|
|
282
|
+
export function extractReflectionSlices(reflectionText: string): ReflectionSlices {
|
|
283
|
+
return extractReflectionSlicesWithSanitizer(reflectionText, sanitizeReflectionSliceLines);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function extractInjectableReflectionSlices(reflectionText: string): ReflectionSlices {
|
|
287
|
+
return extractReflectionSlicesWithSanitizer(reflectionText, sanitizeInjectableReflectionLines);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildReflectionSliceItemsFromSlices(slices: ReflectionSlices): ReflectionSliceItem[] {
|
|
245
291
|
const invariantGroupSize = slices.invariants.length;
|
|
246
292
|
const derivedGroupSize = slices.derived.length;
|
|
247
293
|
|
|
@@ -262,3 +308,11 @@ export function extractReflectionSliceItems(reflectionText: string): ReflectionS
|
|
|
262
308
|
|
|
263
309
|
return [...invariantItems, ...derivedItems];
|
|
264
310
|
}
|
|
311
|
+
|
|
312
|
+
export function extractReflectionSliceItems(reflectionText: string): ReflectionSliceItem[] {
|
|
313
|
+
return buildReflectionSliceItemsFromSlices(extractReflectionSlices(reflectionText));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function extractInjectableReflectionSliceItems(reflectionText: string): ReflectionSliceItem[] {
|
|
317
|
+
return buildReflectionSliceItemsFromSlices(extractInjectableReflectionSlices(reflectionText));
|
|
318
|
+
}
|
package/src/reflection-store.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { MemoryEntry, MemorySearchResult } from "./store.js";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
extractInjectableReflectionSliceItems,
|
|
4
|
+
extractInjectableReflectionSlices,
|
|
5
5
|
sanitizeReflectionSliceLines,
|
|
6
|
+
sanitizeInjectableReflectionLines,
|
|
6
7
|
type ReflectionSlices,
|
|
7
8
|
} from "./reflection-slices.js";
|
|
8
9
|
import { parseReflectionMetadata } from "./reflection-metadata.js";
|
|
@@ -57,7 +58,7 @@ export function buildReflectionStorePayloads(params: BuildReflectionStorePayload
|
|
|
57
58
|
slices: ReflectionSlices;
|
|
58
59
|
payloads: ReflectionStorePayload[];
|
|
59
60
|
} {
|
|
60
|
-
const slices =
|
|
61
|
+
const slices = extractInjectableReflectionSlices(params.reflectionText);
|
|
61
62
|
const eventId = params.eventId || createReflectionEventId({
|
|
62
63
|
runAt: params.runAt,
|
|
63
64
|
sessionKey: params.sessionKey,
|
|
@@ -82,7 +83,7 @@ export function buildReflectionStorePayloads(params: BuildReflectionStorePayload
|
|
|
82
83
|
];
|
|
83
84
|
|
|
84
85
|
const itemPayloads = buildReflectionItemPayloads({
|
|
85
|
-
items:
|
|
86
|
+
items: extractInjectableReflectionSliceItems(params.reflectionText),
|
|
86
87
|
eventId,
|
|
87
88
|
agentId: params.agentId,
|
|
88
89
|
sessionKey: params.sessionKey,
|
|
@@ -287,11 +288,12 @@ function buildInvariantCandidates(
|
|
|
287
288
|
.filter(({ metadata }) => metadata.itemKind === "invariant")
|
|
288
289
|
.flatMap(({ entry, metadata }) => {
|
|
289
290
|
const lines = sanitizeReflectionSliceLines([entry.text]);
|
|
290
|
-
|
|
291
|
+
const safeLines = sanitizeInjectableReflectionLines([entry.text]);
|
|
292
|
+
if (safeLines.length === 0) return [];
|
|
291
293
|
|
|
292
294
|
const defaults = getReflectionItemDecayDefaults("invariant");
|
|
293
295
|
const timestamp = metadataTimestamp(metadata, entry.timestamp);
|
|
294
|
-
return
|
|
296
|
+
return safeLines.map((line) => ({
|
|
295
297
|
line,
|
|
296
298
|
timestamp,
|
|
297
299
|
midpointDays: readPositiveNumber(metadata.decayMidpointDays, defaults.midpointDays),
|
|
@@ -307,7 +309,7 @@ function buildInvariantCandidates(
|
|
|
307
309
|
return legacyRows.flatMap(({ entry, metadata }) => {
|
|
308
310
|
const defaults = getReflectionItemDecayDefaults("invariant");
|
|
309
311
|
const timestamp = metadataTimestamp(metadata, entry.timestamp);
|
|
310
|
-
const lines =
|
|
312
|
+
const lines = sanitizeInjectableReflectionLines(toStringArray(metadata.invariants));
|
|
311
313
|
return lines.map((line) => ({
|
|
312
314
|
line,
|
|
313
315
|
timestamp,
|
|
@@ -328,11 +330,12 @@ function buildDerivedCandidates(
|
|
|
328
330
|
.filter(({ metadata }) => metadata.itemKind === "derived")
|
|
329
331
|
.flatMap(({ entry, metadata }) => {
|
|
330
332
|
const lines = sanitizeReflectionSliceLines([entry.text]);
|
|
331
|
-
|
|
333
|
+
const safeLines = sanitizeInjectableReflectionLines([entry.text]);
|
|
334
|
+
if (safeLines.length === 0) return [];
|
|
332
335
|
|
|
333
336
|
const defaults = getReflectionItemDecayDefaults("derived");
|
|
334
337
|
const timestamp = metadataTimestamp(metadata, entry.timestamp);
|
|
335
|
-
return
|
|
338
|
+
return safeLines.map((line) => ({
|
|
336
339
|
line,
|
|
337
340
|
timestamp,
|
|
338
341
|
midpointDays: readPositiveNumber(metadata.decayMidpointDays, defaults.midpointDays),
|
|
@@ -347,7 +350,7 @@ function buildDerivedCandidates(
|
|
|
347
350
|
|
|
348
351
|
return legacyRows.flatMap(({ entry, metadata }) => {
|
|
349
352
|
const timestamp = metadataTimestamp(metadata, entry.timestamp);
|
|
350
|
-
const lines =
|
|
353
|
+
const lines = sanitizeInjectableReflectionLines(toStringArray(metadata.derived));
|
|
351
354
|
if (lines.length === 0) return [];
|
|
352
355
|
|
|
353
356
|
const defaults = {
|