@psiclawops/hypermem 0.9.2 → 0.9.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/INSTALL.md +73 -70
  3. package/README.md +33 -51
  4. package/assets/default-config.json +47 -0
  5. package/bin/hypermem-doctor.mjs +76 -2
  6. package/bin/hypermem-status.mjs +255 -7
  7. package/dist/adaptive-lifecycle.d.ts +39 -0
  8. package/dist/adaptive-lifecycle.d.ts.map +1 -1
  9. package/dist/adaptive-lifecycle.js +87 -9
  10. package/dist/background-indexer.d.ts.map +1 -1
  11. package/dist/background-indexer.js +7 -5
  12. package/dist/compositor.d.ts.map +1 -1
  13. package/dist/compositor.js +239 -20
  14. package/dist/hybrid-retrieval.d.ts +8 -0
  15. package/dist/hybrid-retrieval.d.ts.map +1 -1
  16. package/dist/hybrid-retrieval.js +112 -10
  17. package/dist/index.d.ts +15 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +17 -0
  20. package/dist/message-store.d.ts +62 -1
  21. package/dist/message-store.d.ts.map +1 -1
  22. package/dist/message-store.js +355 -2
  23. package/dist/open-domain.d.ts.map +1 -1
  24. package/dist/open-domain.js +3 -2
  25. package/dist/proactive-pass.d.ts +42 -2
  26. package/dist/proactive-pass.d.ts.map +1 -1
  27. package/dist/proactive-pass.js +294 -39
  28. package/dist/topic-synthesizer.d.ts.map +1 -1
  29. package/dist/topic-synthesizer.js +9 -3
  30. package/dist/types.d.ts +99 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/vector-store.d.ts +10 -1
  33. package/dist/vector-store.d.ts.map +1 -1
  34. package/dist/vector-store.js +45 -9
  35. package/docs/DIAGNOSTICS.md +87 -0
  36. package/docs/INTEGRATION_VALIDATION.md +40 -1
  37. package/docs/ROADMAP.md +25 -12
  38. package/docs/TUNING.md +45 -4
  39. package/install.sh +5 -60
  40. package/memory-plugin/dist/index.d.ts +24 -0
  41. package/memory-plugin/dist/index.js +570 -0
  42. package/memory-plugin/openclaw.plugin.json +199 -2
  43. package/memory-plugin/package.json +3 -3
  44. package/package.json +24 -10
  45. package/plugin/dist/index.d.ts +210 -0
  46. package/plugin/dist/index.d.ts.map +1 -0
  47. package/plugin/dist/index.js +3641 -0
  48. package/plugin/dist/index.js.map +1 -0
  49. package/plugin/openclaw.plugin.json +199 -2
  50. package/plugin/package.json +4 -4
  51. package/scripts/install-packed-runtime.mjs +99 -0
  52. package/scripts/install-runtime.mjs +164 -4
@@ -27,7 +27,7 @@ import { KnowledgeStore } from './knowledge-store.js';
27
27
  import { TemporalStore, hasTemporalSignals } from './temporal-store.js';
28
28
  import { isOpenDomainQuery, searchOpenDomain } from './open-domain.js';
29
29
  import { TRIM_BUDGET_POLICY, resolveTrimBudgets } from './budget-policy.js';
30
- import { resolveAdaptiveLifecyclePolicy } from './adaptive-lifecycle.js';
30
+ import { resolveAdaptiveLifecyclePolicy, countTopicBearingTurns } from './adaptive-lifecycle.js';
31
31
  import { formatToolChainStub, parseToolChainStub, formatArtifactRef, isArtifactRef } from './degradation.js';
32
32
  import { ToolArtifactStore } from './tool-artifact-store.js';
33
33
  import { insertCompositionSnapshot, getLatestValidCompositionSnapshot, listCompositionSnapshots, MAX_WARM_RESTORE_REPAIR_DEPTH, } from './composition-snapshot-store.js';
@@ -42,6 +42,8 @@ export const OPENCLAW_BOOTSTRAP_FILES = new Set([
42
42
  'AGENTS.md', 'HEARTBEAT.md', 'MEMORY.md', 'BOOTSTRAP.md',
43
43
  ]);
44
44
  const CACHE_PREFIX_BOUNDARY_SLOT = 'cache-prefix-boundary';
