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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(gh pr:*)",
5
+ "Bash(gh run:*)",
6
+ "Bash(git:*)"
7
+ ]
8
+ }
9
+ }
@@ -20,7 +20,7 @@ jobs:
20
20
  numOfAssignee: 1
21
21
 
22
22
  assign-prs:
23
- if: github.event_name == 'pull_request'
23
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
24
24
  runs-on: ubuntu-latest
25
25
  permissions:
26
26
  pull-requests: write
@@ -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 (Jina / custom) | ❌ | ✅ |
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**, or any compatible endpoint
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(Jina / 自定义) | ❌ | ✅ |
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
- 说明:`voyage` 发送 `{ model, query, documents }` 格式(不含 `top_n`),响应从 `data[].relevance_score` 解析。
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
- extractReflectionMappedMemoryItems,
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?: "jina" | "siliconflow" | "voyage" | "pinecone";
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 = extractReflectionMappedMemoryItems(reflectionText);
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>> = [];
@@ -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.7",
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, or any service with a similar interface."
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 pinecone",
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.7",
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
 
@@ -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 = "create" | "merge" | "skip" | "support" | "contextualize" | "contradict";
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. */
@@ -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
- export function extractReflectionMappedMemoryItems(reflectionText: string): ReflectionMappedMemoryItem[] {
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 = sanitizeReflectionSliceLines(parseSectionBullets(reflectionText, heading));
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 extractReflectionSlices(reflectionText: string): ReflectionSlices {
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 = sanitizeReflectionSliceLines(invariantSection).filter(isInvariantRuleLike);
216
- const derivedPrimary = sanitizeReflectionSliceLines(derivedSection).filter(isDerivedDeltaLike);
254
+ const invariantsPrimary = sanitizeLines(invariantSection).filter(isInvariantRuleLike);
255
+ const derivedPrimary = sanitizeLines(derivedSection).filter(isDerivedDeltaLike);
217
256
 
218
- const invariantLinesLegacy = sanitizeReflectionSliceLines(
257
+ const invariantLinesLegacy = sanitizeLines(
219
258
  mergedSection.filter((line) => /invariant|stable|policy|rule/i.test(line))
220
259
  ).filter(isInvariantRuleLike);
221
- const reflectionLinesLegacy = sanitizeReflectionSliceLines(
260
+ const reflectionLinesLegacy = sanitizeLines(
222
261
  mergedSection.filter((line) => /reflect|inherit|derive|change|apply/i.test(line))
223
262
  ).filter(isDerivedDeltaLike);
224
- const openLoopLines = sanitizeReflectionSliceLines(parseSectionBullets(reflectionText, "Open loops / next actions"))
263
+ const openLoopLines = sanitizeLines(parseSectionBullets(reflectionText, "Open loops / next actions"))
225
264
  .filter(isOpenLoopAction)
226
265
  .filter(isDerivedDeltaLike);
227
- const durableDecisionLines = sanitizeReflectionSliceLines(parseSectionBullets(reflectionText, "Decisions (durable)"))
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 extractReflectionSliceItems(reflectionText: string): ReflectionSliceItem[] {
244
- const slices = extractReflectionSlices(reflectionText);
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
+ }
@@ -1,8 +1,9 @@
1
1
  import type { MemoryEntry, MemorySearchResult } from "./store.js";
2
2
  import {
3
- extractReflectionSliceItems,
4
- extractReflectionSlices,
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 = extractReflectionSlices(params.reflectionText);
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: extractReflectionSliceItems(params.reflectionText),
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
- if (lines.length === 0) return [];
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 lines.map((line) => ({
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 = sanitizeReflectionSliceLines(toStringArray(metadata.invariants));
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
- if (lines.length === 0) return [];
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 lines.map((line) => ({
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 = sanitizeReflectionSliceLines(toStringArray(metadata.derived));
353
+ const lines = sanitizeInjectableReflectionLines(toStringArray(metadata.derived));
351
354
  if (lines.length === 0) return [];
352
355
 
353
356
  const defaults = {