claude-memory-layer 1.0.45 → 1.0.47

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/dist/mcp/index.js CHANGED
@@ -10410,13 +10410,22 @@ var tools = [
10410
10410
  enum: ["off", "safe", "aggressive"],
10411
10411
  description: "Optional context-pack compression mode. safe preserves source refs and high-signal errors/log lines; aggressive is more compact."
10412
10412
  },
10413
+ retrievalMode: {
10414
+ type: "string",
10415
+ enum: ["session-event-hybrid", "event"],
10416
+ description: "Optional production context-pack retrieval mode. Defaults to session-event-hybrid (session-event hybrid), which rescues query-relevant sibling events from sessions already hit by direct retrieval. Use event for exact event-only debugging."
10417
+ },
10413
10418
  maxChars: {
10414
10419
  type: "number",
10415
- description: "Maximum final context-pack characters after safe compression (default: no hard cap, max: 50000)"
10420
+ minimum: 1e3,
10421
+ maximum: 5e4,
10422
+ description: "Maximum final context-pack characters after the selected compression mode and final assembly trimming (default: no hard cap, max: 50000)"
10416
10423
  },
10417
10424
  maxTokens: {
10418
10425
  type: "number",
10419
- description: "Maximum final context-pack tokens, estimated at ~4 chars/token, after safe compression (default: no hard cap)"
10426
+ minimum: 250,
10427
+ maximum: 12500,
10428
+ description: "Maximum final context-pack tokens, estimated at ~4 chars/token after the selected compression mode and final assembly trimming (default: no hard cap)"
10420
10429
  },
10421
10430
  refreshLatest: {
10422
10431
  type: "boolean",
@@ -10563,6 +10572,16 @@ var tools = [
10563
10572
  type: "number",
10564
10573
  description: "Maximum recent events to scan for ID resolution (default: 10000, max: 50000)"
10565
10574
  },
10575
+ includeNeighbors: {
10576
+ type: "boolean",
10577
+ description: "When true, include privacy-safe neighbor events from the same session around each resolved source reference."
10578
+ },
10579
+ neighborWindow: {
10580
+ type: "number",
10581
+ minimum: 0,
10582
+ maximum: 5,
10583
+ description: "Number of before/after session neighbor events to include when includeNeighbors is true (default: 1, max: 5)."
10584
+ },
10566
10585
  projectPath: projectPathProperty
10567
10586
  },
10568
10587
  required: ["ids"]
