@oh-my-pi/pi-mnemopi 18.12.6 → 18.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.13.0] - 2026-10-26
6
+
7
+ ### Fixed
8
+
9
+ - Fixed polyphonic recall's graph voice finding no memories for non-English names (e.g. `Hà Nội`) or names of three or more words, and Vietnamese words like `tôi` being stored as the participant `i` ([#16723](https://github.com/can1357/oh-my-pi/issues/16723))
10
+
5
11
  ## [18.12.4] - 2026-10-20
6
12
 
7
13
  ### Fixed
@@ -55,6 +55,15 @@ export interface EpisodicGraphOptions {
55
55
  readonly db?: Database;
56
56
  readonly dbPath?: DatabasePath;
57
57
  }
58
+ /**
59
+ * Capitalized proper nouns of at most two words, in any cased script (Latin, Greek,
60
+ * Cyrillic, Vietnamese, …), deduplicated case-insensitively. Gist participants are
61
+ * stored with this extractor and the polyphonic graph voice looks gists up with it, so
62
+ * a query entity matches a stored participant exactly (`findGistsByParticipant`).
63
+ * Unicode property escapes and `\p{L}`-based lookaround boundaries replace `[A-Z][a-z]+`
64
+ * and ASCII `\b`, which matched no non-English name (issues #7918, #16723).
65
+ */
66
+ export declare function extractProperNouns(text: string): string[];
58
67
  export declare class EpisodicGraph {
59
68
  readonly db: Database;
60
69
  readonly dbPath: DatabasePath;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-mnemopi",
4
- "version": "18.12.6",
4
+ "version": "18.13.0",
5
5
  "description": "Local SQLite memory engine for omp agents",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -42,10 +42,10 @@
42
42
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
43
43
  },
44
44
  "dependencies": {
45
- "@oh-my-pi/pi-ai": "18.12.6",
46
- "@oh-my-pi/pi-catalog": "18.12.6",
47
- "@oh-my-pi/pi-natives": "18.12.6",
48
- "@oh-my-pi/pi-utils": "18.12.6"
45
+ "@oh-my-pi/pi-ai": "18.13.0",
46
+ "@oh-my-pi/pi-catalog": "18.13.0",
47
+ "@oh-my-pi/pi-natives": "18.13.0",
48
+ "@oh-my-pi/pi-utils": "18.13.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "fastembed": "3.0.0",
@@ -134,6 +134,23 @@ function unique(values: Iterable<string>, limit = Number.MAX_SAFE_INTEGER): stri
134
134
  return out;
135
135
  }
136
136
 
137
+ /**
138
+ * Capitalized proper nouns of at most two words, in any cased script (Latin, Greek,
139
+ * Cyrillic, Vietnamese, …), deduplicated case-insensitively. Gist participants are
140
+ * stored with this extractor and the polyphonic graph voice looks gists up with it, so
141
+ * a query entity matches a stored participant exactly (`findGistsByParticipant`).
142
+ * Unicode property escapes and `\p{L}`-based lookaround boundaries replace `[A-Z][a-z]+`
143
+ * and ASCII `\b`, which matched no non-English name (issues #7918, #16723).
144
+ */
145
+ export function extractProperNouns(text: string): string[] {
146
+ return unique(
147
+ Array.from(
148
+ text.matchAll(/(?<![\p{L}\p{N}_])(\p{Lu}\p{Ll}+(?:\s+\p{Lu}\p{Ll}+)?)(?![\p{L}\p{N}_])/gu),
149
+ match => match[1] ?? "",
150
+ ),
151
+ );
152
+ }
153
+
137
154
  function parseJsonStringArray(value: string | null): string[] {
138
155
  if (value === null || value === "") return [];
139
156
  try {
@@ -604,19 +621,13 @@ export class EpisodicGraph {
604
621
  }
605
622
 
606
623
  private extractParticipants(content: string): string[] {
607
- // Match capitalized proper nouns across cased scripts (Latin, Greek, Cyrillic, …).
608
- // `[A-Z][a-z]+` plus ASCII `\b` matched no non-Latin token, so any non-English
609
- // memory produced zero participants (issue #7918). Unicode property escapes with
610
- // the `u` flag, and `\p{L}`-based lookaround boundaries in place of `\b`, fix that.
611
- const names = Array.from(
612
- content.matchAll(/(?<![\p{L}\p{N}_])(\p{Lu}\p{Ll}+(?:\s+\p{Lu}\p{Ll}+)?)(?![\p{L}\p{N}_])/gu),
613
- match => match[1] ?? "",
614
- );
624
+ // `\p{L}` boundaries, not ASCII `\b`: with `i`, `\b` matched the trailing `i` of
625
+ // words like Vietnamese `tôi` as the pronoun `I` (issue #16723).
615
626
  const pronouns = Array.from(
616
- content.matchAll(/\b(I|you|we|they|he|she|it|me|us|them|him|her)\b/gi),
627
+ content.matchAll(/(?<![\p{L}\p{N}_])(I|you|we|they|he|she|it|me|us|them|him|her)(?![\p{L}\p{N}_])/giu),
617
628
  match => match[1] ?? "",
618
629
  );
619
- return unique([...names, ...pronouns], 5);
630
+ return unique([...extractProperNouns(content), ...pronouns], 5);
620
631
  }
621
632
 
622
633
  private extractTemporalScope(content: string): string | null {
@@ -4,7 +4,7 @@ import { type Env, polyphonicRecallEnabled } from "../config";
4
4
  import { closeQuietly, type DatabasePath, openDatabase } from "../db";
5
5
  import { backfillConsolidatedFacts, ensureVeracityConsolidator } from "./beam/consolidate";
6
6
  import type { BeamMemoryState, JsonValue, Metadata, RecallResult, RecallTierLabel } from "./beam/types";
7
- import { EpisodicGraph } from "./episodic-graph";
7
+ import { EpisodicGraph, extractProperNouns } from "./episodic-graph";
8
8
  import { STOP_WORDS } from "./synonyms";
9
9
  import { VeracityConsolidator } from "./veracity-consolidation";
10
10
 
@@ -214,16 +214,6 @@ function cosineAgainstUnit(unit: Float32Array, raw: unknown): number | null {
214
214
  return dot / Math.sqrt(normSq);
215
215
  }
216
216
 
217
- function extractEntities(text: string): string[] {
218
- const seen = new Set<string>();
219
- const matches = text.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g);
220
- for (const match of matches) {
221
- const entity = match[0];
222
- if (entity.length > 0) seen.add(entity);
223
- }
224
- return [...seen];
225
- }
226
-
227
217
  function queryTokens(query: string): string[] {
228
218
  const tokens: string[] = [];
229
219
  for (const match of query.toLowerCase().matchAll(/[\p{L}\p{N}_-]+/gu)) tokens.push(match[0]);
@@ -420,7 +410,7 @@ export class PolyphonicRecallEngine {
420
410
  const existing = seeds.get(hit.memoryId);
421
411
  if (existing === undefined || hit.score > existing.score) seeds.set(hit.memoryId, hit);
422
412
  };
423
- for (const entity of extractEntities(query)) {
413
+ for (const entity of extractProperNouns(query)) {
424
414
  for (const gist of this.graph.findGistsByParticipant(entity)) {
425
415
  const memoryId = gist.id.startsWith("gist_") ? gist.id.slice(5) : gist.id;
426
416
  addSeed({ memoryId, score: 0.6, voice: "graph", metadata: { entity, gist: gist.text } });