45
+ const LITERAL_ANTECEDENT_GUARD_MAX_MESSAGES = 3;
46
+ const LITERAL_ANTECEDENT_GUARD_MAX_TOKENS = 4_000;
45
47
  /**
46
48
  * Model context window sizes by provider/model string (or partial match).
47
49
  * Used as fallback when tokenBudget is not passed by the runtime.
@@ -419,6 +421,33 @@ const TOOL_RECENT_OVERSIZE_CHAR_THRESHOLD = 40_000;
419
421
  const TOOL_RECENT_OVERSIZE_TARGET_CHARS = 40_000;
420
422
  const TOOL_RECENT_OVERSIZE_MAX_TAIL_CHARS = 12_000;
421
423
  const TOOL_TRIM_NOTE_PREFIX = '[hypermem_tool_result_trim';
424
+ // 0.9.4 Packet 3: afterTurn protected warming floor.
425
+ // refreshRedisGradient only receives the resolved trim target, not the full
426
+ // lifecycle policy. Keep this private and infer the early-session bands from
427
+ // the canonical Packet 1 trim targets so no public API/export surface changes.
428
+ // The floor is intentionally disabled once the lifecycle reaches elevated or
429
+ // worse; pressure bands at/above elevated are allowed to reclaim warming.
430
+ const AFTERTURN_PROTECTED_WARMING_ELEVATED_FLOOR_FRACTION = 0.34;
431
+ const AFTERTURN_PROTECTED_WARMING_BOOTSTRAP_FLOOR_FRACTION = Math.max(AFTERTURN_PROTECTED_WARMING_ELEVATED_FLOOR_FRACTION, 0.62 * 0.60);
432
+ const AFTERTURN_PROTECTED_WARMING_WARMUP_FLOOR_FRACTION = Math.max(AFTERTURN_PROTECTED_WARMING_ELEVATED_FLOOR_FRACTION, 0.55 * 0.60);
433
+ const AFTERTURN_PROTECTED_WARMING_TRIM_EPSILON = 0.0001;
434
+ function resolveAfterTurnProtectedWarmingFloor(tokenBudget, trimSoftTarget) {
435
+ const safeBudget = Math.max(0, Math.floor(tokenBudget || 0));
436
+ if (safeBudget <= 0 || trimSoftTarget == null) {
437
+ return { floorFraction: 0, floorTokens: 0, band: null };
438
+ }
439
+ // Packet 1 trim targets: bootstrap=0.72, warmup=0.68. Steady=0.65 and
440
+ // elevated=0.60 do not get this protected floor.
441
+ if (Math.abs(trimSoftTarget - 0.72) <= AFTERTURN_PROTECTED_WARMING_TRIM_EPSILON) {
442
+ const floorFraction = AFTERTURN_PROTECTED_WARMING_BOOTSTRAP_FLOOR_FRACTION;
443
+ return { floorFraction, floorTokens: Math.floor(safeBudget * floorFraction), band: 'bootstrap' };
444
+ }
445
+ if (Math.abs(trimSoftTarget - 0.68) <= AFTERTURN_PROTECTED_WARMING_TRIM_EPSILON) {
446
+ const floorFraction = AFTERTURN_PROTECTED_WARMING_WARMUP_FLOOR_FRACTION;
447
+ return { floorFraction, floorTokens: Math.floor(safeBudget * floorFraction), band: 'warmup' };
448
+ }
449
+ return { floorFraction: 0, floorTokens: 0, band: null };
450
+ }
422
451
  // ─── Trigger Registry ────────────────────────────────────────────
423
452
  // Moved to src/trigger-registry.ts (W5).
424
453
  // CollectionTrigger, DEFAULT_TRIGGERS, matchTriggers imported above.
@@ -465,6 +494,57 @@ function clusterNeutralMessages(messages) {
465
494
  }
466
495
  return clusters;
467
496
  }
497
+ function resolveLiteralAntecedentGuardClusterIndices(clusters, opts) {
498
+ const protectedIndices = new Set();
499
+ const currentPrompt = opts.currentPrompt?.trim() ?? '';
500
+ if (!opts.enabled || clusters.length < (currentPrompt ? 1 : 2))
501
+ return protectedIndices;
502
+ let boundaryClusterIdx;
503
+ if (currentPrompt) {
504
+ for (let i = clusters.length - 1; i >= 0; i--) {
505
+ const containsCurrentPrompt = clusters[i].messages.some(msg => msg.role === 'user' && (msg.textContent ?? '').trim() === currentPrompt);
506
+ if (containsCurrentPrompt) {
507
+ boundaryClusterIdx = i;
508
+ break;
509
+ }
510
+ }
511
+ // In the plugin compose path the current prompt is often request.prompt and
512
+ // is appended after compose, so it has no persisted cluster yet. Treat the
513
+ // end of persisted history as the prompt boundary in that case.
514
+ if (boundaryClusterIdx == null)
515
+ boundaryClusterIdx = clusters.length;
516
+ }
517
+ else {
518
+ const latestClusterIdx = clusters.length - 1;
519
+ const latestCluster = clusters[latestClusterIdx];
520
+ if (!latestCluster.messages.some(msg => msg.role === 'user'))
521
+ return protectedIndices;
522
+ boundaryClusterIdx = latestClusterIdx;
523
+ }
524
+ if (boundaryClusterIdx <= 0)
525
+ return protectedIndices;
526
+ const maxMessages = Math.max(0, Math.floor(opts.maxMessages ?? LITERAL_ANTECEDENT_GUARD_MAX_MESSAGES));
527
+ const maxTokens = Math.max(0, Math.floor(opts.maxTokens ?? LITERAL_ANTECEDENT_GUARD_MAX_TOKENS));
528
+ if (maxMessages <= 0 || maxTokens <= 0)
529
+ return protectedIndices;
530
+ let messageCount = 0;
531
+ let tokenCount = 0;
532
+ for (let i = boundaryClusterIdx - 1; i >= 0; i--) {
533
+ const cluster = clusters[i];
534
+ const nextMessageCount = messageCount + cluster.messages.length;
535
+ const nextTokenCount = tokenCount + cluster.tokenCost;
536
+ if (nextMessageCount > maxMessages)
537
+ break;
538
+ if (nextTokenCount > maxTokens)
539
+ break;
540
+ protectedIndices.add(i);
541
+ messageCount = nextMessageCount;
542
+ tokenCount = nextTokenCount;
543
+ if (messageCount >= maxMessages)
544
+ break;
545
+ }
546
+ return protectedIndices;
547
+ }
468
548
  export function orderClustersForAdaptiveEviction(clusters, policy, opts = {}) {
469
549
  const plan = policy.evictionPlan;
470
550
  const protectedIndices = new Set();
@@ -1043,6 +1123,17 @@ export function resolveArtifactOversizeThreshold(effectiveBudget) {
1043
1123
  function isExplicitNewSessionPrompt(prompt) {
1044
1124
  return /^\/new(?:\s|$)/i.test((prompt ?? '').trim());
1045
1125
  }
1126
+ function parsePersistedTopicBearingTurnCount(raw) {
1127
+ if (raw == null)
1128
+ return null;
1129
+ const trimmed = raw.trim();
1130
+ if (trimmed.length === 0)
1131
+ return null;
1132
+ const parsed = Number.parseInt(trimmed, 10);
1133
+ if (!Number.isFinite(parsed) || parsed < 0)
1134
+ return null;
1135
+ return Math.floor(parsed);
1136
+ }
1046
1137
  /**
1047
1138
  * C2: Degrade an oversized doc chunk to a canonical ArtifactRef string.
1048
1139
  *
@@ -1625,6 +1716,14 @@ export class Compositor {
1625
1716
  const s09SampleTokens = sampleMessages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
1626
1717
  const s09EvictionPressure = computeUnifiedPressure(s09SampleTokens, budget, PRESSURE_SOURCE.COMPOSE_PRE_RECALL);
1627
1718
  let s09ObservedUserTurnCount = sampleMessages.filter(m => m.role === 'user').length;
1719
+ let s09PersistedTopicBearingTurnCount = null;
1720
+ try {
1721
+ s09PersistedTopicBearingTurnCount = parsePersistedTopicBearingTurnCount(await this.cache.getSlot(request.agentId, request.sessionKey, 'topicBearingTurnCount'));
1722
+ }
1723
+ catch {
1724
+ s09PersistedTopicBearingTurnCount = null;
1725
+ }
1726
+ let s09TopicBearingTurnCount = s09PersistedTopicBearingTurnCount ?? countTopicBearingTurns(sampleMessages);
1628
1727
  const s09ForkedContextSeed = request.forkedContext?.enabled ? request.forkedContext : undefined;
1629
1728
  const s09ForkedParentPressure = typeof s09ForkedContextSeed?.parentPressureFraction === 'number'
1630
1729
  && Number.isFinite(s09ForkedContextSeed.parentPressureFraction)
@@ -1638,6 +1737,7 @@ export class Compositor {
1638
1737
  const evictionLifecyclePolicy = resolveAdaptiveLifecyclePolicy({
1639
1738
  pressureFraction: s09EvictionPolicyPressure,
1640
1739
  userTurnCount: s09ObservedUserTurnCount,
1740
+ topicBearingTurnCount: s09TopicBearingTurnCount,
1641
1741
  explicitNewSession: isExplicitNewSessionPrompt(request.prompt ?? null),
1642
1742
  forkedContext: Boolean(s09ForkedContextSeed),
1643
1743
  forkedParentPressureFraction: s09ForkedParentPressure,
@@ -1775,6 +1875,9 @@ export class Compositor {
1775
1875
  // C1: total tool-chain degradation counters across history budget-fit and safety-valve passes.
1776
1876
  let c1CoEjections = 0;
1777
1877
  let c1StubReplacements = 0;
1878
+ let literalAntecedentGuardHits = 0;
1879
+ const literalAntecedentGuardMessages = new Set();
1880
+ const literalAntecedentGuardHitMessages = new Set();
1778
1881
  // Hoisted: activeTopicId/name resolved inside history block, used for window dual-write (VS-1) and wiki page injection
1779
1882
  let composedActiveTopicId;
1780
1883
  let composedActiveTopicName;
@@ -1824,20 +1927,58 @@ export class Compositor {
1824
1927
  composedActiveTopicName = activeTopic?.name;
1825
1928
  const rawHistoryMessages = await this.getHistory(request.agentId, request.sessionKey, s4EffectiveDepth, // Sprint 4: adaptive depth (replaces fixed maxHistoryMessages)
1826
1929
  store, activeTopicId, fenceMessageId, activeContext);
1930
+ // Continuity guard: force a small tail of meaningful transcript rows into
1931
+ // the candidate window before budget selection. Tool carrier rows can have
1932
+ // empty text_content; in dense tool loops they can consume raw history depth
1933
+ // and push the immediate conversational antecedent out of the selected
1934
+ // window. The full tool stream still comes from getHistory(); this guard
1935
+ // only repairs the human-readable transcript tail.
1936
+ let continuityTail = [];
1937
+ try {
1938
+ const conversation = store.getConversation(request.sessionKey);
1939
+ if (conversation) {
1940
+ continuityTail = store.getRecentMeaningfulMessages(conversation.id, 8, fenceMessageId);
1941
+ }
1942
+ }
1943
+ catch {
1944
+ continuityTail = [];
1945
+ }
1946
+ const mergedRawHistory = continuityTail.length > 0
1947
+ ? [...rawHistoryMessages, ...continuityTail].sort((a, b) => {
1948
+ const ai = a.messageIndex ?? 0;
1949
+ const bi = b.messageIndex ?? 0;
1950
+ return ai - bi;
1951
+ })
1952
+ : rawHistoryMessages;
1827
1953
  // Deduplicate history by StoredMessage.id (second line of defense after
1828
1954
  // pushHistory() tail-check dedup). Guards against any duplicates that
1829
1955
  // slipped through the warm path — e.g. bootstrap re-runs on existing sessions.
1830
- const seenIds = new Set();
1831
- const historyMessages = rawHistoryMessages.filter(m => {
1956
+ // If the continuity transcript projection overlaps with full runtime
1957
+ // history, prefer the full row. Transcript projections intentionally null
1958
+ // tool_calls/tool_results and must never replace a tool-bearing history row.
1959
+ const historyById = new Map();
1960
+ const historyMessagesNoId = [];
1961
+ const rowScore = (m) => (m.toolCalls != null ? 2 : 0) + (m.toolResults != null ? 2 : 0) + (m.textContent ? 1 : 0);
1962
+ for (const m of mergedRawHistory) {
1832
1963
  const sm = m;
1833
- if (sm.id != null) {
1834
- if (seenIds.has(sm.id))
1835
- return false;
1836
- seenIds.add(sm.id);
1964
+ if (sm.id == null) {
1965
+ historyMessagesNoId.push(m);
1966
+ continue;
1967
+ }
1968
+ const existing = historyById.get(sm.id);
1969
+ if (!existing || rowScore(sm) > rowScore(existing)) {
1970
+ historyById.set(sm.id, sm);
1837
1971
  }
1838
- return true;
1972
+ }
1973
+ const historyMessages = [...historyMessagesNoId, ...historyById.values()].sort((a, b) => {
1974
+ const ai = a.messageIndex ?? 0;
1975
+ const bi = b.messageIndex ?? 0;
1976
+ return ai - bi;
1839
1977
  });
1840
1978
  s09ObservedUserTurnCount = Math.max(s09ObservedUserTurnCount, historyMessages.filter(m => m.role === 'user').length);
1979
+ if (s09PersistedTopicBearingTurnCount == null) {
1980
+ s09TopicBearingTurnCount = Math.max(s09TopicBearingTurnCount, countTopicBearingTurns(historyMessages));
1981
+ }
1841
1982
  composeTopicMessageCount = historyMessages.length;
1842
1983
  composeTopicStampedMessageCount = historyMessages.filter(m => typeof m.topicId === 'string').length;
1843
1984
  // ── Transform-first: apply gradient tool treatment BEFORE budget math ──
@@ -1871,6 +2012,12 @@ export class Compositor {
1871
2012
  // drop inactive-topic non-tool clusters first when an active topic is
1872
2013
  // known. Bootstrap/warmup/steady reproduce the historical newest-first
1873
2014
  // sweep exactly (preferTopicAwareDrop=false → evictedByPlan stays empty).
2015
+ const literalAntecedentGuardIndices = resolveLiteralAntecedentGuardClusterIndices(budgetClusters, {
2016
+ enabled: evictionLifecyclePolicy.band !== 'critical',
2017
+ currentPrompt: request.prompt,
2018
+ maxMessages: LITERAL_ANTECEDENT_GUARD_MAX_MESSAGES,
2019
+ maxTokens: LITERAL_ANTECEDENT_GUARD_MAX_TOKENS,
2020
+ });
1874
2021
  const adaptiveOrdering = orderClustersForAdaptiveEviction(budgetClusters, evictionLifecyclePolicy, { activeTopicId });
1875
2022
  adaptiveEvictionTopicAwareEligibleClusters = adaptiveOrdering.telemetry.topicAwareEligibleClusters;
1876
2023
  adaptiveEvictionProtectedClusters = adaptiveOrdering.telemetry.protectedClusters;
@@ -1897,6 +2044,16 @@ export class Compositor {
1897
2044
  break;
1898
2045
  if (adaptiveOrdering.protectedIndices.has(idx))
1899
2046
  continue;
2047
+ if (literalAntecedentGuardIndices.has(idx)) {
2048
+ for (const msg of budgetClusters[idx].messages) {
2049
+ literalAntecedentGuardMessages.add(msg);
2050
+ if (!literalAntecedentGuardHitMessages.has(msg)) {
2051
+ literalAntecedentGuardHitMessages.add(msg);
2052
+ literalAntecedentGuardHits++;
2053
+ }
2054
+ }
2055
+ continue;
2056
+ }
1900
2057
  evictedByPlan.add(idx);
1901
2058
  projectedTokens -= budgetClusters[idx].tokenCost;
1902
2059
  }
@@ -1907,12 +2064,29 @@ export class Compositor {
1907
2064
  if (evictedByPlan.has(i))
1908
2065
  continue;
1909
2066
  const cluster = budgetClusters[i];
2067
+ const isLiteralAntecedentGuarded = literalAntecedentGuardIndices.has(i);
1910
2068
  if (historyTokens + cluster.tokenCost > historyFillCap && includedClusters.length > 0) {
2069
+ if (isLiteralAntecedentGuarded) {
2070
+ includedClusters.unshift(cluster);
2071
+ historyTokens += cluster.tokenCost;
2072
+ for (const msg of cluster.messages) {
2073
+ literalAntecedentGuardMessages.add(msg);
2074
+ if (!literalAntecedentGuardHitMessages.has(msg)) {
2075
+ literalAntecedentGuardHitMessages.add(msg);
2076
+ literalAntecedentGuardHits++;
2077
+ }
2078
+ }
2079
+ continue;
2080
+ }
1911
2081
  truncationCutIndex = i;
1912
2082
  break;
1913
2083
  }
1914
2084
  includedClusters.unshift(cluster);
1915
2085
  historyTokens += cluster.tokenCost;
2086
+ if (isLiteralAntecedentGuarded) {
2087
+ for (const msg of cluster.messages)
2088
+ literalAntecedentGuardMessages.add(msg);
2089
+ }
1916
2090
  }
1917
2091
  if (truncationCutIndex >= 0 || evictedByPlan.size > 0) {
1918
2092
  const droppedIndices = [];
@@ -2403,6 +2577,7 @@ export class Compositor {
2403
2577
  const composeLifecyclePolicy = resolveAdaptiveLifecyclePolicy({
2404
2578
  pressureFraction: s09ComposePolicyPressure,
2405
2579
  userTurnCount: s09ObservedUserTurnCount,
2580
+ topicBearingTurnCount: s09TopicBearingTurnCount,
2406
2581
  explicitNewSession: isExplicitNewSessionPrompt(request.prompt ?? this.getLastUserMessage(messages)),
2407
2582
  forkedContext: Boolean(s09ForkedContextSeed),
2408
2583
  forkedParentPressureFraction: s09ForkedParentPressure,
@@ -2411,6 +2586,8 @@ export class Compositor {
2411
2586
  const recallBreadth = scaleRecallBreadth(remaining, composeLifecyclePolicy.smartRecallMultiplier);
2412
2587
  let diagAdaptiveRecallBudgetTokens;
2413
2588
  let diagAdaptiveRecallCandidateLimit;
2589
+ let diagComposeAdjacencyBoosted = 0;
2590
+ let diagComposeAdjacencyDeltaTotalMs = 0;
2414
2591
  if (request.includeSemanticRecall !== false && remaining > 500 && (this.vectorStore || libDb)) {
2415
2592
  const lastUserMsg = request.prompt?.trim() || this.getLastUserMessage(messages);
2416
2593
  if (lastUserMsg) {
@@ -2435,6 +2612,9 @@ export class Compositor {
2435
2612
  diagRerankerStatus = ev.status;
2436
2613
  diagRerankerCandidates = ev.candidates;
2437
2614
  diagRerankerProvider = ev.provider;
2615
+ }, (ev) => {
2616
+ diagComposeAdjacencyBoosted += ev.boostedCount;
2617
+ diagComposeAdjacencyDeltaTotalMs += ev.averageDeltaMs * ev.boostedCount;
2438
2618
  }, recallBreadth.candidateLimit);
2439
2619
  if (semanticContent) {
2440
2620
  const tokens = estimateTokens(semanticContent);
@@ -2585,7 +2765,7 @@ export class Compositor {
2585
2765
  }
2586
2766
  const fallbackContent = await Promise.race([
2587
2767
  this.buildSemanticRecall(lastMsg, request.agentId, recallBreadth.fallbackBudgetTokens, libDb || undefined, undefined, contextFingerprints, // C2: skip results already in Active Facts
2588
- undefined, recallBreadth.candidateLimit),
2768
+ undefined, undefined, recallBreadth.candidateLimit),
2589
2769
  new Promise((_, reject) => setTimeout(() => reject(new Error('fallback_knn_timeout')), 3000)),
2590
2770
  ]);
2591
2771
  if (fallbackContent) {
@@ -2768,6 +2948,15 @@ export class Compositor {
2768
2948
  // Don't trim the last user message (current prompt).
2769
2949
  if (i === messages.length - 1 && messages[i].role === 'user')
2770
2950
  break;
2951
+ // Packet 5: preserve the bounded immediate antecedent for literal follow-ups.
2952
+ if (literalAntecedentGuardMessages.has(messages[i])) {
2953
+ if (!literalAntecedentGuardHitMessages.has(messages[i])) {
2954
+ literalAntecedentGuardHitMessages.add(messages[i]);
2955
+ literalAntecedentGuardHits++;
2956
+ }
2957
+ i++;
2958
+ continue;
2959
+ }
2771
2960
  // Sprint 4: Don't trim the volatile context block (dynamicBoundary marker).
2772
2961
  const meta = messages[i].metadata;
2773
2962
  if (meta?.dynamicBoundary) {
@@ -3040,6 +3229,9 @@ export class Compositor {
3040
3229
  adaptiveSmartRecallMultiplier: composeLifecyclePolicy.smartRecallMultiplier,
3041
3230
  adaptiveTrimSoftTarget: composeLifecyclePolicy.trimSoftTarget,
3042
3231
  adaptiveCompactionTargetFraction: composeLifecyclePolicy.compactionTargetFraction,
3232
+ adaptiveProtectedWarmingFraction: composeLifecyclePolicy.protectedWarmingMetadata.isProtected
3233
+ ? composeLifecyclePolicy.protectedWarmingMetadata.floor
3234
+ : undefined,
3043
3235
  adaptiveBreadcrumbPackage: composeLifecyclePolicy.emitBreadcrumbPackage,
3044
3236
  adaptiveTopicCentroidEviction: composeLifecyclePolicy.enableTopicCentroidEviction,
3045
3237
  adaptiveProactiveCompaction: composeLifecyclePolicy.triggerProactiveCompaction,
@@ -3053,6 +3245,11 @@ export class Compositor {
3053
3245
  adaptiveEvictionProtectedClusters,
3054
3246
  adaptiveEvictionTopicIdCoveragePct,
3055
3247
  adaptiveEvictionBypassReason,
3248
+ composeAdjacencyBoosted: diagComposeAdjacencyBoosted > 0 ? diagComposeAdjacencyBoosted : undefined,
3249
+ composeAdjacencyAverageDeltaMs: diagComposeAdjacencyBoosted > 0
3250
+ ? Math.round(diagComposeAdjacencyDeltaTotalMs / diagComposeAdjacencyBoosted)
3251
+ : undefined,
3252
+ evictionAdjacencyGuardHits: literalAntecedentGuardHits > 0 ? literalAntecedentGuardHits : undefined,
3056
3253
  composeTopicSource,
3057
3254
  composeTopicState,
3058
3255
  composeTopicMessageCount,
@@ -3092,6 +3289,9 @@ export class Compositor {
3092
3289
  compactionEligibleRatio: diagCompactionEligibleRatio,
3093
3290
  compactionProcessedCount: diagCompactionProcessedCount,
3094
3291
  };
3292
+ if (literalAntecedentGuardHits > 0) {
3293
+ diagnostics.literalAntecedentGuardHits = literalAntecedentGuardHits;
3294
+ }
3095
3295
  if (pressureHigh) {
3096
3296
  warnings.push(`SESSION_PRESSURE_HIGH: avg_turn_cost=${avgTurnCost} tokens, dynamic reserve capped at ${Math.round(dynamicReserve * 100)}%`);
3097
3297
  }
@@ -3203,7 +3403,7 @@ export class Compositor {
3203
3403
  catch (error) {
3204
3404
  console.warn(`[hypermem:compositor] composition snapshot write skipped: ${error.message}`);
3205
3405
  }
3206
- console.log(`[hypermem:compose] agent=${request.agentId} triggers=${diagTriggerHits} fallback=${diagTriggerFallbackUsed} facts=${diagFactsIncluded} semantic=${diagSemanticResults} chunks=${diagDocChunkCollections} scopeFiltered=${diagScopeFiltered} mode=${diagRetrievalMode} crossTopicKeystones=${diagCrossTopicKeystones} c2_degradations=${c2ArtifactDegradations} c2_threshold=${c2ArtifactThresholdTokens}`);
3406
+ console.log(`[hypermem:compose] agent=${request.agentId} triggers=${diagTriggerHits} fallback=${diagTriggerFallbackUsed} facts=${diagFactsIncluded} semantic=${diagSemanticResults} chunks=${diagDocChunkCollections} scopeFiltered=${diagScopeFiltered} mode=${diagRetrievalMode} crossTopicKeystones=${diagCrossTopicKeystones} c2_degradations=${c2ArtifactDegradations} c2_threshold=${c2ArtifactThresholdTokens} literal_antecedent_guard_hits=${literalAntecedentGuardHits} adjacency_boosted=${diagComposeAdjacencyBoosted} adjacency_avg_delta_ms=${diagComposeAdjacencyBoosted > 0 ? Math.round(diagComposeAdjacencyDeltaTotalMs / diagComposeAdjacencyBoosted) : 0} eviction_adjacency_guard_hits=${literalAntecedentGuardHits}`);
3207
3407
  return {
3208
3408
  messages: outputMessages,
3209
3409
  tokenCount: totalTokens,
@@ -3454,23 +3654,37 @@ export class Compositor {
3454
3654
  let historyToWrite = transformedHistory;
3455
3655
  if (tokenBudget && tokenBudget > 0) {
3456
3656
  const budgetCap = gradientAssembleBudget;
3657
+ const protectedFloor = resolveAfterTurnProtectedWarmingFloor(tokenBudget, trimSoftTarget);
3457
3658
  let runningTokens = 0;
3659
+ let protectedClustersKept = 0;
3458
3660
  const clusters = clusterNeutralMessages(transformedHistory);
3459
3661
  const cappedClusters = [];
3460
3662
  // Walk newest-first, keep whole clusters so tool-call/result pairs survive together.
3663
+ // Packet 3: during bootstrap/warmup, do not let cluster-boundary underfill
3664
+ // cut the refreshed hot window below the protected warming floor. This may
3665
+ // keep one or more whole older clusters past the soft cap, but only until
3666
+ // the protected floor is satisfied. Elevated/high/critical bands get no
3667
+ // floor, so the existing cap behavior remains intact under real pressure.
3461
3668
  for (let i = clusters.length - 1; i >= 0; i--) {
3462
3669
  const cluster = clusters[i];
3463
- if (runningTokens + cluster.tokenCost > budgetCap && cappedClusters.length > 0)
3670
+ const wouldExceedCap = runningTokens + cluster.tokenCost > budgetCap && cappedClusters.length > 0;
3671
+ const belowProtectedFloor = protectedFloor.floorTokens > 0 && runningTokens < protectedFloor.floorTokens;
3672
+ if (wouldExceedCap && !belowProtectedFloor)
3464
3673
  break;
3674
+ if (wouldExceedCap && belowProtectedFloor)
3675
+ protectedClustersKept++;
3465
3676
  cappedClusters.unshift(cluster);
3466
3677
  runningTokens += cluster.tokenCost;
3467
- if (runningTokens >= budgetCap)
3678
+ if (runningTokens >= budgetCap && runningTokens >= protectedFloor.floorTokens)
3468
3679
  break;
3469
3680
  }
3470
3681
  historyToWrite = cappedClusters.flatMap(cluster => cluster.messages);
3471
3682
  if (historyToWrite.length < transformedHistory.length) {
3683
+ const protectedNote = protectedFloor.floorTokens > 0
3684
+ ? `, protectedFloor=${protectedFloor.floorTokens}, protectedClustersKept=${protectedClustersKept}`
3685
+ : '';
3472
3686
  console.log(`[hypermem] refreshRedisGradient: cluster-capped ${transformedHistory.length}→${historyToWrite.length} messages ` +
3473
- `for ${agentId}/${sessionKey} (budgetCap=${budgetCap}, tokenCost=${runningTokens})`);
3687
+ `for ${agentId}/${sessionKey} (budgetCap=${budgetCap}, tokenCost=${runningTokens}${protectedNote})`);
3474
3688
  }
3475
3689
  }
3476
3690
  await this.cache.replaceHistory(agentId, sessionKey, historyToWrite, refreshHistoryLimit);
@@ -3706,7 +3920,7 @@ export class Compositor {
3706
3920
  */
