mark-improving-agent 2.3.3 → 2.3.5

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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.3.3
1
+ 2.3.5
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Binary Vector Memory Compression
3
+ *
4
+ * High-performance binary vector quantization for memory embeddings.
5
+ * Based on QuIVer: Rethinking ANN Graph Topology via Training-Free Binary Quantization (Xiao et al., 2026)
6
+ *
7
+ * Achieves 32x compression: 1536-dim float32 (6144 bytes) → 192 bytes binary
8
+ * Uses Hamming distance (XOR + POPCOUNT) for fast similarity search.
9
+ *
10
+ * @module core/memory
11
+ * @fileoverview Binary vector quantization for memory compression
12
+ */
13
+ import { createLogger } from '../../utils/logger.js';
14
+ const logger = createLogger('[BinaryVector]');
15
+ /**
16
+ * Default configuration for 1536-dim embeddings (OpenAI text-embedding-3-small)
17
+ */
18
+ export const DEFAULT_BINARY_CONFIG = {
19
+ dimension: 1536,
20
+ normalized: true,
21
+ trackStats: true,
22
+ };
23
+ /**
24
+ * Create a binary vector index
25
+ *
26
+ * @param config - Configuration for the index
27
+ * @returns Empty binary index ready for vectors
28
+ */
29
+ export function createBinaryIndex(config) {
30
+ logger.info('Creating binary vector index', { dimension: config.dimension });
31
+ return {
32
+ vectors: new Uint8Array(0),
33
+ indices: [],
34
+ metadata: [],
35
+ config,
36
+ };
37
+ }
38
+ /**
39
+ * Quantize a float32 vector to binary using sign-bit quantization
40
+ *
41
+ * @param vector - Float32 array (dimension length)
42
+ * @param normalized - Whether vector is L2 normalized (skip magnitude check if true)
43
+ * @returns Uint8Array of packed binary bits
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const vector = new Float32Array([0.5, -0.3, 0.1, -0.8]);
48
+ * const binary = quantizeToBinary(vector);
49
+ * // Result: Uint8Array [0b10001001] (4 dimensions = 4 bits = 1 byte)
50
+ * ```
51
+ */
52
+ export function quantizeToBinary(vector, normalized = true) {
53
+ const dim = vector.length;
54
+ const bytes = Math.ceil(dim / 8);
55
+ const result = new Uint8Array(bytes);
56
+ for (let i = 0; i < dim; i++) {
57
+ // Sign bit: positive = 1, negative = 0
58
+ const bitIndex = i % 8;
59
+ const byteIndex = Math.floor(i / 8);
60
+ if (vector[i] >= 0) {
61
+ result[byteIndex] |= (1 << (7 - bitIndex));
62
+ }
63
+ }
64
+ return result;
65
+ }
66
+ /**
67
+ * Dequantize binary vector back to approximate float32
68
+ *
69
+ * Note: This is lossy - returns center-of-mass values (±0.5 for normalized vectors)
70
+ *
71
+ * @param binary - Packed binary bits
72
+ * @param dimension - Original dimension
73
+ * @param normalized - Whether to return normalized values
74
+ * @returns Approximate float32 vector
75
+ */
76
+ export function dequantizeFromBinary(binary, dimension, normalized = true) {
77
+ const result = new Float32Array(dimension);
78
+ for (let i = 0; i < dimension; i++) {
79
+ const bitIndex = i % 8;
80
+ const byteIndex = Math.floor(i / 8);
81
+ const bit = (binary[byteIndex] >> (7 - bitIndex)) & 1;
82
+ // For normalized vectors: 1 → +0.5, 0 → -0.5
83
+ // This gives approximate reconstruction
84
+ result[i] = normalized ? (bit ? 0.5 : -0.5) : (bit ? 0 : 0);
85
+ }
86
+ return result;
87
+ }
88
+ /**
89
+ * Calculate Hamming distance between two binary vectors
90
+ *
91
+ * @param a - First binary vector
92
+ * @param b - Second binary vector
93
+ * @returns Hamming distance (count of differing bits)
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const dist = hammingDistance(binaryA, binaryB);
98
+ * // dist = 0 means identical, dist = dimension means opposite
99
+ * ```
100
+ */
101
+ export function hammingDistance(a, b) {
102
+ if (a.length !== b.length) {
103
+ throw new Error(`Binary vectors must have same length: ${a.length} vs ${b.length}`);
104
+ }
105
+ let distance = 0;
106
+ for (let i = 0; i < a.length; i++) {
107
+ // XOR gives 1s where bits differ, POPCOUNT counts them
108
+ const xored = a[i] ^ b[i];
109
+ // POPCOUNT via built-in
110
+ let count = xored;
111
+ count = (count & 0x55) + ((count >> 1) & 0x55);
112
+ count = (count & 0x33) + ((count >> 2) & 0x33);
113
+ count = (count & 0x0F) + ((count >> 4) & 0x0F);
114
+ distance += count;
115
+ }
116
+ return distance;
117
+ }
118
+ /**
119
+ * Calculate similarity from Hamming distance
120
+ *
121
+ * @param hammingDist - Hamming distance
122
+ * @param dimension - Vector dimension
123
+ * @returns Similarity score 0-1 (1 = identical)
124
+ */
125
+ export function hammingToSimilarity(hammingDist, dimension) {
126
+ return 1 - (hammingDist / dimension);
127
+ }
128
+ /**
129
+ * Add vectors to the binary index
130
+ *
131
+ * @param index - Binary index to add to
132
+ * @param vectors - Float32Array vectors (each dimension-long)
133
+ * @param ids - Unique identifiers for each vector
134
+ * @param metadata - Optional metadata (importance, timestamp)
135
+ * @returns Updated index
136
+ */
137
+ export function addToBinaryIndex(index, vectors, ids, metadata) {
138
+ const dim = index.config.dimension;
139
+ if (vectors.length !== ids.length) {
140
+ throw new Error(`Vectors count (${vectors.length}) must match IDs count (${ids.length})`);
141
+ }
142
+ // Calculate total bytes needed
143
+ const bytesPerVector = Math.ceil(dim / 8);
144
+ const newTotalBytes = index.vectors.length + (vectors.length * bytesPerVector);
145
+ // Create new expanded vectors array
146
+ const newVectors = new Uint8Array(newTotalBytes);
147
+ if (index.vectors.length > 0) {
148
+ newVectors.set(index.vectors);
149
+ }
150
+ // Quantize and add each vector
151
+ let offset = index.vectors.length;
152
+ for (let i = 0; i < vectors.length; i++) {
153
+ if (vectors[i].length !== dim) {
154
+ throw new Error(`Vector dimension ${vectors[i].length} doesn't match index dimension ${dim}`);
155
+ }
156
+ const binary = quantizeToBinary(vectors[i], index.config.normalized);
157
+ newVectors.set(binary, offset);
158
+ offset += bytesPerVector;
159
+ }
160
+ return {
161
+ vectors: newVectors,
162
+ indices: [...index.indices, ...ids.map((_, i) => index.indices.length + i)],
163
+ metadata: [...index.metadata, ...metadata || ids.map((id, i) => ({ id, importance: 0.5, timestamp: Date.now() }))],
164
+ config: index.config,
165
+ };
166
+ }
167
+ /**
168
+ * Search for similar vectors in the binary index using Hamming distance
169
+ *
170
+ * @param index - Binary index to search
171
+ * @param queryVector - Query vector to search for
172
+ * @param k - Number of results to return
173
+ * @returns Top-k results sorted by similarity (highest first)
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * const results = searchBinaryIndex(index, queryEmbedding, 10);
178
+ * // Returns top 10 most similar memories
179
+ * ```
180
+ */
181
+ export function searchBinaryIndex(index, queryVector, k) {
182
+ const dim = index.config.dimension;
183
+ const bytesPerVector = Math.ceil(dim / 8);
184
+ if (queryVector.length !== dim) {
185
+ throw new Error(`Query dimension ${queryVector.length} doesn't match index dimension ${dim}`);
186
+ }
187
+ // Quantize query
188
+ const queryBinary = quantizeToBinary(queryVector, index.config.normalized);
189
+ // Search all vectors
190
+ const results = [];
191
+ let totalHamming = 0;
192
+ for (let i = 0; i < index.indices.length; i++) {
193
+ const offset = i * bytesPerVector;
194
+ const vectorBinary = index.vectors.subarray(offset, offset + bytesPerVector);
195
+ const hDist = hammingDistance(queryBinary, vectorBinary);
196
+ totalHamming += hDist;
197
+ results.push({
198
+ index: i,
199
+ id: index.metadata[i]?.id || String(index.indices[i]),
200
+ hammingDistance: hDist,
201
+ similarity: hammingToSimilarity(hDist, dim),
202
+ metadata: index.metadata[i] || { importance: 0.5, timestamp: Date.now() },
203
+ });
204
+ }
205
+ // Sort by similarity (highest first)
206
+ results.sort((a, b) => b.similarity - a.similarity);
207
+ // Return top-k
208
+ const topK = results.slice(0, k);
209
+ if (index.config.trackStats) {
210
+ logger.debug(`Search completed: ${index.indices.length} vectors, avg Hamming: ${(totalHamming / index.indices.length).toFixed(2)}`);
211
+ }
212
+ return topK;
213
+ }
214
+ /**
215
+ * Get compression statistics
216
+ *
217
+ * @param index - Binary index
218
+ * @returns Compression statistics
219
+ */
220
+ export function getCompressionStats(index) {
221
+ const dim = index.config.dimension;
222
+ const float32Bytes = dim * 4; // 4 bytes per float32
223
+ const binaryBytes = Math.ceil(dim / 8);
224
+ const vectorsCount = index.indices.length;
225
+ const originalBytes = vectorsCount * float32Bytes;
226
+ const compressedBytes = vectorsCount * binaryBytes;
227
+ return {
228
+ originalBytes,
229
+ compressedBytes,
230
+ compressionRatio: originalBytes / compressedBytes,
231
+ vectorsStored: vectorsCount,
232
+ dimension: dim,
233
+ avgHammingDistance: 0, // Calculated on demand
234
+ };
235
+ }
236
+ /**
237
+ * Save binary index to binary format
238
+ *
239
+ * @param index - Binary index to save
240
+ * @returns Buffer containing serialized index
241
+ */
242
+ export function serializeBinaryIndex(index) {
243
+ const configBytes = JSON.stringify(index.config).length;
244
+ const metadataBytes = JSON.stringify(index.metadata).length;
245
+ const indicesBytes = index.indices.length * 4; // 4 bytes per number
246
+ const totalBytes = 4 + configBytes + 4 + metadataBytes + 4 + indicesBytes + index.vectors.length + 4 + index.indices.length * 8;
247
+ const buffer = new ArrayBuffer(totalBytes);
248
+ const view = new DataView(buffer);
249
+ const uint8 = new Uint8Array(buffer);
250
+ let offset = 0;
251
+ // Config
252
+ const configStr = JSON.stringify(index.config);
253
+ view.setUint32(offset, configStr.length);
254
+ offset += 4;
255
+ uint8.set(new TextEncoder().encode(configStr), offset);
256
+ offset += configStr.length;
257
+ // Metadata
258
+ const metadataStr = JSON.stringify(index.metadata);
259
+ view.setUint32(offset, metadataStr.length);
260
+ offset += 4;
261
+ uint8.set(new TextEncoder().encode(metadataStr), offset);
262
+ offset += metadataStr.length;
263
+ // Indices
264
+ view.setUint32(offset, index.indices.length);
265
+ offset += 4;
266
+ for (const idx of index.indices) {
267
+ view.setUint32(offset, idx);
268
+ offset += 4;
269
+ }
270
+ // Vectors
271
+ uint8.set(index.vectors, offset);
272
+ offset += index.vectors.length;
273
+ // Stored IDs count for compatibility
274
+ view.setUint32(offset, index.metadata.length);
275
+ return buffer;
276
+ }
277
+ /**
278
+ * Load binary index from buffer
279
+ *
280
+ * @param buffer - Serialized index buffer
281
+ * @returns Reconstructed BinaryIndex
282
+ */
283
+ export function deserializeBinaryIndex(buffer) {
284
+ const view = new DataView(buffer);
285
+ const uint8 = new Uint8Array(buffer);
286
+ let offset = 0;
287
+ // Config
288
+ const configLen = view.getUint32(offset);
289
+ offset += 4;
290
+ const configStr = new TextDecoder().decode(uint8.subarray(offset, offset + configLen));
291
+ offset += configLen;
292
+ const config = JSON.parse(configStr);
293
+ // Metadata
294
+ const metadataLen = view.getUint32(offset);
295
+ offset += 4;
296
+ const metadataStr = new TextDecoder().decode(uint8.subarray(offset, offset + metadataLen));
297
+ offset += metadataLen;
298
+ const metadata = JSON.parse(metadataStr);
299
+ // Indices
300
+ const indicesLen = view.getUint32(offset);
301
+ offset += 4;
302
+ const indices = [];
303
+ for (let i = 0; i < indicesLen; i++) {
304
+ indices.push(view.getUint32(offset));
305
+ offset += 4;
306
+ }
307
+ // Vectors
308
+ const vectorsLen = buffer.byteLength - offset - 4;
309
+ const vectors = uint8.subarray(offset, offset + vectorsLen);
310
+ return {
311
+ vectors,
312
+ indices,
313
+ metadata,
314
+ config,
315
+ };
316
+ }
317
+ /**
318
+ * Create a binary vector compressor for memory embeddings
319
+ * Integrates with existing HeartFlow memory system
320
+ *
321
+ * @param config - Configuration for the compressor
322
+ * @returns Memory compression interface
323
+ */
324
+ export function createBinaryVectorCompressor(config = {}) {
325
+ const fullConfig = { ...DEFAULT_BINARY_CONFIG, ...config };
326
+ let index = createBinaryIndex(fullConfig);
327
+ logger.info('Binary vector compressor initialized', { ...fullConfig });
328
+ return {
329
+ /**
330
+ * Compress and store an embedding
331
+ */
332
+ store(id, embedding, importance = 0.5) {
333
+ index = addToBinaryIndex(index, [embedding], [id], [{ id, importance, timestamp: Date.now() }]);
334
+ logger.debug(`Stored embedding ${id}, total: ${index.indices.length}`);
335
+ },
336
+ /**
337
+ * Compress and store multiple embeddings
338
+ */
339
+ storeBatch(ids, embeddings, importances) {
340
+ const metadata = ids.map((id, i) => ({
341
+ id,
342
+ importance: importances?.[i] ?? 0.5,
343
+ timestamp: Date.now(),
344
+ }));
345
+ index = addToBinaryIndex(index, embeddings, ids, metadata);
346
+ logger.debug(`Stored ${ids.length} embeddings, total: ${index.indices.length}`);
347
+ },
348
+ /**
349
+ * Search for similar embeddings
350
+ */
351
+ search(queryEmbedding, k = 10) {
352
+ return searchBinaryIndex(index, queryEmbedding, k);
353
+ },
354
+ /**
355
+ * Get compression statistics
356
+ */
357
+ getStats() {
358
+ const stats = getCompressionStats(index);
359
+ return {
360
+ ...stats,
361
+ stored: index.indices.length,
362
+ compressionPercent: `${stats.compressionRatio.toFixed(1)}x`,
363
+ };
364
+ },
365
+ /**
366
+ * Clear all stored vectors
367
+ */
368
+ clear() {
369
+ index = createBinaryIndex(fullConfig);
370
+ logger.info('Binary vector index cleared');
371
+ },
372
+ /**
373
+ * Export serialized index
374
+ */
375
+ export() {
376
+ return serializeBinaryIndex(index);
377
+ },
378
+ /**
379
+ * Import serialized index
380
+ */
381
+ import(buffer) {
382
+ index = deserializeBinaryIndex(buffer);
383
+ logger.info(`Imported binary index with ${index.indices.length} vectors`);
384
+ },
385
+ };
386
+ }
387
+ /**
388
+ * Common embedding dimensions for major embedding models
389
+ */
390
+ export const EMBEDDING_DIMENSIONS = {
391
+ /** OpenAI text-embedding-3-small, text-embedding-3-large */
392
+ OPENAI_1536: 1536,
393
+ /** OpenAI text-embedding-ada-002 */
394
+ OPENAI_1536_ADA: 1536,
395
+ /** OpenAI text-embedding-3-large (3072 dim) */
396
+ OPENAI_3072: 3072,
397
+ /** Cohere embed-english-v3.0 */
398
+ COHERE_1024: 1024,
399
+ /** Cohere embed-multilingual-v3.0 */
400
+ COHERE_MULTILINGUAL: 1024,
401
+ /** Vertex AI textembedding-gecko */
402
+ VERTEX_GECKO: 768,
403
+ /** Default dimension */
404
+ DEFAULT: 1536,
405
+ };
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Budget-Aware Memory Retrieval System
3
+ *
4
+ * A deterministic, token-budget-conscious memory retrieval system that:
5
+ * - Respects max token budgets during retrieval
6
+ * - Provides sub-millisecond query latency
7
+ * - Ensures identical queries return identical results
8
+ * - Supports semantic caching for similar queries
9
+ *
10
+ * Based on: Doorman11991/budget-aware-mcp (https://github.com/Doorman11991/budget-aware-mcp)
11
+ *
12
+ * Key concepts:
13
+ * - Hop-based graph walk: expand outward from anchor symbols hop by hop
14
+ * - Token budget: retrieval stops when budget is exhausted
15
+ * - Deterministic ordering: alphabetical within each hop level
16
+ * - Semantic cache: similar queries hit cache instantly
17
+ *
18
+ * @module core/memory
19
+ * @fileoverview Budget-aware memory retrieval with token control
20
+ */
21
+ import { createLogger } from '../../utils/logger.js';
22
+ import { cosineSimilarity } from './embedder.js';
23
+ const logger = createLogger('[BudgetRetrieval]');
24
+ /**
25
+ * Trigram-based similarity for semantic caching
26
+ */
27
+ function extractTrigrams(text) {
28
+ const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, ' ');
29
+ const words = normalized.split(/\s+/).filter(w => w.length > 0);
30
+ const trigrams = [];
31
+ for (const word of words) {
32
+ if (word.length >= 3) {
33
+ for (let i = 0; i <= word.length - 3; i++) {
34
+ trigrams.push(word.slice(i, i + 3));
35
+ }
36
+ }
37
+ }
38
+ return trigrams;
39
+ }
40
+ /**
41
+ * Jaccard similarity between two trigram sets
42
+ */
43
+ function trigramSimilarity(a, b) {
44
+ if (a.length === 0 || b.length === 0)
45
+ return 0;
46
+ const setA = new Set(a);
47
+ const setB = new Set(b);
48
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
49
+ const union = new Set([...setA, ...setB]);
50
+ return intersection.size / union.size;
51
+ }
52
+ /**
53
+ * Estimate tokens for a memory entry (rough approximation)
54
+ */
55
+ function estimateTokens(entry) {
56
+ // Rough estimate: ~4 characters per token for typical English text
57
+ // Plus overhead for metadata
58
+ const contentTokens = Math.ceil(entry.content.length / 4);
59
+ const metadataTokens = 20; // tags, id, timestamps, etc.
60
+ return contentTokens + metadataTokens;
61
+ }
62
+ /**
63
+ * Create a deterministic hash from query
64
+ */
65
+ function hashQuery(query) {
66
+ const parts = [
67
+ query.query.toLowerCase().trim(),
68
+ query.anchorId || '',
69
+ query.tier || '',
70
+ (query.maxTokens || 0).toString(),
71
+ (query.tags || []).sort().join(','),
72
+ (query.importanceThreshold || 0).toString()
73
+ ];
74
+ // Simple hash function for determinism
75
+ const str = parts.join('|');
76
+ let hash = 0;
77
+ for (let i = 0; i < str.length; i++) {
78
+ const char = str.charCodeAt(i);
79
+ hash = ((hash << 5) - hash) + char;
80
+ hash = hash & hash; // Convert to 32-bit integer
81
+ }
82
+ return Math.abs(hash).toString(36);
83
+ }
84
+ /**
85
+ * Alphabetical sort for deterministic ordering within hop levels
86
+ */
87
+ function deterministicSort(entries) {
88
+ return entries.sort((a, b) => {
89
+ // First by hop level
90
+ if (a.hopLevel !== b.hopLevel) {
91
+ return a.hopLevel - b.hopLevel;
92
+ }
93
+ // Then alphabetically by content
94
+ return a.entry.content.localeCompare(b.entry.content);
95
+ });
96
+ }
97
+ /**
98
+ * Default configuration
99
+ */
100
+ export const DEFAULT_BUDGET_CONFIG = {
101
+ maxTokens: 4000,
102
+ tokensPerEntry: 200,
103
+ maxHops: 3,
104
+ enableCache: true,
105
+ cacheTTLMs: 5 * 60 * 1000, // 5 minutes
106
+ cacheSimilarityThreshold: 0.7,
107
+ enableImportanceFilter: true,
108
+ importanceThreshold: 0.3
109
+ };
110
+ /**
111
+ * Create a budget-aware retrieval engine
112
+ */
113
+ export function createBudgetRetrievalEngine(config = {}, embedder) {
114
+ const fullConfig = { ...DEFAULT_BUDGET_CONFIG, ...config };
115
+ logger.info('Creating BudgetRetrievalEngine', { ...fullConfig });
116
+ // Memory storage
117
+ const entries = new Map();
118
+ const entriesByTier = new Map();
119
+ const entriesByTag = new Map();
120
+ // Embedding index (if embedder provided)
121
+ const entryEmbeddings = new Map();
122
+ // Semantic cache
123
+ const cache = new Map();
124
+ let cacheStats = { hits: 0, misses: 0, hitRate: 0, avgSimilarity: 0 };
125
+ /**
126
+ * Check semantic cache for similar query
127
+ */
128
+ function checkCache(query) {
129
+ if (!fullConfig.enableCache)
130
+ return null;
131
+ const queryTrigrams = extractTrigrams(query.query);
132
+ const queryHash = hashQuery(query);
133
+ // Check exact match first
134
+ const exactEntry = cache.get(queryHash);
135
+ if (exactEntry) {
136
+ const age = Date.now() - exactEntry.timestamp;
137
+ if (age < fullConfig.cacheTTLMs) {
138
+ return exactEntry;
139
+ }
140
+ }
141
+ // Check similar queries
142
+ let bestMatch = null;
143
+ let bestSimilarity = 0;
144
+ for (const entry of cache.values()) {
145
+ const age = Date.now() - entry.timestamp;
146
+ if (age >= fullConfig.cacheTTLMs)
147
+ continue;
148
+ const similarity = trigramSimilarity(queryTrigrams, entry.trigrams);
149
+ if (similarity >= fullConfig.cacheSimilarityThreshold && similarity > bestSimilarity) {
150
+ bestSimilarity = similarity;
151
+ bestMatch = entry;
152
+ }
153
+ }
154
+ return bestMatch;
155
+ }
156
+ /**
157
+ * Store result in cache
158
+ */
159
+ function storeCache(query, result) {
160
+ if (!fullConfig.enableCache)
161
+ return;
162
+ const queryHash = hashQuery(query);
163
+ const trigrams = extractTrigrams(query.query);
164
+ cache.set(queryHash, {
165
+ queryHash,
166
+ queryText: query.query,
167
+ result,
168
+ timestamp: Date.now(),
169
+ trigrams
170
+ });
171
+ // Cleanup old entries
172
+ const now = Date.now();
173
+ for (const [key, entry] of cache.entries()) {
174
+ if (now - entry.timestamp > fullConfig.cacheTTLMs) {
175
+ cache.delete(key);
176
+ }
177
+ }
178
+ }
179
+ /**
180
+ * Calculate relevance score for an entry
181
+ */
182
+ function calculateRelevance(entry, query, anchorEntry) {
183
+ let relevance = 0;
184
+ // Base relevance from importance
185
+ relevance += entry.importance * 0.3;
186
+ // Text similarity if query provided
187
+ if (query.query && embedder) {
188
+ const queryEmb = embedder.embed(query.query);
189
+ const entryEmb = entryEmbeddings.get(entry.id);
190
+ if (queryEmb && entryEmb) {
191
+ const sim = cosineSimilarity(queryEmb, entryEmb);
192
+ relevance += sim * 0.4;
193
+ }
194
+ }
195
+ // Content match
196
+ const queryLower = query.query.toLowerCase();
197
+ const contentLower = entry.content.toLowerCase();
198
+ if (contentLower.includes(queryLower)) {
199
+ relevance += 0.3;
200
+ }
201
+ // Tag match
202
+ if (query.tags && query.tags.length > 0) {
203
+ const entryTags = new Set(entry.tags.map(t => t.toLowerCase()));
204
+ const matchCount = query.tags.filter(t => entryTags.has(t.toLowerCase())).length;
205
+ relevance += (matchCount / query.tags.length) * 0.2;
206
+ }
207
+ // Hop-based relevance from anchor
208
+ if (anchorEntry) {
209
+ // Entries referenced by anchor = hop 1
210
+ const anchor = anchorEntry;
211
+ const ent = entry;
212
+ if (anchor.references?.includes(ent.id)) {
213
+ return relevance + 0.5 - 0.1; // hop 1 bonus
214
+ }
215
+ // Entries referencing anchor = hop 1
216
+ if (ent.references?.includes(anchor.id)) {
217
+ return relevance + 0.5 - 0.1;
218
+ }
219
+ // Same session = hop 2
220
+ if (anchor.sessionId && anchor.sessionId === ent.sessionId) {
221
+ return relevance + 0.4 - 0.2;
222
+ }
223
+ }
224
+ return Math.min(1, relevance);
225
+ }
226
+ /**
227
+ * Perform budget-aware retrieval
228
+ */
229
+ async function retrieve(query) {
230
+ const startMs = Date.now();
231
+ const maxTokens = query.maxTokens || fullConfig.maxTokens;
232
+ logger.debug('Retrieving with budget', { query: query.query, maxTokens });
233
+ // Check cache first
234
+ const cached = checkCache(query);
235
+ if (cached) {
236
+ cacheStats.hits++;
237
+ cacheStats.avgSimilarity = (cacheStats.avgSimilarity * (cacheStats.hits - 1) + (cached.result.cacheSimilarity || 1)) / cacheStats.hits;
238
+ cacheStats.hitRate = cacheStats.hits / (cacheStats.hits + cacheStats.misses);
239
+ logger.debug('Cache hit', { similarity: cached.result.cacheSimilarity });
240
+ return {
241
+ ...cached.result,
242
+ cacheHit: true,
243
+ processingMs: Date.now() - startMs
244
+ };
245
+ }
246
+ cacheStats.misses++;
247
+ cacheStats.hitRate = cacheStats.hits / (cacheStats.hits + cacheStats.misses);
248
+ // Get anchor entry if specified
249
+ const anchorEntry = query.anchorId ? entries.get(query.anchorId) : undefined;
250
+ // Collect candidate entries
251
+ let candidateIds;
252
+ if (query.tier) {
253
+ const tierSet = entriesByTier.get(query.tier);
254
+ candidateIds = tierSet ? Array.from(tierSet) : [];
255
+ }
256
+ else {
257
+ candidateIds = Array.from(entries.keys());
258
+ }
259
+ // Filter by tags if specified
260
+ if (query.tags && query.tags.length > 0) {
261
+ candidateIds = candidateIds.filter(id => {
262
+ const entry = entries.get(id);
263
+ if (!entry)
264
+ return false;
265
+ const entryTags = new Set(entry.tags.map(t => t.toLowerCase()));
266
+ return query.tags.some(t => entryTags.has(t.toLowerCase()));
267
+ });
268
+ }
269
+ // Score and filter candidates
270
+ const scoredCandidates = [];
271
+ for (const id of candidateIds) {
272
+ const entry = entries.get(id);
273
+ if (!entry)
274
+ continue;
275
+ // Importance filter
276
+ if (fullConfig.enableImportanceFilter && entry.importance < fullConfig.importanceThreshold) {
277
+ continue;
278
+ }
279
+ if (query.importanceThreshold && entry.importance < query.importanceThreshold) {
280
+ continue;
281
+ }
282
+ const relevance = calculateRelevance(entry, query, anchorEntry);
283
+ const tokenEstimate = estimateTokens(entry);
284
+ // Determine hop level
285
+ let hopLevel = 0;
286
+ if (anchorEntry) {
287
+ if (id === anchorEntry.id) {
288
+ hopLevel = 0;
289
+ }
290
+ else if ((anchorEntry.references?.includes(id)) || (entry.references?.includes(anchorEntry.id))) {
291
+ hopLevel = 1;
292
+ }
293
+ else {
294
+ hopLevel = 2;
295
+ }
296
+ }
297
+ scoredCandidates.push({
298
+ entry,
299
+ relevance,
300
+ hopLevel,
301
+ tokenEstimate,
302
+ cumulativeTokens: 0 // Will be calculated after sorting
303
+ });
304
+ }
305
+ // Sort deterministically
306
+ const sorted = deterministicSort(scoredCandidates);
307
+ // Build budget-constrained result
308
+ const budget = {
309
+ maxTokens,
310
+ usedTokens: 0,
311
+ remainingTokens: maxTokens,
312
+ entriesSelected: 0
313
+ };
314
+ const selected = [];
315
+ for (const candidate of sorted) {
316
+ if (budget.remainingTokens < candidate.tokenEstimate) {
317
+ continue; // Can't afford this entry
318
+ }
319
+ if (candidate.hopLevel > fullConfig.maxHops) {
320
+ continue; // Exceeded max hops
321
+ }
322
+ candidate.cumulativeTokens = budget.usedTokens + candidate.tokenEstimate;
323
+ budget.usedTokens = candidate.cumulativeTokens;
324
+ budget.remainingTokens = maxTokens - budget.usedTokens;
325
+ budget.entriesSelected++;
326
+ selected.push(candidate);
327
+ }
328
+ const result = {
329
+ entries: selected,
330
+ tokensUsed: budget.usedTokens,
331
+ budgetRemaining: budget.remainingTokens,
332
+ cacheHit: false,
333
+ queryHash: hashQuery(query),
334
+ processingMs: Date.now() - startMs,
335
+ totalAvailable: candidateIds.length
336
+ };
337
+ // Store in cache
338
+ storeCache(query, result);
339
+ logger.debug('Retrieval complete', {
340
+ tokensUsed: result.tokensUsed,
341
+ entriesSelected: result.entries.length,
342
+ processingMs: result.processingMs,
343
+ cacheHit: result.cacheHit
344
+ });
345
+ return result;
346
+ }
347
+ /**
348
+ * Index a memory entry
349
+ */
350
+ function index(entry) {
351
+ entries.set(entry.id, entry);
352
+ // Index by tier
353
+ if (!entriesByTier.has(entry.tier)) {
354
+ entriesByTier.set(entry.tier, new Set());
355
+ }
356
+ entriesByTier.get(entry.tier).add(entry.id);
357
+ // Index by tags
358
+ for (const tag of entry.tags) {
359
+ if (!entriesByTag.has(tag)) {
360
+ entriesByTag.set(tag, new Set());
361
+ }
362
+ entriesByTag.get(tag).add(entry.id);
363
+ }
364
+ // Embed for semantic search
365
+ if (embedder) {
366
+ entryEmbeddings.set(entry.id, embedder.embed(entry.content));
367
+ }
368
+ }
369
+ /**
370
+ * Remove entry from index
371
+ */
372
+ function remove(id) {
373
+ const entry = entries.get(id);
374
+ if (!entry)
375
+ return;
376
+ entries.delete(id);
377
+ // Remove from tier index
378
+ entriesByTier.get(entry.tier)?.delete(id);
379
+ // Remove from tag indices
380
+ for (const tag of entry.tags) {
381
+ entriesByTag.get(tag)?.delete(id);
382
+ }
383
+ // Remove embedding
384
+ entryEmbeddings.delete(id);
385
+ }
386
+ /**
387
+ * Clear all entries
388
+ */
389
+ function clear() {
390
+ entries.clear();
391
+ entriesByTier.clear();
392
+ entriesByTag.clear();
393
+ entryEmbeddings.clear();
394
+ cache.clear();
395
+ logger.info('Budget retrieval engine cleared');
396
+ }
397
+ /**
398
+ * Get cache statistics
399
+ */
400
+ function getCacheStats() {
401
+ return { ...cacheStats };
402
+ }
403
+ /**
404
+ * Update configuration
405
+ */
406
+ function updateConfig(config) {
407
+ Object.assign(fullConfig, config);
408
+ logger.debug('Config updated', config);
409
+ }
410
+ return {
411
+ retrieve,
412
+ index,
413
+ remove,
414
+ clear,
415
+ getCacheStats,
416
+ updateConfig
417
+ };
418
+ }
419
+ // Export utilities for external use
420
+ export { extractTrigrams, trigramSimilarity, estimateTokens, hashQuery };
@@ -11,3 +11,5 @@ export { createContextFragmentationEngine } from './context-fragmentation.js';
11
11
  export { createHybridSearchEngine, createBM25Index, bm25Score, normalizeBM25Scores, DEFAULT_HYBRID_CONFIG } from './hybrid-search.js';
