@linxiraos/pi-mnemopi 1.1.27 → 1.2.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
@@ -1,7 +1,14 @@
1
- # Changelog
2
-
3
1
  ## [Unreleased]
4
2
 
3
+ ## [1.2.0] - 2026-10-12
4
+
5
+ - Fixed proactive linking freezing the host for seconds per stored memory on large banks; a memory's graph links are now written in one commit instead of one per link ([#14998](https://github.com/can1357/oh-my-pi/issues/14998)).
6
+ - Fixed long retained transcripts exhausting local memory extraction models by limiting each extraction input to 8192 characters while keeping the opening context and newest turns ([#14956](https://github.com/can1357/oh-my-pi/issues/14956)).
7
+
8
+ ### Fixed
9
+
10
+ - Fixed new memories gaining graph links (`related_to`, `references`, `ctx`) to memories that were already invalidated, superseded, or expired; proactive linking and consolidation now link only to memories recall can still return, so the graph stops growing toward retired memories ([#14427](https://github.com/can1357/oh-my-pi/pull/14427) by [@tickernelz](https://github.com/tickernelz)).
11
+
5
12
  ## [1.1.27] - 2026-10-07
6
13
 
7
14
  - 版本线推进至 1.1.27;本版无独立用户可见变化。
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @linxiraos/pi-mnemopi
2
2
 
3
- Local SQLite memory engine for omp agents.
3
+ Local SQLite memory engine for zetacode agents.
4
4
 
5
5
  This package is the Bun/TypeScript port of the Mnemosyne memory engine. It provides:
6
6
 
@@ -1,6 +1,7 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import type { VeracityConsolidator } from "../veracity-consolidation.js";
3
3
  import type { BeamCaches, BeamConfig, BeamEvent, BeamMemoryOptions, BeamMemoryState, BeamStats, ImportStats, MemoriaRetrieveResult, Metadata, RecallEnhancedOptions, RecallOptions, RecallResult, RememberBatchItem, RememberBatchOptions, RememberOptions, SleepResult } from "./types.js";
4
+ export { clipRecallContent } from "./recall.js";
4
5
  export { initBeam } from "./schema.js";
5
6
  export type * from "./types.js";
6
7
  export declare class BeamMemory implements BeamMemoryState {
@@ -91,7 +91,6 @@ export declare class EpisodicGraph {
91
91
  private knownMemoryIds;
92
92
  private memoryContent;
93
93
  private entityOverlapScore;
94
- private temporalContextScore;
95
94
  private memoryFeatures;
96
95
  private scoreFeatures;
97
96
  }
@@ -0,0 +1,4 @@
1
+ /** Default character budget for a single embedding or fact-extraction input. */
2
+ export declare const DEFAULT_INPUT_CHARS = 8192;
3
+ /** Preserve the opening context and newest turns of an oversized chronological input. */
4
+ export declare function clipToWindow(text: string, max: number): string;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-mnemopi",
4
- "version": "1.1.27",
5
- "description": "Local SQLite memory engine for omp agents",
4
+ "version": "1.2.0",
5
+ "description": "Local SQLite memory engine for Zeta agents",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Linxira-OS",
8
8
  "contributors": [
@@ -39,10 +39,10 @@
39
39
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
40
40
  },
41
41
  "dependencies": {
42
- "@linxiraos/pi-ai": "1.1.27",
43
- "@linxiraos/pi-catalog": "1.1.27",
44
- "@linxiraos/pi-natives": "1.1.27",
45
- "@linxiraos/pi-utils": "1.1.27"
42
+ "@linxiraos/pi-ai": "1.2.0",
43
+ "@linxiraos/pi-catalog": "1.2.0",
44
+ "@linxiraos/pi-natives": "1.2.0",
45
+ "@linxiraos/pi-utils": "1.2.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "fastembed": "3.0.0",
@@ -1,5 +1,5 @@
1
1
  import type { Database } from "bun:sqlite";
2
- import { logger } from "@linxiraos/pi-utils";
2
+ import { logger, withLoopPhase } from "@linxiraos/pi-utils";
3
3
  import { generateId as generateTimedId, sha256Hex16, stableMemoryId } from "../../util/ids";
4
4
  import { cjkFtsTerms, containsSpacelessCjk, ftsQueryTerms, hasCjk, isCjkChar, recallTokens } from "../../util/regex";
5
5
  import { currentEmbeddingModel, embed } from "../embeddings";
@@ -727,29 +727,33 @@ async function runEmbedding(beam: BeamMemoryState, items: readonly EmbedItem[]):
727
727
  try {
728
728
  const matrix = await embed(items.map(item => item.content));
729
729
  if (matrix === null) return;
730
- const model = currentEmbeddingModel();
731
- using insertEmbedding = beam.db.prepare(
732
- "INSERT OR REPLACE INTO memory_embeddings(memory_id, embedding_json, model) VALUES (?, ?, ?)",
733
- );
734
- let committed = 0;
735
- const insertMany = beam.db.transaction((rows: readonly EmbedItem[]) => {
736
- for (let i = 0; i < rows.length; i += 1) {
737
- const vector = matrix[i];
738
- const item = rows[i];
739
- if (vector === undefined || item === undefined) continue;
740
- insertEmbedding.run(item.memoryId, JSON.stringify(Array.from(vector)), model);
741
- committed += 1;
742
- }
730
+ // Persisting the vectors runs synchronously on the caller's event loop; label it so a
731
+ // stall is attributable. The embedding itself is awaited above, outside the label.
732
+ withLoopPhase("mnemopi.embed-persist", () => {
733
+ const model = currentEmbeddingModel();
734
+ using insertEmbedding = beam.db.prepare(
735
+ "INSERT OR REPLACE INTO memory_embeddings(memory_id, embedding_json, model) VALUES (?, ?, ?)",
736
+ );
737
+ let committed = 0;
738
+ const insertMany = beam.db.transaction((rows: readonly EmbedItem[]) => {
739
+ for (let i = 0; i < rows.length; i += 1) {
740
+ const vector = matrix[i];
741
+ const item = rows[i];
742
+ if (vector === undefined || item === undefined) continue;
743
+ insertEmbedding.run(item.memoryId, JSON.stringify(Array.from(vector)), model);
744
+ committed += 1;
745
+ }
746
+ });
747
+ insertMany(items);
748
+ // A recall taken while these vectors were still generating cached an FTS-only ranking, and
749
+ // committing vectors changes what recall returns -- so that cache must be dropped, or the
750
+ // pre-embedding order keeps being served for the whole cache TTL. Gated on `committed`: a
751
+ // batch whose provider returned a short or empty matrix inserts nothing, changes no ranking,
752
+ // and invalidating there would only discard a still-valid cache. Only the query cache is
753
+ // affected; the polyphonic subject dictionary is built from facts/gists, which an embedding
754
+ // batch never touches.
755
+ if (committed > 0) beam.caches.queryCache?.invalidate();
743
756
  });
744
- insertMany(items);
745
- // A recall taken while these vectors were still generating cached an FTS-only ranking, and
746
- // committing vectors changes what recall returns -- so that cache must be dropped, or the
747
- // pre-embedding order keeps being served for the whole cache TTL. Gated on `committed`: a
748
- // batch whose provider returned a short or empty matrix inserts nothing, changes no ranking,
749
- // and invalidating there would only discard a still-valid cache. Only the query cache is
750
- // affected; the polyphonic subject dictionary is built from facts/gists, which an embedding
751
- // batch never touches.
752
- if (committed > 0) beam.caches.queryCache?.invalidate();
753
757
  } catch (error) {
754
758
  // Background embedding generation is best-effort: a failing provider, a closed DB
755
759
  // during shutdown, or a transient API error must never disrupt the synchronous
@@ -57,6 +57,7 @@ import type {
57
57
  SleepResult,
58
58
  } from "./types";
59
59
 
60
+ export { clipRecallContent } from "./recall";
60
61
  export { initBeam } from "./schema";
61
62
  export type * from "./types";
62
63
 
@@ -1,3 +1,4 @@
1
+ import { popLoopPhase, pushLoopPhase, withLoopPhase } from "@linxiraos/pi-utils";
1
2
  import { normalizedRecallWeights, polyphonicRecallEnabled, temporalHalflifeHours } from "../../config";
2
3
  import { hasCjk, matchesWordForm } from "../../util/regex";
3
4
  import { embedQuery } from "../embeddings";
@@ -923,35 +924,37 @@ export async function recall(
923
924
  const derived = query.length > 0 ? await embedQuery(query) : null;
924
925
  temporalOptions.queryEmbedding = derived === null ? null : Array.from(derived);
925
926
  }
926
- let weights = normalizedRecallWeights(
927
- options.vecWeight ?? beam.config.vecWeight,
928
- options.ftsWeight ?? beam.config.ftsWeight,
929
- options.importanceWeight ?? beam.config.importanceWeight,
930
- );
931
- if (options.useIntent === true) {
932
- const intent = classifyIntent(query);
933
- weights = adjustWeights(weights[0], weights[1], weights[2], intent);
934
- }
935
- const useSynonyms = options.useSynonyms !== false;
936
- const tokens = expandedTokens(query, useSynonyms);
937
- const tokenGroups = expandedTokenGroups(query, useSynonyms);
938
- const candidates = collectMemoryCandidates(beam, query, topK, temporalOptions);
939
- const scored: RecallResult[] = [];
940
- for (const candidate of candidates) {
941
- const result = scoreCandidate(candidate, tokens, tokenGroups, weights, temporalOptions);
942
- if (result !== null) scored.push(result);
943
- }
944
- scored.sort((left, right) => (right.score ?? 0) - (left.score ?? 0));
945
- let finalResults = dedupCrossTierSummaryLinks(beam, dedupeResults(scored));
946
- if (query.length > 0 && tokens.length >= 4 && finalResults.length > topK)
947
- finalResults = diversifyByCoverage(finalResults, tokens, topK);
948
- if (options.useMmr === true && finalResults.length > 1) {
949
- finalResults = rerankRecallResults(finalResults, options.mmrLambda ?? 0.7, topK);
950
- } else {
951
- finalResults = finalResults.slice(0, topK);
952
- }
953
- if (temporalOptions.updateRecallCounts !== false) updateRecallCounts(beam, finalResults, temporalOptions);
954
- return finalResults;
927
+ return withLoopPhase("mnemopi.recall", () => {
928
+ let weights = normalizedRecallWeights(
929
+ options.vecWeight ?? beam.config.vecWeight,
930
+ options.ftsWeight ?? beam.config.ftsWeight,
931
+ options.importanceWeight ?? beam.config.importanceWeight,
932
+ );
933
+ if (options.useIntent === true) {
934
+ const intent = classifyIntent(query);
935
+ weights = adjustWeights(weights[0], weights[1], weights[2], intent);
936
+ }
937
+ const useSynonyms = options.useSynonyms !== false;
938
+ const tokens = expandedTokens(query, useSynonyms);
939
+ const tokenGroups = expandedTokenGroups(query, useSynonyms);
940
+ const candidates = collectMemoryCandidates(beam, query, topK, temporalOptions);
941
+ const scored: RecallResult[] = [];
942
+ for (const candidate of candidates) {
943
+ const result = scoreCandidate(candidate, tokens, tokenGroups, weights, temporalOptions);
944
+ if (result !== null) scored.push(result);
945
+ }
946
+ scored.sort((left, right) => (right.score ?? 0) - (left.score ?? 0));
947
+ let finalResults = dedupCrossTierSummaryLinks(beam, dedupeResults(scored));
948
+ if (query.length > 0 && tokens.length >= 4 && finalResults.length > topK)
949
+ finalResults = diversifyByCoverage(finalResults, tokens, topK);
950
+ if (options.useMmr === true && finalResults.length > 1) {
951
+ finalResults = rerankRecallResults(finalResults, options.mmrLambda ?? 0.7, topK);
952
+ } else {
953
+ finalResults = finalResults.slice(0, topK);
954
+ }
955
+ if (temporalOptions.updateRecallCounts !== false) updateRecallCounts(beam, finalResults, temporalOptions);
956
+ return finalResults;
957
+ });
955
958
  }
956
959
 
957
960
  function diversifyByCoverage(
@@ -1034,37 +1037,44 @@ export async function recallEnhanced(
1034
1037
  let cache: QueryCache<RecallResult> | null = null;
1035
1038
  let token = "";
1036
1039
  let scope = "";
1037
- if (cacheEnabled) {
1038
- beam.caches.queryCache ??= new QueryCache<RecallResult>({
1039
- maxSize: ENHANCED_RECALL_CACHE_MAX_ENTRIES,
1040
- ttlSeconds: ENHANCED_RECALL_CACHE_TTL_SECONDS,
1041
- });
1042
- cache = beam.caches.queryCache;
1043
- // Explicit hooks invalidate on this beam's own writes; the token also catches writes
1044
- // no hook covers (sleep, graph ingest) and commits from other connections.
1045
- token = databaseWriteToken(beam);
1046
- if (beam.caches.queryCacheToken !== token) {
1047
- cache.invalidate();
1048
- beam.caches.queryCacheToken = token;
1049
- }
1050
- scope = enhancedRecallCacheScope(beam, topK, options, polyphonic);
1051
- const cached = cache.get(query, queryEmbedding, scope);
1052
- if (cached !== null) {
1053
- const results = structuredClone(cached) as RecallResult[];
1054
- if (countRecalls) countRecallsKeepingCache(beam, results, runOptions, token);
1055
- return results;
1040
+ pushLoopPhase("mnemopi.recall");
1041
+ try {
1042
+ if (cacheEnabled) {
1043
+ beam.caches.queryCache ??= new QueryCache<RecallResult>({
1044
+ maxSize: ENHANCED_RECALL_CACHE_MAX_ENTRIES,
1045
+ ttlSeconds: ENHANCED_RECALL_CACHE_TTL_SECONDS,
1046
+ });
1047
+ cache = beam.caches.queryCache;
1048
+ // Explicit hooks invalidate on this beam's own writes; the token also catches writes
1049
+ // no hook covers (sleep, graph ingest) and commits from other connections.
1050
+ token = databaseWriteToken(beam);
1051
+ if (beam.caches.queryCacheToken !== token) {
1052
+ cache.invalidate();
1053
+ beam.caches.queryCacheToken = token;
1054
+ }
1055
+ scope = enhancedRecallCacheScope(beam, topK, options, polyphonic);
1056
+ const cached = cache.get(query, queryEmbedding, scope);
1057
+ if (cached !== null) {
1058
+ const results = structuredClone(cached) as RecallResult[];
1059
+ if (countRecalls) countRecallsKeepingCache(beam, results, runOptions, token);
1060
+ return results;
1061
+ }
1056
1062
  }
1063
+ } finally {
1064
+ popLoopPhase();
1057
1065
  }
1058
1066
 
1059
1067
  const results = polyphonic
1060
1068
  ? await polyphonicRecallEnhanced(beam, query, topK, runOptions)
1061
1069
  : await linearRecallEnhanced(beam, query, topK, runOptions);
1062
- // A write committed while recall awaited may be missing from this ranking: skip the
1063
- // put and keep the stale token so the next lookup starts from an empty cache.
1064
- const cacheable = cache !== null && databaseWriteToken(beam) === token;
1065
- if (cacheable) cache?.put(query, structuredClone(results), queryEmbedding, scope);
1066
- if (countRecalls) countRecallsKeepingCache(beam, results, runOptions, cacheable ? token : null);
1067
- return results;
1070
+ return withLoopPhase("mnemopi.recall", () => {
1071
+ // A write committed while recall awaited may be missing from this ranking: skip the
1072
+ // put and keep the stale token so the next lookup starts from an empty cache.
1073
+ const cacheable = cache !== null && databaseWriteToken(beam) === token;
1074
+ if (cacheable) cache?.put(query, structuredClone(results), queryEmbedding, scope);
1075
+ if (countRecalls) countRecallsKeepingCache(beam, results, runOptions, cacheable ? token : null);
1076
+ return results;
1077
+ });
1068
1078
  }
1069
1079
 
1070
1080
  async function linearRecallEnhanced(
@@ -1084,14 +1094,16 @@ async function linearRecallEnhanced(
1084
1094
  ...enhancedOptions,
1085
1095
  updateRecallCounts: false,
1086
1096
  });
1087
- if (options.includeFacts === true) {
1088
- const facts = factRecall(beam, query, factRecallLimit(topK));
1089
- results.push(...facts);
1090
- }
1091
- results.sort((left, right) => (right.score ?? 0) - (left.score ?? 0));
1092
- const finalResults = rerankRecallResults(results, options.mmrLambda ?? 0.7, topK);
1093
- if (enhancedOptions.updateRecallCounts !== false) updateRecallCounts(beam, finalResults, enhancedOptions);
1094
- return finalResults;
1097
+ return withLoopPhase("mnemopi.recall", () => {
1098
+ if (options.includeFacts === true) {
1099
+ const facts = factRecall(beam, query, factRecallLimit(topK));
1100
+ results.push(...facts);
1101
+ }
1102
+ results.sort((left, right) => (right.score ?? 0) - (left.score ?? 0));
1103
+ const finalResults = rerankRecallResults(results, options.mmrLambda ?? 0.7, topK);
1104
+ if (enhancedOptions.updateRecallCounts !== false) updateRecallCounts(beam, finalResults, enhancedOptions);
1105
+ return finalResults;
1106
+ });
1095
1107
  }
1096
1108
 
1097
1109
  /**
@@ -1,5 +1,5 @@
1
1
  import type { Database, SQLQueryBindings } from "bun:sqlite";
2
- import { logger } from "@linxiraos/pi-utils";
2
+ import { logger, withLoopPhase } from "@linxiraos/pi-utils";
3
3
  import { transaction } from "../../db";
4
4
  import { toUtcIso } from "../../util/datetime";
5
5
  import { generateId } from "../../util/ids";
@@ -307,9 +307,11 @@ function proactiveLinkIfEnabled(
307
307
  async function runFactExtraction(beam: BeamMemoryState, memoryId: string, content: string): Promise<void> {
308
308
  try {
309
309
  const extracted = await extractFactCategoriesSafe(content);
310
- if (countExtractedFactCategories(extracted) === 0) return;
311
- storeExtractedFactCategories(beam, extracted, 0, memoryId);
312
- invalidateCaches(beam);
310
+ withLoopPhase("mnemopi.extract", () => {
311
+ if (countExtractedFactCategories(extracted) === 0) return;
312
+ storeExtractedFactCategories(beam, extracted, 0, memoryId);
313
+ invalidateCaches(beam);
314
+ });
313
315
  } catch {
314
316
  // Background fact extraction is best-effort and never surfaces to the caller.
315
317
  }
@@ -740,7 +742,7 @@ export function updateWorking(
740
742
  export function get(beam: BeamMemoryState, memoryId: string): Row | null {
741
743
  using workingStatement = beam.db.prepare(`
742
744
  SELECT id, content, source, timestamp, session_id,
743
- importance, metadata_json, veracity, created_at
745
+ importance, metadata_json, veracity, created_at, valid_until, superseded_by
744
746
  FROM working_memory
745
747
  WHERE id = ?
746
748
  `);
@@ -749,7 +751,7 @@ export function get(beam: BeamMemoryState, memoryId: string): Row | null {
749
751
 
750
752
  using episodicStatement = beam.db.prepare(`
751
753
  SELECT id, content, source, timestamp, session_id,
752
- importance, metadata_json, veracity, created_at
754
+ importance, metadata_json, veracity, created_at, valid_until, superseded_by
753
755
  FROM episodic_memory
754
756
  WHERE id = ? AND (session_id = ? OR scope = 'global')
755
757
  `);
@@ -11,6 +11,7 @@ import {
11
11
  fetchWithRetry,
12
12
  getFastembedCacheDir,
13
13
  logger,
14
+ withLoopPhase,
14
15
  } from "@linxiraos/pi-utils";
15
16
  import { LRUCache } from "@linxiraos/pi-utils/lru";
16
17
  import type { EmbeddingModel } from "fastembed";
@@ -21,6 +22,7 @@ import {
21
22
  mnemopiDebugEnabled,
22
23
  resolveEmbeddingProvider,
23
24
  } from "./runtime-options";
25
+ import { clipToWindow, DEFAULT_INPUT_CHARS } from "./text-window";
24
26
 
25
27
  export type { EmbeddingOutput } from "./runtime-options";
26
28
  export { cosineSimilarity } from "./vector-math";
@@ -166,29 +168,7 @@ function effectiveMaxInputChars(): number {
166
168
  if (override !== undefined) return Math.max(0, Math.trunc(override));
167
169
  const envValue = Number.parseInt($env.MNEMOPI_EMBEDDING_MAX_INPUT_CHARS ?? "", 10);
168
170
  if (Number.isFinite(envValue) && envValue >= 0) return envValue;
169
- return 8192;
170
- }
171
-
172
- /** Elision marker injected between the retained head and tail of an oversized input. */
173
- const EMBEDDING_ELISION_MARKER = "\n\n[...]\n\n";
174
-
175
- /**
176
- * Right-clip a single oversized input to {@link max} chars while preserving
177
- * both ends. Retention transcripts are chronological (oldest → newest), so a
178
- * naive `slice(0, max)` would drop the most recent — and most semantically
179
- * loaded — turns once a session passed the cap, leaving every later retained
180
- * episode with essentially the same prefix vector. Keeping a head/tail split
181
- * lets the embedding capture the topic setup at the start AND the latest
182
- * exchanges at the end. Falls back to a tail-only clip when `max` is too
183
- * small to fit the elision marker plus a useful slice on either side.
184
- */
185
- function clipToWindow(text: string, max: number): string {
186
- if (text.length <= max) return text;
187
- if (max <= EMBEDDING_ELISION_MARKER.length + 16) return text.slice(text.length - max);
188
- const budget = max - EMBEDDING_ELISION_MARKER.length;
189
- const headLen = budget >>> 1;
190
- const tailLen = budget - headLen;
191
- return text.slice(0, headLen) + EMBEDDING_ELISION_MARKER + text.slice(text.length - tailLen);
171
+ return DEFAULT_INPUT_CHARS;
192
172
  }
193
173
 
194
174
  /**
@@ -315,9 +295,11 @@ export function embeddingDimFor(modelName: string): number {
315
295
  async function collectMatrix(batches: EmbeddingOutput): Promise<EmbeddingMatrix> {
316
296
  const rows: Vector[] = [];
317
297
  for await (const batch of batches) {
318
- for (const row of batch) {
319
- rows.push(new Float32Array(row));
320
- }
298
+ withLoopPhase("mnemopi.embed", () => {
299
+ for (const row of batch) {
300
+ rows.push(new Float32Array(row));
301
+ }
302
+ });
321
303
  }
322
304
  return rows;
323
305
  }
@@ -409,12 +391,15 @@ async function embedApi(texts: readonly string[]): Promise<EmbeddingMatrix | nul
409
391
  if (!response.ok) {
410
392
  return null;
411
393
  }
412
- const { data: rows } = (await response.json()) as { data?: Array<{ embedding: number[] }> };
413
- if (rows === undefined) {
414
- return null;
415
- }
416
- apiCallCount += 1;
417
- return rows.map(row => new Float32Array(row.embedding));
394
+ const data = (await response.json()) as { data?: Array<{ embedding: number[] }> };
395
+ return withLoopPhase("mnemopi.embed", () => {
396
+ const { data: rows } = data;
397
+ if (rows === undefined) {
398
+ return null;
399
+ }
400
+ apiCallCount += 1;
401
+ return rows.map(row => new Float32Array(row.embedding));
402
+ });
418
403
  } catch (error) {
419
404
  logger.debug("mnemopi embedding request failed", { status: extractHttpStatusFromError(error) });
420
405
  return null;
@@ -546,7 +531,7 @@ export async function embed(texts: readonly string[]): Promise<EmbeddingMatrix |
546
531
  return null;
547
532
  }
548
533
  try {
549
- const vectors = await collectMatrix(model.embed([...texts]));
534
+ const vectors = await withLoopPhase("mnemopi.embed", () => collectMatrix(model.embed([...texts])));
550
535
  if (vectors.length === 1) {
551
536
  const vector = vectors[0];
552
537
  if (vector !== undefined) {