3707
3921
  async buildSemanticRecall(userMessage, agentId, maxTokens, libraryDb, precomputedEmbedding, existingFingerprints, // C2: skip results already in Active Facts
3708
3922
  onRerankerTelemetry, // Sprint 1: surface reranker status at assemble level
3709
- resultLimit) {
3923
+ onAdjacencyTelemetry, resultLimit) {
3710
3924
  const libDb = libraryDb || this.libraryDb;
3711
3925
  if (!libDb && !this.vectorStore)
3712
3926
  return null;
@@ -3736,6 +3950,7 @@ export class Compositor {
3736
3950
  rerankerTopK: this.rerankerTopK,
3737
3951
  // Sprint 1: thread reranker telemetry into compose diagnostics
3738
3952
  onRerankerTelemetry,
3953
+ onAdjacencyTelemetry,
3739
3954
  });
3740
3955
  if (results.length === 0)
3741
3956
  return null;
@@ -3861,9 +4076,6 @@ export class Compositor {
3861
4076
  * Build cross-session context by finding recent activity
3862
4077
  * in other sessions for this agent.
3863
4078
  */
3864
- // TODO Phase 1: buildCrossSessionContext queries OTHER conversations. Each has its
3865
- // own compaction fence. Per-conversation fence filtering should be added here so
3866
- // zombie messages from other sessions don't leak into cross-session context.
3867
4079
  buildCrossSessionContext(agentId, currentSessionKey, db, _libraryDb, existingFingerprints // C3: skip entries already in facts/semantic recall
3868
4080
  ) {
3869
4081
  const conversation = db.prepare('SELECT id FROM conversations WHERE session_key = ?').get(currentSessionKey);
@@ -3873,10 +4085,14 @@ export class Compositor {
3873
4085
  SELECT m.text_content, m.role, c.channel_type, m.created_at
3874
4086
  FROM messages m
3875
4087
  JOIN conversations c ON m.conversation_id = c.id
4088
+ LEFT JOIN compaction_fences cf ON cf.conversation_id = m.conversation_id
3876
4089
  WHERE c.agent_id = ?
3877
4090
  AND m.conversation_id != ?
3878
4091
  AND c.status = 'active'
4092
+ AND (cf.fence_message_id IS NULL OR m.id >= cf.fence_message_id)
4093
+ AND m.role IN ('user', 'assistant')
3879
4094
  AND m.text_content IS NOT NULL
4095
+ AND trim(m.text_content) != ''
3880
4096
  AND m.is_heartbeat = 0
3881
4097
  ORDER BY m.created_at DESC
3882
4098
  LIMIT 10
@@ -4014,9 +4230,10 @@ export class Compositor {
4014
4230
  AND m.id < ?
4015
4231
  ${fenceClause}
4016
4232
  ${contextClause}
4233
+ AND m.role IN ('user', 'assistant')
4017
4234
  AND m.text_content IS NOT NULL
4235
+ AND trim(m.text_content) != ''
4018
4236
  AND m.is_heartbeat = 0
4019
- AND m.text_content != ''
4020
4237
  LIMIT 200
4021
4238
  `;
4022
4239
  let candidateRows;
@@ -4046,9 +4263,10 @@ export class Compositor {
4046
4263
  AND m.id < ?
4047
4264
  ${fenceClause}
4048
4265
  ${contextClause}
4266
+ AND m.role IN ('user', 'assistant')
4049
4267
  AND m.text_content IS NOT NULL
4268
+ AND trim(m.text_content) != ''
4050
4269
  AND m.is_heartbeat = 0
4051
- AND m.text_content != ''
4052
4270
  AND m.id IN (
4053
4271
  SELECT rowid FROM messages_fts
4054
4272
  WHERE messages_fts MATCH ?
@@ -4187,8 +4405,9 @@ export class Compositor {
4187
4405
  AND m.topic_id = ?
4188
4406
  ${topicFenceClause}
4189
4407
  ${topicContextClause}
4408
+ AND m.role IN ('user', 'assistant')
4190
4409
  AND m.text_content IS NOT NULL
4191
- AND m.text_content != ''
4410
+ AND trim(m.text_content) != ''
4192
4411
  AND m.is_heartbeat = 0
4193
4412
  ORDER BY m.message_index DESC
4194
4413
  LIMIT 50
@@ -25,6 +25,12 @@ export interface RerankerTelemetry {
25
25
  /** Outcome for this invocation. */
26
26
  status: RerankerStatus;
27
27
  }
28
+ export interface AdjacencyTelemetry {
29
+ /** Number of candidates boosted by adjacency scoring. */
30
+ boostedCount: number;
31
+ /** Average antecedent-to-successor wall-clock delta in milliseconds. */
32
+ averageDeltaMs: number;
33
+ }
28
34
  export interface HybridSearchResult {
29
35
  sourceTable: string;
30
36
  sourceId: number;
@@ -86,6 +92,8 @@ export interface HybridSearchOptions {
86
92
  rerankerTimeoutMs?: number;
87
93
  /** Optional telemetry sink. When omitted, falls back to emitRerankerLog. */
88
94
  onRerankerTelemetry?: (ev: RerankerTelemetry) => void;
95
+ /** Optional metadata-only telemetry for adjacency boosts. */
96
+ onAdjacencyTelemetry?: (ev: AdjacencyTelemetry) => void;
89
97
  }
90
98
  /**
91
99
  * Build an FTS5 query from a natural language string.
@@ -1 +1 @@
1
- {"version":3,"file":"hybrid-retrieval.d.ts","sourceRoot":"","sources":["../src/hybrid-retrieval.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,mBAAmB,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQtD,MAAM,MAAM,cAAc,GACtB,SAAS,GACT,oBAAoB,GACpB,wBAAwB,GACxB,QAAQ,GACR,SAAS,CAAC;AAEd,MAAM,WAAW,iBAAiB;IAChC,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,MAAM,EAAE,cAAc,CAAC;CACxB;AAcD,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,OAAO,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,oBAAoB,CAAC,EAAE,YAAY,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACnC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACvD;AAqBD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBnD;AA8ND;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,YAAY,EACvB,WAAW,EAAE,WAAW,GAAG,IAAI,EAC/B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAgJ/B"}
1
+ {"version":3,"file":"hybrid-retrieval.d.ts","sourceRoot":"","sources":["../src/hybrid-retrieval.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,mBAAmB,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQtD,MAAM,MAAM,cAAc,GACtB,SAAS,GACT,oBAAoB,GACpB,wBAAwB,GACxB,QAAQ,GACR,SAAS,CAAC;AAEd,MAAM,WAAW,iBAAiB;IAChC,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,cAAc,EAAE,MAAM,CAAC;CACxB;AAcD,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,OAAO,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,oBAAoB,CAAC,EAAE,YAAY,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACnC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,mBAAmB,CAAC,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACtD,6DAA6D;IAC7D,oBAAoB,CAAC,EAAE,CAAC,EAAE,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACzD;AAqBD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBnD;AAkUD;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,YAAY,EACvB,WAAW,EAAE,WAAW,GAAG,IAAI,EAC/B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAqJ/B"}