12
12
  export { createPatternRecognizer } from './pattern-recognizer.js';
13
13
  export { createMemoryObserver } from './observer.js';
14
+ export { createBinaryVectorCompressor, quantizeToBinary, hammingDistance, hammingToSimilarity, createBinaryIndex, addToBinaryIndex, searchBinaryIndex, getCompressionStats, serializeBinaryIndex, deserializeBinaryIndex, DEFAULT_BINARY_CONFIG, EMBEDDING_DIMENSIONS } from './binary-vector.js';
15
+ export { createBudgetRetrievalEngine, DEFAULT_BUDGET_CONFIG, extractTrigrams, trigramSimilarity, estimateTokens, hashQuery } from './budget-retrieval.js';
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Execution Governor - Cross-Context Tool Call Governance
3
+ *
4
+ * Inspired by chitin (Execution kernel for AI coding agents)
5
+ * Provides declarative policy enforcement for tool calls across different execution contexts.
6
+ *
7
+ * Features:
8
+ * - Gating tool calls against typed policies
9
+ * - Severity ladder with lockdown counter
10
+ * - Tamper-evident audit chain
11
+ * - Bounds enforcement on push-shaped actions
12
+ * - Heuristic signals for blast-radius, floundering, drift
13
+ *
14
+ * @module core/security
15
+ * @fileoverview Execution governance for HeartFlow
16
+ */
17
+ import { createLogger } from '../../utils/logger.js';
18
+ import * as crypto from 'crypto';
19
+ const logger = createLogger('[ExecutionGovernor]');
20
+ // ============================================================
21
+ // Default Configuration
22
+ // ============================================================
23
+ const DEFAULT_CONFIG = {
24
+ enableAuditChain: true,
25
+ enableSeverityLockdown: true,
26
+ maxLockdownCount: 5,
27
+ severityThresholds: {
28
+ low: 3,
29
+ medium: 5,
30
+ high: 10,
31
+ critical: 20,
32
+ },
33
+ policies: [
34
+ {
35
+ id: 'default-read',
36
+ action: 'read',
37
+ allow: true,
38
+ severity: 'info',
39
+ },
40
+ {
41
+ id: 'default-write',
42
+ action: 'write',
43
+ allow: true,
44
+ conditions: {
45
+ bounds: {
46
+ maxLinesChanged: 500,
47
+ maxFilesAffected: 10,
48
+ },
49
+ requireConfirmation: true,
50
+ },
51
+ severity: 'medium',
52
+ },
53
+ {
54
+ id: 'default-delete',
55
+ action: 'delete',
56
+ allow: true,
57
+ conditions: {
58
+ bounds: {
59
+ maxFilesAffected: 1,
60
+ },
61
+ requireConfirmation: true,
62
+ },
63
+ severity: 'high',
64
+ },
65
+ {
66
+ id: 'default-execute',
67
+ action: 'execute',
68
+ allow: false,
69
+ severity: 'critical',
70
+ },
71
+ {
72
+ id: 'default-network',
73
+ action: 'network',
74
+ allow: true,
75
+ conditions: {
76
+ maxPerHour: 50,
77
+ },
78
+ severity: 'medium',
79
+ },
80
+ ],
81
+ defaultPolicy: {
82
+ id: 'default-unknown',
83
+ action: 'unknown',
84
+ allow: false,
85
+ severity: 'high',
86
+ },
87
+ };
88
+ // ============================================================
89
+ // Helper Functions
90
+ // ============================================================
91
+ /**
92
+ * Classify a tool call into a canonical action
93
+ */
94
+ function classifyAction(toolName, args) {
95
+ const lowerTool = toolName.toLowerCase();
96
+ // Read actions
97
+ if (lowerTool.includes('read') || lowerTool.includes('get') || lowerTool.includes('fetch') ||
98
+ lowerTool.includes('search') || lowerTool.includes('query') || lowerTool.includes('select')) {
99
+ return 'read';
100
+ }
101
+ // Write actions
102
+ if (lowerTool.includes('write') || lowerTool.includes('create') || lowerTool.includes('add') ||
103
+ lowerTool.includes('insert') || lowerTool.includes('post') || lowerTool.includes('put')) {
104
+ return 'write';
105
+ }
106
+ // Delete actions
107
+ if (lowerTool.includes('delete') || lowerTool.includes('remove') || lowerTool.includes('drop') ||
108
+ lowerTool.includes('unlink') || lowerTool.includes('rm')) {
109
+ return 'delete';
110
+ }
111
+ // Execute actions
112
+ if (lowerTool.includes('exec') || lowerTool.includes('run') || lowerTool.includes('execute') ||
113
+ lowerTool.includes('spawn') || lowerTool.includes('fork') || lowerTool.includes('shell')) {
114
+ return 'execute';
115
+ }
116
+ // Network actions
117
+ if (lowerTool.includes('http') || lowerTool.includes('fetch') || lowerTool.includes('request') ||
118
+ lowerTool.includes('send') || lowerTool.includes('post') || lowerTool.includes('url')) {
119
+ return 'network';
120
+ }
121
+ // Filesystem actions
122
+ if (lowerTool.includes('file') || lowerTool.includes('dir') || lowerTool.includes('path') ||
123
+ lowerTool.includes('mkdir') || lowerTool.includes('stat') || lowerTool.includes('ls')) {
124
+ return 'filesystem';
125
+ }
126
+ // Process actions
127
+ if (lowerTool.includes('process') || lowerTool.includes('pid') || lowerTool.includes('kill') ||
128
+ lowerTool.includes('signal')) {
129
+ return 'process';
130
+ }
131
+ // Memory actions
132
+ if (lowerTool.includes('memory') || lowerTool.includes('store') || lowerTool.includes('recall') ||
133
+ lowerTool.includes('embed')) {
134
+ return 'memory';
135
+ }
136
+ // Agent actions
137
+ if (lowerTool.includes('agent') || lowerTool.includes('spawn') || lowerTool.includes('delegate') ||
138
+ lowerTool.includes('subagent')) {
139
+ return 'agent';
140
+ }
141
+ return 'unknown';
142
+ }
143
+ /**
144
+ * Calculate heuristic signals from envelope context
145
+ */
146
+ function calculateHeuristics(envelope) {
147
+ const context = envelope.context;
148
+ return {
149
+ blastRadius: context.blastRadius || 'low',
150
+ floundering: context.floundering || false,
151
+ drift: context.drift || 0,
152
+ repetitionCount: 0, // Tracked in agent state
153
+ urgency: 0.5, // Default
154
+ stakes: envelope.action === 'delete' || envelope.action === 'execute' ? 0.9 : 0.3,
155
+ };
156
+ }
157
+ /**
158
+ * Hash data for audit chain
159
+ */
160
+ function hashData(data, prevHash) {
161
+ const payload = JSON.stringify({ data, prevHash, timestamp: Date.now() });
162
+ return crypto.createHash('sha256').update(payload).digest('hex');
163
+ }
164
+ // ============================================================
165
+ // Execution Governor Class
166
+ // ============================================================
167
+ export class ExecutionGovernor {
168
+ config;
169
+ agentStates = new Map();
170
+ auditChain = [];
171
+ decisionHistory = new Map();
172
+ constructor(config = {}) {
173
+ this.config = { ...DEFAULT_CONFIG, ...config };
174
+ logger.info('ExecutionGovernor initialized', {
175
+ policiesCount: this.config.policies.length,
176
+ auditChainEnabled: this.config.enableAuditChain,
177
+ });
178
+ }
179
+ /**
180
+ * Process a tool call and return governance decision
181
+ */
182
+ async gate(envelope) {
183
+ // Auto-classify action if not provided
184
+ if (envelope.action === 'unknown') {
185
+ envelope.action = classifyAction(envelope.toolName, envelope.args);
186
+ }
187
+ // Calculate heuristic signals
188
+ const signals = calculateHeuristics(envelope);
189
+ // Get or create agent state
190
+ let state = this.agentStates.get(envelope.context.sessionId);
191
+ if (!state) {
192
+ state = this.createAgentState(envelope.context.sessionId);
193
+ this.agentStates.set(envelope.context.sessionId, state);
194
+ }
195
+ // Check for lockdown
196
+ if (state.locked && this.config.enableSeverityLockdown) {
197
+ const decision = this.createDecision(envelope, 'deny', 'Agent is in lockdown due to severity threshold breach');
198
+ this.appendAudit('decision', decision);
199
+ return decision;
200
+ }
201
+ // Check severity thresholds
202
+ if (this.config.enableSeverityLockdown) {
203
+ const severityLevel = this.checkSeverityThreshold(state);
204
+ if (severityLevel) {
205
+ state.lockdownCount++;
206
+ if (state.lockdownCount >= this.config.maxLockdownCount) {
207
+ state.locked = true;
208
+ this.appendAudit('lockdown', { sessionId: envelope.context.sessionId, count: state.lockdownCount });
209
+ logger.warn('Agent locked due to severity threshold', { sessionId: envelope.context.sessionId });
210
+ }
211
+ }
212
+ }
213
+ // Find matching policy
214
+ const policy = this.findMatchingPolicy(envelope.action);
215
+ // Evaluate bounds if present
216
+ let reason = 'Policy evaluation';
217
+ let decision = 'allow';
218
+ if (policy) {
219
+ if (!policy.allow) {
220
+ decision = 'deny';
221
+ reason = `Policy ${policy.id} explicitly denies ${envelope.action}`;
222
+ }
223
+ else if (policy.conditions?.requireConfirmation) {
224
+ decision = 'escalate';
225
+ reason = `${envelope.action} requires operator confirmation`;
226
+ }
227
+ else if (policy.conditions?.bounds) {
228
+ const boundCheck = this.checkBounds(envelope, policy.conditions.bounds);
229
+ if (!boundCheck.allowed) {
230
+ decision = 'deny';
231
+ reason = boundCheck.reason || 'Bounds check failed';
232
+ }
233
+ }
234
+ // Update severity counts
235
+ if (this.config.enableSeverityLockdown && decision !== 'allow') {
236
+ state.severityCounts[policy.severity] = (state.severityCounts[policy.severity] || 0) + 1;
237
+ }
238
+ }
239
+ else {
240
+ decision = this.config.defaultPolicy.allow ? 'allow' : 'deny';
241
+ reason = `No matching policy, using default: ${this.config.defaultPolicy.allow ? 'allow' : 'deny'}`;
242
+ }
243
+ // Update state
244
+ state.totalToolCalls++;
245
+ state.lastActivity = Date.now();
246
+ // Create decision
247
+ const governanceDecision = this.createDecision(envelope, decision, reason, policy ?? undefined);
248
+ this.decisionHistory.set(envelope.id, governanceDecision);
249
+ this.appendAudit('decision', governanceDecision);
250
+ if (decision === 'deny') {
251
+ logger.info('Tool call denied', { envelopeId: envelope.id, action: envelope.action, reason });
252
+ }
253
+ else if (decision === 'escalate') {
254
+ logger.info('Tool call escalated', { envelopeId: envelope.id, action: envelope.action, reason });
255
+ }
256
+ return governanceDecision;
257
+ }
258
+ /**
259
+ * Create a new tool call envelope
260
+ */
261
+ createEnvelope(toolName, args, context) {
262
+ return {
263
+ id: crypto.randomUUID(),
264
+ action: classifyAction(toolName, args),
265
+ toolName,
266
+ args,
267
+ context,
268
+ };
269
+ }
270
+ /**
271
+ * Unlock a locked agent
272
+ */
273
+ unlock(sessionId) {
274
+ const state = this.agentStates.get(sessionId);
275
+ if (!state)
276
+ return false;
277
+ state.locked = false;
278
+ state.lockdownCount = 0;
279
+ state.severityCounts = {};
280
+ this.appendAudit('unlock', { sessionId });
281
+ logger.info('Agent unlocked', { sessionId });
282
+ return true;
283
+ }
284
+ /**
285
+ * Reset agent state
286
+ */
287
+ resetState(sessionId) {
288
+ const state = this.agentStates.get(sessionId);
289
+ if (!state)
290
+ return false;
291
+ const newState = this.createAgentState(sessionId);
292
+ this.agentStates.set(sessionId, newState);
293
+ this.appendAudit('state_reset', { sessionId });
294
+ logger.info('Agent state reset', { sessionId });
295
+ return true;
296
+ }
297
+ /**
298
+ * Get agent state
299
+ */
300
+ getAgentState(sessionId) {
301
+ return this.agentStates.get(sessionId) || null;
302
+ }
303
+ /**
304
+ * Get audit chain
305
+ */
306
+ getAuditChain() {
307
+ return [...this.auditChain];
308
+ }
309
+ /**
310
+ * Get decision from history
311
+ */
312
+ getDecision(envelopeId) {
313
+ return this.decisionHistory.get(envelopeId) || null;
314
+ }
315
+ /**
316
+ * Verify audit chain integrity
317
+ */
318
+ verifyAuditChain() {
319
+ for (let i = 1; i < this.auditChain.length; i++) {
320
+ if (this.auditChain[i].prevHash !== this.auditChain[i - 1].hash) {
321
+ return { valid: false, brokenAt: i };
322
+ }
323
+ }
324
+ return { valid: true };
325
+ }
326
+ // Private helper methods
327
+ createAgentState(sessionId) {
328
+ return {
329
+ sessionId,
330
+ severityCounts: {},
331
+ lockdownCount: 0,
332
+ totalToolCalls: 0,
333
+ lastActivity: Date.now(),
334
+ locked: false,
335
+ };
336
+ }
337
+ checkSeverityThreshold(state) {
338
+ const counts = state.severityCounts;
339
+ if ((counts['critical'] || 0) >= this.config.severityThresholds.critical)
340
+ return 'critical';
341
+ if ((counts['high'] || 0) >= this.config.severityThresholds.high)
342
+ return 'high';
343
+ if ((counts['medium'] || 0) >= this.config.severityThresholds.medium)
344
+ return 'medium';
345
+ if ((counts['low'] || 0) >= this.config.severityThresholds.low)
346
+ return 'low';
347
+ return null;
348
+ }
349
+ findMatchingPolicy(action) {
350
+ return this.config.policies.find(p => p.action === action) || null;
351
+ }
352
+ checkBounds(envelope, bounds) {
353
+ if (!bounds)
354
+ return { allowed: true };
355
+ // Check filesystem bounds
356
+ if (bounds.allowedPaths || bounds.deniedPaths) {
357
+ const path = this.extractPath(envelope.args);
358
+ if (path) {
359
+ if (bounds.deniedPaths?.some(p => path.includes(p))) {
360
+ return { allowed: false, reason: `Path ${path} is in denied list` };
361
+ }
362
+ if (bounds.allowedPaths && !bounds.allowedPaths.some(p => path.includes(p))) {
363
+ return { allowed: false, reason: `Path ${path} is not in allowed list` };
364
+ }
365
+ }
366
+ }
367
+ // Check data size
368
+ if (bounds.maxDataSize) {
369
+ const size = this.estimateDataSize(envelope.args);
370
+ if (size > bounds.maxDataSize) {
371
+ return { allowed: false, reason: `Data size ${size} exceeds limit ${bounds.maxDataSize}` };
372
+ }
373
+ }
374
+ return { allowed: true };
375
+ }
376
+ extractPath(args) {
377
+ for (const key of ['path', 'file', 'filePath', 'target', 'destination']) {
378
+ if (args[key] && typeof args[key] === 'string') {
379
+ return args[key];
380
+ }
381
+ }
382
+ return null;
383
+ }
384
+ estimateDataSize(args) {
385
+ try {
386
+ return JSON.stringify(args).length;
387
+ }
388
+ catch {
389
+ return 0;
390
+ }
391
+ }
392
+ createDecision(envelope, decision, reason, policy) {
393
+ const decisionData = {
394
+ envelopeId: envelope.id,
395
+ action: envelope.action,
396
+ decision,
397
+ reason,
398
+ timestamp: Date.now(),
399
+ };
400
+ return {
401
+ ...decisionData,
402
+ matchedPolicy: policy,
403
+ hash: hashData(decisionData, this.auditChain.length > 0 ? this.auditChain[this.auditChain.length - 1].hash : 'genesis'),
404
+ };
405
+ }
406
+ appendAudit(type, payload) {
407
+ if (!this.config.enableAuditChain)
408
+ return;
409
+ const event = {
410
+ type,
411
+ payload,
412
+ timestamp: Date.now(),
413
+ hash: '',
414
+ prevHash: this.auditChain.length > 0 ? this.auditChain[this.auditChain.length - 1].hash : 'genesis',
415
+ };
416
+ event.hash = hashData({ type, payload, timestamp: event.timestamp }, event.prevHash);
417
+ this.auditChain.push(event);
418
+ // Keep chain bounded to last 10000 events
419
+ if (this.auditChain.length > 10000) {
420
+ this.auditChain = this.auditChain.slice(-5000);
421
+ }
422
+ }
423
+ }
424
+ // ============================================================
425
+ // Factory Function
426
+ // ============================================================
427
+ /**
428
+ * Create an ExecutionGovernor instance with optional custom config
429
+ */
430
+ export function createExecutionGovernor(config) {
431
+ return new ExecutionGovernor(config);
432
+ }
433
+ // ============================================================
434
+ // Export default config for customization
435
+ // ============================================================
436
+ export { DEFAULT_CONFIG };
@@ -1,2 +1,3 @@
1
1
  export * from './privacy.js';
2
2
  export * from './agent-shield.js';
3
+ export { ExecutionGovernor, createExecutionGovernor, DEFAULT_CONFIG } from './execution-governor.js';
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '2.3.3';
1
+ export const VERSION = '2.3.5';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mark-improving-agent",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "description": "Self-evolving AI agent with permanent memory, identity continuity, and self-evolution — for AI agents that need to remember, learn, and evolve across sessions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",