@remnic/core 9.62.0 → 9.62.1

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 (158) hide show
  1. package/dist/access-admin-ops-surface.js +11 -11
  2. package/dist/access-authorization-probe.js +12 -12
  3. package/dist/access-boundary.js +11 -11
  4. package/dist/access-cli.js +43 -43
  5. package/dist/access-extraction-force-flush.js +11 -11
  6. package/dist/access-http-query.js +11 -11
  7. package/dist/access-http.js +20 -20
  8. package/dist/access-identity-continuity-surface.js +11 -11
  9. package/dist/access-lcm-surface.js +11 -11
  10. package/dist/access-mcp.js +16 -16
  11. package/dist/access-namespace-preflight.js +11 -11
  12. package/dist/access-observe-write-surface.js +11 -11
  13. package/dist/access-operations-batch.js +12 -12
  14. package/dist/access-operations.js +15 -15
  15. package/dist/access-recall-concurrency.js +11 -11
  16. package/dist/access-recall-response.js +11 -11
  17. package/dist/access-recall-surface.js +11 -11
  18. package/dist/access-service.js +11 -11
  19. package/dist/access-surface-catalog.js +13 -13
  20. package/dist/active-memory-bridge.js +2 -2
  21. package/dist/active-recall.js +3 -3
  22. package/dist/briefing.js +4 -4
  23. package/dist/causal-consolidation.js +5 -5
  24. package/dist/{chunk-KAB7TZN3.js → chunk-3G6YU5YD.js} +2 -2
  25. package/dist/{chunk-XD4VGLDL.js → chunk-3XG4QEA2.js} +35 -31
  26. package/dist/chunk-3XG4QEA2.js.map +1 -0
  27. package/dist/{chunk-HEEEMJ26.js → chunk-4E67XCT2.js} +3 -3
  28. package/dist/{chunk-H32ANIFP.js → chunk-4G45LJJP.js} +2 -2
  29. package/dist/{chunk-4XAVQLBR.js → chunk-4Y2RHVCV.js} +2 -2
  30. package/dist/{chunk-S3B3UIQ3.js → chunk-67MKNKJQ.js} +2 -2
  31. package/dist/{chunk-PA5OPZX2.js → chunk-75U2FR7U.js} +2 -2
  32. package/dist/{chunk-I67HKRBF.js → chunk-7JAXC526.js} +2 -2
  33. package/dist/{chunk-3GPBFDXH.js → chunk-7PVTOZR4.js} +2 -2
  34. package/dist/{chunk-DSA47WBN.js → chunk-BHO3IVAN.js} +2 -2
  35. package/dist/{chunk-MYZ4NA43.js → chunk-DYDACAK2.js} +2 -2
  36. package/dist/{chunk-4JGSG7QN.js → chunk-ETCVSZLG.js} +2 -2
  37. package/dist/{chunk-EIQFIJDM.js → chunk-EU74U4GT.js} +2 -2
  38. package/dist/{chunk-L33N3BCA.js → chunk-GZRDOVQN.js} +2 -2
  39. package/dist/{chunk-AWRJBAIH.js → chunk-HKYNBEL4.js} +6 -6
  40. package/dist/{chunk-MJ4UGUR6.js → chunk-IIRK5V5Z.js} +2 -2
  41. package/dist/{chunk-2RIDW23Y.js → chunk-IXJ5U6ZL.js} +8 -8
  42. package/dist/{chunk-QRKYABS6.js → chunk-JDJOAL4B.js} +2 -2
  43. package/dist/{chunk-GN7HO2LA.js → chunk-JF6YSHJM.js} +7 -7
  44. package/dist/{chunk-D5HDJWNN.js → chunk-JLMH5YMU.js} +3 -3
  45. package/dist/{chunk-FW73HN33.js → chunk-K4WIUTNH.js} +2 -2
  46. package/dist/{chunk-TE4MKFT2.js → chunk-KCFZOSR6.js} +4 -4
  47. package/dist/{chunk-VWDTV6SH.js → chunk-KYI3CM42.js} +2 -2
  48. package/dist/{chunk-CIA6ZUBA.js → chunk-LAM6UDFV.js} +2 -2
  49. package/dist/{chunk-W4Q54HM3.js → chunk-LLLY5IBK.js} +2 -2
  50. package/dist/{chunk-CM4SZZBN.js → chunk-MJFDJTDC.js} +2 -2
  51. package/dist/{chunk-VOELBUS4.js → chunk-NXH34IPL.js} +2 -2
  52. package/dist/{chunk-MU3TPZOV.js → chunk-PBH2Z2NY.js} +2 -2
  53. package/dist/chunk-PJKTFXE2.js +119 -0
  54. package/dist/chunk-PJKTFXE2.js.map +1 -0
  55. package/dist/{chunk-G3YBWFUC.js → chunk-QN7XQEIT.js} +34 -17
  56. package/dist/chunk-QN7XQEIT.js.map +1 -0
  57. package/dist/{chunk-HQBVQICL.js → chunk-QNMNNFKD.js} +2 -2
  58. package/dist/{chunk-EGW4O32Z.js → chunk-QRA3XVJX.js} +7 -4
  59. package/dist/chunk-QRA3XVJX.js.map +1 -0
  60. package/dist/{chunk-L4DLLKPN.js → chunk-SZ4HFZKB.js} +2 -2
  61. package/dist/{chunk-N4OYPGZS.js → chunk-TLUSSS2F.js} +2 -2
  62. package/dist/{chunk-N4EPIKSY.js → chunk-U27G4E5G.js} +3 -3
  63. package/dist/{chunk-W3Y6FYRK.js → chunk-UIRU3MIJ.js} +2 -2
  64. package/dist/{chunk-URN7XWGC.js → chunk-WANOCU3K.js} +2 -2
  65. package/dist/{chunk-XDK6ZFLL.js → chunk-WTMG5GAK.js} +2 -2
  66. package/dist/{chunk-C3R732UY.js → chunk-XIABRKF6.js} +37 -32
  67. package/dist/chunk-XIABRKF6.js.map +1 -0
  68. package/dist/{chunk-D4OCJSLV.js → chunk-YDPAE2RD.js} +2 -2
  69. package/dist/{chunk-FQSDU634.js → chunk-YG56A6XY.js} +2 -2
  70. package/dist/{chunk-XBR5BAKW.js → chunk-YSUSUZR4.js} +3 -3
  71. package/dist/{chunk-YUIBXMZY.js → chunk-ZJKLGS76.js} +2 -2
  72. package/dist/cli.js +32 -32
  73. package/dist/compounding/engine.js +4 -4
  74. package/dist/config.js +3 -3
  75. package/dist/connectors/codex-materialize-runner.js +4 -4
  76. package/dist/connectors/index.js +4 -4
  77. package/dist/convergence-refresh.js +6 -6
  78. package/dist/corpus-watermark.js +6 -6
  79. package/dist/entity-retrieval.js +4 -4
  80. package/dist/external-wiki-access.js +12 -12
  81. package/dist/extraction-source-grounding.js +3 -3
  82. package/dist/extraction.js +7 -7
  83. package/dist/harmonic-retrieval.js +1 -1
  84. package/dist/index.d.ts +1 -0
  85. package/dist/index.js +56 -43
  86. package/dist/maintenance/memory-governance.js +4 -4
  87. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  88. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  89. package/dist/namespaces/migrate.js +5 -5
  90. package/dist/namespaces/storage.js +4 -4
  91. package/dist/operator-doctor-corpus.js +7 -7
  92. package/dist/operator-toolkit.js +12 -12
  93. package/dist/orchestration/maintenance.js +9 -9
  94. package/dist/orchestrator.js +43 -43
  95. package/dist/provenance.js +2 -2
  96. package/dist/resume-bundles.js +4 -4
  97. package/dist/semantic-consolidation.js +5 -5
  98. package/dist/semantic-rule-promotion.js +4 -4
  99. package/dist/semantic-rule-verifier.js +4 -4
  100. package/dist/source-agent-qualifier.js +6 -6
  101. package/dist/storage.js +3 -3
  102. package/dist/support-passport/index.js +14 -14
  103. package/dist/verified-recall.js +4 -4
  104. package/dist/whitespace.d.ts +26 -1
  105. package/dist/whitespace.js +11 -3
  106. package/package.json +2 -2
  107. package/src/cli.ts +14 -13
  108. package/src/entity-retrieval.ts +4 -7
  109. package/src/harmonic-construction.test.ts +187 -2
  110. package/src/harmonic-retrieval.ts +54 -32
  111. package/src/index.ts +3 -4
  112. package/src/orchestration/recall-section-coordinator.ts +20 -14
  113. package/src/whitespace.test.ts +178 -0
  114. package/src/whitespace.ts +119 -0
  115. package/dist/chunk-6MKAMLQL.js +0 -16
  116. package/dist/chunk-6MKAMLQL.js.map +0 -1
  117. package/dist/chunk-C3R732UY.js.map +0 -1
  118. package/dist/chunk-EGW4O32Z.js.map +0 -1
  119. package/dist/chunk-G3YBWFUC.js.map +0 -1
  120. package/dist/chunk-XD4VGLDL.js.map +0 -1
  121. /package/dist/{chunk-KAB7TZN3.js.map → chunk-3G6YU5YD.js.map} +0 -0
  122. /package/dist/{chunk-HEEEMJ26.js.map → chunk-4E67XCT2.js.map} +0 -0
  123. /package/dist/{chunk-H32ANIFP.js.map → chunk-4G45LJJP.js.map} +0 -0
  124. /package/dist/{chunk-4XAVQLBR.js.map → chunk-4Y2RHVCV.js.map} +0 -0
  125. /package/dist/{chunk-S3B3UIQ3.js.map → chunk-67MKNKJQ.js.map} +0 -0
  126. /package/dist/{chunk-PA5OPZX2.js.map → chunk-75U2FR7U.js.map} +0 -0
  127. /package/dist/{chunk-I67HKRBF.js.map → chunk-7JAXC526.js.map} +0 -0
  128. /package/dist/{chunk-3GPBFDXH.js.map → chunk-7PVTOZR4.js.map} +0 -0
  129. /package/dist/{chunk-DSA47WBN.js.map → chunk-BHO3IVAN.js.map} +0 -0
  130. /package/dist/{chunk-MYZ4NA43.js.map → chunk-DYDACAK2.js.map} +0 -0
  131. /package/dist/{chunk-4JGSG7QN.js.map → chunk-ETCVSZLG.js.map} +0 -0
  132. /package/dist/{chunk-EIQFIJDM.js.map → chunk-EU74U4GT.js.map} +0 -0
  133. /package/dist/{chunk-L33N3BCA.js.map → chunk-GZRDOVQN.js.map} +0 -0
  134. /package/dist/{chunk-AWRJBAIH.js.map → chunk-HKYNBEL4.js.map} +0 -0
  135. /package/dist/{chunk-MJ4UGUR6.js.map → chunk-IIRK5V5Z.js.map} +0 -0
  136. /package/dist/{chunk-2RIDW23Y.js.map → chunk-IXJ5U6ZL.js.map} +0 -0
  137. /package/dist/{chunk-QRKYABS6.js.map → chunk-JDJOAL4B.js.map} +0 -0
  138. /package/dist/{chunk-GN7HO2LA.js.map → chunk-JF6YSHJM.js.map} +0 -0
  139. /package/dist/{chunk-D5HDJWNN.js.map → chunk-JLMH5YMU.js.map} +0 -0
  140. /package/dist/{chunk-FW73HN33.js.map → chunk-K4WIUTNH.js.map} +0 -0
  141. /package/dist/{chunk-TE4MKFT2.js.map → chunk-KCFZOSR6.js.map} +0 -0
  142. /package/dist/{chunk-VWDTV6SH.js.map → chunk-KYI3CM42.js.map} +0 -0
  143. /package/dist/{chunk-CIA6ZUBA.js.map → chunk-LAM6UDFV.js.map} +0 -0
  144. /package/dist/{chunk-W4Q54HM3.js.map → chunk-LLLY5IBK.js.map} +0 -0
  145. /package/dist/{chunk-CM4SZZBN.js.map → chunk-MJFDJTDC.js.map} +0 -0
  146. /package/dist/{chunk-VOELBUS4.js.map → chunk-NXH34IPL.js.map} +0 -0
  147. /package/dist/{chunk-MU3TPZOV.js.map → chunk-PBH2Z2NY.js.map} +0 -0
  148. /package/dist/{chunk-HQBVQICL.js.map → chunk-QNMNNFKD.js.map} +0 -0
  149. /package/dist/{chunk-L4DLLKPN.js.map → chunk-SZ4HFZKB.js.map} +0 -0
  150. /package/dist/{chunk-N4OYPGZS.js.map → chunk-TLUSSS2F.js.map} +0 -0
  151. /package/dist/{chunk-N4EPIKSY.js.map → chunk-U27G4E5G.js.map} +0 -0
  152. /package/dist/{chunk-W3Y6FYRK.js.map → chunk-UIRU3MIJ.js.map} +0 -0
  153. /package/dist/{chunk-URN7XWGC.js.map → chunk-WANOCU3K.js.map} +0 -0
  154. /package/dist/{chunk-XDK6ZFLL.js.map → chunk-WTMG5GAK.js.map} +0 -0
  155. /package/dist/{chunk-D4OCJSLV.js.map → chunk-YDPAE2RD.js.map} +0 -0
  156. /package/dist/{chunk-FQSDU634.js.map → chunk-YG56A6XY.js.map} +0 -0
  157. /package/dist/{chunk-XBR5BAKW.js.map → chunk-YSUSUZR4.js.map} +0 -0
  158. /package/dist/{chunk-YUIBXMZY.js.map → chunk-ZJKLGS76.js.map} +0 -0
