@animalabs/connectome-host 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +156 -10
- package/HEADLESS-FLEET-PLAN.md +22 -0
- package/README.md +12 -1
- package/docs/AGENT-ONBOARDING.md +1 -1
- package/docs/debug-context-api.md +2 -2
- package/docs/retrieval-traces.md +173 -0
- package/docs/webui-deployment.md +2 -1
- package/package.json +2 -2
- package/scripts/audit-module-optins.ts +288 -0
- package/src/framework-strategy.ts +13 -4
- package/src/headless.ts +14 -0
- package/src/index.ts +12 -9
- package/src/modules/fleet-module.ts +60 -1
- package/src/modules/fleet-types.ts +30 -1
- package/src/modules/mcpl-admin-module.ts +33 -4
- package/src/modules/retrieval-module.ts +249 -51
- package/src/modules/retrieval-trace-page.ts +254 -0
- package/src/modules/retrieval-trace.ts +904 -0
- package/src/modules/tts-relay-module.ts +33 -18
- package/src/modules/web-ui-module.ts +445 -894
- package/src/recipe.ts +55 -4
- package/src/retrieval-config.ts +39 -0
- package/src/strategies/frontdesk-strategy.ts +34 -125
- package/src/tui.ts +325 -54
- package/src/web/panel-data.ts +1187 -0
- package/src/web/protocol.ts +75 -10
- package/test/audit-module-optins.test.ts +167 -0
- package/test/fleet-panel-request.test.ts +90 -0
- package/test/framework-strategy-defaults.test.ts +22 -0
- package/test/frontdesk-strategy.test.ts +25 -37
- package/test/headless-panel-request.test.ts +201 -0
- package/test/mcpl-admin-module.test.ts +23 -0
- package/test/mock-headless-child.ts +14 -0
- package/test/retrieval-auth-loopback.test.ts +49 -0
- package/test/retrieval-config.test.ts +74 -0
- package/test/retrieval-module.test.ts +821 -0
- package/test/tui-format.test.ts +106 -0
- package/test/web-ui-context-coverage.test.ts +1 -1
- package/test/web-ui-module.test.ts +189 -3
- package/test/web-ui-observers.test.ts +8 -5
- package/test/web-ui-protocol.test.ts +0 -0
- package/web/src/App.tsx +159 -44
- package/web/src/Context.tsx +35 -8
- package/web/src/ContextDocument.tsx +20 -5
- package/web/src/Files.tsx +2 -8
- package/web/src/Lessons.tsx +2 -38
- package/web/src/Mcpl.tsx +80 -14
- package/web/src/Pins.tsx +5 -0
- package/web/src/Settings.tsx +5 -0
- package/web/vite.config.ts +8 -2
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* RetrievalModule — LLM-as-retriever for semantic memory.
|
|
3
3
|
*
|
|
4
4
|
* Three-step retrieval pipeline running in gatherContext():
|
|
5
|
-
* 1. Flag concepts
|
|
6
|
-
*
|
|
5
|
+
* 1. Flag concepts: identify concepts being discussed that might benefit
|
|
6
|
+
* from background knowledge
|
|
7
7
|
* 2. Mechanical query: keyword-match against LessonsModule
|
|
8
|
-
* 3. Validate relevance
|
|
8
|
+
* 3. Validate relevance: filter to actually relevant lessons
|
|
9
9
|
*
|
|
10
|
-
* Steps 1 and 3 use
|
|
10
|
+
* Steps 1 and 3 use the configured retrieval model and optional reasoning.
|
|
11
11
|
* Results are cached to avoid redundant calls on unchanged context.
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -24,16 +24,35 @@ import type {
|
|
|
24
24
|
import type { Membrane, NormalizedRequest } from '@animalabs/membrane';
|
|
25
25
|
import type { ContextInjection } from '@animalabs/context-manager';
|
|
26
26
|
import type { LessonsModule, Lesson } from './lessons-module.js';
|
|
27
|
+
import {
|
|
28
|
+
RetrievalTraceStore,
|
|
29
|
+
type RetrievalTraceListOptions,
|
|
30
|
+
type RetrievalTraceRun,
|
|
31
|
+
} from './retrieval-trace.js';
|
|
27
32
|
|
|
28
33
|
// ---------------------------------------------------------------------------
|
|
29
34
|
// Configuration
|
|
30
35
|
// ---------------------------------------------------------------------------
|
|
31
36
|
|
|
37
|
+
export type RetrievalReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
38
|
+
|
|
39
|
+
export interface RetrievalReasoningConfig {
|
|
40
|
+
effort: RetrievalReasoningEffort;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface RecentRetrievalContext {
|
|
44
|
+
text: string;
|
|
45
|
+
messageCount: number;
|
|
46
|
+
messageIds: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
export interface RetrievalModuleConfig {
|
|
33
|
-
/** Membrane instance for
|
|
50
|
+
/** Membrane instance for retrieval calls */
|
|
34
51
|
membrane: Membrane;
|
|
35
52
|
/** Model to use for retrieval calls (default: claude-haiku-4-5-20251001) */
|
|
36
53
|
retrievalModel?: string;
|
|
54
|
+
/** Optional OpenAI reasoning effort, applied to both retrieval LLM stages. */
|
|
55
|
+
retrievalReasoning?: RetrievalReasoningConfig;
|
|
37
56
|
/** Max lessons to inject (default: 5) */
|
|
38
57
|
maxInjectedLessons?: number;
|
|
39
58
|
/** Minimum lesson confidence for injection (default: 0.3) */
|
|
@@ -67,6 +86,10 @@ export class RetrievalModule implements Module {
|
|
|
67
86
|
private config: RetrievalModuleConfig;
|
|
68
87
|
private lastContextHash = '';
|
|
69
88
|
private cachedInjections: ContextInjection[] = [];
|
|
89
|
+
private cachedLessonIds: string[] = [];
|
|
90
|
+
private cachedLessons: Lesson[] = [];
|
|
91
|
+
private cachedSourceTraceId: number | undefined;
|
|
92
|
+
private readonly traceStore = new RetrievalTraceStore();
|
|
70
93
|
|
|
71
94
|
constructor(config: RetrievalModuleConfig) {
|
|
72
95
|
this.config = config;
|
|
@@ -93,50 +116,115 @@ export class RetrievalModule implements Module {
|
|
|
93
116
|
return {};
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
/** Recent retrieval runs, newest first. Exact conversation inputs are opt-in. */
|
|
120
|
+
getRetrievalTraces(options?: RetrievalTraceListOptions) {
|
|
121
|
+
return this.traceStore.list(options);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private beginTrace(agentName: string): RetrievalTraceRun | undefined {
|
|
125
|
+
try {
|
|
126
|
+
const providerParams = this.retrievalProviderParams();
|
|
127
|
+
return this.traceStore.begin({
|
|
128
|
+
agentName,
|
|
129
|
+
model: this.config.retrievalModel ?? 'claude-haiku-4-5-20251001',
|
|
130
|
+
...(this.config.retrievalReasoning
|
|
131
|
+
? { requestedReasoning: this.config.retrievalReasoning }
|
|
132
|
+
: {}),
|
|
133
|
+
...(providerParams ? { providerParams } : {}),
|
|
134
|
+
minConfidence: this.config.minConfidence ?? 0.3,
|
|
135
|
+
maxCandidates: 20,
|
|
136
|
+
maxInjectedLessons: this.config.maxInjectedLessons ?? 5,
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
96
143
|
/**
|
|
97
144
|
* Run the 3-step retrieval pipeline before each inference.
|
|
98
145
|
*/
|
|
99
|
-
async gatherContext(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
async gatherContext(agentName: string): Promise<ContextInjection[]> {
|
|
147
|
+
const trace = this.beginTrace(agentName);
|
|
148
|
+
if (!this.ctx) {
|
|
149
|
+
trace?.finish('not-started');
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let lessons: Lesson[];
|
|
154
|
+
let recentMessages: string;
|
|
155
|
+
let contextHash: string;
|
|
156
|
+
try {
|
|
157
|
+
// These lookups, eligibility checks, context rendering, and hashing are
|
|
158
|
+
// pre-existing throwing paths. Record their failure, then preserve the
|
|
159
|
+
// upstream rejection rather than applying the provider-stage fail-open.
|
|
160
|
+
const lessonsModule = this.ctx.getModule<LessonsModule>('lessons');
|
|
161
|
+
if (!lessonsModule) {
|
|
162
|
+
trace?.finish('no-lessons-module');
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
lessons = lessonsModule.getLessons().filter(
|
|
167
|
+
l => !l.deprecated && l.confidence >= (this.config.minConfidence ?? 0.3)
|
|
168
|
+
);
|
|
169
|
+
if (lessons.length === 0) {
|
|
170
|
+
trace?.finish('no-eligible-lessons');
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const recent = this.getRecentContext();
|
|
175
|
+
if (!recent) {
|
|
176
|
+
trace?.finish('no-recent-context');
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
recentMessages = recent.text;
|
|
180
|
+
|
|
181
|
+
// Check cache: if context hasn't changed, reuse cached results.
|
|
182
|
+
contextHash = this.hashContext(recentMessages);
|
|
183
|
+
trace?.setContext(contextHash, recentMessages, recent.messageCount, recent.messageIds);
|
|
184
|
+
if (contextHash === this.lastContextHash && this.cachedInjections.length > 0) {
|
|
185
|
+
trace?.recordCacheHit(
|
|
186
|
+
this.cachedSourceTraceId,
|
|
187
|
+
this.cachedLessonIds,
|
|
188
|
+
this.cachedLessons,
|
|
189
|
+
this.cachedInjections,
|
|
190
|
+
);
|
|
191
|
+
trace?.finish('cache-hit');
|
|
192
|
+
return this.cachedInjections;
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
trace?.finish('error', error);
|
|
196
|
+
throw error;
|
|
119
197
|
}
|
|
120
198
|
|
|
121
199
|
try {
|
|
122
|
-
// Step 1: Flag concepts
|
|
123
|
-
const concepts = await this.flagConcepts(recentMessages);
|
|
200
|
+
// Step 1: Flag concepts
|
|
201
|
+
const concepts = await this.flagConcepts(recentMessages, trace);
|
|
124
202
|
if (concepts.length === 0) {
|
|
125
203
|
this.lastContextHash = contextHash;
|
|
126
204
|
this.cachedInjections = [];
|
|
205
|
+
this.cachedLessonIds = [];
|
|
206
|
+
this.cachedLessons = [];
|
|
207
|
+
this.cachedSourceTraceId = undefined;
|
|
208
|
+
trace?.finish('no-concepts');
|
|
127
209
|
return [];
|
|
128
210
|
}
|
|
129
211
|
|
|
130
212
|
// Step 2: Mechanical query (keyword matching)
|
|
131
213
|
const candidates = this.queryCandidates(concepts, lessons);
|
|
214
|
+
trace?.recordCandidates(concepts, candidates);
|
|
132
215
|
if (candidates.length === 0) {
|
|
133
216
|
this.lastContextHash = contextHash;
|
|
134
217
|
this.cachedInjections = [];
|
|
218
|
+
this.cachedLessonIds = [];
|
|
219
|
+
this.cachedLessons = [];
|
|
220
|
+
this.cachedSourceTraceId = undefined;
|
|
221
|
+
trace?.finish('no-candidates');
|
|
135
222
|
return [];
|
|
136
223
|
}
|
|
137
224
|
|
|
138
|
-
// Step 3: Validate relevance
|
|
139
|
-
const relevant = await this.validateRelevance(recentMessages, candidates);
|
|
225
|
+
// Step 3: Validate relevance
|
|
226
|
+
const relevant = await this.validateRelevance(recentMessages, candidates, trace);
|
|
227
|
+
trace?.recordRelevant(relevant);
|
|
140
228
|
|
|
141
229
|
// Build injection
|
|
142
230
|
const maxLessons = this.config.maxInjectedLessons ?? 5;
|
|
@@ -145,6 +233,10 @@ export class RetrievalModule implements Module {
|
|
|
145
233
|
if (injected.length === 0) {
|
|
146
234
|
this.lastContextHash = contextHash;
|
|
147
235
|
this.cachedInjections = [];
|
|
236
|
+
this.cachedLessonIds = [];
|
|
237
|
+
this.cachedLessons = [];
|
|
238
|
+
this.cachedSourceTraceId = undefined;
|
|
239
|
+
trace?.finish('no-relevant-lessons');
|
|
148
240
|
return [];
|
|
149
241
|
}
|
|
150
242
|
|
|
@@ -164,9 +256,15 @@ export class RetrievalModule implements Module {
|
|
|
164
256
|
|
|
165
257
|
this.lastContextHash = contextHash;
|
|
166
258
|
this.cachedInjections = injections;
|
|
259
|
+
this.cachedLessons = this.safeLessonSnapshots(injected);
|
|
260
|
+
this.cachedLessonIds = this.safeLessonIds(this.cachedLessons);
|
|
261
|
+
this.cachedSourceTraceId = trace?.id;
|
|
262
|
+
trace?.recordInjection(injected, injections);
|
|
263
|
+
trace?.finish('injected');
|
|
167
264
|
return injections;
|
|
168
265
|
} catch (err) {
|
|
169
266
|
// Fail open — don't block inference if retrieval fails
|
|
267
|
+
trace?.finish('error', err);
|
|
170
268
|
console.error('RetrievalModule: retrieval failed:', err);
|
|
171
269
|
return [];
|
|
172
270
|
}
|
|
@@ -176,24 +274,44 @@ export class RetrievalModule implements Module {
|
|
|
176
274
|
// Pipeline Steps
|
|
177
275
|
// =========================================================================
|
|
178
276
|
|
|
277
|
+
/** Build OpenAI request parameters for configured retrieval reasoning effort. */
|
|
278
|
+
private retrievalProviderParams(): Record<string, unknown> | undefined {
|
|
279
|
+
const reasoning = this.config.retrievalReasoning;
|
|
280
|
+
if (!reasoning) return undefined;
|
|
281
|
+
return { reasoning: { effort: reasoning.effort } };
|
|
282
|
+
}
|
|
283
|
+
|
|
179
284
|
/**
|
|
180
|
-
* Step 1: Use
|
|
285
|
+
* Step 1: Use the configured retrieval model to identify concepts that might benefit from background knowledge.
|
|
181
286
|
*/
|
|
182
|
-
private async flagConcepts(
|
|
287
|
+
private async flagConcepts(
|
|
288
|
+
recentContext: string,
|
|
289
|
+
trace?: RetrievalTraceRun,
|
|
290
|
+
): Promise<string[]> {
|
|
183
291
|
const model = this.config.retrievalModel ?? 'claude-haiku-4-5-20251001';
|
|
292
|
+
const providerParams = this.retrievalProviderParams();
|
|
293
|
+
const input = `Recent conversation:\n${recentContext}`;
|
|
184
294
|
|
|
185
295
|
const request: NormalizedRequest = {
|
|
186
296
|
messages: [
|
|
187
297
|
{
|
|
188
298
|
participant: 'user',
|
|
189
|
-
content: [{ type: 'text', text:
|
|
299
|
+
content: [{ type: 'text', text: input }],
|
|
190
300
|
},
|
|
191
301
|
],
|
|
192
302
|
system: CONCEPT_FLAG_PROMPT,
|
|
193
303
|
config: { model, maxTokens: 500, temperature: 0 },
|
|
304
|
+
...(providerParams ? { providerParams } : {}),
|
|
194
305
|
};
|
|
195
306
|
|
|
196
|
-
|
|
307
|
+
trace?.startConceptExtraction(CONCEPT_FLAG_PROMPT, input);
|
|
308
|
+
let response;
|
|
309
|
+
try {
|
|
310
|
+
response = await this.config.membrane.complete(request);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
trace?.recordStageError('conceptExtraction', error);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
197
315
|
const text = response.content
|
|
198
316
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
199
317
|
.map(b => b.text)
|
|
@@ -202,16 +320,28 @@ export class RetrievalModule implements Module {
|
|
|
202
320
|
try {
|
|
203
321
|
const parsed = JSON.parse(text);
|
|
204
322
|
if (Array.isArray(parsed)) {
|
|
205
|
-
|
|
323
|
+
const concepts = parsed.filter((value): value is string => typeof value === 'string');
|
|
324
|
+
trace?.finishConceptExtraction(text, concepts, 'json', response.content);
|
|
325
|
+
return concepts;
|
|
206
326
|
}
|
|
327
|
+
trace?.finishConceptExtraction(text, [], 'invalid', response.content);
|
|
207
328
|
} catch {
|
|
208
|
-
// Try to extract from markdown
|
|
329
|
+
// Try to extract an array from a prose/markdown wrapper.
|
|
209
330
|
const match = text.match(/\[([^\]]*)\]/);
|
|
210
331
|
if (match) {
|
|
211
332
|
try {
|
|
212
|
-
|
|
333
|
+
const parsed = JSON.parse(`[${match[1]}]`);
|
|
334
|
+
if (Array.isArray(parsed)) {
|
|
335
|
+
const traceValues = parsed.filter((value): value is string => typeof value === 'string');
|
|
336
|
+
trace?.finishConceptExtraction(text, traceValues, 'array-extraction', response.content);
|
|
337
|
+
// Preserve the historical malformed-wrapper behavior exactly: mixed
|
|
338
|
+
// arrays proceed to queryCandidates(), which then fails open rather
|
|
339
|
+
// than silently becoming a different, partially valid concept set.
|
|
340
|
+
return parsed as string[];
|
|
341
|
+
}
|
|
213
342
|
} catch { /* fall through */ }
|
|
214
343
|
}
|
|
344
|
+
trace?.finishConceptExtraction(text, [], 'invalid', response.content);
|
|
215
345
|
}
|
|
216
346
|
return [];
|
|
217
347
|
}
|
|
@@ -250,32 +380,47 @@ export class RetrievalModule implements Module {
|
|
|
250
380
|
}
|
|
251
381
|
|
|
252
382
|
/**
|
|
253
|
-
* Step 3: Use
|
|
383
|
+
* Step 3: Use the configured retrieval model to validate which candidates are actually relevant.
|
|
254
384
|
*/
|
|
255
|
-
private async validateRelevance(
|
|
385
|
+
private async validateRelevance(
|
|
386
|
+
recentContext: string,
|
|
387
|
+
candidates: Lesson[],
|
|
388
|
+
trace?: RetrievalTraceRun,
|
|
389
|
+
): Promise<Lesson[]> {
|
|
256
390
|
if (candidates.length <= 3) {
|
|
257
|
-
// If only a few candidates, skip validation — they're probably all relevant
|
|
391
|
+
// If only a few candidates, skip validation — they're probably all relevant.
|
|
392
|
+
trace?.recordRelevanceSkipped('three-or-fewer-candidates');
|
|
258
393
|
return candidates;
|
|
259
394
|
}
|
|
260
395
|
|
|
261
396
|
const model = this.config.retrievalModel ?? 'claude-haiku-4-5-20251001';
|
|
397
|
+
const providerParams = this.retrievalProviderParams();
|
|
262
398
|
|
|
263
399
|
const candidateList = candidates.map(l =>
|
|
264
400
|
`[${l.id}] (${l.confidence.toFixed(2)}) ${l.content}`
|
|
265
401
|
).join('\n');
|
|
402
|
+
const input = `Current conversation:\n${recentContext}\n\nCandidate lessons:\n${candidateList}`;
|
|
266
403
|
|
|
267
404
|
const request: NormalizedRequest = {
|
|
268
405
|
messages: [
|
|
269
406
|
{
|
|
270
407
|
participant: 'user',
|
|
271
|
-
content: [{ type: 'text', text:
|
|
408
|
+
content: [{ type: 'text', text: input }],
|
|
272
409
|
},
|
|
273
410
|
],
|
|
274
411
|
system: RELEVANCE_VALIDATION_PROMPT,
|
|
275
412
|
config: { model, maxTokens: 500, temperature: 0 },
|
|
413
|
+
...(providerParams ? { providerParams } : {}),
|
|
276
414
|
};
|
|
277
415
|
|
|
278
|
-
|
|
416
|
+
trace?.startRelevance(RELEVANCE_VALIDATION_PROMPT, input);
|
|
417
|
+
let response;
|
|
418
|
+
try {
|
|
419
|
+
response = await this.config.membrane.complete(request);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
trace?.recordStageError('relevance', error);
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
279
424
|
const text = response.content
|
|
280
425
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
281
426
|
.map(b => b.text)
|
|
@@ -284,38 +429,91 @@ export class RetrievalModule implements Module {
|
|
|
284
429
|
try {
|
|
285
430
|
const parsed = JSON.parse(text);
|
|
286
431
|
if (Array.isArray(parsed)) {
|
|
287
|
-
const
|
|
432
|
+
const parsedIds = parsed.filter((value): value is string => typeof value === 'string');
|
|
433
|
+
trace?.finishRelevance(text, parsedIds, 'json', response.content);
|
|
434
|
+
const relevantIds = new Set(parsedIds);
|
|
288
435
|
return candidates.filter(l => relevantIds.has(l.id));
|
|
289
436
|
}
|
|
290
437
|
} catch {
|
|
291
|
-
//
|
|
292
|
-
return candidates.slice(0, 5);
|
|
438
|
+
// Fall through to confidence-ordered fallback.
|
|
293
439
|
}
|
|
294
|
-
|
|
440
|
+
|
|
441
|
+
const fallback = candidates.slice(0, 5);
|
|
442
|
+
trace?.finishRelevance(text, [], 'fallback', response.content);
|
|
443
|
+
return fallback;
|
|
295
444
|
}
|
|
296
445
|
|
|
297
446
|
// =========================================================================
|
|
298
447
|
// Helpers
|
|
299
448
|
// =========================================================================
|
|
300
449
|
|
|
301
|
-
private getRecentContext():
|
|
450
|
+
private getRecentContext(): RecentRetrievalContext | null {
|
|
302
451
|
if (!this.ctx) return null;
|
|
303
452
|
|
|
304
|
-
// Get the last few messages from the conversation
|
|
453
|
+
// Get the last few messages from the conversation.
|
|
305
454
|
const { messages } = this.ctx.queryMessages({});
|
|
306
455
|
if (messages.length === 0) return null;
|
|
307
456
|
|
|
308
|
-
// Take the last 10 messages for context
|
|
457
|
+
// Take the last 10 messages for context.
|
|
309
458
|
const recent = messages.slice(-10);
|
|
310
|
-
|
|
459
|
+
const text = recent
|
|
311
460
|
.map(m => {
|
|
312
|
-
const
|
|
461
|
+
const messageText = m.content
|
|
313
462
|
.filter((b): b is { type: 'text'; text: string } => b.type === 'text')
|
|
314
463
|
.map(b => b.text)
|
|
315
464
|
.join('\n');
|
|
316
|
-
return `${m.participant}: ${
|
|
465
|
+
return `${m.participant}: ${messageText}`;
|
|
317
466
|
})
|
|
318
467
|
.join('\n\n');
|
|
468
|
+
const messageIds = recent.flatMap(message => {
|
|
469
|
+
try {
|
|
470
|
+
const candidate = (message as unknown as { id?: unknown }).id;
|
|
471
|
+
return typeof candidate === 'string' || typeof candidate === 'number'
|
|
472
|
+
? [String(candidate)]
|
|
473
|
+
: [];
|
|
474
|
+
} catch {
|
|
475
|
+
// Trace-only metadata must never alter retrieval behavior.
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
return { text, messageCount: recent.length, messageIds };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private safeLessonIds(lessons: Lesson[]): string[] {
|
|
484
|
+
const ids: string[] = [];
|
|
485
|
+
for (const lesson of lessons) {
|
|
486
|
+
try {
|
|
487
|
+
if (typeof lesson.id === 'string') ids.push(lesson.id);
|
|
488
|
+
} catch {
|
|
489
|
+
// Cache provenance is observability metadata; omit unreadable IDs.
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return ids;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private safeLessonSnapshots(lessons: Lesson[]): Lesson[] {
|
|
496
|
+
const snapshots: Lesson[] = [];
|
|
497
|
+
for (const item of lessons) {
|
|
498
|
+
try {
|
|
499
|
+
snapshots.push({
|
|
500
|
+
id: item.id,
|
|
501
|
+
content: item.content,
|
|
502
|
+
confidence: item.confidence,
|
|
503
|
+
tags: [...item.tags],
|
|
504
|
+
evidence: [...item.evidence],
|
|
505
|
+
created: item.created,
|
|
506
|
+
updated: item.updated,
|
|
507
|
+
deprecated: item.deprecated,
|
|
508
|
+
...(item.deprecationReason !== undefined
|
|
509
|
+
? { deprecationReason: item.deprecationReason }
|
|
510
|
+
: {}),
|
|
511
|
+
});
|
|
512
|
+
} catch {
|
|
513
|
+
// Trace-only lesson snapshots must not alter retrieval behavior.
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return snapshots;
|
|
319
517
|
}
|
|
320
518
|
|
|
321
519
|
private hashContext(text: string): string {
|