@jungjaehoon/mama-core 1.6.0 → 1.8.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 (53) hide show
  1. package/db/migrations/037-create-context-packets.sql +32 -0
  2. package/db/migrations/038-create-vnext-operator-contracts.sql +57 -0
  3. package/db/migrations/039-add-connector-event-operator-ingest-seq.sql +96 -0
  4. package/db/migrations/040-create-operator-memory-commit-intents.sql +19 -0
  5. package/db/migrations/041-enforce-operator-memory-commit-claim-invariant.sql +74 -0
  6. package/db/migrations/042-embedding-prefix-scheme.sql +46 -0
  7. package/dist/cases/wiki-page-index.js +6 -2
  8. package/dist/context-compile/boundary-defaults.d.ts +12 -0
  9. package/dist/context-compile/boundary-defaults.js +105 -0
  10. package/dist/context-compile/compiler-policy.d.ts +39 -0
  11. package/dist/context-compile/compiler-policy.js +147 -0
  12. package/dist/context-compile/compiler.d.ts +18 -0
  13. package/dist/context-compile/compiler.js +401 -0
  14. package/dist/context-compile/index.d.ts +9 -0
  15. package/dist/context-compile/index.js +25 -0
  16. package/dist/context-compile/packet-store.d.ts +9 -0
  17. package/dist/context-compile/packet-store.js +255 -0
  18. package/dist/context-compile/ref.d.ts +7 -0
  19. package/dist/context-compile/ref.js +102 -0
  20. package/dist/context-compile/source-readers.d.ts +69 -0
  21. package/dist/context-compile/source-readers.js +707 -0
  22. package/dist/context-compile/types.d.ts +138 -0
  23. package/dist/context-compile/types.js +11 -0
  24. package/dist/context-compile/visibility.d.ts +21 -0
  25. package/dist/context-compile/visibility.js +200 -0
  26. package/dist/db-adapter/node-sqlite-adapter.d.ts +17 -1
  27. package/dist/db-adapter/node-sqlite-adapter.js +399 -6
  28. package/dist/db-manager.d.ts +9 -10
  29. package/dist/db-manager.js +59 -4
  30. package/dist/edges/ref-validation.js +55 -16
  31. package/dist/edges/types.d.ts +2 -0
  32. package/dist/embedding-client.d.ts +3 -1
  33. package/dist/embedding-client.js +3 -2
  34. package/dist/embedding-server/index.d.ts +2 -0
  35. package/dist/embedding-server/index.js +10 -2
  36. package/dist/embeddings.d.ts +8 -3
  37. package/dist/embeddings.js +48 -15
  38. package/dist/entities/store.d.ts +4 -1
  39. package/dist/entities/store.js +206 -78
  40. package/dist/entities/types.d.ts +5 -1
  41. package/dist/entities/types.js +1 -1
  42. package/dist/index.d.ts +4 -2
  43. package/dist/index.js +8 -4
  44. package/dist/mama-api.js +1 -1
  45. package/dist/memory/api.d.ts +6 -1
  46. package/dist/memory/api.js +223 -29
  47. package/dist/memory/provenance.d.ts +1 -0
  48. package/dist/memory/provenance.js +2 -0
  49. package/dist/memory/types.d.ts +1 -0
  50. package/dist/memory-inject.js +1 -1
  51. package/dist/provenance/source-ref.d.ts +29 -0
  52. package/dist/provenance/source-ref.js +204 -0
  53. package/package.json +4 -2
@@ -9,15 +9,15 @@ const TABLE_COLUMN_PRAGMAS = {
9
9
  case_truth: 'PRAGMA table_info(case_truth)',
10
10
  };
11
11
  const tableColumnCache = new WeakMap();
12
+ // Keep memory-truth quarantine semantics excluded for legacy/backcompat rows even though the
13
+ // current decisions.status CHECK only admits superseded/contradicted/stale terminal states.
14
+ const EXCLUDED_MEMORY_STATUSES = new Set(['superseded', 'quarantined', 'contradicted', 'stale']);
12
15
  function scopeKey(scope) {
13
16
  return `${scope.kind}\0${scope.id}`;
14
17
  }
15
18
  function hasScopes(scopes) {
16
19
  return Array.isArray(scopes) && scopes.length > 0;
17
20
  }