package/src/cli.ts CHANGED
@@ -12,6 +12,7 @@ import { resolveNamespaceCapabilities,
12
12
  import { ThreadingManager } from "./threading.js";
13
13
  import { bumpMemoryCorpusVersionForDir } from "./memory-corpus-version.js";
14
14
  import { utcDayRange } from "./transcript.js";
15
+ import { padEndDisplay, truncateGraphemeSafe } from "./whitespace.js";
15
16
  import { runWearablesCliCommand } from "./wearables/cli.js";
16
17
  import { registerMeetingsCommands } from "./cli/meetings-commands.js";
17
18
  import { registerResearchStatusCommands } from "./cli/research-status-commands.js";
@@ -5672,7 +5673,7 @@ export function registerCli(
5672
5673
  console.log(` raw excerpts (${r.rawExcerpts.length}):`);
5673
5674
  for (const ex of r.rawExcerpts) {
5674
5675
  console.log(
5675
- ` [turn ${ex.turnIndex}, ${ex.role}] ${ex.content.slice(0, 200)}`,
5676
+ ` [turn ${ex.turnIndex}, ${ex.role}] ${truncateGraphemeSafe(ex.content, 200)}`,
5676
5677
  );
5677
5678
  }
5678
5679
  }
@@ -7614,7 +7615,7 @@ export function registerCli(
7614
7615
  console.log(` ${r.path} (score: ${r.score.toFixed(3)})`);
7615
7616
  if (r.snippet) {
7616
7617
  console.log(
7617
- ` ${r.snippet.slice(0, 150).replace(/\n/g, " ")}`,
7618
+ ` ${truncateGraphemeSafe(r.snippet, 150).replace(/\n/g, " ")}`,
7618
7619
  );
7619
7620
  }
7620
7621
  console.log();
@@ -7640,7 +7641,7 @@ export function registerCli(
7640
7641
  console.log(`\n=== Text Search Fallback: "${query}" (${matches.length} results) ===\n`);
7641
7642
  console.log(`QMD status: ${qmdStatus}\n`);
7642
7643
  for (const m of matches.slice(0, maxResults)) {
7643
- console.log(` [${m.frontmatter.category}] ${m.content.slice(0, 120)}`);
7644
+ console.log(` [${m.frontmatter.category}] ${truncateGraphemeSafe(m.content, 120)}`);
7644
7645
  }
7645
7646
  }
7646
7647
  });
