@joshuaswarren/openclaw-engram 8.3.88 → 8.3.89

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/index.js CHANGED
@@ -136,6 +136,7 @@ function parseConfig(raw) {
136
136
  }
137
137
  const sharedCrossSignalSemanticEnabled = cfg.sharedCrossSignalSemanticEnabled === true || cfg.crossSignalsSemanticEnabled === true;
138
138
  const sharedCrossSignalSemanticTimeoutMs = typeof cfg.sharedCrossSignalSemanticTimeoutMs === "number" ? Math.max(1, Math.floor(cfg.sharedCrossSignalSemanticTimeoutMs)) : typeof cfg.crossSignalsSemanticTimeoutMs === "number" ? Math.max(1, Math.floor(cfg.crossSignalsSemanticTimeoutMs)) : 4e3;
139
+ const recallPipelineConfig = buildRecallPipelineConfig(cfg);
139
140
  return {
140
141
  openaiApiKey: apiKey,
141
142
  openaiBaseUrl: baseUrl,
@@ -367,6 +368,8 @@ function parseConfig(raw) {
367
368
  knowledgeIndexEnabled: cfg.knowledgeIndexEnabled !== false,
368
369
  knowledgeIndexMaxEntities: typeof cfg.knowledgeIndexMaxEntities === "number" ? cfg.knowledgeIndexMaxEntities : 40,
369
370
  knowledgeIndexMaxChars: typeof cfg.knowledgeIndexMaxChars === "number" ? cfg.knowledgeIndexMaxChars : 4e3,
371
+ recallBudgetChars: recallPipelineConfig.recallBudgetChars,
372
+ recallPipeline: recallPipelineConfig.pipeline,
370
373
  entityRelationshipsEnabled: cfg.entityRelationshipsEnabled !== false,
371
374
  entityActivityLogEnabled: cfg.entityActivityLogEnabled !== false,
372
375
  entityActivityLogMaxEntries: typeof cfg.entityActivityLogMaxEntries === "number" ? cfg.entityActivityLogMaxEntries : 20,
@@ -454,6 +457,100 @@ function parseConfig(raw) {
454
457
  tmtSummaryMaxTokens: typeof cfg.tmtSummaryMaxTokens === "number" ? cfg.tmtSummaryMaxTokens : 300
455
458
  };
456
459
  }
460
+ function clampNonNegativeNumber(value) {
461
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
462
+ return Math.max(0, Math.floor(value));
463
+ }
464
+ function parseRecallSectionEntry(raw) {
465
+ const entry = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
466
+ return {
467
+ id: typeof entry.id === "string" ? entry.id.trim() : "",
468
+ enabled: entry.enabled !== false,
469
+ maxChars: entry.maxChars === null ? null : clampNonNegativeNumber(entry.maxChars),
470
+ consolidateTriggerLines: clampNonNegativeNumber(entry.consolidateTriggerLines),
471
+ consolidateTargetLines: clampNonNegativeNumber(entry.consolidateTargetLines),
472
+ maxEntities: clampNonNegativeNumber(entry.maxEntities),
473
+ maxResults: clampNonNegativeNumber(entry.maxResults),
474
+ maxTurns: clampNonNegativeNumber(entry.maxTurns),
475
+ maxTokens: clampNonNegativeNumber(entry.maxTokens),
476
+ lookbackHours: clampNonNegativeNumber(entry.lookbackHours),
477
+ maxCount: clampNonNegativeNumber(entry.maxCount),
478
+ topK: clampNonNegativeNumber(entry.topK),
479
+ timeoutMs: clampNonNegativeNumber(entry.timeoutMs),
480
+ maxPatterns: clampNonNegativeNumber(entry.maxPatterns)
481
+ };
482
+ }
483
+ function buildDefaultRecallPipeline(cfg) {
484
+ return [
485
+ {
486
+ id: "shared-context",
487
+ enabled: cfg.sharedContextEnabled === true,
488
+ maxChars: typeof cfg.sharedContextMaxInjectChars === "number" ? Math.max(0, Math.floor(cfg.sharedContextMaxInjectChars)) : 4e3
489
+ },
490
+ {
491
+ id: "profile",
492
+ enabled: true,
493
+ consolidateTriggerLines: 100,
494
+ consolidateTargetLines: 50
495
+ },
496
+ {
497
+ id: "identity-continuity",
498
+ enabled: cfg.identityContinuityEnabled === true
499
+ },
500
+ {
501
+ id: "knowledge-index",
502
+ enabled: cfg.knowledgeIndexEnabled !== false,
503
+ maxChars: typeof cfg.knowledgeIndexMaxChars === "number" ? Math.max(0, Math.floor(cfg.knowledgeIndexMaxChars)) : 4e3,
504
+ maxEntities: typeof cfg.knowledgeIndexMaxEntities === "number" ? Math.max(0, Math.floor(cfg.knowledgeIndexMaxEntities)) : 40
505
+ },
506
+ { id: "verbatim-artifacts", enabled: cfg.verbatimArtifactsEnabled === true },
507
+ { id: "memory-boxes", enabled: cfg.memoryBoxesEnabled === true },
508
+ { id: "temporal-memory-tree", enabled: cfg.temporalMemoryTreeEnabled === true },
509
+ {
510
+ id: "memories",
511
+ enabled: true,
512
+ maxResults: typeof cfg.qmdMaxResults === "number" ? Math.max(0, Math.floor(cfg.qmdMaxResults)) : 8
513
+ },
514
+ {
515
+ id: "compression-guidelines",
516
+ enabled: cfg.compressionGuidelineLearningEnabled === true
517
+ },
518
+ {
519
+ id: "transcript",
520
+ enabled: cfg.transcriptEnabled !== false,
521
+ maxTurns: typeof cfg.maxTranscriptTurns === "number" ? Math.max(0, Math.floor(cfg.maxTranscriptTurns)) : 50,
522
+ maxTokens: typeof cfg.maxTranscriptTokens === "number" ? Math.max(0, Math.floor(cfg.maxTranscriptTokens)) : 1e3,
523
+ lookbackHours: typeof cfg.transcriptRecallHours === "number" ? Math.max(0, Math.floor(cfg.transcriptRecallHours)) : 12
524
+ },
525
+ {
526
+ id: "summaries",
527
+ enabled: cfg.hourlySummariesEnabled !== false,
528
+ maxCount: typeof cfg.maxSummaryCount === "number" ? Math.max(0, Math.floor(cfg.maxSummaryCount)) : 6,
529
+ lookbackHours: typeof cfg.summaryRecallHours === "number" ? Math.max(0, Math.floor(cfg.summaryRecallHours)) : 24
530
+ },
531
+ {
532
+ id: "conversation-recall",
533
+ enabled: cfg.conversationIndexEnabled === true,
534
+ topK: typeof cfg.conversationRecallTopK === "number" ? Math.max(0, Math.floor(cfg.conversationRecallTopK)) : 3,
535
+ maxChars: typeof cfg.conversationRecallMaxChars === "number" ? Math.max(0, Math.floor(cfg.conversationRecallMaxChars)) : 2500,
536
+ timeoutMs: typeof cfg.conversationRecallTimeoutMs === "number" ? Math.max(0, Math.floor(cfg.conversationRecallTimeoutMs)) : 800
537
+ },
538
+ {
539
+ id: "compounding",
540
+ enabled: cfg.compoundingEnabled === true && cfg.compoundingInjectEnabled !== false,
541
+ maxPatterns: 40
542
+ },
543
+ { id: "questions", enabled: cfg.injectQuestions === true }
544
+ ];
545
+ }
546
+ function buildRecallPipelineConfig(cfg) {
547
+ const maxMemoryTokens = typeof cfg.maxMemoryTokens === "number" ? Math.max(0, Math.floor(cfg.maxMemoryTokens)) : 2e3;
548
+ const recallBudgetCharsRaw = clampNonNegativeNumber(cfg.recallBudgetChars);
549
+ const recallBudgetChars = recallBudgetCharsRaw ?? maxMemoryTokens * 4;
550
+ const rawPipeline = cfg.recallPipeline;
551
+ const pipeline2 = Array.isArray(rawPipeline) ? rawPipeline.map(parseRecallSectionEntry).filter((entry) => entry.id.length > 0) : buildDefaultRecallPipeline(cfg);
552
+ return { recallBudgetChars, pipeline: pipeline2 };
553
+ }
457
554
 
458
555
  // src/orchestrator.ts
459
556
  import path28 from "path";
@@ -1713,13 +1810,16 @@ var ConsolidationResultSchema = z.object({
1713
1810
  profileUpdates: z.array(z.string()).describe("New profile statements to add or update"),
1714
1811
  entityUpdates: z.array(EntityMentionSchema).describe("Entity updates from consolidation analysis")
1715
1812
  });
1716
- var ProfileConsolidationResultSchema = z.object({
1717
- consolidatedProfile: z.string().describe(
1718
- "The full consolidated profile as markdown. Preserve all ## section headers. Merge duplicate or near-duplicate bullets into single clear statements. Remove stale or superseded information. Keep the most important and durable observations. Target roughly 400 lines."
1719
- ),
1720
- removedCount: z.number().describe("Number of bullets removed or merged during consolidation"),
1721
- summary: z.string().describe("Brief summary of what was consolidated")
1722
- });
1813
+ function buildProfileConsolidationResultSchema(targetLines) {
1814
+ return z.object({
1815
+ consolidatedProfile: z.string().describe(
1816
+ `The full consolidated profile as markdown. Preserve all ## section headers. Merge duplicate or near-duplicate bullets into single clear statements. Remove stale or superseded information. Keep the most important and durable observations. Target roughly ${targetLines} lines.`
1817
+ ),
1818
+ removedCount: z.number().describe("Number of bullets removed or merged during consolidation"),
1819
+ summary: z.string().describe("Brief summary of what was consolidated")
1820
+ });
1821
+ }
1822
+ var ProfileConsolidationResultSchema = buildProfileConsolidationResultSchema(50);
1723
1823
  var IdentityConsolidationResultSchema = z.object({
1724
1824
  learnedPatterns: z.array(z.string()).describe(
1725
1825
  "Consolidated behavioral patterns and lessons learned, each a concise standalone statement"
@@ -2870,13 +2970,13 @@ Respond with valid JSON matching this schema:
2870
2970
  * The LLM merges duplicates, removes stale info, and preserves section structure.
2871
2971
  * Returns the consolidated markdown or null on failure.
2872
2972
  */
2873
- async consolidateProfile(fullProfileContent) {
2973
+ async consolidateProfile(fullProfileContent, targetLines = 50) {
2874
2974
  const pTraceId = crypto.randomUUID();
2875
2975
  this.emit({ kind: "llm_start", traceId: pTraceId, model: this.config.model, operation: "profile_consolidation", input: fullProfileContent.slice(0, 2e3) });
2876
2976
  const pStartTime = Date.now();
2877
2977
  if (this.config.localLlmEnabled) {
2878
2978
  try {
2879
- const localResult = await this.consolidateProfileWithLocalLlm(fullProfileContent);
2979
+ const localResult = await this.consolidateProfileWithLocalLlm(fullProfileContent, targetLines);
2880
2980
  if (localResult) {
2881
2981
  const durationMs = Date.now() - pStartTime;
2882
2982
  this.emit({ kind: "llm_end", traceId: pTraceId, model: this.config.localLlmModel, operation: "profile_consolidation", durationMs });
@@ -2900,7 +3000,7 @@ Respond with valid JSON matching this schema:
2900
3000
  pTraceId,
2901
3001
  "profile_consolidation",
2902
3002
  pStartTime,
2903
- ProfileConsolidationResultSchema,
3003
+ buildProfileConsolidationResultSchema(targetLines),
2904
3004
  [
2905
3005
  {
2906
3006
  role: "system",
@@ -2911,7 +3011,7 @@ Respond with valid JSON matching this schema:
2911
3011
  3. REMOVES stale information that has been superseded by newer bullets
2912
3012
  4. REMOVES trivial or overly specific operational details that won't be useful across sessions
2913
3013
  5. KEEPS the most important, durable observations about the user's preferences, habits, identity, and working style
2914
- 6. Target roughly 400 lines \u2014 this is a soft target, prioritize quality over length
3014
+ 6. Target roughly ${targetLines} lines \u2014 this is a soft target, prioritize quality over length
2915
3015
  7. Write in the same style as the existing profile \u2014 concise bullets, no fluff
2916
3016
 
2917
3017
  The output should be the COMPLETE consolidated profile as valid markdown, starting with "# Behavioral Profile".`
@@ -2942,13 +3042,13 @@ The output should be the COMPLETE consolidated profile as valid markdown, starti
2942
3042
  3. REMOVES stale information that has been superseded by newer bullets
2943
3043
  4. REMOVES trivial or overly specific operational details that won't be useful across sessions
2944
3044
  5. KEEPS the most important, durable observations about the user's preferences, habits, identity, and working style
2945
- 6. Target roughly 400 lines \u2014 this is a soft target, prioritize quality over length
3045
+ 6. Target roughly ${targetLines} lines \u2014 this is a soft target, prioritize quality over length
2946
3046
  7. Write in the same style as the existing profile \u2014 concise bullets, no fluff
2947
3047
 
2948
3048
  The output should be the COMPLETE consolidated profile as valid markdown, starting with "# Behavioral Profile".`,
2949
3049
  input: fullProfileContent,
2950
3050
  text: {
2951
- format: zodTextFormat(ProfileConsolidationResultSchema, "profile_consolidation_result")
3051
+ format: zodTextFormat(buildProfileConsolidationResultSchema(targetLines), "profile_consolidation_result")
2952
3052
  }
2953
3053
  });
2954
3054
  const pDurationMs = Date.now() - pStartTime;
@@ -2986,7 +3086,7 @@ The output should be the COMPLETE consolidated profile as valid markdown, starti
2986
3086
  /**
2987
3087
  * Consolidate profile using local LLM.
2988
3088
  */
2989
- async consolidateProfileWithLocalLlm(fullProfileContent) {
3089
+ async consolidateProfileWithLocalLlm(fullProfileContent, targetLines = 50) {
2990
3090
  const contextSizes = this.modelRegistry.calculateContextSizes(
2991
3091
  this.config.localLlmModel,
2992
3092
  this.config.localLlmMaxContext
@@ -2999,7 +3099,7 @@ The output should be the COMPLETE consolidated profile as valid markdown, starti
2999
3099
  3. REMOVES stale information that has been superseded by newer bullets
3000
3100
  4. REMOVES trivial or overly specific operational details that won't be useful across sessions
3001
3101
  5. KEEPS the most important, durable observations about the user's preferences, habits, identity, and working style
3002
- 6. Target roughly 400 lines \u2014 this is a soft target, prioritize quality over length
3102
+ 6. Target roughly ${targetLines} lines \u2014 this is a soft target, prioritize quality over length
3003
3103
  7. Write in the same style as the existing profile \u2014 concise bullets, no fluff
3004
3104
 
3005
3105
  Profile to consolidate:
@@ -5612,11 +5712,12 @@ ${sanitized.text}
5612
5712
  }
5613
5713
  }
5614
5714
  /** Check if profile.md exceeds the max line cap and needs LLM consolidation */
5615
- async profileNeedsConsolidation() {
5715
+ async profileNeedsConsolidation(triggerLines) {
5616
5716
  const profile = await this.readProfile();
5617
5717
  if (!profile) return false;
5618
5718
  const lineCount = profile.split("\n").length;
5619
- return lineCount > _StorageManager.PROFILE_MAX_LINES;
5719
+ const threshold = typeof triggerLines === "number" ? Math.max(0, Math.floor(triggerLines)) : _StorageManager.PROFILE_MAX_LINES;
5720
+ return lineCount > threshold;
5620
5721
  }
5621
5722
  async readAllMemories() {
5622
5723
  const memories = [];
@@ -6616,13 +6717,14 @@ ${reflection}
6616
6717
  * Build the Knowledge Index: a compact markdown table of top-scored entities.
6617
6718
  * Respects maxEntities and maxChars limits from config.
6618
6719
  */
6619
- async buildKnowledgeIndex(config) {
6620
- if (this.knowledgeIndexCache && Date.now() - this.knowledgeIndexCache.builtAt < _StorageManager.KNOWLEDGE_INDEX_CACHE_TTL_MS) {
6720
+ async buildKnowledgeIndex(config, overrides) {
6721
+ const useDefaultLimits = overrides?.maxEntities === void 0 && overrides?.maxChars === void 0;
6722
+ if (useDefaultLimits && this.knowledgeIndexCache && Date.now() - this.knowledgeIndexCache.builtAt < _StorageManager.KNOWLEDGE_INDEX_CACHE_TTL_MS) {
6621
6723
  return { result: this.knowledgeIndexCache.result, cached: true };
6622
6724
  }
6623
6725
  const entities = await this.readAllEntityFiles();
6624
6726
  if (entities.length === 0) {
6625
- this.knowledgeIndexCache = { result: "", builtAt: Date.now() };
6727
+ if (useDefaultLimits) this.knowledgeIndexCache = { result: "", builtAt: Date.now() };
6626
6728
  return { result: "", cached: false };
6627
6729
  }
6628
6730
  const now = /* @__PURE__ */ new Date();
@@ -6635,26 +6737,28 @@ ${reflection}
6635
6737
  topRelationships: e.relationships.slice(0, 3).map((r) => r.target)
6636
6738
  }));
6637
6739
  scored.sort((a, b) => b.score - a.score);
6638
- const topN = scored.slice(0, config.knowledgeIndexMaxEntities);
6740
+ const maxEntities = typeof overrides?.maxEntities === "number" ? Math.max(0, Math.floor(overrides.maxEntities)) : config.knowledgeIndexMaxEntities;
6741
+ const topN = scored.slice(0, maxEntities);
6639
6742
  if (topN.length === 0) {
6640
- this.knowledgeIndexCache = { result: "", builtAt: Date.now() };
6743
+ if (useDefaultLimits) this.knowledgeIndexCache = { result: "", builtAt: Date.now() };
6641
6744
  return { result: "", cached: false };
6642
6745
  }
6643
6746
  const header = "## Knowledge Index\n\n| Entity | Type | Summary | Connected to |\n|--------|------|---------|-------------|";
6644
6747
  const rows = [];
6645
6748
  let totalChars = header.length;
6749
+ const maxChars = typeof overrides?.maxChars === "number" ? Math.max(0, Math.floor(overrides.maxChars)) : config.knowledgeIndexMaxChars;
6646
6750
  for (const entity of topN) {
6647
6751
  const summary = entity.summary || `${entity.factCount} facts`;
6648
6752
  const connected = entity.topRelationships.length > 0 ? entity.topRelationships.join(", ") : "\u2014";
6649
6753
  const row = `| ${entity.name} | ${entity.type} | ${summary} | ${connected} |`;
6650
- if (totalChars + row.length + 1 > config.knowledgeIndexMaxChars) break;
6754
+ if (totalChars + row.length + 1 > maxChars) break;
6651
6755
  rows.push(row);
6652
6756
  totalChars += row.length + 1;
6653
6757
  }
6654
6758
  const result = rows.length === 0 ? "" : `${header}
6655
6759
  ${rows.join("\n")}
6656
6760
  `;
6657
- this.knowledgeIndexCache = { result, builtAt: Date.now() };
6761
+ if (useDefaultLimits) this.knowledgeIndexCache = { result, builtAt: Date.now() };
6658
6762
  return { result, cached: false };
6659
6763
  }
6660
6764
  /** Invalidate the Knowledge Index cache (call after entity mutations). */
@@ -15483,11 +15587,69 @@ ${r.snippet.trim()}
15483
15587
  log.debug(`last graph recall write failed: ${err}`);
15484
15588
  }
15485
15589
  }
15590
+ getRecallSectionEntry(sectionId) {
15591
+ const pipeline2 = Array.isArray(this.config.recallPipeline) ? this.config.recallPipeline : [];
15592
+ return pipeline2.find((entry) => entry.id === sectionId);
15593
+ }
15594
+ isRecallSectionEnabled(sectionId, defaultEnabled = true) {
15595
+ const entry = this.getRecallSectionEntry(sectionId);
15596
+ if (!entry) return defaultEnabled;
15597
+ return entry.enabled !== false;
15598
+ }
15599
+ getRecallSectionMaxChars(sectionId) {
15600
+ const entry = this.getRecallSectionEntry(sectionId);
15601
+ if (!entry) return void 0;
15602
+ if (entry.maxChars === null) return null;
15603
+ if (typeof entry.maxChars !== "number") return void 0;
15604
+ return Math.max(0, Math.floor(entry.maxChars));
15605
+ }
15606
+ getRecallSectionNumber(sectionId, key) {
15607
+ const entry = this.getRecallSectionEntry(sectionId);
15608
+ if (!entry) return void 0;
15609
+ const value = entry[key];
15610
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
15611
+ return Math.max(0, Math.floor(value));
15612
+ }
15613
+ appendRecallSection(sectionBuckets, sectionId, content) {
15614
+ if (!this.isRecallSectionEnabled(sectionId)) return;
15615
+ const trimmed = content.trim();
15616
+ if (trimmed.length === 0) return;
15617
+ const maxChars = this.getRecallSectionMaxChars(sectionId);
15618
+ let finalContent = trimmed;
15619
+ if (maxChars === 0) return;
15620
+ if (typeof maxChars === "number" && finalContent.length > maxChars) {
15621
+ finalContent = `${finalContent.slice(0, maxChars)}
15622
+
15623
+ ...(trimmed)
15624
+ `;
15625
+ }
15626
+ const existing = sectionBuckets.get(sectionId) ?? [];
15627
+ existing.push(finalContent);
15628
+ sectionBuckets.set(sectionId, existing);
15629
+ }
15630
+ assembleRecallSections(sectionBuckets) {
15631
+ const ordered = [];
15632
+ const pipeline2 = Array.isArray(this.config.recallPipeline) ? this.config.recallPipeline : [];
15633
+ const orderedIds = pipeline2.filter((entry) => entry.enabled !== false).map((entry) => entry.id);
15634
+ const seen = /* @__PURE__ */ new Set();
15635
+ for (const id of orderedIds) {
15636
+ const chunks = sectionBuckets.get(id);
15637
+ if (!chunks || chunks.length === 0) continue;
15638
+ ordered.push(chunks.join("\n\n"));
15639
+ seen.add(id);
15640
+ }
15641
+ for (const [id, chunks] of sectionBuckets.entries()) {
15642
+ if (seen.has(id)) continue;
15643
+ if (chunks.length === 0) continue;
15644
+ ordered.push(chunks.join("\n\n"));
15645
+ }
15646
+ return ordered;
15647
+ }
15486
15648
  async recallInternal(prompt, sessionKey) {
15487
15649
  const recallStart = Date.now();
15488
15650
  const timings = {};
15489
15651
  const promptHash = createHash6("sha256").update(prompt).digest("hex");
15490
- const sections = [];
15652
+ const sectionBuckets = /* @__PURE__ */ new Map();
15491
15653
  const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
15492
15654
  cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
15493
15655
  cronRecallNormalizedQueryMaxChars: this.config.cronRecallNormalizedQueryMaxChars,
@@ -15517,7 +15679,10 @@ ${r.snippet.trim()}
15517
15679
  0,
15518
15680
  Math.min(this.config.qmdMaxResults, this.config.recallPlannerMaxQmdResultsMinimal)
15519
15681
  );
15520
- const recallResultLimit = recallMode !== "no_recall" && queryPolicy.retrievalBudgetMode === "minimal" ? Math.min(plannerRecallResultLimit, policyMinimalLimit) : plannerRecallResultLimit;
15682
+ const baseRecallResultLimit = recallMode !== "no_recall" && queryPolicy.retrievalBudgetMode === "minimal" ? Math.min(plannerRecallResultLimit, policyMinimalLimit) : plannerRecallResultLimit;
15683
+ const memoriesSectionEnabled = this.isRecallSectionEnabled("memories");
15684
+ const memorySectionMaxResults = this.getRecallSectionNumber("memories", "maxResults");
15685
+ const recallResultLimit = memoriesSectionEnabled ? memorySectionMaxResults !== void 0 ? Math.min(baseRecallResultLimit, memorySectionMaxResults) : baseRecallResultLimit : 0;
15521
15686
  const recallHeadroom = this.config.verbatimArtifactsEnabled ? Math.max(12, this.config.verbatimArtifactsMaxRecall * 4) : 12;
15522
15687
  const computedFetchLimit = recallResultLimit === 0 ? 0 : Math.max(recallResultLimit, Math.min(200, recallResultLimit + recallHeadroom));
15523
15688
  const qmdFetchLimit = computedFetchLimit;
@@ -15561,6 +15726,7 @@ ${r.snippet.trim()}
15561
15726
  const recallNamespaces = recallNamespacesForPrincipal(principal, this.config);
15562
15727
  const profileStorage = await this.storageRouter.storageFor(selfNamespace);
15563
15728
  const sharedContextPromise = (async () => {
15729
+ if (!this.isRecallSectionEnabled("shared-context", this.config.sharedContextEnabled === true)) return null;
15564
15730
  if (!this.sharedContext) return null;
15565
15731
  const t0 = Date.now();
15566
15732
  const [priorities, roundtable] = await Promise.all([
@@ -15579,12 +15745,14 @@ ${r.snippet.trim()}
15579
15745
  return trimmed.trim().length > 0 ? trimmed : null;
15580
15746
  })();
15581
15747
  const profilePromise = (async () => {
15748
+ if (!this.isRecallSectionEnabled("profile")) return null;
15582
15749
  const t0 = Date.now();
15583
15750
  const profile2 = await profileStorage.readProfile();
15584
15751
  timings.profile = `${Date.now() - t0}ms`;
15585
15752
  return profile2 || null;
15586
15753
  })();
15587
15754
  const identityContinuityPromise = (async () => {
15755
+ if (!this.isRecallSectionEnabled("identity-continuity", this.config.identityContinuityEnabled === true)) return null;
15588
15756
  const t0 = Date.now();
15589
15757
  const section = await this.buildIdentityContinuitySection({
15590
15758
  storage: profileStorage,
@@ -15595,10 +15763,14 @@ ${r.snippet.trim()}
15595
15763
  return section;
15596
15764
  })();
15597
15765
  const knowledgeIndexPromise = (async () => {
15766
+ if (!this.isRecallSectionEnabled("knowledge-index", this.config.knowledgeIndexEnabled)) return null;
15598
15767
  if (!this.config.knowledgeIndexEnabled) return null;
15599
15768
  const t0 = Date.now();
15600
15769
  try {
15601
- const ki = await this.storage.buildKnowledgeIndex(this.config);
15770
+ const ki = await this.storage.buildKnowledgeIndex(this.config, {
15771
+ maxEntities: this.getRecallSectionNumber("knowledge-index", "maxEntities"),
15772
+ maxChars: this.getRecallSectionNumber("knowledge-index", "maxChars")
15773
+ });
15602
15774
  timings.ki = `${Date.now() - t0}ms${ki.cached ? " (cached)" : ""}`;
15603
15775
  return ki.result ? ki : null;
15604
15776
  } catch (err) {
@@ -15608,6 +15780,7 @@ ${r.snippet.trim()}
15608
15780
  }
15609
15781
  })();
15610
15782
  const artifactsPromise = (async () => {
15783
+ if (!this.isRecallSectionEnabled("verbatim-artifacts", this.config.verbatimArtifactsEnabled === true)) return [];
15611
15784
  if (!this.config.verbatimArtifactsEnabled) return [];
15612
15785
  const t0 = Date.now();
15613
15786
  const targetCount = computeArtifactRecallLimit(
@@ -15651,26 +15824,162 @@ ${r.snippet.trim()}
15651
15824
  timings.qmd = `${Date.now() - t0}ms`;
15652
15825
  return { memoryResultsLists: [filteredResults], globalResults: [] };
15653
15826
  })();
15654
- const [sharedCtx, profile, identityContinuity, kiResult, artifacts, qmdResult] = await Promise.all([
15827
+ const transcriptPromise = (async () => {
15828
+ const t0 = Date.now();
15829
+ if (!this.config.transcriptEnabled || !this.isRecallSectionEnabled("transcript", true)) {
15830
+ timings.transcript = "skip";
15831
+ return null;
15832
+ }
15833
+ const transcriptMaxTokens = this.getRecallSectionNumber("transcript", "maxTokens") ?? this.config.maxTranscriptTokens;
15834
+ const transcriptMaxTurns = this.getRecallSectionNumber("transcript", "maxTurns") ?? this.config.maxTranscriptTurns;
15835
+ const transcriptLookbackHours = this.getRecallSectionNumber("transcript", "lookbackHours") ?? this.config.transcriptRecallHours;
15836
+ if (transcriptMaxTokens === 0 || transcriptMaxTurns === 0 || transcriptLookbackHours === 0) {
15837
+ timings.transcript = "skip(limit=0)";
15838
+ return null;
15839
+ }
15840
+ let section = null;
15841
+ let checkpointInjected = false;
15842
+ if (this.config.checkpointEnabled) {
15843
+ const checkpoint = await this.transcript.loadCheckpoint(sessionKey);
15844
+ log.debug(`recall: checkpoint loaded, turns=${checkpoint?.turns?.length ?? 0}`);
15845
+ if (checkpoint && checkpoint.turns.length > 0) {
15846
+ const formatted = this.transcript.formatForRecall(checkpoint.turns, transcriptMaxTokens);
15847
+ if (formatted) {
15848
+ section = `## Working Context (Recovered)
15849
+
15850
+ ${formatted}`;
15851
+ checkpointInjected = true;
15852
+ await this.transcript.clearCheckpoint();
15853
+ }
15854
+ }
15855
+ }
15856
+ if (!checkpointInjected) {
15857
+ const entries = await this.transcript.readRecent(transcriptLookbackHours, sessionKey);
15858
+ log.debug(`recall: read ${entries.length} transcript entries for sessionKey=${sessionKey}`);
15859
+ const cappedEntries = entries.slice(-transcriptMaxTurns);
15860
+ if (cappedEntries.length > 0) {
15861
+ log.debug(`recall: injecting ${cappedEntries.length} transcript entries`);
15862
+ const formatted = this.transcript.formatForRecall(cappedEntries, transcriptMaxTokens);
15863
+ if (formatted) section = formatted;
15864
+ }
15865
+ }
15866
+ timings.transcript = `${Date.now() - t0}ms`;
15867
+ return section;
15868
+ })();
15869
+ const summariesPromise = (async () => {
15870
+ const t0 = Date.now();
15871
+ if (!this.config.hourlySummariesEnabled || !sessionKey || !this.isRecallSectionEnabled("summaries", true)) {
15872
+ timings.summaries = "skip";
15873
+ return null;
15874
+ }
15875
+ const summariesLookbackHours = this.getRecallSectionNumber("summaries", "lookbackHours") ?? this.config.summaryRecallHours;
15876
+ const summariesMaxCount = this.getRecallSectionNumber("summaries", "maxCount") ?? this.config.maxSummaryCount;
15877
+ if (summariesLookbackHours <= 0 || summariesMaxCount <= 0) {
15878
+ timings.summaries = "skip(limit=0)";
15879
+ return null;
15880
+ }
15881
+ const summaries = await this.summarizer.readRecent(sessionKey, summariesLookbackHours);
15882
+ const cappedSummaries = summaries.slice(0, summariesMaxCount);
15883
+ const section = cappedSummaries.length > 0 ? this.summarizer.formatForRecall(cappedSummaries, summariesMaxCount) : null;
15884
+ timings.summaries = `${Date.now() - t0}ms`;
15885
+ return section;
15886
+ })();
15887
+ const conversationRecallPromise = (async () => {
15888
+ const t0 = Date.now();
15889
+ if (!this.config.conversationIndexEnabled || queryPolicy.skipConversationRecall || !this.isRecallSectionEnabled("conversation-recall", true)) {
15890
+ timings.convRecall = "skip";
15891
+ return null;
15892
+ }
15893
+ const topKOverride = this.getRecallSectionNumber("conversation-recall", "topK");
15894
+ if (topKOverride === 0) {
15895
+ timings.convRecall = "skip(topK=0)";
15896
+ return null;
15897
+ }
15898
+ const startedAtMs = Date.now();
15899
+ const timeoutMs = Math.max(
15900
+ 200,
15901
+ this.getRecallSectionNumber("conversation-recall", "timeoutMs") ?? this.config.conversationRecallTimeoutMs
15902
+ );
15903
+ const topK = Math.max(
15904
+ 1,
15905
+ topKOverride ?? this.config.conversationRecallTopK
15906
+ );
15907
+ const maxChars = Math.max(
15908
+ 400,
15909
+ this.getRecallSectionNumber("conversation-recall", "maxChars") ?? this.config.conversationRecallMaxChars
15910
+ );
15911
+ const results = await Promise.race([
15912
+ this.searchConversationRecallResults(retrievalQuery, topK),
15913
+ new Promise((resolve) => setTimeout(() => resolve([]), timeoutMs))
15914
+ ]).catch(() => []);
15915
+ const durationMs = Date.now() - startedAtMs;
15916
+ if (durationMs >= timeoutMs) {
15917
+ log.debug(`conversation recall: timed out after ${timeoutMs}ms`);
15918
+ }
15919
+ const section = this.formatConversationRecallSection(results, maxChars);
15920
+ timings.convRecall = `${Date.now() - t0}ms`;
15921
+ return section;
15922
+ })();
15923
+ const compoundingPromise = (async () => {
15924
+ const t0 = Date.now();
15925
+ if (!this.compounding || !this.config.compoundingInjectEnabled || !this.isRecallSectionEnabled("compounding", true)) {
15926
+ timings.compounding = "skip";
15927
+ return null;
15928
+ }
15929
+ const mistakes = await this.compounding.readMistakes();
15930
+ if (!mistakes || !Array.isArray(mistakes.patterns) || mistakes.patterns.length === 0) {
15931
+ timings.compounding = `${Date.now() - t0}ms`;
15932
+ return null;
15933
+ }
15934
+ const maxPatterns = this.getRecallSectionNumber("compounding", "maxPatterns") ?? 40;
15935
+ if (maxPatterns === 0) {
15936
+ timings.compounding = "skip(limit=0)";
15937
+ return null;
15938
+ }
15939
+ const lines = [
15940
+ "## Institutional Learning (Compounded)",
15941
+ "",
15942
+ "Avoid repeating these patterns:",
15943
+ ...mistakes.patterns.slice(0, maxPatterns).map((p) => `- ${p}`)
15944
+ ];
15945
+ timings.compounding = `${Date.now() - t0}ms`;
15946
+ return lines.join("\n");
15947
+ })();
15948
+ const [
15949
+ sharedCtx,
15950
+ profile,
15951
+ identityContinuity,
15952
+ kiResult,
15953
+ artifacts,
15954
+ qmdResult,
15955
+ transcriptSection,
15956
+ summariesSection,
15957
+ conversationRecallSection,
15958
+ compoundingSection
15959
+ ] = await Promise.all([
15655
15960
  sharedContextPromise,
15656
15961
  profilePromise,
15657
15962
  identityContinuityPromise,
15658
15963
  knowledgeIndexPromise,
15659
15964
  artifactsPromise,
15660
- qmdPromise
15965
+ qmdPromise,
15966
+ transcriptPromise,
15967
+ summariesPromise,
15968
+ conversationRecallPromise,
15969
+ compoundingPromise
15661
15970
  ]);
15662
- if (sharedCtx) sections.push(sharedCtx);
15663
- if (profile) sections.push(`## User Profile
15971
+ if (sharedCtx) this.appendRecallSection(sectionBuckets, "shared-context", sharedCtx);
15972
+ if (profile) this.appendRecallSection(sectionBuckets, "profile", `## User Profile
15664
15973
 
15665
15974
  ${profile}`);
15666
15975
  if (identityContinuity) {
15667
- sections.push(identityContinuity.section);
15976
+ this.appendRecallSection(sectionBuckets, "identity-continuity", identityContinuity.section);
15668
15977
  identityInjectionModeUsed = identityContinuity.mode;
15669
15978
  identityInjectedChars = identityContinuity.injectedChars;
15670
15979
  identityInjectionTruncated = identityContinuity.truncated;
15671
15980
  }
15672
15981
  if (kiResult?.result) {
15673
- sections.push(kiResult.result);
15982
+ this.appendRecallSection(sectionBuckets, "knowledge-index", kiResult.result);
15674
15983
  log.debug(`Knowledge Index: ${kiResult.result.split("\n").length - 4} entities, ${kiResult.result.length} chars${kiResult.cached ? " (cached)" : ""}`);
15675
15984
  }
15676
15985
  if (artifacts.length > 0) {
@@ -15680,11 +15989,11 @@ ${profile}`);
15680
15989
  const created = createdRaw ? createdRaw.slice(0, 19).replace("T", " ") : "unknown-time";
15681
15990
  return `- [${artifactType}] "${this.truncateArtifactForRecall(a.content)}" (${created})`;
15682
15991
  });
15683
- sections.push(`## Verbatim Artifacts
15992
+ this.appendRecallSection(sectionBuckets, "verbatim-artifacts", `## Verbatim Artifacts
15684
15993
 
15685
15994
  ${lines.join("\n")}`);
15686
15995
  }
15687
- if (this.config.memoryBoxesEnabled && this.config.boxRecallDays > 0) {
15996
+ if (this.isRecallSectionEnabled("memory-boxes", this.config.memoryBoxesEnabled === true) && this.config.memoryBoxesEnabled && this.config.boxRecallDays > 0) {
15688
15997
  const recentBoxes = await this.boxBuilderFor(profileStorage).readRecentBoxes(this.config.boxRecallDays).catch(() => []);
15689
15998
  if (recentBoxes.length > 0) {
15690
15999
  const boxLines = recentBoxes.slice(0, 5).map((b) => {
@@ -15692,16 +16001,16 @@ ${lines.join("\n")}`);
15692
16001
  const traceNote = b.traceId ? ` [trace: ${b.traceId.slice(0, 12)}]` : "";
15693
16002
  return `- [${sealedDate}${traceNote}] Topics: ${b.topics.join(", ")} (${b.memoryIds.length} memories)`;
15694
16003
  });
15695
- sections.push(`## Recent Topic Windows
16004
+ this.appendRecallSection(sectionBuckets, "memory-boxes", `## Recent Topic Windows
15696
16005
 
15697
16006
  ${boxLines.join("\n")}`);
15698
16007
  }
15699
16008
  }
15700
- if (this.config.temporalMemoryTreeEnabled && recallMode !== "minimal" && recallMode !== "no_recall") {
16009
+ if (this.isRecallSectionEnabled("temporal-memory-tree", this.config.temporalMemoryTreeEnabled === true) && this.config.temporalMemoryTreeEnabled && recallMode !== "minimal" && recallMode !== "no_recall") {
15701
16010
  const tmtNode = await this.tmtBuilder.getMostRelevantNode();
15702
16011
  if (tmtNode) {
15703
16012
  const levelLabel = tmtNode.level.charAt(0).toUpperCase() + tmtNode.level.slice(1);
15704
- sections.push(`## Memory Timeline (${levelLabel})
16013
+ this.appendRecallSection(sectionBuckets, "temporal-memory-tree", `## Memory Timeline (${levelLabel})
15705
16014
 
15706
16015
  ${tmtNode.summary}`);
15707
16016
  }
@@ -15789,7 +16098,7 @@ ${tmtNode.summary}`);
15789
16098
  this.publishRecallResults({
15790
16099
  title: "Relevant Memories",
15791
16100
  results: memoryResults,
15792
- sections,
16101
+ sectionBuckets,
15793
16102
  retrievalQuery,
15794
16103
  sessionKey,
15795
16104
  identityInjection: {
@@ -15817,7 +16126,7 @@ ${tmtNode.summary}`);
15817
16126
  this.publishRecallResults({
15818
16127
  title: "Relevant Memories",
15819
16128
  results: scoped,
15820
- sections,
16129
+ sectionBuckets,
15821
16130
  retrievalQuery,
15822
16131
  sessionKey,
15823
16132
  identityInjection: {
@@ -15840,7 +16149,7 @@ ${tmtNode.summary}`);
15840
16149
  this.publishRecallResults({
15841
16150
  title: "Long-Term Memories (Fallback)",
15842
16151
  results: longTerm,
15843
- sections,
16152
+ sectionBuckets,
15844
16153
  retrievalQuery,
15845
16154
  sessionKey,
15846
16155
  identityInjection: {
@@ -15854,13 +16163,17 @@ ${tmtNode.summary}`);
15854
16163
  }
15855
16164
  }
15856
16165
  if (globalResults.length > 0) {
15857
- sections.push(
16166
+ this.appendRecallSection(
16167
+ sectionBuckets,
16168
+ "workspace-context",
15858
16169
  this.formatQmdResults("Workspace Context", globalResults)
15859
16170
  );
15860
16171
  }
15861
16172
  timings.qmdPost = `${Date.now() - t0}ms`;
15862
16173
  if (isDisagreementPrompt(prompt)) {
15863
- sections.push(
16174
+ this.appendRecallSection(
16175
+ sectionBuckets,
16176
+ "memories",
15864
16177
  [
15865
16178
  "## Retrieval Feedback Helper",
15866
16179
  "",
@@ -15891,7 +16204,7 @@ ${tmtNode.summary}`);
15891
16204
  this.publishRecallResults({
15892
16205
  title: "Relevant Memories",
15893
16206
  results: scoped,
15894
- sections,
16207
+ sectionBuckets,
15895
16208
  retrievalQuery,
15896
16209
  sessionKey,
15897
16210
  identityInjection: {
@@ -15931,7 +16244,7 @@ ${tmtNode.summary}`);
15931
16244
  this.publishRecallResults({
15932
16245
  title: "Recent Memories",
15933
16246
  results: recent,
15934
- sections,
16247
+ sectionBuckets,
15935
16248
  retrievalQuery,
15936
16249
  sessionKey,
15937
16250
  identityInjection: {
@@ -15954,7 +16267,7 @@ ${tmtNode.summary}`);
15954
16267
  this.publishRecallResults({
15955
16268
  title: "Long-Term Memories (Fallback)",
15956
16269
  results: longTerm,
15957
- sections,
16270
+ sectionBuckets,
15958
16271
  retrievalQuery,
15959
16272
  sessionKey,
15960
16273
  identityInjection: {
@@ -15979,7 +16292,7 @@ ${tmtNode.summary}`);
15979
16292
  this.publishRecallResults({
15980
16293
  title: "Long-Term Memories (Fallback)",
15981
16294
  results: longTerm,
15982
- sections,
16295
+ sectionBuckets,
15983
16296
  retrievalQuery,
15984
16297
  sessionKey,
15985
16298
  identityInjection: {
@@ -15993,7 +16306,9 @@ ${tmtNode.summary}`);
15993
16306
  }
15994
16307
  }
15995
16308
  if (isDisagreementPrompt(prompt)) {
15996
- sections.push(
16309
+ this.appendRecallSection(
16310
+ sectionBuckets,
16311
+ "memories",
15997
16312
  [
15998
16313
  "## Retrieval Feedback Helper",
15999
16314
  "",
@@ -16007,108 +16322,37 @@ ${tmtNode.summary}`);
16007
16322
  );
16008
16323
  }
16009
16324
  }
16010
- const compressionGuidelineSection = await this.buildCompressionGuidelineRecallSection();
16011
- if (compressionGuidelineSection) {
16012
- sections.push(compressionGuidelineSection);
16013
- }
16014
- const transcriptT0 = Date.now();
16015
- log.debug(`recall: transcriptEnabled=${this.config.transcriptEnabled}, sessionKey=${sessionKey}`);
16016
- if (this.config.transcriptEnabled) {
16017
- let checkpointInjected = false;
16018
- if (this.config.checkpointEnabled) {
16019
- const checkpoint = await this.transcript.loadCheckpoint(sessionKey);
16020
- log.debug(`recall: checkpoint loaded, turns=${checkpoint?.turns?.length ?? 0}`);
16021
- if (checkpoint && checkpoint.turns.length > 0) {
16022
- const formatted = this.transcript.formatForRecall(
16023
- checkpoint.turns,
16024
- this.config.maxTranscriptTokens
16025
- );
16026
- if (formatted) {
16027
- sections.push(`## Working Context (Recovered)
16028
-
16029
- ${formatted}`);
16030
- checkpointInjected = true;
16031
- await this.transcript.clearCheckpoint();
16032
- }
16033
- }
16034
- }
16035
- if (!checkpointInjected) {
16036
- const entries = await this.transcript.readRecent(
16037
- this.config.transcriptRecallHours,
16038
- sessionKey
16039
- );
16040
- log.debug(`recall: read ${entries.length} transcript entries for sessionKey=${sessionKey}`);
16041
- const cappedEntries = entries.slice(-this.config.maxTranscriptTurns);
16042
- if (cappedEntries.length > 0) {
16043
- log.debug(`recall: injecting ${cappedEntries.length} transcript entries`);
16044
- const formatted = this.transcript.formatForRecall(
16045
- cappedEntries,
16046
- this.config.maxTranscriptTokens
16047
- );
16048
- if (formatted) {
16049
- sections.push(formatted);
16050
- }
16051
- }
16325
+ if (this.isRecallSectionEnabled("compression-guidelines", this.config.compressionGuidelineLearningEnabled === true)) {
16326
+ const compressionGuidelineSection = await this.buildCompressionGuidelineRecallSection();
16327
+ if (compressionGuidelineSection) {
16328
+ this.appendRecallSection(sectionBuckets, "compression-guidelines", compressionGuidelineSection);
16052
16329
  }
16053
16330
  }
16054
- timings.transcript = `${Date.now() - transcriptT0}ms`;
16055
- const summariesT0 = Date.now();
16056
- if (this.config.hourlySummariesEnabled && sessionKey) {
16057
- const summaries = await this.summarizer.readRecent(
16058
- sessionKey,
16059
- this.config.summaryRecallHours
16060
- );
16061
- const cappedSummaries = summaries.slice(0, this.config.maxSummaryCount);
16062
- if (cappedSummaries.length > 0) {
16063
- const formatted = this.summarizer.formatForRecall(
16064
- cappedSummaries,
16065
- this.config.maxSummaryCount
16066
- );
16067
- sections.push(formatted);
16068
- }
16331
+ if (transcriptSection) {
16332
+ this.appendRecallSection(sectionBuckets, "transcript", transcriptSection);
16069
16333
  }
16070
- timings.summaries = `${Date.now() - summariesT0}ms`;
16071
- const convT0 = Date.now();
16072
- if (this.config.conversationIndexEnabled && !queryPolicy.skipConversationRecall) {
16073
- const startedAtMs = Date.now();
16074
- const timeoutMs = Math.max(200, this.config.conversationRecallTimeoutMs);
16075
- const topK = Math.max(1, this.config.conversationRecallTopK);
16076
- const maxChars = Math.max(400, this.config.conversationRecallMaxChars);
16077
- const results = await Promise.race([
16078
- this.searchConversationRecallResults(retrievalQuery, topK),
16079
- new Promise((resolve) => setTimeout(() => resolve([]), timeoutMs))
16080
- ]).catch(() => []);
16081
- const durationMs = Date.now() - startedAtMs;
16082
- if (durationMs >= timeoutMs) {
16083
- log.debug(`conversation recall: timed out after ${timeoutMs}ms`);
16084
- }
16085
- const formattedConversationRecall = this.formatConversationRecallSection(results, maxChars);
16086
- if (formattedConversationRecall) {
16087
- sections.push(formattedConversationRecall);
16088
- }
16334
+ if (summariesSection) {
16335
+ this.appendRecallSection(sectionBuckets, "summaries", summariesSection);
16089
16336
  }
16090
- timings.convRecall = `${Date.now() - convT0}ms`;
16091
- if (this.compounding && this.config.compoundingInjectEnabled) {
16092
- const mistakes = await this.compounding.readMistakes();
16093
- if (mistakes && Array.isArray(mistakes.patterns) && mistakes.patterns.length > 0) {
16094
- const lines = [
16095
- "## Institutional Learning (Compounded)",
16096
- "",
16097
- "Avoid repeating these patterns:",
16098
- ...mistakes.patterns.slice(0, 40).map((p) => `- ${p}`)
16099
- ];
16100
- sections.push(lines.join("\n"));
16101
- }
16337
+ if (conversationRecallSection) {
16338
+ this.appendRecallSection(sectionBuckets, "conversation-recall", conversationRecallSection);
16102
16339
  }
16103
- if (this.config.injectQuestions) {
16340
+ if (compoundingSection) {
16341
+ this.appendRecallSection(sectionBuckets, "compounding", compoundingSection);
16342
+ }
16343
+ if (this.config.injectQuestions && this.isRecallSectionEnabled("questions", true)) {
16104
16344
  const questions = await profileStorage.readQuestions({ unresolvedOnly: true });
16105
16345
  if (questions.length > 0) {
16106
16346
  const topQuestion = questions[0];
16107
- sections.push(`## Open Question
16347
+ this.appendRecallSection(
16348
+ sectionBuckets,
16349
+ "questions",
16350
+ `## Open Question
16108
16351
 
16109
16352
  Something I've been curious about: ${topQuestion.question}
16110
16353
 
16111
- _Context: ${topQuestion.context}_`);
16354
+ _Context: ${topQuestion.context}_`
16355
+ );
16112
16356
  }
16113
16357
  }
16114
16358
  timings.total = `${Date.now() - recallStart}ms`;
@@ -16127,7 +16371,8 @@ _Context: ${topQuestion.context}_`);
16127
16371
  }
16128
16372
  }).catch((err) => log.debug(`last recall record failed: ${err}`));
16129
16373
  }
16130
- const context = sections.length === 0 ? "" : sections.join("\n\n---\n\n");
16374
+ const orderedSections = this.assembleRecallSections(sectionBuckets);
16375
+ const context = orderedSections.length === 0 ? "" : orderedSections.join("\n\n---\n\n");
16131
16376
  this.emitTrace({
16132
16377
  kind: "recall_summary",
16133
16378
  traceId: createHash6("sha256").update(`${sessionKey ?? "default"}:${Date.now()}:${promptHash}`).digest("hex").slice(0, 16),
@@ -17333,11 +17578,17 @@ _Context: ${topQuestion.context}_`);
17333
17578
  if (this.config.identityEnabled) {
17334
17579
  await this.autoConsolidateIdentity();
17335
17580
  }
17336
- if (await this.storage.profileNeedsConsolidation()) {
17581
+ const profileSection = this.getRecallSectionEntry("profile");
17582
+ const profileConsolidationTriggerLines = typeof profileSection?.consolidateTriggerLines === "number" ? Math.max(0, Math.floor(profileSection.consolidateTriggerLines)) : void 0;
17583
+ const profileConsolidationTargetLines = typeof profileSection?.consolidateTargetLines === "number" ? Math.max(0, Math.floor(profileSection.consolidateTargetLines)) : 50;
17584
+ if (await this.storage.profileNeedsConsolidation(profileConsolidationTriggerLines)) {
17337
17585
  log.info("profile.md exceeds max lines \u2014 running smart consolidation");
17338
17586
  const currentProfile = await this.storage.readProfile();
17339
17587
  if (currentProfile) {
17340
- const profileResult = await this.extraction.consolidateProfile(currentProfile);
17588
+ const profileResult = await this.extraction.consolidateProfile(
17589
+ currentProfile,
17590
+ profileConsolidationTargetLines
17591
+ );
17341
17592
  if (profileResult) {
17342
17593
  await this.storage.writeProfile(profileResult.consolidatedProfile);
17343
17594
  log.info(`profile.md consolidated: removed ${profileResult.removedCount} items \u2014 ${profileResult.summary}`);
@@ -17901,7 +18152,11 @@ ${lines.join("\n\n")}`;
17901
18152
  identityInjection: options.identityInjection
17902
18153
  }).catch((err) => log.debug(`last recall record failed: ${err}`));
17903
18154
  }
17904
- options.sections.push(this.formatQmdResults(options.title, options.results));
18155
+ this.appendRecallSection(
18156
+ options.sectionBuckets,
18157
+ "memories",
18158
+ this.formatQmdResults(options.title, options.results)
18159
+ );
17905
18160
  }
17906
18161
  async searchEmbeddingFallback(query, limit) {
17907
18162
  if (!this.config.embeddingFallbackEnabled) return [];
@@ -25644,7 +25899,8 @@ var index_default = {
25644
25899
  const context = await orchestrator.recall(prompt, sessionKey);
25645
25900
  log.debug(`before_agent_start: recall returned ${context?.length ?? 0} chars`);
25646
25901
  if (!context) return;
25647
- const maxChars = cfg.maxMemoryTokens * 4;
25902
+ const maxChars = cfg.recallBudgetChars;
25903
+ if (maxChars === 0) return;
25648
25904
  const trimmed = context.length > maxChars ? context.slice(0, maxChars) + "\n\n...(memory context trimmed)" : context;
25649
25905
  log.debug(`before_agent_start: returning system prompt with ${trimmed.length} chars`);
25650
25906
  return {