18
- function hasConnectors(connectors) {
19
- return Array.isArray(connectors) && connectors.length > 0;
20
- }
21
21
  function hasProjectRefs(projectRefs) {
22
22
  return Array.isArray(projectRefs) && projectRefs.length > 0;
23
23
  }
@@ -27,16 +27,33 @@ function visibilityFromScopes(scopes) {
27
27
  function asOfMs(visibility) {
28
28
  return typeof visibility.asOfMs === 'number' ? visibility.asOfMs : null;
29
29
  }
30
- function isAtOrBeforeAsOf(value, visibility) {
30
+ function startMs(visibility) {
31
+ return typeof visibility.startMs === 'number' ? visibility.startMs : null;
32
+ }
33
+ function isWithinVisibilityTime(value, visibility) {
34
+ const start = startMs(visibility);
31
35
  const asOf = asOfMs(visibility);
32
- return asOf === null || (typeof value === 'number' && value <= asOf);
36
+ if (start === null && asOf === null) {
37
+ return true;
38
+ }
39
+ return (typeof value === 'number' &&
40
+ (start === null || value >= start) &&
41
+ (asOf === null || value <= asOf));
33
42
  }
34
43
  function parseTimestamp(value) {
35
44
  if (typeof value === 'number' && Number.isFinite(value)) {
36
45
  return Math.floor(value);
37
46
  }
38
47
  if (typeof value === 'string') {
39
- const parsed = Date.parse(value);
48
+ const trimmed = value.trim();
49
+ if (trimmed.length === 0) {
50
+ return null;
51
+ }
52
+ const numeric = Number(trimmed);
53
+ if (Number.isFinite(numeric)) {
54
+ return Math.floor(numeric);
55
+ }
56
+ const parsed = Date.parse(trimmed);
40
57
  return Number.isFinite(parsed) ? parsed : null;
41
58
  }
42
59
  return null;
@@ -61,9 +78,14 @@ function tableColumns(adapter, table) {
61
78
  return columns;
62
79
  }
63
80
  function isMemoryVisible(adapter, id, visibility) {
81
+ const columns = tableColumns(adapter, 'decisions');
82
+ const statusSelect = columns.has('status') ? 'status' : 'NULL AS status';
83
+ const supersededBySelect = columns.has('superseded_by')
84
+ ? 'superseded_by'
85
+ : 'NULL AS superseded_by';
64
86
  const row = adapter
65
87
  .prepare(`
66
- SELECT created_at, event_datetime
88
+ SELECT created_at, event_datetime, ${statusSelect}, ${supersededBySelect}
67
89
  FROM decisions
68
90
  WHERE id = ?
69
91
  LIMIT 1
@@ -72,7 +94,12 @@ function isMemoryVisible(adapter, id, visibility) {
72
94
  if (!row) {
73
95
  return false;
74
96
  }
75
- if (!isAtOrBeforeAsOf(row.event_datetime ?? row.created_at, visibility)) {
97
+ const status = typeof row.status === 'string' ? row.status : 'active';
98
+ if (EXCLUDED_MEMORY_STATUSES.has(status) ||
99
+ (typeof row.superseded_by === 'string' && row.superseded_by.length > 0)) {
100
+ return false;
101
+ }
102
+ if (!isWithinVisibilityTime(row.event_datetime ?? row.created_at, visibility)) {
76
103
  return false;
77
104
  }
78
105
  const scopes = visibility.scopes;
@@ -94,7 +121,6 @@ function isMemoryVisible(adapter, id, visibility) {
94
121
  if (bindingRow?.ok) {
95
122
  return true;
96
123
  }
97
- const columns = tableColumns(adapter, 'decisions');
98
124
  if (columns.has('memory_scope_kind') && columns.has('memory_scope_id')) {
99
125
  const row = adapter
100
126
  .prepare(`
@@ -133,7 +159,7 @@ function isCaseVisible(adapter, id, visibility) {
133
159
  return false;
134
160
  }
135
161
  const timestamp = parseTimestamp(row.last_activity_at) ?? parseTimestamp(row.updated_at);
136
- if (!isAtOrBeforeAsOf(timestamp, visibility)) {
162
+ if (!isWithinVisibilityTime(timestamp, visibility)) {
137
163
  return false;
138
164
  }
139
165
  const scopes = visibility.scopes;
@@ -158,7 +184,7 @@ function isRawVisible(adapter, id, visibility) {
158
184
  if (!row) {
159
185
  return false;
160
186
  }
161
- if (hasConnectors(visibility.connectors) &&
187
+ if (Array.isArray(visibility.connectors) &&
162
188
  !visibility.connectors.includes(String(row.source_connector))) {
163
189
  return false;
164
190
  }
@@ -171,7 +197,12 @@ function isRawVisible(adapter, id, visibility) {
171
197
  row.tenant_id !== visibility.tenantId) {
172
198
  return false;
173
199
  }
174
- if (!isAtOrBeforeAsOf(Number(row.event_datetime ?? row.source_timestamp_ms), visibility)) {
200
+ const rawTs = row.event_datetime ?? row.source_timestamp_ms;
201
+ if (rawTs === null || rawTs === undefined || (typeof rawTs === 'string' && rawTs.trim() === '')) {
202
+ return false;
203
+ }
204
+ const eventTsMs = parseTimestamp(rawTs);
205
+ if (eventTsMs === null || eventTsMs < 0 || !isWithinVisibilityTime(eventTsMs, visibility)) {
175
206
  return false;
176
207
  }
177
208
  if (!hasScopes(visibility.scopes)) {
@@ -186,7 +217,7 @@ function isEntityVisible(adapter, id, visibility) {
186
217
  if (!row) {
187
218
  return false;
188
219
  }
189
- if (!isAtOrBeforeAsOf(row.created_at, visibility)) {
220
+ if (!isWithinVisibilityTime(row.created_at, visibility)) {
190
221
  return false;
191
222
  }
192
223
  const scopes = visibility.scopes;
@@ -222,7 +253,7 @@ function isTwinRefVisible(adapter, ref, visibility, visitedEdges, edgeCache) {
222
253
  if (!edge) {
223
254
  return false;
224
255
  }
225
- if (!isAtOrBeforeAsOf(edge.created_at, visibility)) {
256
+ if (!isWithinVisibilityTime(edge.created_at, visibility)) {
226
257
  return false;
227
258
  }
228
259
  const pathWithCurrent = new Set(visitedEdges);
@@ -245,13 +276,21 @@ function listVisibleTwinEdgesForRefs(adapter, refs, options = {}) {
245
276
  const edgeCache = new Map();
246
277
  const edgeTypes = normalizeEdgeTypes(options.edgeTypes);
247
278
  const edges = (0, store_js_1.listTwinEdgesForRefs)(adapter, refs).filter((edge) => (edgeTypes.size === 0 || edgeTypes.has(edge.edge_type)) &&
279
+ (typeof options.startMs !== 'number' || edge.created_at >= options.startMs) &&
248
280
  (typeof options.asOfMs !== 'number' || edge.created_at <= options.asOfMs) &&
249
281
  isTwinRefVisible(adapter, edge.subject_ref, options, new Set(), edgeCache) &&
250
282
  isTwinRefVisible(adapter, edge.object_ref, options, new Set(), edgeCache));
251
283
  const limit = typeof options.limit === 'number' && Number.isFinite(options.limit)
252
284
  ? Math.max(0, Math.floor(options.limit))
253
- : edges.length;
254
- return edges.slice(0, limit);
285
+ : null;
286
+ const sortedEdges = [...edges].sort((left, right) => {
287
+ const createdDiff = right.created_at - left.created_at;
288
+ return createdDiff !== 0 ? createdDiff : left.edge_id.localeCompare(right.edge_id);
289
+ });
290
+ if (limit === null) {
291
+ return sortedEdges;
292
+ }
293
+ return sortedEdges.slice(0, limit);
255
294
  }
256
295
  function normalizeEdgeTypes(edgeTypes) {
257
296
  if (!Array.isArray(edgeTypes) || edgeTypes.length === 0) {
@@ -27,6 +27,7 @@ export interface TwinVisibility {
27
27
  connectors?: string[];
28
28
  projectRefs?: TwinProjectRef[];
29
29
  tenantId?: string | null;
30
+ startMs?: number | null;
30
31
  asOfMs?: number | null;
31
32
  }
32
33
  export interface InsertTwinEdgeInput {
@@ -80,6 +81,7 @@ export interface ListVisibleTwinEdgesOptions {
80
81
  projectRefs?: TwinProjectRef[];
81
82
  tenantId?: string | null;
82
83
  edgeTypes?: TwinEdgeType[];
84
+ startMs?: number | null;
83
85
  asOfMs?: number | null;
84
86
  limit?: number;
85
87
  }
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * @module embedding-client
13
13
  */
14
+ import type { EmbeddingRole } from './embeddings.js';
14
15
  export declare const DEFAULT_PORT: number;
15
16
  export declare const HOST = "127.0.0.1";
16
17
  export declare const TIMEOUT_MS = 500;
@@ -33,9 +34,10 @@ export declare function isServerRunning(): Promise<boolean>;
33
34
  * Generate embedding via HTTP server
34
35
  *
35
36
  * @param text - Text to embed
37
+ * @param role - e5 role: 'passage' (default, stored text) or 'query' (search)
36
38
  * @returns Embedding or null if failed
37
39
  */
38
- export declare function getEmbeddingFromServer(text: string): Promise<Float32Array | null>;
40
+ export declare function getEmbeddingFromServer(text: string, role?: EmbeddingRole): Promise<Float32Array | null>;
39
41
  /**
40
42
  * Get server status
41
43
  */
@@ -76,9 +76,10 @@ async function isServerRunning() {
76
76
  * Generate embedding via HTTP server
77
77
  *
78
78
  * @param text - Text to embed
79
+ * @param role - e5 role: 'passage' (default, stored text) or 'query' (search)
79
80
  * @returns Embedding or null if failed
80
81
  */
81
- async function getEmbeddingFromServer(text) {
82
+ async function getEmbeddingFromServer(text, role = 'passage') {
82
83
  const port = getServerPort();
83
84
  try {
84
85
  const controller = new AbortController();
@@ -86,7 +87,7 @@ async function getEmbeddingFromServer(text) {
86
87
  const response = await fetch(`http://${exports.HOST}:${port}/embed`, {
87
88
  method: 'POST',
88
89
  headers: { 'Content-Type': 'application/json' },
89
- body: JSON.stringify({ text }),
90
+ body: JSON.stringify({ text, role }),
90
91
  signal: controller.signal,
91
92
  });
92
93
  clearTimeout(timeout);
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import type { IncomingMessage, ServerResponse, Server as HTTPServer } from 'http';
16
16
  export declare const SHUTDOWN_TOKEN: string;
17
+ import type { EmbeddingRole } from '../embeddings.js';
17
18
  export declare const DEFAULT_PORT: number;
18
19
  export declare const HOST = "127.0.0.1";
19
20
  export declare const PORT_FILE: string;
@@ -42,6 +43,7 @@ export interface ServerOptions {
42
43
  sessionStore?: SessionStore;
43
44
  graphHandler?: RequestHandler;
44
45
  }
46
+ export declare function resolveEmbeddingRole(raw: unknown): EmbeddingRole;
45
47
  /**
46
48
  * Clean up port file on exit
47
49
  */
@@ -18,6 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.PORT_FILE = exports.HOST = exports.DEFAULT_PORT = exports.SHUTDOWN_TOKEN = void 0;
21
+ exports.resolveEmbeddingRole = resolveEmbeddingRole;
21
22
  exports.cleanupPortFile = cleanupPortFile;
22
23
  exports.startEmbeddingServer = startEmbeddingServer;
23
24
  exports.warmModel = warmModel;
@@ -63,6 +64,11 @@ const connections = new Set();
63
64
  // Graph handler (optional, passed from standalone)
64
65
  let graphHandler = null;
65
66
  let _wssInstance = null;
67
+ // Resolve the e5 role from an HTTP body value. Absent/unknown -> passage, so old
68
+ // clients that never send a role keep getting passage vectors (backward compatible).
69
+ function resolveEmbeddingRole(raw) {
70
+ return raw === 'query' ? 'query' : 'passage';
71
+ }
66
72
  /**
67
73
  * Read request body as JSON
68
74
  */
@@ -158,7 +164,8 @@ async function handleRequest(req, res) {
158
164
  res.end(JSON.stringify({ error: 'Missing or invalid "text" field' }));
159
165
  return;
160
166
  }
161
- const embedding = await (0, embeddings_js_1.generateEmbedding)(body.text);
167
+ const role = resolveEmbeddingRole(body.role);
168
+ const embedding = await (0, embeddings_js_1.generateEmbedding)(body.text, role);
162
169
  modelLoaded = true;
163
170
  const latency = Date.now() - startTime;
164
171
  res.writeHead(200);
@@ -186,7 +193,8 @@ async function handleRequest(req, res) {
186
193
  res.end(JSON.stringify({ error: 'Missing or invalid "texts" array' }));
187
194
  return;
188
195
  }
189
- const embeddings = await Promise.all(body.texts.map((text) => (0, embeddings_js_1.generateEmbedding)(text).then((e) => Array.from(e))));
196
+ const role = resolveEmbeddingRole(body.role);
197
+ const embeddings = await Promise.all(body.texts.map((text) => (0, embeddings_js_1.generateEmbedding)(text, role).then((e) => Array.from(e))));
190
198
  modelLoaded = true;
191
199
  const latency = Date.now() - startTime;
192
200
  res.writeHead(200);
@@ -11,6 +11,11 @@
11
11
  */
12
12
  import { embeddingCache } from './embedding-cache.js';
13
13
  import { loadConfig, getModelName, getEmbeddingDim, getQuantized } from './config-loader.js';
14
+ export declare const EMBEDDING_MODEL_MAX_LENGTH = 512;
15
+ export declare const EMBEDDING_MAX_TOKENISH_SEGMENTS = 480;
16
+ export type EmbeddingRole = 'passage' | 'query';
17
+ export declare const EMBEDDING_PREFIX_SCHEME = "e5-prefixed-v1";
18
+ export declare function prepareEmbeddingText(text: string): string;
14
19
  /**
15
20
  * Decision object for enhanced embedding generation
16
21
  */
@@ -35,7 +40,7 @@ export interface DecisionForEmbedding {
35
40
  * @returns Embedding vector (dimension from config)
36
41
  * @throws Error if text is empty or embedding fails
37
42
  */
38
- export declare function generateEmbedding(text: string): Promise<Float32Array>;
43
+ export declare function generateEmbedding(text: string, role?: EmbeddingRole): Promise<Float32Array>;
39
44
  /**
40
45
  * Generate enhanced embedding with content + metadata
41
46
  *
@@ -45,7 +50,7 @@ export declare function generateEmbedding(text: string): Promise<Float32Array>;
45
50
  * @param decision - Decision object
46
51
  * @returns 1024-dim enhanced embedding
47
52
  */
48
- export declare function generateEnhancedEmbedding(decision: DecisionForEmbedding): Promise<Float32Array>;
53
+ export declare function generateEnhancedEmbedding(decision: DecisionForEmbedding, role?: EmbeddingRole): Promise<Float32Array>;
49
54
  /**
50
55
  * Batch generate embeddings (optimized)
51
56
  *
@@ -57,7 +62,7 @@ export declare function generateEnhancedEmbedding(decision: DecisionForEmbedding
57
62
  * @param texts - Array of texts to embed (max 10 per batch)
58
63
  * @returns Array of embeddings
59
64
  */
60
- export declare function generateBatchEmbeddings(texts: string[]): Promise<Float32Array[]>;
65
+ export declare function generateBatchEmbeddings(texts: string[], role?: EmbeddingRole): Promise<Float32Array[]>;
61
66
  /**
62
67
  * Calculate cosine similarity between two embeddings
63
68
  *
@@ -14,7 +14,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.getQuantized = exports.getEmbeddingDim = exports.getModelName = exports.loadConfig = exports.MODEL_NAME = exports.EMBEDDING_DIM = exports.embeddingCache = void 0;
17
+ exports.getQuantized = exports.getEmbeddingDim = exports.getModelName = exports.loadConfig = exports.MODEL_NAME = exports.EMBEDDING_DIM = exports.embeddingCache = exports.EMBEDDING_PREFIX_SCHEME = exports.EMBEDDING_MAX_TOKENISH_SEGMENTS = exports.EMBEDDING_MODEL_MAX_LENGTH = void 0;
18
+ exports.prepareEmbeddingText = prepareEmbeddingText;
18
19
  exports.generateEmbedding = generateEmbedding;
19
20
  exports.generateEnhancedEmbedding = generateEnhancedEmbedding;
20
21
  exports.generateBatchEmbeddings = generateBatchEmbeddings;
@@ -33,6 +34,13 @@ Object.defineProperty(exports, "getQuantized", { enumerable: true, get: function
33
34
  // Shared cache directory (not in node_modules)
34
35
  const DEFAULT_CACHE_DIR = path_1.default.join(os_1.default.homedir(), '.cache', 'huggingface', 'transformers');
35
36
  const TIER3_ENV_VALUES = new Set(['1', 'true', 'yes']);
37
+ exports.EMBEDDING_MODEL_MAX_LENGTH = 512;
38
+ exports.EMBEDDING_MAX_TOKENISH_SEGMENTS = 480;
39
+ exports.EMBEDDING_PREFIX_SCHEME = 'e5-prefixed-v1';
40
+ function applyRolePrefix(preparedText, role) {
41
+ return `${role}: ${preparedText}`;
42
+ }
43
+ const TOKENISH_SEGMENT_PATTERN = /[\p{Script=Hangul}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]|\S+/gu;
36
44
  // Singleton pattern for model loading
37
45
  let embeddingPipeline = null;
38
46
  let currentModelName = null;
@@ -46,6 +54,19 @@ function assertEmbeddingsEnabled() {
46
54
  'Tier 3 test mode must use lexical/no-vector fallback instead of loading the embedding model.');
47
55
  }
48
56
  }
57
+ function prepareEmbeddingText(text) {
58
+ const trimmed = text.trim();
59
+ let count = 0;
60
+ let cutIndex = trimmed.length;
61
+ for (const match of trimmed.matchAll(TOKENISH_SEGMENT_PATTERN)) {
62
+ count++;
63
+ if (count > exports.EMBEDDING_MAX_TOKENISH_SEGMENTS) {
64
+ cutIndex = match.index ?? trimmed.length;
65
+ break;
66
+ }
67
+ }
68
+ return cutIndex === trimmed.length ? trimmed : trimmed.slice(0, cutIndex).trimEnd();
69
+ }
49
70
  /**
50
71
  * Load embedding model (configurable)
51
72
  *
@@ -111,13 +132,15 @@ async function loadModel() {
111
132
  * @returns Embedding vector (dimension from config)
112
133
  * @throws Error if text is empty or embedding fails
113
134
  */
114
- async function generateEmbedding(text) {
115
- if (!text || text.trim().length === 0) {
135
+ async function generateEmbedding(text, role = 'passage') {
136
+ if (typeof text !== 'string' || text.trim().length === 0) {
116
137
  throw new Error('Text cannot be empty');
117
138
  }
118
139
  assertEmbeddingsEnabled();
119
- // Task 2: Check cache first (AC #3)
120
- const cached = embedding_cache_js_1.embeddingCache.get(text);
140
+ const preparedText = prepareEmbeddingText(text);
141
+ const modelInput = applyRolePrefix(preparedText, role); // e5 instruction prefix
142
+ // Cache key is the PREFIXED input: a passage vector can never be served for a query.
143
+ const cached = embedding_cache_js_1.embeddingCache.get(modelInput);
121
144
  if (cached) {
122
145
  return cached;
123
146
  }
@@ -126,9 +149,11 @@ async function generateEmbedding(text) {
126
149
  const model = await loadModel();
127
150
  const expectedDim = (0, config_loader_js_1.getEmbeddingDim)();
128
151
  // Generate embedding
129
- const output = await model(text, {
152
+ const output = await model(modelInput, {
130
153
  pooling: 'mean', // Mean pooling over tokens
131
154
  normalize: true, // L2 normalization
155
+ truncation: true,
156
+ max_length: exports.EMBEDDING_MODEL_MAX_LENGTH,
132
157
  });
133
158
  // Extract Float32Array
134
159
  const embedding = output.data;
@@ -139,7 +164,7 @@ async function generateEmbedding(text) {
139
164
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
140
165
  const _latency = Date.now() - startTime;
141
166
  // Task 2: Store in cache (AC #3)
142
- embedding_cache_js_1.embeddingCache.set(text, embedding);
167
+ embedding_cache_js_1.embeddingCache.set(modelInput, embedding);
143
168
  return embedding;
144
169
  }
145
170
  catch (error) {
@@ -156,7 +181,7 @@ async function generateEmbedding(text) {
156
181
  * @param decision - Decision object
157
182
  * @returns 1024-dim enhanced embedding
158
183
  */
159
- async function generateEnhancedEmbedding(decision) {
184
+ async function generateEnhancedEmbedding(decision, role = 'passage') {
160
185
  // Construct enriched text representation with narrative fields (Story 2.2)
161
186
  const parts = [
162
187
  `Topic: ${decision.topic}`,
@@ -187,7 +212,7 @@ async function generateEnhancedEmbedding(decision) {
187
212
  parts.push(`Risks: ${decision.risks}`);
188
213
  }
189
214
  const enrichedText = parts.join('\n').trim();
190
- return generateEmbedding(enrichedText);
215
+ return generateEmbedding(enrichedText, role);
191
216
  }
192
217
  /**
193
218
  * Batch generate embeddings (optimized)
@@ -200,26 +225,34 @@ async function generateEnhancedEmbedding(decision) {
200
225
  * @param texts - Array of texts to embed (max 10 per batch)
201
226
  * @returns Array of embeddings
202
227
  */
203
- async function generateBatchEmbeddings(texts) {
204
- assertEmbeddingsEnabled();
228
+ async function generateBatchEmbeddings(texts, role = 'passage') {
205
229
  if (!Array.isArray(texts) || texts.length === 0) {
206
230
  throw new Error('Texts must be a non-empty array');
207
231
  }
208
- // Validate all texts
209
232
  for (const text of texts) {
210
- if (!text || text.trim().length === 0) {
211
- throw new Error('All texts must be non-empty');
233
+ if (typeof text !== 'string' || text.trim().length === 0) {
234
+ throw new Error('All texts must be non-empty strings');
235
+ }
236
+ }
237
+ assertEmbeddingsEnabled();
238
+ const preparedTexts = texts.map((text) => prepareEmbeddingText(text));
239
+ for (const text of preparedTexts) {
240
+ if (!text || text.length === 0) {
241
+ throw new Error('All texts must be non-empty strings');
212
242
  }
213
243
  }
244
+ const modelInputs = preparedTexts.map((t) => applyRolePrefix(t, role));
214
245
  const startTime = Date.now();
215
246
  try {
216
247
  const model = await loadModel();
217
248
  const expectedDim = (0, config_loader_js_1.getEmbeddingDim)();
218
249
  // Native batch processing - single model forward pass
219
250
  // This is significantly faster than sequential calls
220
- const outputs = await model(texts, {
251
+ const outputs = await model(modelInputs, {
221
252
  pooling: 'mean',
222
253
  normalize: true,
254
+ truncation: true,
255
+ max_length: exports.EMBEDDING_MODEL_MAX_LENGTH,
223
256
  });
224
257
  // Extract embeddings from batch output
225
258
  const embeddings = [];
@@ -13,9 +13,11 @@ export declare class EntityMergeError extends Error {
13
13
  readonly code: string;
14
14
  constructor(code: string, message: string);
15
15
  }
16
+ declare const OBSERVATION_CONTEXT_KEY_COLUMNS: readonly ["workspace_context_key", "channel_context_key", "thread_context_key", "actor_context_key"];
16
17
  type CreateEntityNodeInput = Omit<EntityNode, 'created_at' | 'updated_at'>;
17
18
  type AttachEntityAliasInput = Omit<EntityAlias, 'created_at'>;
18
- type UpsertEntityObservationInput = Omit<EntityObservation, 'created_at'>;
19
+ type ObservationContextKeyColumn = (typeof OBSERVATION_CONTEXT_KEY_COLUMNS)[number];
20
+ type UpsertEntityObservationInput = Omit<EntityObservation, 'created_at'> & Partial<Record<ObservationContextKeyColumn, string | null>>;
19
21
  type AppendEntityTimelineEventInput = {
20
22
  event: Omit<EntityTimelineEvent, 'id' | 'created_at'> & {
21
23
  id?: string;
@@ -27,6 +29,7 @@ export interface UpsertEntityObservationResult {
27
29
  id: string;
28
30
  created: boolean;
29
31
  }
32
+ export declare function clearEntityTableColumnCache(adapter: EntityStoreAdapter): void;
30
33
  export declare function parseObservationRow(row: Record<string, unknown>): EntityObservation;
31
34
  export declare function createEntityNode(input: CreateEntityNodeInput): Promise<EntityNode>;
32
35
  export declare function getEntityNode(id: string, adapter?: EntityStoreAdapter): EntityNode | null;