@@ -7783,12 +7784,12 @@ export function registerCli(
7783
7784
  for (const cluster of result.clusters) {
7784
7785
  console.log(`\n Category: ${cluster.category} (${cluster.memories.length} memories, overlap=${cluster.overlapScore.toFixed(2)})`);
7785
7786
  for (const m of cluster.memories) {
7786
- const preview = m.content.length > 80 ? m.content.slice(0, 80) + "..." : m.content;
7787
+ const preview = m.content.length > 80 ? `${truncateGraphemeSafe(m.content, 80)}...` : m.content;
7787
7788
  console.log(` - ${m.frontmatter.id}: ${preview}`);
7788
7789
  }
7789
7790
  if (cluster.canonicalContent) {
7790
7791
  const preview = cluster.canonicalContent.length > 120
7791
- ? cluster.canonicalContent.slice(0, 120) + "..."
7792
+ ? `${truncateGraphemeSafe(cluster.canonicalContent, 120)}...`
7792
7793
  : cluster.canonicalContent;
7793
7794
  console.log(` → Canonical: ${preview}`);
7794
7795
  }
@@ -8018,7 +8019,7 @@ export function registerCli(
8018
8019
  const lastAccessed = m.frontmatter.lastAccessed
8019
8020
  ? new Date(m.frontmatter.lastAccessed).toLocaleDateString()
8020
8021
  : "unknown";
8021
- console.log(` ${m.frontmatter.accessCount}x [${m.frontmatter.category}] ${m.content.slice(0, 80)}`);
8022
+ console.log(` ${m.frontmatter.accessCount}x [${m.frontmatter.category}] ${truncateGraphemeSafe(m.content, 80)}`);
8022
8023
  console.log(` Last accessed: ${lastAccessed} ID: ${m.frontmatter.id}`);
8023
8024
  console.log();
8024
8025
  }
@@ -8071,7 +8072,7 @@ export function registerCli(
8071
8072
  console.log("\n=== Importance Distribution ===\n");
8072
8073
  for (const [level, count] of Object.entries(levelCounts)) {
8073
8074
  const bar = "█".repeat(Math.min(count, 50));
8074
- console.log(` ${level.padEnd(10)} ${count.toString().padStart(4)} ${bar}`);
8075
+ console.log(` ${padEndDisplay(level, 10)} ${count.toString().padStart(4)} ${bar}`);
8075
8076
  }
8076
8077
  console.log(`\n Total scored: ${withImportance.length} / ${memories.length} memories\n`);
8077
8078
 
@@ -8104,7 +8105,7 @@ export function registerCli(
8104
8105
  console.log(
8105
8106
  ` ${imp.score.toFixed(2)} [${imp.level}] [${m.frontmatter.category}]`,
8106
8107
  );
8107
- console.log(` ${m.content.slice(0, 100)}`);
8108
+ console.log(` ${truncateGraphemeSafe(m.content, 100)}`);
8108
8109
  if (imp.keywords.length > 0) {
8109
8110
  console.log(` Keywords: ${imp.keywords.join(", ")}`);
8110
8111
  }
@@ -8131,7 +8132,7 @@ export function registerCli(
8131
8132
 
8132
8133
  for (const topic of topics.slice(0, top)) {
8133
8134
  const bar = "█".repeat(Math.min(Math.round(topic.score * 10), 30));
8134
- console.log(` ${topic.term.padEnd(20)} ${topic.score.toFixed(3)} (${topic.count}x) ${bar}`);
8135
+ console.log(` ${padEndDisplay(topic.term, 20)} ${topic.score.toFixed(3)} (${topic.count}x) ${bar}`);
8135
8136
  }
8136
8137
  });
8137
8138
 
@@ -8163,7 +8164,7 @@ export function registerCli(
8163
8164
  console.log(` Time range: ${summary.timeRangeStart.slice(0, 10)} to ${summary.timeRangeEnd.slice(0, 10)}`);
8164
8165
  console.log(` Source memories: ${summary.sourceEpisodeIds.length}`);
8165
8166
  console.log(` Key facts: ${summary.keyFacts.length}`);
8166
- console.log(`\n Summary: ${summary.summaryText.slice(0, 200)}...`);
8167
+ console.log(`\n Summary: ${truncateGraphemeSafe(summary.summaryText, 200)}...`);
8167
8168
  console.log();
8168
8169
  }
8169
8170
  });
@@ -8255,13 +8256,13 @@ export function registerCli(
8255
8256
  const parent = memories.find((m) => m.frontmatter.id === parentId);
8256
8257
  console.log(`\n=== Chunks for ${parentId} ===\n`);
8257
8258
  if (parent) {
8258
- console.log(`Parent: ${parent.content.slice(0, 100)}...`);
8259
+ console.log(`Parent: ${truncateGraphemeSafe(parent.content, 100)}...`);
8259
8260
  console.log();
8260
8261
  }
8261
8262
 
8262
8263
  for (const chunk of chunks) {
8263
8264
  console.log(
8264
- ` [${(chunk.frontmatter.chunkIndex ?? 0) + 1}/${chunk.frontmatter.chunkTotal}] ${chunk.content.slice(0, 80)}...`,
8265
+ ` [${(chunk.frontmatter.chunkIndex ?? 0) + 1}/${chunk.frontmatter.chunkTotal}] ${truncateGraphemeSafe(chunk.content, 80)}...`,
8265
8266
  );
8266
8267
  }
8267
8268
  return;
@@ -9408,7 +9409,7 @@ function formatTranscript(entries: TranscriptEntry[]): string {
9408
9409
  return entries
9409
9410
  .map((e) => {
9410
9411
  const time = e.timestamp.slice(11, 16); // HH:MM
9411
- return `[${time}] ${e.role}: ${e.content.slice(0, 200)}${e.content.length > 200 ? "..." : ""}`;
9412
+ return `[${time}] ${e.role}: ${truncateGraphemeSafe(e.content, 200)}${e.content.length > 200 ? "..." : ""}`;
9412
9413
  })
9413
9414
  .join("\n");
9414
9415
  }
@@ -8,12 +8,9 @@ import { collectNativeKnowledgeChunks, type NativeKnowledgeChunk } from "./nativ
8
8
  import { compareEntityTimestamps, normalizeEntityName, type StorageManager } from "./storage.js";
9
9
  import { normalizeEntityText, resolveRequestedEntitySectionKeys } from "./entity-schema.js";
10
10
  import type { EntityStructuredSection, MemoryFile, PluginConfig, TranscriptEntry } from "./types.js";
11
+ import { truncateGraphemeSafe } from "./whitespace.js";
11
12
  import { containsPhrase } from "./entity-retrieval-boundaries.js";
12
- import {
13
- buildOriginStructuredSections,
14
- sanitizeOriginatedFacts,
15
- type EntityOriginStructuredSection,
16
- } from "./entity-origin-fields.js";
13
+ import { buildOriginStructuredSections, sanitizeOriginatedFacts, type EntityOriginStructuredSection } from "./entity-origin-fields.js";
17
14
  import {
18
15
  checkEntityRecallAbort,
19
16
  entityRecallSectionAbsent,
@@ -106,7 +103,7 @@ function uniqueStrings(values: string[]): string[] {
106
103
  function compactLine(value: string, maxLength: number = 220): string {
107
104
  const normalized = value.replace(/\s+/g, " ").trim();
108
105
  if (normalized.length <= maxLength) return normalized;
109
- return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
106
+ return `${truncateGraphemeSafe(normalized, maxLength - 1).trimEnd()}…`;
110
107
  }
111
108
  function dedupeHintSnippetsByText(snippets: EntityHintSnippet[]): EntityHintSnippet[] {
112
109
  const deduped = new Map<string, EntityHintSnippet>();
@@ -1052,7 +1049,7 @@ function formatEntityHintSection(
1052
1049
 
1053
1050
  let result = lines.join("\n");
1054
1051
  if (result.length > maxChars) {
1055
- result = `${result.slice(0, Math.max(0, maxChars - 15)).trimEnd()}\n\n...(trimmed)\n`;
1052
+ result = `${truncateGraphemeSafe(result, Math.max(0, maxChars - 15)).trimEnd()}\n\n...(trimmed)\n`;
1056
1053
  }
1057
1054
  return result.trim().length > 0 ? result.trimEnd() : null;
1058
1055
  }
@@ -1,10 +1,11 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { spawn } from "node:child_process";
3
- import { appendFile, mkdir, mkdtemp, open, readFile, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
3
+ import { appendFile, mkdir, mkdtemp, open, readdir, readFile, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import test from "node:test";
7
7
  import {
8
+ HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY,
8
9
  getAbstractionNodeStoreStatus,
9
10
  upsertAbstractionNode,
10
11
  upsertAbstractionNodes,
@@ -26,12 +27,14 @@ import {
26
27
  deriveHarmonicRecords,
27
28
  normalizeCueAnchorInputs,
28
29
  } from "./harmonic-construction.js";
29
- import { searchHarmonicRetrieval } from "./harmonic-retrieval.js";
30
+ import { searchHarmonicRetrieval, type HarmonicRetrievalResult } from "./harmonic-retrieval.js";
30
31
  import { readJsonFile, withJsonStoreMutationLock } from "./json-store.js";
31
32
  import {
32
33
  filterHarmonicEntityMentions,
33
34
  persistConstructedHarmonicRecords,
34
35
  } from "./orchestration/harmonic-construction-persist.js";
36
+ import { compareDeterministicStrings } from "./deterministic-order.js";
37
+ import { stripAttributesSuffix } from "./structured-attributes.js";
35
38
  import { Orchestrator } from "./orchestrator.js";
36
39
  import { ExtractedFactSchema, ExtractionResultSchema } from "./schemas.js";
37
40
  import type { ExtractionResult, MemoryFile } from "./types.js";
@@ -1259,6 +1262,188 @@ test("harmonic anchors stay scoped to projected active sources in a mixed episod
1259
1262
  }
1260
1263
  });
1261
1264
 
1265
+ async function persistRecencyFixture(memoryDir: string): Promise<{
1266
+ storageDir: string;
1267
+ persistedIds: string[];
1268
+ contentById: Map<string, string>;
1269
+ }> {
1270
+ const config = parseConfig({
1271
+ openaiApiKey: "sk-test",
1272
+ memoryDir,
1273
+ workspaceDir: path.join(memoryDir, "workspace"),
1274
+ qmdEnabled: false,
1275
+ embeddingFallbackEnabled: false,
1276
+ chunkingEnabled: false,
1277
+ harmonicRetrievalEnabled: true,
1278
+ abstractionAnchorsEnabled: false,
1279
+ });
1280
+ const orchestrator = new Orchestrator(config) as unknown as PersistenceHarness;
1281
+ const storage = await orchestrator.getStorage("default");
1282
+ await storage.ensureDirectories();
1283
+ const contents = [
1284
+ "Atlas fact zero of the oldest insertion pair.",
1285
+ "Atlas fact one of the oldest insertion pair.",
1286
+ "Atlas fact two at the middle insertion time.",
1287
+ "Atlas fact three of the newest insertion pair.",
1288
+ "Atlas fact four of the newest insertion pair.",
1289
+ ];
1290
+ const { persistedIds } = await orchestrator.persistExtraction(
1291
+ {
1292
+ facts: contents.map((content) => ({
1293
+ category: "fact",
1294
+ content,
1295
+ confidence: 0.99,
1296
+ tags: ["atlas"],
1297
+ entityRef: "atlas-project",
1298
+ cueAnchors: [],
1299
+ })),
1300
+ entities: [{ name: "atlas-project", type: "company", facts: [] }],
1301
+ profileUpdates: [],
1302
+ questions: [],
1303
+ relationships: [],
1304
+ episodeTitle: "Atlas insertion ordering",
1305
+ },
1306
+ storage,
1307
+ null,
1308
+ { sessionKey: "session:harmonic-recency" }
1309
+ );
1310
+ const expectedContents = new Set(contents);
1311
+ const contentById = new Map<string, string>();
1312
+ const factIds: string[] = [];
1313
+ for (const persistedId of persistedIds) {
1314
+ const memory = await storage.getMemoryById(persistedId);
1315
+ if (!memory) continue;
1316
+ const content = memory.frontmatter.structuredAttributes
1317
+ ? stripAttributesSuffix(memory.content)
1318
+ : memory.content;
1319
+ if (!expectedContents.has(content)) continue;
1320
+ factIds.push(persistedId);
1321
+ contentById.set(persistedId, content);
1322
+ }
1323
+ assert.equal(factIds.length, 5, `expected the five fact memories, got ${persistedIds.length} persisted`);
1324
+ return { storageDir: storage.dir, persistedIds: factIds, contentById };
1325
+ }
1326
+
1327
+ async function rewriteTopicInsertedAt(
1328
+ storageDir: string,
1329
+ insertedAtById: Record<string, string> | null,
1330
+ recordedAt?: string
1331
+ ): Promise<void> {
1332
+ const nodesDir = path.join(storageDir, "state", "abstraction-nodes", "nodes");
1333
+ for (const dayEntry of await readdir(nodesDir, { withFileTypes: true })) {
1334
+ if (!dayEntry.isDirectory()) continue;
1335
+ for (const entryName of await readdir(path.join(nodesDir, dayEntry.name))) {
1336
+ if (!entryName.endsWith(".json")) continue;
1337
+ const filePath = path.join(nodesDir, dayEntry.name, entryName);
1338
+ const node = JSON.parse(await readFile(filePath, "utf8")) as Record<string, unknown>;
1339
+ if (node.kind !== "topic") continue;
1340
+ const metadata = { ...(node.metadata as Record<string, string>) };
1341
+ if (insertedAtById === null) {
1342
+ delete metadata[HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY];
1343
+ } else {
1344
+ metadata[HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY] = JSON.stringify(insertedAtById);
1345
+ }
1346
+ await writeFile(
1347
+ filePath,
1348
+ `${JSON.stringify({ ...node, metadata, ...(recordedAt ? { recordedAt } : {}) }, null, 2)}\n`
1349
+ );
1350
+ }
1351
+ }
1352
+ }
1353
+
1354
+ async function findProjectedTopic(
1355
+ storageDir: string
1356
+ ): Promise<HarmonicRetrievalResult | undefined> {
1357
+ const results = await searchHarmonicRetrieval({
1358
+ memoryDir: storageDir,
1359
+ query: "atlas",
1360
+ maxResults: 5,
1361
+ anchorsEnabled: false,
1362
+ });
1363
+ return results.find((result) => result.node.kind === "topic");
1364
+ }
1365
+
1366
+ test("harmonic projection orders sources newest-first with deterministic tiebreak", async () => {
1367
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-harmonic-recency-"));
1368
+ try {
1369
+ const { storageDir, persistedIds, contentById } = await persistRecencyFixture(memoryDir);
1370
+ const ascendingIds = [...persistedIds].sort(compareDeterministicStrings);
1371
+ const [first, second, third, fourth, fifth] = ascendingIds.map((memoryId) => memoryId);
1372
+ assert.ok(first && second && third && fourth && fifth);
1373
+ const oldest = "2026-01-01T00:00:00.000Z";
1374
+ const middle = "2026-02-01T00:00:00.000Z";
1375
+ const newest = "2026-03-01T00:00:00.000Z";
1376
+ const insertedAtByRank = [oldest, oldest, middle, newest, newest];
1377
+ await rewriteTopicInsertedAt(
1378
+ storageDir,
1379
+ Object.fromEntries(ascendingIds.map((memoryId, index) => [memoryId, insertedAtByRank[index]]))
1380
+ );
1381
+
1382
+ const topic = await findProjectedTopic(storageDir);
1383
+ assert.ok(topic, "topic node must stay retrievable");
1384
+ assert.deepEqual(topic.node.sourceMemoryIds, [fourth, fifth, third, first, second]);
1385
+ assert.equal(topic.node.title, contentById.get(fourth));
1386
+ assert.equal(
1387
+ topic.node.summary,
1388
+ [fourth, fifth, third].map((memoryId) => contentById.get(memoryId)).join("; ")
1389
+ );
1390
+ } finally {
1391
+ await rm(memoryDir, { recursive: true, force: true });
1392
+ }
1393
+ });
1394
+
1395
+ test("harmonic projection keeps ascending id order for equal or missing insertion timestamps", async () => {
1396
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-harmonic-recency-equal-"));
1397
+ try {
1398
+ const { storageDir, persistedIds, contentById } = await persistRecencyFixture(memoryDir);
1399
+ const ascendingIds = [...persistedIds].sort(compareDeterministicStrings);
1400
+ const [first, second, third, fourth, fifth] = ascendingIds.map((memoryId) => memoryId);
1401
+ assert.ok(first && second && third && fourth && fifth);
1402
+ const expectedSummary = [first, second, third]
1403
+ .map((memoryId) => contentById.get(memoryId))
1404
+ .join("; ");
1405
+
1406
+ await rewriteTopicInsertedAt(
1407
+ storageDir,
1408
+ Object.fromEntries(ascendingIds.map((memoryId) => [memoryId, "2026-01-01T00:00:00.000Z"]))
1409
+ );
1410
+ let topic = await findProjectedTopic(storageDir);
1411
+ assert.ok(topic);
1412
+ assert.deepEqual(topic.node.sourceMemoryIds, ascendingIds);
1413
+ assert.equal(topic.node.title, contentById.get(first));
1414
+ assert.equal(topic.node.summary, expectedSummary);
1415
+
1416
+ await rewriteTopicInsertedAt(storageDir, null);
1417
+ topic = await findProjectedTopic(storageDir);
1418
+ assert.ok(topic);
1419
+ assert.deepEqual(topic.node.sourceMemoryIds, ascendingIds);
1420
+ assert.equal(topic.node.title, contentById.get(first));
1421
+ assert.equal(topic.node.summary, expectedSummary);
1422
+
1423
+ await rewriteTopicInsertedAt(
1424
+ storageDir,
1425
+ {
1426
+ [first]: "not-a-timestamp",
1427
+ [second]: "2026-01-01T00:00:00.000Z",
1428
+ [third]: "2026-02-01T00:00:00.000Z",
1429
+ [fourth]: "definitely not a date",
1430
+ [fifth]: "2026-03-01T00:00:00.000Z",
1431
+ },
1432
+ "2026-06-01T00:00:00.000Z"
1433
+ );
1434
+ topic = await findProjectedTopic(storageDir);
1435
+ assert.ok(topic);
1436
+ assert.deepEqual(topic.node.sourceMemoryIds, [first, fourth, fifth, third, second]);
1437
+ assert.equal(topic.node.title, contentById.get(first));
1438
+ assert.equal(
1439
+ topic.node.summary,
1440
+ [first, fourth, fifth].map((memoryId) => contentById.get(memoryId)).join("; ")
1441
+ );
1442
+ } finally {
1443
+ await rm(memoryDir, { recursive: true, force: true });
1444
+ }
1445
+ });
1446
+
1262
1447
  test("non-default namespace construction ignores the default store override", async () => {
1263
1448
  const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-harmonic-namespace-"));
1264
1449
  try {
@@ -13,6 +13,7 @@ import { compareDeterministicStrings } from "./deterministic-order.js";
13
13
  import { countRecallTokenOverlap, normalizeRecallTokens } from "./recall-tokenization.js";
14
14
  import { stripAttributesSuffix } from "./structured-attributes.js";
15
15
  import { isValidityExpiredNow } from "./temporal-validity.js";
16
+ import { isRecord } from "./store-contract.js";
16
17
  import type { MemoryFile } from "./types.js";
17
18
 
18
19
  type SourceMemoryMap = Map<string, MemoryFile>;
@@ -42,6 +43,22 @@ async function readSourceMemories(options: {
42
43
  return sourceMemories;
43
44
  }
44
45
 
46
+ function parseSourceMemoryInsertedAt(node: AbstractionNode): Map<string, string> {
47
+ const insertedAtById = new Map<string, string>();
48
+ const insertedAtRaw = node.metadata?.[HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY];
49
+ if (!insertedAtRaw) return insertedAtById;
50
+ try {
51
+ const parsed = JSON.parse(insertedAtRaw) as unknown;
52
+ if (!isRecord(parsed)) return insertedAtById;
53
+ for (const [memoryId, value] of Object.entries(parsed)) {
54
+ if (typeof value === "string") insertedAtById.set(memoryId, value);
55
+ }
56
+ } catch {
57
+ // Malformed insertion metadata leaves ordering on the recordedAt fallback.
58
+ }
59
+ return insertedAtById;
60
+ }
61
+
45
62
  function projectSourceBackedNode(
46
63
  node: AbstractionNode,
47
64
  sourceMemories: SourceMemoryMap,
@@ -62,6 +79,19 @@ function projectSourceBackedNode(
62
79
  });
63
80
  if (activeSourceMemoryIds.length === 0) return null;
64
81
 
82
+ // Newest-first source order keeps the projected title and summary on the most
83
+ // recently inserted facts. Equal or missing timestamps fall back to ascending
84
+ // id order, which matches the stored sourceMemoryIds order.
85
+ const insertedAtById = parseSourceMemoryInsertedAt(node);
86
+ const insertedAtMs = (memoryId: string): number => {
87
+ const parsedMs = Date.parse(insertedAtById.get(memoryId) ?? "");
88
+ return Number.isFinite(parsedMs) ? parsedMs : Date.parse(node.recordedAt);
89
+ };
90
+ activeSourceMemoryIds.sort(
91
+ (left, right) =>
92
+ insertedAtMs(right) - insertedAtMs(left) || compareDeterministicStrings(left, right)
93
+ );
94
+
65
95
  const activeMemories = activeSourceMemoryIds.flatMap((memoryId) => {
66
96
  const memory = sourceMemories.get(memoryId);
67
97
  return memory ? [memory] : [];
@@ -69,40 +99,32 @@ function projectSourceBackedNode(
69
99
  const activeContents = activeMemories.map((memory) =>
70
100
  memory.frontmatter.structuredAttributes ? stripAttributesSuffix(memory.content) : memory.content
71
101
  );
72
- let metadata: Record<string, string> | undefined;
73
- const insertedAtRaw = node.metadata?.[HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY];
74
- if (insertedAtRaw) {
75
- try {
76
- const insertedAt = JSON.parse(insertedAtRaw) as Record<string, unknown>;
77
- const activeInsertedAt = activeSourceMemoryIds.flatMap((memoryId) => {
78
- const value = insertedAt[memoryId];
79
- return typeof value === "string" ? [[memoryId, value] as const] : [];
80
- });
81
- if (activeInsertedAt.length > 0) {
82
- metadata = {
83
- [HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY]: JSON.stringify(Object.fromEntries(activeInsertedAt)),
84
- };
85
- }
86
- } catch {
87
- metadata = undefined;
88
- }
102
+ let metadata: Record<string, string> | undefined;
103
+ const activeInsertedAt = activeSourceMemoryIds.flatMap((memoryId) => {
104
+ const value = insertedAtById.get(memoryId);
105
+ return typeof value === "string" ? [[memoryId, value] as const] : [];
106
+ });
107
+ if (activeInsertedAt.length > 0) {
108
+ metadata = {
109
+ [HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY]: JSON.stringify(Object.fromEntries(activeInsertedAt)),
110
+ };
89
111
  }
90
112
 
91
- return {
92
- ...node,
93
- sourceMemoryIds: activeSourceMemoryIds,
94
- title: activeContents[0]?.slice(0, 80) ?? node.title,
95
- summary: activeContents.slice(0, 3).join("; ").slice(0, 400),
96
- tags: [...new Set(activeMemories.flatMap((memory) => memory.frontmatter.tags))].sort(compareDeterministicStrings),
97
- entityRefs: [
98
- ...new Set(
99
- activeMemories.flatMap((memory) =>
100
- typeof memory.frontmatter.entityRef === "string" ? [memory.frontmatter.entityRef] : []
101
- )
102
- ),
103
- ].sort(compareDeterministicStrings),
104
- metadata,
105
- };
113
+ return {
114
+ ...node,
115
+ sourceMemoryIds: activeSourceMemoryIds,
116
+ title: activeContents[0]?.slice(0, 80) ?? node.title,
117
+ summary: activeContents.slice(0, 3).join("; ").slice(0, 400),
118
+ tags: [...new Set(activeMemories.flatMap((memory) => memory.frontmatter.tags))].sort(compareDeterministicStrings),
119
+ entityRefs: [
120
+ ...new Set(
121
+ activeMemories.flatMap((memory) =>
122
+ typeof memory.frontmatter.entityRef === "string" ? [memory.frontmatter.entityRef] : []
123
+ )
124
+ ),
125
+ ].sort(compareDeterministicStrings),
126
+ metadata,
127
+ };
106
128
  }
107
129
 
108
130
  function anchorMatchesProjectedNode(
package/src/index.ts CHANGED
@@ -11,6 +11,8 @@
11
11
  *
12
12
  */
13
13
 
14
+ export { collapseWhitespace, displayWidth, graphemeUnits, padEndDisplay, truncateCodePointSafe, truncateGraphemeSafe } from "./whitespace.js";
15
+
14
16
  // ---------------------------------------------------------------------------
15
17
  // Plugin entry resolution
16
18
  // ---------------------------------------------------------------------------
@@ -1446,10 +1448,7 @@ export {
1446
1448
 
1447
1449
  // Coding-graph engine contract (#1551 PR1), implemented by optional @remnic/coding-graph.
1448
1450
 
1449
- export {
1450
- CODING_GRAPH_ENGINE_VERSION,
1451
- TIER_1_LANGUAGES,
1452
- } from "./coding/coding-graph-types.js";
1451
+ export { CODING_GRAPH_ENGINE_VERSION, TIER_1_LANGUAGES } from "./coding/coding-graph-types.js";
1453
1452
 
1454
1453
  export type {
1455
1454
  CodingGraphEngine,
@@ -19,6 +19,7 @@
19
19
 
20
20
  import type { LastRecallBudgetSummary } from "../recall-state.js";
21
21
  import { estimateTokenCount } from "../token-estimate.js";
22
+ import { graphemeUnits, truncateGraphemeSafe } from "../whitespace.js";
22
23
  import type { PluginConfig, RecallSectionConfig } from "../types.js";
23
24
 
24
25
  export interface RecallSectionAppendOptions {
@@ -145,7 +146,7 @@ export class RecallSectionCoordinator {
145
146
  finalContent =
146
147
  sectionId === "profile"
147
148
  ? this.truncateProfileToBoundary(finalContent, maxChars)
148
- : `${finalContent.slice(0, maxChars)}\n\n...(trimmed)\n`;
149
+ : `${truncateGraphemeSafe(finalContent, maxChars)}\n\n...(trimmed)\n`;
149
150
  }
150
151
  if (finalContent.length === 0) return false;
151
152
  const existing = sectionBuckets.get(sectionId) ?? [];
@@ -174,11 +175,16 @@ export class RecallSectionCoordinator {
174
175
  const contentLimit = maxChars - suffix.length;
175
176
  if (contentLimit <= 0) {
176
177
  const boundary = content.lastIndexOf("\n", maxChars);
177
- return content.slice(0, boundary > 0 ? boundary : maxChars).trimEnd();
178
+ const clipped = boundary > 0
179
+ ? content.slice(0, boundary)
180
+ : truncateGraphemeSafe(content, maxChars);
181
+ return clipped.trimEnd();
178
182
  }
179
183
  const boundary = content.lastIndexOf("\n", contentLimit);
180
- const end = boundary > 0 ? boundary : contentLimit;
181
- return `${content.slice(0, end).trimEnd()}${suffix}`;
184
+ const clipped = boundary > 0
185
+ ? content.slice(0, boundary)
186
+ : truncateGraphemeSafe(content, contentLimit);
187
+ return `${clipped.trimEnd()}${suffix}`;
182
188
  }
183
189
  truncateRecallSectionToBudget(
184
190
  content: string,
@@ -191,20 +197,20 @@ export class RecallSectionCoordinator {
191
197
  content.length <= maxChars
192
198
  ? content
193
199
  : maxChars <= suffix.length
194
- ? content.slice(0, maxChars)
195
- : `${content.slice(0, maxChars - suffix.length)}${suffix}`;
200
+ ? truncateGraphemeSafe(content, maxChars)
201
+ : `${truncateGraphemeSafe(content, maxChars - suffix.length)}${suffix}`;
196
202
  if (maxTokens === undefined || estimateTokenCount(charBounded) <= maxTokens) {
197
203
  return charBounded;
198
204
  }
199
205
  const markerFits =
200
206
  maxChars > suffix.length && estimateTokenCount(suffix) <= maxTokens;
201
- const tokenSource = content.length > maxChars ? content.slice(0, maxChars) : content;
202
- const codePoints = [...tokenSource];
207
+ const tokenSource = content.length > maxChars ? truncateGraphemeSafe(content, maxChars) : content;
208
+ const units = graphemeUnits(tokenSource);
203
209
  let low = 0;
204
- let high = codePoints.length;
210
+ let high = units.length;
205
211
  while (low < high) {
206
212
  const mid = Math.ceil((low + high) / 2);
207
- const prefix = codePoints.slice(0, mid).join("");
213
+ const prefix = units.slice(0, mid).join("");
208
214
  const candidate = markerFits ? `${prefix}${suffix}` : prefix;
209
215
  if (candidate.length <= maxChars && estimateTokenCount(candidate) <= maxTokens) {
210
216
  low = mid;
@@ -214,19 +220,19 @@ export class RecallSectionCoordinator {
214
220
  }
215
221
  if (markerFits && low === 0) {
216
222
  low = 0;
217
- high = codePoints.length;
223
+ high = units.length;
218
224
  while (low < high) {
219
225
  const mid = Math.ceil((low + high) / 2);
220
- const prefix = codePoints.slice(0, mid).join("");
226
+ const prefix = units.slice(0, mid).join("");
221
227
  if (prefix.length <= maxChars && estimateTokenCount(prefix) <= maxTokens) {
222
228
  low = mid;
223
229
  } else {
224
230
  high = mid - 1;
225
231
  }
226
232
  }
227
- return codePoints.slice(0, low).join("");
233
+ return units.slice(0, low).join("");
228
234
  }
229
- const prefix = codePoints.slice(0, low).join("");
235
+ const prefix = units.slice(0, low).join("");
230
236
  return markerFits ? `${prefix}${suffix}` : prefix;
231
237
  }
232
238