@@ -13643,6 +13662,7 @@ var VectorOutbox = class {
13643
13662
  // src/core/retrieval-debug-lanes.ts
13644
13663
  var RETRIEVAL_DEBUG_LANE_NAMES = [
13645
13664
  "raw_event",
13665
+ "session_event",
13646
13666
  "session_summary",
13647
13667
  "graph_path",
13648
13668
  "facet_match"
@@ -21118,6 +21138,10 @@ var LOW_SIGNAL_CONTEXT_PATTERNS = [
21118
21138
  /<turn_aborted>/i,
21119
21139
  /^#\s*AGENTS\.md\s+instructions\b[\s\S]*<INSTRUCTIONS>/i,
21120
21140
  /^\s*(?:understood[,\s.]*)?(?:stopping|stopped|pausing|paused)\s+here\b[\s\S]{0,180}\blet\s+me\s+know\s+when\s+you(?:'d|\s+would)?\s+like\s+to\s+continue\b/i,
21141
+ /^\s*\[?CONTEXT\s+COMPACTION\s*[—-]\s*REFERENCE\s+ONLY\]?\b[\s\S]{0,600}\b(?:earlier\s+turns\s+were\s+compacted|handoff\s+from\s+a\s+previous\s+context\s+window|active\s+task)\b/i,
21142
+ /^\s*Summary\s+generation\s+was\s+unavailable\.\s*\d+\s+message\(s\)\s+were\s+removed\s+to\s+free\s+context\s+space\b/i,
21143
+ /^\s*---\s*END\s+OF\s+CONTEXT\s+SUMMARY\b/i,
21144
+ /^\s*\[Your\s+active\s+task\s+list\s+was\s+preserved\s+across\s+context\s+compression\]/i,
21121
21145
  /^➜\s+\S+\s+git:\([^)]*\)\s+/i,
21122
21146
  /^\$\s+\S+/i
21123
21147
  ];
@@ -21132,7 +21156,7 @@ var SHORT_REPAIR_FOLLOW_UP_PATTERNS = [
21132
21156
  var CURRENT_STATE_QUERY_PATTERNS = [
21133
21157
  /\bcurrent\b.*\b(?:state|status|deployment|blocker|pr|pull request)\b/i,
21134
21158
  /\b(?:still|as current|current)\b.*\b(?:unresolved|open|pending|not completed)\b/i,
21135
- /\b(?:old|obsolete|stale|resolved|already resolved)\b.*\b(?:current|still|unresolved|open|state|status)\b/i,
21159
+ /\b(?:old|obsolete|stale|resolved|already resolved)\b.*\b(?:current|still|unresolved|open|status)\b/i,
21136
21160
  /(?:현재|아직|이전|오래된|해결된).*(?:상태|미해결|열린|블로커|PR|풀리퀘스트)/i
21137
21161
  ];
21138
21162
  var STALE_CONTENT_PATTERNS = [
@@ -21310,6 +21334,16 @@ function isShortRepairFollowUpQuery(query) {
21310
21334
  if (tokens.length > 8) return false;
21311
21335
  return SHORT_REPAIR_FOLLOW_UP_PATTERNS.some((pattern) => pattern.test(trimmed));
21312
21336
  }
21337
+ function isLowConfidenceContextFallbackQuery(query) {
21338
+ const trimmed = query.trim();
21339
+ if (!trimmed) return false;
21340
+ if (isGenericContinuationQuery(trimmed) || isShortRepairFollowUpQuery(trimmed)) return true;
21341
+ const terms = new Set(tokenizeQualityText(trimmed));
21342
+ if ((terms.has("compacted") || terms.has("compaction")) && terms.has("handoff")) return false;
21343
+ const hasContinuationRecall = /^(?:continue|resume)\b/i.test(trimmed) && (terms.has("work") || terms.has("step") || terms.has("task") || terms.has("last") || terms.has("completed"));
21344
+ const hasValidationGateRecall = terms.has("validation") && (terms.has("gate") || terms.has("check")) && (terms.has("run") || terms.has("before") || terms.has("commit") || terms.has("committing") || terms.has("change"));
21345
+ return hasContinuationRecall || hasValidationGateRecall;
21346
+ }
21313
21347
  function isCurrentStateQuery(query) {
21314
21348
  const trimmed = query.trim();
21315
21349
  if (!trimmed) return false;
@@ -21483,6 +21517,7 @@ var Retriever = class {
21483
21517
  }
21484
21518
  async retrieve(query, options = {}) {
21485
21519
  const opts = { ...DEFAULT_OPTIONS, ...options };
21520
+ const retrievalMode = options.retrievalMode ?? ((options.strategy ?? DEFAULT_OPTIONS.strategy) === "auto" ? "session-event-hybrid" : "event");
21486
21521
  const sessionFilter = opts.scope?.sessionId ?? opts.sessionId;
21487
21522
  const fallbackTrace = [];
21488
21523
  const qualityQuery = buildRetrievalQualityQuery(query);
@@ -21513,6 +21548,7 @@ var Retriever = class {
21513
21548
  decayPolicy: opts.decayPolicy,
21514
21549
  intentRewrite: opts.intentRewrite === true,
21515
21550
  graphHop: opts.graphHop,
21551
+ retrievalMode,
21516
21552
  projectScopeMode: opts.projectScopeMode,
21517
21553
  projectHash: opts.projectHash,
21518
21554
  allowedProjectHashes: opts.allowedProjectHashes,
@@ -21531,6 +21567,7 @@ var Retriever = class {
21531
21567
  rerankWeights: opts.rerankWeights,
21532
21568
  decayPolicy: opts.decayPolicy,
21533
21569
  graphHop: opts.graphHop,
21570
+ retrievalMode,
21534
21571
  projectScopeMode: opts.projectScopeMode,
21535
21572
  projectHash: opts.projectHash,
21536
21573
  allowedProjectHashes: opts.allowedProjectHashes,
@@ -21550,6 +21587,7 @@ var Retriever = class {
21550
21587
  rerankWeights: opts.rerankWeights,
21551
21588
  decayPolicy: opts.decayPolicy,
21552
21589
  graphHop: opts.graphHop,
21590
+ retrievalMode,
21553
21591
  projectScopeMode: opts.projectScopeMode,
21554
21592
  projectHash: opts.projectHash,
21555
21593
  allowedProjectHashes: opts.allowedProjectHashes,
@@ -21570,14 +21608,37 @@ var Retriever = class {
21570
21608
  query,
21571
21609
  minScore: opts.minScore
21572
21610
  });
21611
+ const expandedSummary = retrievalMode === "session-event-hybrid" ? await this.expandSessionEventHybrid(filteredSummary, {
21612
+ query: qualityQuery,
21613
+ currentStateQuery: query,
21614
+ limit: opts.topK * 4
21615
+ }) : filteredSummary;
21616
+ const scopedExpandedSummary = retrievalMode === "session-event-hybrid" ? await this.applyScopeFilters(expandedSummary, {
21617
+ scope: opts.scope,
21618
+ projectScopeMode: opts.projectScopeMode,
21619
+ projectHash: opts.projectHash,
21620
+ allowedProjectHashes: opts.allowedProjectHashes,
21621
+ facets: opts.facets
21622
+ }) : expandedSummary;
21623
+ const finalSummary = retrievalMode === "session-event-hybrid" ? this.applyQualityFilters(scopedExpandedSummary, {
21624
+ query,
21625
+ minScore: opts.minScore
21626
+ }) : scopedExpandedSummary;
21573
21627
  current = {
21574
- results: filteredSummary,
21575
- candidateResults: filteredSummary,
21576
- matchResult: this.matcher.matchSearchResults(filteredSummary, () => 0)
21628
+ results: finalSummary,
21629
+ candidateResults: finalSummary,
21630
+ matchResult: this.matcher.matchSearchResults(finalSummary, () => 0)
21577
21631
  };
21578
21632
  fallbackTrace.push("fallback:summary");
21579
21633
  }
21580
- const memories = await this.enrichResults(current.results.slice(0, opts.topK), opts);
21634
+ const selectedResults = current.results.slice(0, opts.topK).filter((result2) => {
21635
+ if (current.matchResult.confidence !== "none") return true;
21636
+ if (isLowConfidenceContextFallbackQuery(query)) {
21637
+ return (result2.semanticScore ?? result2.score) >= 0.5 || result2.score >= 0.5;
21638
+ }
21639
+ return (result2.semanticScore ?? result2.score) >= 0.62 || result2.score >= 0.62;
21640
+ });
21641
+ const memories = await this.enrichResults(selectedResults, opts, query);
21581
21642
  const context = this.buildContext(memories, opts.maxTokens);
21582
21643
  return {
21583
21644
  memories,
@@ -21585,7 +21646,7 @@ var Retriever = class {
21585
21646
  totalTokens: this.estimateTokens(context),
21586
21647
  context,
21587
21648
  fallbackTrace,
21588
- selectedDebug: current.results.slice(0, opts.topK).map((r) => this.debugDetailForResult(r)),
21649
+ selectedDebug: selectedResults.map((r) => this.debugDetailForResult(r)),
21589
21650
  candidateDebug: (current.candidateResults || []).slice(0, Math.max(opts.topK * 3, 20)).map((r) => this.debugDetailForResult(r)),
21590
21651
  rawQueryText: current.queryRewriteKind ? query : void 0,
21591
21652
  effectiveQueryText: current.effectiveQueryText,
@@ -21652,13 +21713,18 @@ var Retriever = class {
21652
21713
  initialResults = this.mergeResults(initialResults, rewrittenResults, input.topK * 3);
21653
21714
  }
21654
21715
  }
21655
- const expandedResults = input.graphHop?.enabled === false ? initialResults : await this.expandGraphHops(initialResults, {
21716
+ const graphExpandedResults = input.graphHop?.enabled === false ? initialResults : await this.expandGraphHops(initialResults, {
21656
21717
  query,
21657
21718
  queryGraphEnabled: this.queryGraphExpansionEnabled,
21658
21719
  maxHops: clampGraphHops(input.graphHop?.maxHops ?? 1),
21659
21720
  hopPenalty: Math.max(0, input.graphHop?.hopPenalty ?? 0.08),
21660
21721
  limit: input.topK * 4
21661
21722
  });
21723
+ const expandedResults = input.retrievalMode === "session-event-hybrid" ? await this.expandSessionEventHybrid(graphExpandedResults, {
21724
+ query: rerankQuery,
21725
+ currentStateQuery: query,
21726
+ limit: input.topK * 4
21727
+ }) : graphExpandedResults;
21662
21728
  const rerankedResults = input.rerankWithKeyword ? this.rerankByKeywordOverlap(expandedResults, rerankQuery, input.rerankWeights, input.decayPolicy) : expandedResults;
21663
21729
  const filtered = await this.applyScopeFilters(rerankedResults, {
21664
21730
  scope: input.scope,
@@ -21680,6 +21746,7 @@ var Retriever = class {
21680
21746
  if (isCurrentStateQuery(options.query)) {
21681
21747
  filtered = filtered.filter((result2) => !isStaleOrSupersededContent(result2.content));
21682
21748
  }
21749
+ filtered = filtered.filter((result2) => !isLowSignalContextContent(result2.content));
21683
21750
  filtered = filtered.filter(
21684
21751
  (result2) => this.isGraphPathResult(result2) || hasDiscriminativeTermOverlap(options.query, result2.content)
21685
21752
  );
@@ -21705,6 +21772,47 @@ var Retriever = class {
21705
21772
  }
21706
21773
  return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, limit);
21707
21774
  }
21775
+ async expandSessionEventHybrid(seeds, opts) {
21776
+ if (seeds.length === 0 || opts.limit <= seeds.length) return seeds;
21777
+ const queryTokens = this.tokenize(opts.query);
21778
+ if (queryTokens.length === 0) return seeds;
21779
+ const byId = /* @__PURE__ */ new Map();
21780
+ for (const seed of seeds) byId.set(seed.eventId, seed);
21781
+ const bestSeedBySession = /* @__PURE__ */ new Map();
21782
+ for (const seed of [...seeds].sort((a, b) => b.score - a.score || compareStable(a.eventId, b.eventId))) {
21783
+ if (!seed.sessionId || bestSeedBySession.has(seed.sessionId)) continue;
21784
+ bestSeedBySession.set(seed.sessionId, seed);
21785
+ }
21786
+ const suppressStaleState = isCurrentStateQuery(opts.currentStateQuery);
21787
+ for (const [sessionId, seed] of bestSeedBySession) {
21788
+ const sessionEvents = await this.eventStore.getSessionEvents(sessionId);
21789
+ for (const event of [...sessionEvents].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())) {
21790
+ if (byId.has(event.id)) continue;
21791
+ if (isLowSignalContextContent(event.content)) continue;
21792
+ if (suppressStaleState && isStaleOrSupersededContent(event.content)) continue;
21793
+ const lexicalScore = this.keywordOverlap(queryTokens, this.tokenize(event.content));
21794
+ if (lexicalScore <= 0) continue;
21795
+ if (shouldApplyTechnicalGuard(opts.query) && !hasTechnicalTermOverlap(opts.query, event.content)) continue;
21796
+ const score = Math.min(0.95, Math.max(0.35, seed.score * 0.72 + lexicalScore * 0.28));
21797
+ const row = withRetrievalLane({
21798
+ id: `session-event-${seed.eventId}-${event.id}`,
21799
+ eventId: event.id,
21800
+ content: event.content,
21801
+ score,
21802
+ sessionId: event.sessionId,
21803
+ eventType: event.eventType,
21804
+ timestamp: event.timestamp.toISOString(),
21805
+ semanticScore: seed.semanticScore ?? seed.score,
21806
+ lexicalScore,
21807
+ recencyScore: seed.recencyScore
21808
+ }, { lane: "session_event", reason: `same_session:${seed.eventId}`, score });
21809
+ byId.set(row.eventId, row);
21810
+ if (byId.size >= opts.limit) break;
21811
+ }
21812
+ if (byId.size >= opts.limit) break;
21813
+ }
21814
+ return [...byId.values()].sort((a, b) => b.score - a.score || compareStable(a.eventId, b.eventId)).slice(0, opts.limit);
21815
+ }
21708
21816
  async expandGraphHops(seeds, opts) {
21709
21817
  const byId = /* @__PURE__ */ new Map();
21710
21818
  for (const s of seeds) byId.set(s.eventId, s);
@@ -22020,7 +22128,7 @@ var Retriever = class {
22020
22128
  async retrieveRecent(limit = 100) {
22021
22129
  return this.eventStore.getRecentEvents(limit);
22022
22130
  }
22023
- async enrichResults(results, options) {
22131
+ async enrichResults(results, options, query) {
22024
22132
  const memories = [];
22025
22133
  for (const result2 of results) {
22026
22134
  const event = await this.eventStore.getEvent(result2.eventId);
@@ -22030,13 +22138,13 @@ var Retriever = class {
22030
22138
  }
22031
22139
  let sessionContext;
22032
22140
  if (options.includeSessionContext) {
22033
- sessionContext = await this.getSessionContext(event.sessionId, event.id);
22141
+ sessionContext = await this.getSessionContext(event.sessionId, event.id, query);
22034
22142
  }
22035
22143
  memories.push({ event, score: result2.score, sessionContext });
22036
22144
  }
22037
22145
  return memories;
22038
22146
  }
22039
- async getSessionContext(sessionId, eventId) {
22147
+ async getSessionContext(sessionId, eventId, query) {
22040
22148
  const sessionEvents = await this.eventStore.getSessionEvents(sessionId);
22041
22149
  const eventIndex = sessionEvents.findIndex((e) => e.id === eventId);
22042
22150
  if (eventIndex === -1) return void 0;
@@ -22044,7 +22152,9 @@ var Retriever = class {
22044
22152
  const end = Math.min(sessionEvents.length, eventIndex + 2);
22045
22153
  const contextEvents = sessionEvents.slice(start, end);
22046
22154
  if (contextEvents.length <= 1) return void 0;
22047
- return contextEvents.filter((e) => e.id !== eventId).map((e) => `[${e.eventType}]: ${e.content.slice(0, 200)}...`).join("\n");
22155
+ const suppressStaleState = isCurrentStateQuery(query);
22156
+ const contextLines = contextEvents.filter((e) => e.id !== eventId).filter((e) => !isLowSignalContextContent(e.content)).filter((e) => !(suppressStaleState && isStaleOrSupersededContent(e.content))).map((e) => `[${e.eventType}]: ${e.content.slice(0, 200)}...`);
22157
+ return contextLines.length > 0 ? contextLines.join("\n") : void 0;
22048
22158
  }
22049
22159
  buildUnifiedContext(projectResult, sharedMemories) {
22050
22160
  let context = projectResult.context;
@@ -26689,10 +26799,11 @@ var ContextCompressor = class {
26689
26799
  compress(content, options) {
26690
26800
  const source = safeLabel(options.source) || "unknown";
26691
26801
  const mode = options.mode;
26802
+ const sourceRef = safeSourceRef(options.sourceRef ?? sourceRefFromMetadata(options.metadata ?? {}));
26692
26803
  const contentType = detectContextContentType(content, options.metadata ?? {});
26693
26804
  const originalLines = nonEmptyLines(content).length;
26694
26805
  if (mode === "off") {
26695
- return result(content, {
26806
+ return withSourceRefHint(result(content, {
26696
26807
  mode,
26697
26808
  source,
26698
26809
  contentType,
@@ -26701,21 +26812,21 @@ var ContextCompressor = class {
26701
26812
  originalLines,
26702
26813
  omittedLines: 0,
26703
26814
  signalCount: 0
26704
- });
26815
+ }), sourceRef);
26705
26816
  }
26706
26817
  if (contentType === "log") {
26707
- return this.compressLog(content, mode, source, contentType);
26818
+ return withSourceRefHint(this.compressLog(content, mode, source, contentType), sourceRef);
26708
26819
  }
26709
26820
  if (contentType === "markdown") {
26710
- return this.compressMarkdown(content, mode, source, contentType);
26821
+ return withSourceRefHint(this.compressMarkdown(content, mode, source, contentType), sourceRef);
26711
26822
  }
26712
26823
  if (contentType === "diff") {
26713
- return this.compressDiff(content, mode, source, contentType);
26824
+ return withSourceRefHint(this.compressDiff(content, mode, source, contentType), sourceRef);
26714
26825
  }
26715
26826
  if (contentType === "code") {
26716
- return this.compressCode(content, mode, source, contentType);
26827
+ return withSourceRefHint(this.compressCode(content, mode, source, contentType), sourceRef);
26717
26828
  }
26718
- return this.compressPlain(content, mode, source, contentType);
26829
+ return withSourceRefHint(this.compressPlain(content, mode, source, contentType), sourceRef);
26719
26830
  }
26720
26831
  compressLog(content, mode, source, contentType) {
26721
26832
  const lines = nonEmptyLines(content);
@@ -26853,11 +26964,15 @@ function summarizeCompressionTelemetry(metadata) {
26853
26964
  const totalOriginalChars = metadata.reduce((sum, item) => sum + item.originalChars, 0);
26854
26965
  const totalCompressedChars = metadata.reduce((sum, item) => sum + item.compressedChars, 0);
26855
26966
  const totalSavedChars = metadata.reduce((sum, item) => sum + item.savedChars, 0);
26967
+ const totalOmittedLines = metadata.reduce((sum, item) => sum + item.omittedLines, 0);
26968
+ const sourceRefsPreserved = metadata.filter((item) => item.sourceRefPreserved).length;
26856
26969
  return {
26857
26970
  totalItems: metadata.length,
26858
26971
  totalOriginalChars,
26859
26972
  totalCompressedChars,
26860
26973
  totalSavedChars,
26974
+ totalOmittedLines,
26975
+ sourceRefsPreserved,
26861
26976
  bySource: summarizeBy(metadata, "source"),
26862
26977
  byStrategy: summarizeBy(metadata, "strategy")
26863
26978
  };
@@ -26871,12 +26986,16 @@ function summarizeBy(metadata, key) {
26871
26986
  items: 0,
26872
26987
  originalChars: 0,
26873
26988
  compressedChars: 0,
26874
- savedChars: 0
26989
+ savedChars: 0,
26990
+ omittedLines: 0,
26991
+ sourceRefsPreserved: 0
26875
26992
  };
26876
26993
  existing.items += 1;
26877
26994
  existing.originalChars += item.originalChars;
26878
26995
  existing.compressedChars += item.compressedChars;
26879
26996
  existing.savedChars += item.savedChars;
26997
+ existing.omittedLines += item.omittedLines;
26998
+ if (item.sourceRefPreserved) existing.sourceRefsPreserved += 1;
26880
26999
  groups.set(groupKey, existing);
26881
27000
  }
26882
27001
  return Array.from(groups.values()).sort((a, b) => String(a[key] ?? "").localeCompare(String(b[key] ?? "")));
@@ -26891,7 +27010,8 @@ function result(text, base) {
26891
27010
  ...base,
26892
27011
  compressedChars,
26893
27012
  savedChars,
26894
- compressionRatio
27013
+ compressionRatio,
27014
+ sourceRefPreserved: false
26895
27015
  }
26896
27016
  };
26897
27017
  }
@@ -26940,6 +27060,65 @@ function safeLabel(value) {
26940
27060
  const cleaned = value.replace(/[^A-Za-z0-9_.:-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
26941
27061
  return cleaned || void 0;
26942
27062
  }
27063
+ function sourceRefFromMetadata(metadata) {
27064
+ const explicit = firstString(metadata, ["sourceRef", "source_ref", "sourceReference", "source_reference"]);
27065
+ if (explicit) return explicit;
27066
+ const citation = firstString(metadata, ["citationId", "citation_id", "memoryCitationId", "memory_citation_id"]);
27067
+ if (citation) return citation.startsWith("mem:") ? citation : `mem:${citation}`;
27068
+ const eventId = firstString(metadata, ["eventId", "event_id"]);
27069
+ if (eventId) return eventId.startsWith("event:") ? eventId : `event:${eventId}`;
27070
+ return void 0;
27071
+ }
27072
+ function firstString(metadata, keys) {
27073
+ for (const key of keys) {
27074
+ const value = metadata[key];
27075
+ if (typeof value === "string" && value.trim().length > 0) return value.trim();
27076
+ }
27077
+ return void 0;
27078
+ }
27079
+ function safeSourceRef(value) {
27080
+ if (!value) return void 0;
27081
+ const normalized = value.trim().replace(/^\[?(mem|event):/i, (_, prefix) => `${prefix.toLowerCase()}:`).replace(/\]?$/g, "");
27082
+ if (!normalized || /(?:password|secret|api[_-]?key|token|bearer)/i.test(normalized)) return void 0;
27083
+ const cleaned = normalized.replace(/[^A-Za-z0-9_.:-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 140);
27084
+ return cleaned || void 0;
27085
+ }
27086
+ function withSourceRefHint(compression, sourceRef) {
27087
+ if (!sourceRef) return compression;
27088
+ if (compression.metadata.mode === "off" || compression.metadata.strategy === "none") {
27089
+ return {
27090
+ text: compression.text,
27091
+ metadata: {
27092
+ ...compression.metadata,
27093
+ sourceRef,
27094
+ sourceRefPreserved: false
27095
+ }
27096
+ };
27097
+ }
27098
+ const hint = `sourceRef=${sourceRef} expand=mem-source-ref`;
27099
+ const text = compression.text.includes(hint) ? compression.text : addSourceRefHint(compression.text, hint);
27100
+ const compressedChars = text.length;
27101
+ const savedChars = Math.max(0, compression.metadata.originalChars - compressedChars);
27102
+ const compressionRatio = compression.metadata.originalChars > 0 ? compressedChars / compression.metadata.originalChars : 1;
27103
+ return {
27104
+ text,
27105
+ metadata: {
27106
+ ...compression.metadata,
27107
+ sourceRef,
27108
+ sourceRefPreserved: text.includes(`sourceRef=${sourceRef}`) && text.includes("expand=mem-source-ref"),
27109
+ compressedChars,
27110
+ savedChars,
27111
+ compressionRatio
27112
+ }
27113
+ };
27114
+ }
27115
+ function addSourceRefHint(text, hint) {
27116
+ if (text.startsWith("[compressed ")) {
27117
+ return text.replace(/^\[([^\]]+)\]/, `[$1; ${hint}]`);
27118
+ }
27119
+ return `[${hint}]
27120
+ ${text}`;
27121
+ }
26943
27122
 
26944
27123
  // src/extensions/mcp/handlers.ts
26945
27124
  function resolveMemoryService(args) {
@@ -26977,6 +27156,9 @@ async function handleToolCall(name, args) {
26977
27156
  if (name === "mem-context-pack" && hasMemContextPackPerspectiveArgs(args)) {
26978
27157
  validateMemContextPackPerspectiveArgs(args);
26979
27158
  }
27159
+ if (name === "mem-context-pack") {
27160
+ validateMemContextPackBudgetArgs(args);
27161
+ }
26980
27162
  if (MEMORY_OPERATION_TOOL_NAMES.has(name)) {
26981
27163
  return await handleMemoryOperationTool(name, args);
26982
27164
  }
@@ -27723,9 +27905,11 @@ async function handleExternalMarketContext(args) {
27723
27905
  }
27724
27906
  async function handleMemSearch(memoryService, args) {
27725
27907
  const query = args.query;
27726
- const topK = Math.min(args.topK || 5, 20);
27908
+ const topK = numberArg(args.topK, 5, 1, 20);
27909
+ const fetchTopK = Math.min(topK * 3, 20);
27727
27910
  const sessionId = args.sessionId;
27728
- const search = await retrieveMcpMemories(memoryService, query, { topK, sessionId });
27911
+ const eventType = eventTypeArg(args.eventType);
27912
+ const search = await retrieveMcpMemories(memoryService, query, { topK, fetchTopK, sessionId, eventType });
27729
27913
  const lines = [
27730
27914
  "## Memory Search Results",
27731
27915
  "",
@@ -27739,7 +27923,7 @@ async function handleMemSearch(memoryService, args) {
27739
27923
  const m = search.memories[i];
27740
27924
  const citationId = generateCitationId(m.event.id);
27741
27925
  const date = m.event.timestamp.toISOString().split("T")[0];
27742
- const preview = m.event.content.slice(0, 100) + (m.event.content.length > 100 ? "..." : "");
27926
+ const preview = sanitizeOperationString(m.event.content, 100);
27743
27927
  lines.push(`### ${i + 1}. [mem:${citationId}] (score: ${m.score.toFixed(2)})`);
27744
27928
  lines.push(`**Type**: ${m.event.eventType} | **Date**: ${date}`);
27745
27929
  lines.push(`> ${preview}`);
@@ -27754,29 +27938,43 @@ async function handleMemSearch(memoryService, args) {
27754
27938
  var SEMANTIC_VECTOR_FALLBACK_WARNING = "Warning: semantic/vector retrieval unavailable; used keyword fallback.";
27755
27939
  var SEMANTIC_VECTOR_FALLBACK_FAILED_WARNING = "Warning: semantic/vector retrieval unavailable; keyword fallback failed.";
27756
27940
  async function retrieveMcpMemories(memoryService, query, options) {
27941
+ const fetchTopK = Math.max(options.topK, options.fetchTopK ?? options.topK);
27757
27942
  try {
27758
- const result2 = await memoryService.retrieveMemories(query, {
27759
- topK: options.topK,
27943
+ const retrieveOptions = {
27944
+ topK: fetchTopK,
27760
27945
  sessionId: options.sessionId,
27761
- recordTrace: false
27762
- });
27763
- return { memories: result2.memories };
27946
+ recordTrace: false,
27947
+ ...options.retrievalMode ? { retrievalMode: options.retrievalMode } : {}
27948
+ };
27949
+ const result2 = await memoryService.retrieveMemories(query, retrieveOptions);
27950
+ return { memories: selectMcpMemoryResults(result2.memories, options.topK, options.eventType) };
27764
27951
  } catch (error) {
27765
27952
  if (!isVectorSchemaMismatchError(error)) {
27766
27953
  throw error;
27767
27954
  }
27768
27955
  try {
27769
- const memories = options.sessionId ? rankSessionKeywordMatches(
27956
+ const candidates = options.sessionId ? rankSessionKeywordMatches(
27770
27957
  query,
27771
27958
  await memoryService.getSessionHistory(options.sessionId),
27772
- options.topK
27773
- ) : await memoryService.keywordSearch(query, { topK: options.topK });
27774
- return { memories, warning: SEMANTIC_VECTOR_FALLBACK_WARNING };
27959
+ fetchTopK
27960
+ ) : await memoryService.keywordSearch(query, { topK: fetchTopK });
27961
+ return { memories: selectMcpMemoryResults(candidates, options.topK, options.eventType), warning: SEMANTIC_VECTOR_FALLBACK_WARNING };
27775
27962
  } catch {
27776
27963
  return { memories: [], warning: SEMANTIC_VECTOR_FALLBACK_FAILED_WARNING };
27777
27964
  }
27778
27965
  }
27779
27966
  }
27967
+ function selectMcpMemoryResults(memories, topK, eventType) {
27968
+ return memories.filter((memory) => !isLowSignalContextContent(memory.event.content || "")).filter((memory) => eventType === void 0 || memory.event.eventType === eventType).slice(0, topK);
27969
+ }
27970
+ function eventTypeArg(value) {
27971
+ if (value === void 0 || value === null || value === "") return void 0;
27972
+ const normalized = String(value).trim();
27973
+ if (normalized === "user_prompt" || normalized === "agent_response" || normalized === "tool_observation" || normalized === "session_summary") {
27974
+ return normalized;
27975
+ }
27976
+ throw new Error("Invalid eventType: expected user_prompt, agent_response, tool_observation, or session_summary");
27977
+ }
27780
27978
  function isVectorSchemaMismatchError(error) {
27781
27979
  const message = error instanceof Error ? error.message : String(error);
27782
27980
  return /no vector column/i.test(message) || /query vector dimension/i.test(message) || /vector[^\n]{0,80}dimension/i.test(message) || /dimension[^\n]{0,80}vector/i.test(message) || /lancedb[^\n]{0,120}schema/i.test(message);
@@ -27812,7 +28010,7 @@ async function handleMemTimeline(memoryService, args) {
27812
28010
  lines.push(`Event ${targetId} not found.`);
27813
28011
  continue;
27814
28012
  }
27815
- const sessionEvents = recentEvents.filter((e) => e.sessionId === targetEvent.sessionId).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
28013
+ const sessionEvents = recentEvents.filter((e) => e.sessionId === targetEvent.sessionId).filter((e) => e.id === targetEvent.id || !isLowSignalContextContent(e.content || "")).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
27816
28014
  const eventIndex = sessionEvents.findIndex((e) => e.id === targetEvent.id);
27817
28015
  const start = Math.max(0, eventIndex - windowSize);
27818
28016
  const end = Math.min(sessionEvents.length, eventIndex + windowSize + 1);
@@ -27823,7 +28021,8 @@ async function handleMemTimeline(memoryService, args) {
27823
28021
  const isTarget = e.id === targetEvent.id;
27824
28022
  const marker = isTarget ? "**\u2192**" : " ";
27825
28023
  const time = e.timestamp.toLocaleTimeString();
27826
- const preview = e.content.slice(0, 60) + (e.content.length > 60 ? "..." : "");
28024
+ const lowSignal = isLowSignalContextContent(e.content || "");
28025
+ const preview = lowSignal ? "[low-signal context artifact suppressed; use mem-details for raw source]" : sanitizeOperationString(e.content, 60);
27827
28026
  const citationId = generateCitationId(e.id);
27828
28027
  lines.push(`${marker} ${time} [${citationId}] ${e.eventType}: ${preview}`);
27829
28028
  }
@@ -27875,6 +28074,7 @@ async function handleMemContextPack(memoryService, args) {
27875
28074
  const projectPath = optionalString(args.projectPath);
27876
28075
  const maxContextChars = contextPackMaxCharsArg(args);
27877
28076
  const compression = contextCompressionModeArg(args.compression, maxContextChars !== void 0);
28077
+ const retrievalMode = contextPackRetrievalModeArg(args.retrievalMode);
27878
28078
  const compressionTelemetry = [];
27879
28079
  const formatOptions = {
27880
28080
  compression,
@@ -27895,7 +28095,7 @@ async function handleMemContextPack(memoryService, args) {
27895
28095
  sessionsDir: optionalString(args.sessionsDir),
27896
28096
  stateDb: optionalString(args.stateDb)
27897
28097
  }) : void 0;
27898
- const search = await retrieveMcpMemories(memoryService, query, { topK: retrievalTopK, sessionId });
28098
+ const search = await retrieveMcpMemories(memoryService, query, { topK: retrievalTopK, sessionId, retrievalMode });
27899
28099
  const recentEvents = await memoryService.getRecentEvents(recentLimit);
27900
28100
  const timelineEvents = selectContextPackTimelineEvents(
27901
28101
  recentEvents,
@@ -27955,6 +28155,7 @@ async function handleMemContextPack(memoryService, args) {
27955
28155
  if (compression !== "off" || maxContextChars !== void 0) {
27956
28156
  lines.push("### Compression Notice", "");
27957
28157
  lines.push("- Context-pack compression is applied only to LLM-facing previews; original events remain available through source refs.");
28158
+ lines.push("- Privacy filters run before compression and final preview sanitization runs after compression.");
27958
28159
  if (maxContextChars !== void 0) {
27959
28160
  lines.push(`- Hard output budget: ${maxContextChars} characters; use follow-up lookups to expand trimmed sources.`);
27960
28161
  }
@@ -28096,7 +28297,8 @@ async function handleMemProjectTimeline(memoryService, args) {
28096
28297
  const limit = numberArg(args.limit, 50, 1, 500);
28097
28298
  const sessionLimit = numberArg(args.sessionLimit, 10, 1, 50);
28098
28299
  const recentEvents = await memoryService.getRecentEvents(limit);
28099
- const sessions = summarizeSessions(recentEvents, sessionLimit);
28300
+ const timelineEvents = recentEvents.filter((event) => !isLowSignalContextContent(event.content || ""));
28301
+ const sessions = summarizeSessions(timelineEvents, sessionLimit);
28100
28302
  const lines = [
28101
28303
  "## Project Memory Timeline",
28102
28304
  "",
@@ -28117,7 +28319,10 @@ async function handleMemSourceRef(memoryService, args) {
28117
28319
  const ids = Array.isArray(args.ids) ? args.ids.map((id) => String(id)).filter((id) => id.trim().length > 0) : [];
28118
28320
  const maxContentChars = numberArg(args.maxContentChars, 500, 80, 2e3);
28119
28321
  const lookupLimit = numberArg(args.lookupLimit, 1e4, 1, 5e4);
28322
+ const includeNeighbors = args.includeNeighbors === true;
28323
+ const neighborWindow = includeNeighbors ? numberArg(args.neighborWindow, 1, 0, 5) : 0;
28120
28324
  const recentEvents = await memoryService.getRecentEvents(lookupLimit);
28325
+ const sessionEventCache = /* @__PURE__ */ new Map();
28121
28326
  const lines = ["## Source References", ""];
28122
28327
  if (ids.length === 0) {
28123
28328
  lines.push("No IDs supplied.", "");
@@ -28145,6 +28350,19 @@ async function handleMemSourceRef(memoryService, args) {
28145
28350
  }
28146
28351
  lines.push("- Redacted Preview:");
28147
28352
  lines.push(` > ${safeInline(event.content, maxContentChars)}`);
28353
+ if (neighborWindow > 0) {
28354
+ try {
28355
+ let sessionEventsPromise = sessionEventCache.get(event.sessionId);
28356
+ if (!sessionEventsPromise) {
28357
+ sessionEventsPromise = memoryService.getSessionHistory(event.sessionId);
28358
+ sessionEventCache.set(event.sessionId, sessionEventsPromise);
28359
+ }
28360
+ const sessionEvents = await sessionEventsPromise;
28361
+ appendSourceRefNeighborContext(lines, event, sessionEvents, neighborWindow, maxContentChars);
28362
+ } catch {
28363
+ lines.push("- Neighbor Context: unavailable (details suppressed)");
28364
+ }
28365
+ }
28148
28366
  lines.push("");
28149
28367
  }
28150
28368
  return textResult(lines.join("\n"));
@@ -28374,12 +28592,38 @@ function sourceTypeForEvent(event) {
28374
28592
  if (typeof event.metadata?.transcriptPath === "string") return "transcript";
28375
28593
  return "raw_event";
28376
28594
  }
28595
+ function appendSourceRefNeighborContext(lines, event, sessionEvents, neighborWindow, maxContentChars) {
28596
+ const sorted = sessionEvents.slice().sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime() || a.id.localeCompare(b.id));
28597
+ const targetIndex = sorted.findIndex((candidate) => candidate.id === event.id);
28598
+ lines.push("- Neighbor Context:");
28599
+ if (targetIndex < 0) {
28600
+ lines.push(" - unavailable (source event not found in session history)");
28601
+ return;
28602
+ }
28603
+ const start = Math.max(0, targetIndex - neighborWindow);
28604
+ const end = Math.min(sorted.length, targetIndex + neighborWindow + 1);
28605
+ const neighbors = sorted.map((candidate, index) => ({ event: candidate, index })).slice(start, end).filter((candidate) => candidate.event.id !== event.id);
28606
+ if (neighbors.length === 0) {
28607
+ lines.push(" - none within requested window");
28608
+ return;
28609
+ }
28610
+ const previewChars = Math.max(80, Math.min(maxContentChars, 300));
28611
+ for (const neighbor of neighbors) {
28612
+ const direction = neighbor.index < targetIndex ? "before" : "after";
28613
+ const citationId = generateCitationId(neighbor.event.id);
28614
+ lines.push(
28615
+ ` - ${direction} [mem:${citationId}] sourceRef=event:${neighbor.event.id} type=${neighbor.event.eventType} timestamp=${neighbor.event.timestamp.toISOString()}`
28616
+ );
28617
+ lines.push(` > ${safeInline(neighbor.event.content, previewChars)}`);
28618
+ }
28619
+ }
28377
28620
  function formatRelevantMemoryLine(event, score, index, formatOptions = DEFAULT_CONTEXT_PACK_FORMAT_OPTIONS) {
28378
28621
  const citationId = generateCitationId(event.id);
28622
+ const previewBudget = formatOptions.compression === "off" ? 260 : 180;
28379
28623
  return [
28380
28624
  `${index}. [mem:${citationId}] score=${score.toFixed(2)} type=${event.eventType} date=${event.timestamp.toISOString()} session=${event.sessionId}`,
28381
28625
  ` source=${sourceForEvent(event)}`,
28382
- ` ${contextPackPreview(event, 260, formatOptions)}`,
28626
+ ` ${contextPackPreview(event, previewBudget, formatOptions)}`,
28383
28627
  ""
28384
28628
  ].join("\n");
28385
28629
  }
@@ -28456,6 +28700,7 @@ function contextPackPreview(event, maxLength, options = DEFAULT_CONTEXT_PACK_FOR
28456
28700
  const compressed = MCP_CONTEXT_COMPRESSOR.compress(privacyFiltered, {
28457
28701
  mode: options.compression,
28458
28702
  source: sourceForEvent(event),
28703
+ sourceRef: `mem:${generateCitationId(event.id)}`,
28459
28704
  metadata: {
28460
28705
  ...safeCompressionMetadata(event.metadata),
28461
28706
  eventType: event.eventType
@@ -28483,8 +28728,9 @@ function appendCompressionTelemetry(lines, metadata, maxChars) {
28483
28728
  const summary = summarizeCompressionTelemetry(metadata);
28484
28729
  const sources = summary.bySource.map((item) => `${item.source}:${item.items}`).join(",");
28485
28730
  const strategies = summary.byStrategy.map((item) => `${item.strategy}:${item.items}`).join(",");
28731
+ const sourceRefsAvailable = metadata.filter((item) => item.sourceRef !== void 0).length;
28486
28732
  lines.push(
28487
- `- Compression telemetry: items=${summary.totalItems} originalChars=${summary.totalOriginalChars} compressedChars=${summary.totalCompressedChars} savedChars=${summary.totalSavedChars} sources=${sources || "n/a"} strategies=${strategies || "n/a"}`,
28733
+ `- Compression telemetry: items=${summary.totalItems} originalChars=${summary.totalOriginalChars} compressedChars=${summary.totalCompressedChars} savedChars=${summary.totalSavedChars} omittedLines=${summary.totalOmittedLines} sourceRefsPreserved=${summary.sourceRefsPreserved}/${sourceRefsAvailable} sources=${sources || "n/a"} strategies=${strategies || "n/a"}`,
28488
28734
  ""
28489
28735
  );
28490
28736
  }
@@ -28494,18 +28740,32 @@ function contextCompressionModeArg(value, hasBudget) {
28494
28740
  if (normalized === "off" || normalized === "safe" || normalized === "aggressive") return normalized;
28495
28741
  throw new Error("Invalid compression: expected off, safe, or aggressive");
28496
28742
  }
28743
+ function contextPackRetrievalModeArg(value) {
28744
+ if (value === void 0 || value === null || value === "") return void 0;
28745
+ const normalized = String(value).trim().toLowerCase();
28746
+ if (normalized === "session-event-hybrid") return "session-event-hybrid";
28747
+ if (normalized === "event") return "event";
28748
+ throw new Error("Invalid retrievalMode: expected session-event-hybrid or event");
28749
+ }
28750
+ function validateMemContextPackBudgetArgs(args) {
28751
+ const maxContextChars = contextPackMaxCharsArg(args);
28752
+ contextCompressionModeArg(args.compression, maxContextChars !== void 0);
28753
+ contextPackRetrievalModeArg(args.retrievalMode);
28754
+ }
28497
28755
  function contextPackMaxCharsArg(args) {
28498
- const maxChars = optionalNumberArg(args.maxChars, CONTEXT_PACK_MIN_HARD_BUDGET_CHARS, CONTEXT_PACK_MAX_CHARS_CAP);
28499
- const maxTokens = optionalNumberArg(args.maxTokens, 250, Math.floor(CONTEXT_PACK_MAX_CHARS_CAP / 4));
28756
+ const maxChars = optionalNumberArg(args.maxChars, "maxChars", CONTEXT_PACK_MIN_HARD_BUDGET_CHARS, CONTEXT_PACK_MAX_CHARS_CAP);
28757
+ const maxTokens = optionalNumberArg(args.maxTokens, "maxTokens", 250, Math.floor(CONTEXT_PACK_MAX_CHARS_CAP / 4));
28500
28758
  const tokenChars = maxTokens === void 0 ? void 0 : maxTokens * 4;
28501
28759
  if (maxChars !== void 0 && tokenChars !== void 0) return Math.min(maxChars, tokenChars);
28502
28760
  return maxChars ?? tokenChars;
28503
28761
  }
28504
- function optionalNumberArg(value, min, max) {
28762
+ function optionalNumberArg(value, name, min, max) {
28505
28763
  if (value === void 0 || value === null || value === "") return void 0;
28506
28764
  const parsed = typeof value === "number" ? value : Number(value);
28507
- if (!Number.isFinite(parsed)) return void 0;
28508
- return Math.min(max, Math.max(min, Math.floor(parsed)));
28765
+ if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
28766
+ throw new Error(`Invalid ${name}: expected number between ${min} and ${max}`);
28767
+ }
28768
+ return Math.floor(parsed);
28509
28769
  }
28510
28770
  function applyContextPackBudget(text, maxChars) {
28511
28771
  if (maxChars === void 0 || text.length <= maxChars) return text;