kiro-memory 1.9.0 → 2.1.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/package.json +3 -3
- package/plugin/dist/cli/contextkit.js +305 -174
- package/plugin/dist/hooks/agentSpawn.js +311 -180
- package/plugin/dist/hooks/kiro-hooks.js +299 -168
- package/plugin/dist/hooks/postToolUse.js +303 -172
- package/plugin/dist/hooks/stop.js +308 -177
- package/plugin/dist/hooks/userPromptSubmit.js +303 -172
- package/plugin/dist/index.js +303 -299
- package/plugin/dist/sdk/index.js +299 -172
- package/plugin/dist/services/search/EmbeddingService.js +88 -23
- package/plugin/dist/services/search/HybridSearch.js +190 -84
- package/plugin/dist/services/search/VectorSearch.js +128 -45
- package/plugin/dist/services/search/index.js +192 -223
- package/plugin/dist/services/sqlite/Database.js +55 -153
- package/plugin/dist/services/sqlite/Observations.js +23 -12
- package/plugin/dist/services/sqlite/Search.js +31 -19
- package/plugin/dist/services/sqlite/index.js +108 -183
- package/plugin/dist/viewer.css +1 -0
- package/plugin/dist/viewer.html +2 -179
- package/plugin/dist/viewer.js +15 -24942
- package/plugin/dist/viewer.js.map +7 -0
- package/plugin/dist/worker-service.js +158 -5551
- package/plugin/dist/worker-service.js.map +7 -0
package/plugin/dist/index.js
CHANGED
|
@@ -98,9 +98,25 @@ function consolidateObservations(db, project, options = {}) {
|
|
|
98
98
|
ORDER BY cnt DESC
|
|
99
99
|
`).all(project, minGroupSize);
|
|
100
100
|
if (groups.length === 0) return { merged: 0, removed: 0 };
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
if (options.dryRun) {
|
|
102
|
+
let totalMerged = 0;
|
|
103
|
+
let totalRemoved = 0;
|
|
104
|
+
for (const group of groups) {
|
|
105
|
+
const obsIds = group.ids.split(",").map(Number);
|
|
106
|
+
const placeholders = obsIds.map(() => "?").join(",");
|
|
107
|
+
const count = db.query(
|
|
108
|
+
`SELECT COUNT(*) as cnt FROM observations WHERE id IN (${placeholders})`
|
|
109
|
+
).get(...obsIds)?.cnt || 0;
|
|
110
|
+
if (count >= minGroupSize) {
|
|
111
|
+
totalMerged += 1;
|
|
112
|
+
totalRemoved += count - 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { merged: totalMerged, removed: totalRemoved };
|
|
116
|
+
}
|
|
103
117
|
const runConsolidation = db.transaction(() => {
|
|
118
|
+
let merged = 0;
|
|
119
|
+
let removed = 0;
|
|
104
120
|
for (const group of groups) {
|
|
105
121
|
const obsIds = group.ids.split(",").map(Number);
|
|
106
122
|
const placeholders = obsIds.map(() => "?").join(",");
|
|
@@ -108,11 +124,6 @@ function consolidateObservations(db, project, options = {}) {
|
|
|
108
124
|
`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
|
|
109
125
|
).all(...obsIds);
|
|
110
126
|
if (observations.length < minGroupSize) continue;
|
|
111
|
-
if (options.dryRun) {
|
|
112
|
-
totalMerged += 1;
|
|
113
|
-
totalRemoved += observations.length - 1;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
127
|
const keeper = observations[0];
|
|
117
128
|
const others = observations.slice(1);
|
|
118
129
|
const uniqueTexts = /* @__PURE__ */ new Set();
|
|
@@ -125,18 +136,18 @@ function consolidateObservations(db, project, options = {}) {
|
|
|
125
136
|
const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
|
|
126
137
|
db.run(
|
|
127
138
|
"UPDATE observations SET text = ?, title = ? WHERE id = ?",
|
|
128
|
-
[consolidatedText, `[
|
|
139
|
+
[consolidatedText, `[consolidated x${observations.length}] ${keeper.title}`, keeper.id]
|
|
129
140
|
);
|
|
130
141
|
const removeIds = others.map((o) => o.id);
|
|
131
142
|
const removePlaceholders = removeIds.map(() => "?").join(",");
|
|
132
143
|
db.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
|
|
133
144
|
db.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
merged += 1;
|
|
146
|
+
removed += removeIds.length;
|
|
136
147
|
}
|
|
148
|
+
return { merged, removed };
|
|
137
149
|
});
|
|
138
|
-
runConsolidation();
|
|
139
|
-
return { merged: totalMerged, removed: totalRemoved };
|
|
150
|
+
return runConsolidation();
|
|
140
151
|
}
|
|
141
152
|
var init_Observations = __esm({
|
|
142
153
|
"src/services/sqlite/Observations.ts"() {
|
|
@@ -163,7 +174,7 @@ function escapeLikePattern3(input) {
|
|
|
163
174
|
}
|
|
164
175
|
function sanitizeFTS5Query(query) {
|
|
165
176
|
const trimmed = query.length > 1e4 ? query.substring(0, 1e4) : query;
|
|
166
|
-
const terms = trimmed.replace(/[""]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
|
|
177
|
+
const terms = trimmed.replace(/[""\u0022]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
|
|
167
178
|
return terms.join(" ");
|
|
168
179
|
}
|
|
169
180
|
function searchObservationsFTS(db, query, filters = {}) {
|
|
@@ -328,26 +339,38 @@ function getTimeline(db, anchorId, depthBefore = 5, depthAfter = 5) {
|
|
|
328
339
|
return [...before, ...self, ...after];
|
|
329
340
|
}
|
|
330
341
|
function getProjectStats(db, project) {
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
const sql = `
|
|
343
|
+
WITH
|
|
344
|
+
obs_stats AS (
|
|
345
|
+
SELECT
|
|
346
|
+
COUNT(*) as count,
|
|
347
|
+
COALESCE(SUM(discovery_tokens), 0) as discovery_tokens,
|
|
348
|
+
COALESCE(SUM(
|
|
349
|
+
CAST((LENGTH(COALESCE(title, '')) + LENGTH(COALESCE(narrative, ''))) / 4 AS INTEGER)
|
|
350
|
+
), 0) as read_tokens
|
|
351
|
+
FROM observations WHERE project = ?
|
|
352
|
+
),
|
|
353
|
+
sum_count AS (SELECT COUNT(*) as count FROM summaries WHERE project = ?),
|
|
354
|
+
ses_count AS (SELECT COUNT(*) as count FROM sessions WHERE project = ?),
|
|
355
|
+
prm_count AS (SELECT COUNT(*) as count FROM prompts WHERE project = ?)
|
|
356
|
+
SELECT
|
|
357
|
+
obs_stats.count as observations,
|
|
358
|
+
obs_stats.discovery_tokens,
|
|
359
|
+
obs_stats.read_tokens,
|
|
360
|
+
sum_count.count as summaries,
|
|
361
|
+
ses_count.count as sessions,
|
|
362
|
+
prm_count.count as prompts
|
|
363
|
+
FROM obs_stats, sum_count, ses_count, prm_count
|
|
364
|
+
`;
|
|
365
|
+
const row = db.query(sql).get(project, project, project, project);
|
|
366
|
+
const discoveryTokens = row?.discovery_tokens || 0;
|
|
367
|
+
const readTokens = row?.read_tokens || 0;
|
|
345
368
|
const savings = Math.max(0, discoveryTokens - readTokens);
|
|
346
369
|
return {
|
|
347
|
-
observations:
|
|
348
|
-
summaries:
|
|
349
|
-
sessions:
|
|
350
|
-
prompts:
|
|
370
|
+
observations: row?.observations || 0,
|
|
371
|
+
summaries: row?.summaries || 0,
|
|
372
|
+
sessions: row?.sessions || 0,
|
|
373
|
+
prompts: row?.prompts || 0,
|
|
351
374
|
tokenEconomics: { discoveryTokens, readTokens, savings }
|
|
352
375
|
};
|
|
353
376
|
}
|
|
@@ -402,14 +425,15 @@ var init_Search = __esm({
|
|
|
402
425
|
import BetterSqlite3 from "better-sqlite3";
|
|
403
426
|
var Database = class {
|
|
404
427
|
_db;
|
|
428
|
+
_stmtCache = /* @__PURE__ */ new Map();
|
|
405
429
|
constructor(path, options) {
|
|
406
430
|
this._db = new BetterSqlite3(path, {
|
|
407
|
-
// better-sqlite3
|
|
431
|
+
// better-sqlite3 creates the file by default ('create' not needed)
|
|
408
432
|
readonly: options?.readwrite === false ? true : false
|
|
409
433
|
});
|
|
410
434
|
}
|
|
411
435
|
/**
|
|
412
|
-
*
|
|
436
|
+
* Execute a SQL query without results
|
|
413
437
|
*/
|
|
414
438
|
run(sql, params) {
|
|
415
439
|
const stmt = this._db.prepare(sql);
|
|
@@ -417,51 +441,53 @@ var Database = class {
|
|
|
417
441
|
return result;
|
|
418
442
|
}
|
|
419
443
|
/**
|
|
420
|
-
*
|
|
444
|
+
* Prepare a query with bun:sqlite-compatible interface.
|
|
445
|
+
* Returns a cached prepared statement for repeated queries.
|
|
421
446
|
*/
|
|
422
447
|
query(sql) {
|
|
423
|
-
|
|
448
|
+
let cached = this._stmtCache.get(sql);
|
|
449
|
+
if (!cached) {
|
|
450
|
+
cached = new BunQueryCompat(this._db, sql);
|
|
451
|
+
this._stmtCache.set(sql, cached);
|
|
452
|
+
}
|
|
453
|
+
return cached;
|
|
424
454
|
}
|
|
425
455
|
/**
|
|
426
|
-
*
|
|
456
|
+
* Create a transaction
|
|
427
457
|
*/
|
|
428
458
|
transaction(fn) {
|
|
429
459
|
return this._db.transaction(fn);
|
|
430
460
|
}
|
|
431
461
|
/**
|
|
432
|
-
*
|
|
462
|
+
* Close the connection
|
|
433
463
|
*/
|
|
434
464
|
close() {
|
|
465
|
+
this._stmtCache.clear();
|
|
435
466
|
this._db.close();
|
|
436
467
|
}
|
|
437
468
|
};
|
|
438
469
|
var BunQueryCompat = class {
|
|
439
|
-
|
|
440
|
-
_sql;
|
|
470
|
+
_stmt;
|
|
441
471
|
constructor(db, sql) {
|
|
442
|
-
this.
|
|
443
|
-
this._sql = sql;
|
|
472
|
+
this._stmt = db.prepare(sql);
|
|
444
473
|
}
|
|
445
474
|
/**
|
|
446
|
-
*
|
|
475
|
+
* Returns all rows
|
|
447
476
|
*/
|
|
448
477
|
all(...params) {
|
|
449
|
-
|
|
450
|
-
return params.length > 0 ? stmt.all(...params) : stmt.all();
|
|
478
|
+
return params.length > 0 ? this._stmt.all(...params) : this._stmt.all();
|
|
451
479
|
}
|
|
452
480
|
/**
|
|
453
|
-
*
|
|
481
|
+
* Returns the first row or null
|
|
454
482
|
*/
|
|
455
483
|
get(...params) {
|
|
456
|
-
|
|
457
|
-
return params.length > 0 ? stmt.get(...params) : stmt.get();
|
|
484
|
+
return params.length > 0 ? this._stmt.get(...params) : this._stmt.get();
|
|
458
485
|
}
|
|
459
486
|
/**
|
|
460
|
-
*
|
|
487
|
+
* Execute without results
|
|
461
488
|
*/
|
|
462
489
|
run(...params) {
|
|
463
|
-
|
|
464
|
-
return params.length > 0 ? stmt.run(...params) : stmt.run();
|
|
490
|
+
return params.length > 0 ? this._stmt.run(...params) : this._stmt.run();
|
|
465
491
|
}
|
|
466
492
|
};
|
|
467
493
|
|
|
@@ -722,150 +748,63 @@ function ensureDir(dirPath) {
|
|
|
722
748
|
// src/services/sqlite/Database.ts
|
|
723
749
|
var SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024;
|
|
724
750
|
var SQLITE_CACHE_SIZE_PAGES = 1e4;
|
|
725
|
-
var dbInstance = null;
|
|
726
751
|
var KiroMemoryDatabase = class {
|
|
727
|
-
|
|
752
|
+
_db;
|
|
728
753
|
/**
|
|
729
|
-
*
|
|
730
|
-
*
|
|
754
|
+
* Readonly accessor for the underlying Database instance.
|
|
755
|
+
* Prefer using query() and run() proxy methods directly.
|
|
756
|
+
*/
|
|
757
|
+
get db() {
|
|
758
|
+
return this._db;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* @param dbPath - Path to the SQLite file (default: DB_PATH)
|
|
762
|
+
* @param skipMigrations - If true, skip the migration runner (for high-frequency hooks)
|
|
731
763
|
*/
|
|
732
764
|
constructor(dbPath = DB_PATH, skipMigrations = false) {
|
|
733
765
|
if (dbPath !== ":memory:") {
|
|
734
766
|
ensureDir(DATA_DIR);
|
|
735
767
|
}
|
|
736
|
-
this.
|
|
737
|
-
this.
|
|
738
|
-
this.
|
|
739
|
-
this.
|
|
740
|
-
this.
|
|
741
|
-
this.
|
|
742
|
-
this.
|
|
768
|
+
this._db = new Database(dbPath, { create: true, readwrite: true });
|
|
769
|
+
this._db.run("PRAGMA journal_mode = WAL");
|
|
770
|
+
this._db.run("PRAGMA busy_timeout = 5000");
|
|
771
|
+
this._db.run("PRAGMA synchronous = NORMAL");
|
|
772
|
+
this._db.run("PRAGMA foreign_keys = ON");
|
|
773
|
+
this._db.run("PRAGMA temp_store = memory");
|
|
774
|
+
this._db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
|
|
775
|
+
this._db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
|
|
743
776
|
if (!skipMigrations) {
|
|
744
|
-
const migrationRunner = new MigrationRunner(this.
|
|
777
|
+
const migrationRunner = new MigrationRunner(this._db);
|
|
745
778
|
migrationRunner.runAllMigrations();
|
|
746
779
|
}
|
|
747
780
|
}
|
|
748
781
|
/**
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*/
|
|
752
|
-
withTransaction(fn) {
|
|
753
|
-
const transaction = this.db.transaction(fn);
|
|
754
|
-
return transaction(this.db);
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Close the database connection
|
|
758
|
-
*/
|
|
759
|
-
close() {
|
|
760
|
-
this.db.close();
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
var DatabaseManager = class _DatabaseManager {
|
|
764
|
-
static instance;
|
|
765
|
-
db = null;
|
|
766
|
-
migrations = [];
|
|
767
|
-
static getInstance() {
|
|
768
|
-
if (!_DatabaseManager.instance) {
|
|
769
|
-
_DatabaseManager.instance = new _DatabaseManager();
|
|
770
|
-
}
|
|
771
|
-
return _DatabaseManager.instance;
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Register a migration to be run during initialization
|
|
782
|
+
* Prepare a query (delegates to underlying Database).
|
|
783
|
+
* Proxy method to avoid ctx.db.db.query() double access.
|
|
775
784
|
*/
|
|
776
|
-
|
|
777
|
-
this.
|
|
778
|
-
this.migrations.sort((a, b) => a.version - b.version);
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Initialize database connection with optimized settings
|
|
782
|
-
*/
|
|
783
|
-
async initialize() {
|
|
784
|
-
if (this.db) {
|
|
785
|
-
return this.db;
|
|
786
|
-
}
|
|
787
|
-
ensureDir(DATA_DIR);
|
|
788
|
-
this.db = new Database(DB_PATH, { create: true, readwrite: true });
|
|
789
|
-
this.db.run("PRAGMA journal_mode = WAL");
|
|
790
|
-
this.db.run("PRAGMA synchronous = NORMAL");
|
|
791
|
-
this.db.run("PRAGMA foreign_keys = ON");
|
|
792
|
-
this.db.run("PRAGMA temp_store = memory");
|
|
793
|
-
this.db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
|
|
794
|
-
this.db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
|
|
795
|
-
this.initializeSchemaVersions();
|
|
796
|
-
await this.runMigrations();
|
|
797
|
-
dbInstance = this.db;
|
|
798
|
-
return this.db;
|
|
785
|
+
query(sql) {
|
|
786
|
+
return this._db.query(sql);
|
|
799
787
|
}
|
|
800
788
|
/**
|
|
801
|
-
*
|
|
789
|
+
* Execute a SQL statement without results (delegates to underlying Database).
|
|
790
|
+
* Proxy method to avoid ctx.db.db.run() double access.
|
|
802
791
|
*/
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
throw new Error("Database not initialized. Call initialize() first.");
|
|
806
|
-
}
|
|
807
|
-
return this.db;
|
|
792
|
+
run(sql, params) {
|
|
793
|
+
return this._db.run(sql, params);
|
|
808
794
|
}
|
|
809
795
|
/**
|
|
810
|
-
*
|
|
796
|
+
* Executes a function within an atomic transaction.
|
|
797
|
+
* If fn() throws an error, the transaction is automatically rolled back.
|
|
811
798
|
*/
|
|
812
799
|
withTransaction(fn) {
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
return transaction(db);
|
|
800
|
+
const transaction = this._db.transaction(fn);
|
|
801
|
+
return transaction(this._db);
|
|
816
802
|
}
|
|
817
803
|
/**
|
|
818
804
|
* Close the database connection
|
|
819
805
|
*/
|
|
820
806
|
close() {
|
|
821
|
-
|
|
822
|
-
this.db.close();
|
|
823
|
-
this.db = null;
|
|
824
|
-
dbInstance = null;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Initialize the schema_versions table
|
|
829
|
-
*/
|
|
830
|
-
initializeSchemaVersions() {
|
|
831
|
-
if (!this.db) return;
|
|
832
|
-
this.db.run(`
|
|
833
|
-
CREATE TABLE IF NOT EXISTS schema_versions (
|
|
834
|
-
id INTEGER PRIMARY KEY,
|
|
835
|
-
version INTEGER UNIQUE NOT NULL,
|
|
836
|
-
applied_at TEXT NOT NULL
|
|
837
|
-
)
|
|
838
|
-
`);
|
|
839
|
-
}
|
|
840
|
-
/**
|
|
841
|
-
* Run all pending migrations
|
|
842
|
-
*/
|
|
843
|
-
async runMigrations() {
|
|
844
|
-
if (!this.db) return;
|
|
845
|
-
const query = this.db.query("SELECT version FROM schema_versions ORDER BY version");
|
|
846
|
-
const appliedVersions = query.all().map((row) => row.version);
|
|
847
|
-
const maxApplied = appliedVersions.length > 0 ? Math.max(...appliedVersions) : 0;
|
|
848
|
-
for (const migration of this.migrations) {
|
|
849
|
-
if (migration.version > maxApplied) {
|
|
850
|
-
logger.info("DB", `Applying migration ${migration.version}`);
|
|
851
|
-
const transaction = this.db.transaction(() => {
|
|
852
|
-
migration.up(this.db);
|
|
853
|
-
const insertQuery = this.db.query("INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)");
|
|
854
|
-
insertQuery.run(migration.version, (/* @__PURE__ */ new Date()).toISOString());
|
|
855
|
-
});
|
|
856
|
-
transaction();
|
|
857
|
-
logger.info("DB", `Migration ${migration.version} applied successfully`);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
/**
|
|
862
|
-
* Get current schema version
|
|
863
|
-
*/
|
|
864
|
-
getCurrentVersion() {
|
|
865
|
-
if (!this.db) return 0;
|
|
866
|
-
const query = this.db.query("SELECT MAX(version) as version FROM schema_versions");
|
|
867
|
-
const result = query.get();
|
|
868
|
-
return result?.version || 0;
|
|
807
|
+
this._db.close();
|
|
869
808
|
}
|
|
870
809
|
};
|
|
871
810
|
var MigrationRunner = class {
|
|
@@ -1110,16 +1049,6 @@ var MigrationRunner = class {
|
|
|
1110
1049
|
];
|
|
1111
1050
|
}
|
|
1112
1051
|
};
|
|
1113
|
-
function getDatabase() {
|
|
1114
|
-
if (!dbInstance) {
|
|
1115
|
-
throw new Error("Database not initialized. Call DatabaseManager.getInstance().initialize() first.");
|
|
1116
|
-
}
|
|
1117
|
-
return dbInstance;
|
|
1118
|
-
}
|
|
1119
|
-
async function initializeDatabase() {
|
|
1120
|
-
const manager = DatabaseManager.getInstance();
|
|
1121
|
-
return await manager.initialize();
|
|
1122
|
-
}
|
|
1123
1052
|
|
|
1124
1053
|
// src/services/sqlite/Sessions.ts
|
|
1125
1054
|
function createSession(db, contentSessionId, project, userPrompt) {
|
|
@@ -1370,8 +1299,8 @@ var EmbeddingService = class {
|
|
|
1370
1299
|
initialized = false;
|
|
1371
1300
|
initializing = null;
|
|
1372
1301
|
/**
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1302
|
+
* Initialize the embedding service.
|
|
1303
|
+
* Tries fastembed, then @huggingface/transformers, then fallback to null.
|
|
1375
1304
|
*/
|
|
1376
1305
|
async initialize() {
|
|
1377
1306
|
if (this.initialized) return this.provider !== null;
|
|
@@ -1392,11 +1321,11 @@ var EmbeddingService = class {
|
|
|
1392
1321
|
});
|
|
1393
1322
|
this.provider = "fastembed";
|
|
1394
1323
|
this.initialized = true;
|
|
1395
|
-
logger.info("EMBEDDING", "
|
|
1324
|
+
logger.info("EMBEDDING", "Initialized with fastembed (BGE-small-en-v1.5)");
|
|
1396
1325
|
return true;
|
|
1397
1326
|
}
|
|
1398
1327
|
} catch (error) {
|
|
1399
|
-
logger.debug("EMBEDDING", `fastembed
|
|
1328
|
+
logger.debug("EMBEDDING", `fastembed not available: ${error}`);
|
|
1400
1329
|
}
|
|
1401
1330
|
try {
|
|
1402
1331
|
const transformers = await import("@huggingface/transformers");
|
|
@@ -1407,20 +1336,20 @@ var EmbeddingService = class {
|
|
|
1407
1336
|
});
|
|
1408
1337
|
this.provider = "transformers";
|
|
1409
1338
|
this.initialized = true;
|
|
1410
|
-
logger.info("EMBEDDING", "
|
|
1339
|
+
logger.info("EMBEDDING", "Initialized with @huggingface/transformers (all-MiniLM-L6-v2)");
|
|
1411
1340
|
return true;
|
|
1412
1341
|
}
|
|
1413
1342
|
} catch (error) {
|
|
1414
|
-
logger.debug("EMBEDDING", `@huggingface/transformers
|
|
1343
|
+
logger.debug("EMBEDDING", `@huggingface/transformers not available: ${error}`);
|
|
1415
1344
|
}
|
|
1416
1345
|
this.provider = null;
|
|
1417
1346
|
this.initialized = true;
|
|
1418
|
-
logger.warn("EMBEDDING", "
|
|
1347
|
+
logger.warn("EMBEDDING", "No embedding provider available, semantic search disabled");
|
|
1419
1348
|
return false;
|
|
1420
1349
|
}
|
|
1421
1350
|
/**
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1351
|
+
* Generate embedding for a single text.
|
|
1352
|
+
* Returns Float32Array with 384 dimensions, or null if not available.
|
|
1424
1353
|
*/
|
|
1425
1354
|
async embed(text) {
|
|
1426
1355
|
if (!this.initialized) await this.initialize();
|
|
@@ -1433,46 +1362,111 @@ var EmbeddingService = class {
|
|
|
1433
1362
|
return await this._embedTransformers(truncated);
|
|
1434
1363
|
}
|
|
1435
1364
|
} catch (error) {
|
|
1436
|
-
logger.error("EMBEDDING", `
|
|
1365
|
+
logger.error("EMBEDDING", `Error generating embedding: ${error}`);
|
|
1437
1366
|
}
|
|
1438
1367
|
return null;
|
|
1439
1368
|
}
|
|
1440
1369
|
/**
|
|
1441
|
-
*
|
|
1370
|
+
* Generate embeddings in batch.
|
|
1371
|
+
* Uses native batch support when available (fastembed, transformers),
|
|
1372
|
+
* falls back to serial processing on batch failure.
|
|
1442
1373
|
*/
|
|
1443
1374
|
async embedBatch(texts) {
|
|
1444
1375
|
if (!this.initialized) await this.initialize();
|
|
1445
1376
|
if (!this.provider || !this.model) return texts.map(() => null);
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1377
|
+
if (texts.length === 0) return [];
|
|
1378
|
+
const truncated = texts.map((t) => t.substring(0, 2e3));
|
|
1379
|
+
try {
|
|
1380
|
+
if (this.provider === "fastembed") {
|
|
1381
|
+
return await this._embedBatchFastembed(truncated);
|
|
1382
|
+
} else if (this.provider === "transformers") {
|
|
1383
|
+
return await this._embedBatchTransformers(truncated);
|
|
1453
1384
|
}
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
logger.warn("EMBEDDING", `Batch embedding failed, falling back to serial: ${error}`);
|
|
1454
1387
|
}
|
|
1455
|
-
return
|
|
1388
|
+
return this._embedBatchSerial(truncated);
|
|
1456
1389
|
}
|
|
1457
1390
|
/**
|
|
1458
|
-
*
|
|
1391
|
+
* Check if the service is available.
|
|
1459
1392
|
*/
|
|
1460
1393
|
isAvailable() {
|
|
1461
1394
|
return this.initialized && this.provider !== null;
|
|
1462
1395
|
}
|
|
1463
1396
|
/**
|
|
1464
|
-
*
|
|
1397
|
+
* Name of the active provider.
|
|
1465
1398
|
*/
|
|
1466
1399
|
getProvider() {
|
|
1467
1400
|
return this.provider;
|
|
1468
1401
|
}
|
|
1469
1402
|
/**
|
|
1470
|
-
*
|
|
1403
|
+
* Embedding vector dimensions.
|
|
1471
1404
|
*/
|
|
1472
1405
|
getDimensions() {
|
|
1473
1406
|
return 384;
|
|
1474
1407
|
}
|
|
1475
|
-
// ---
|
|
1408
|
+
// --- Batch implementations ---
|
|
1409
|
+
/**
|
|
1410
|
+
* Native batch embedding with fastembed.
|
|
1411
|
+
* FlagEmbedding.embed() accepts string[] and returns an async iterable of batches.
|
|
1412
|
+
*/
|
|
1413
|
+
async _embedBatchFastembed(texts) {
|
|
1414
|
+
const results = [];
|
|
1415
|
+
const embeddings = this.model.embed(texts, texts.length);
|
|
1416
|
+
for await (const batch of embeddings) {
|
|
1417
|
+
if (batch) {
|
|
1418
|
+
for (const vec of batch) {
|
|
1419
|
+
results.push(vec instanceof Float32Array ? vec : new Float32Array(vec));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
while (results.length < texts.length) {
|
|
1424
|
+
results.push(null);
|
|
1425
|
+
}
|
|
1426
|
+
return results;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Batch embedding with @huggingface/transformers pipeline.
|
|
1430
|
+
* The pipeline accepts string[] and returns a Tensor with shape [N, dims].
|
|
1431
|
+
*/
|
|
1432
|
+
async _embedBatchTransformers(texts) {
|
|
1433
|
+
const output = await this.model(texts, {
|
|
1434
|
+
pooling: "mean",
|
|
1435
|
+
normalize: true
|
|
1436
|
+
});
|
|
1437
|
+
if (!output?.data) {
|
|
1438
|
+
return texts.map(() => null);
|
|
1439
|
+
}
|
|
1440
|
+
const dims = this.getDimensions();
|
|
1441
|
+
const data = output.data instanceof Float32Array ? output.data : new Float32Array(output.data);
|
|
1442
|
+
const results = [];
|
|
1443
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1444
|
+
const offset = i * dims;
|
|
1445
|
+
if (offset + dims <= data.length) {
|
|
1446
|
+
results.push(data.slice(offset, offset + dims));
|
|
1447
|
+
} else {
|
|
1448
|
+
results.push(null);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return results;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Serial fallback: embed texts one at a time.
|
|
1455
|
+
* Used when native batch fails.
|
|
1456
|
+
*/
|
|
1457
|
+
async _embedBatchSerial(texts) {
|
|
1458
|
+
const results = [];
|
|
1459
|
+
for (const text of texts) {
|
|
1460
|
+
try {
|
|
1461
|
+
const embedding = await this.embed(text);
|
|
1462
|
+
results.push(embedding);
|
|
1463
|
+
} catch {
|
|
1464
|
+
results.push(null);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return results;
|
|
1468
|
+
}
|
|
1469
|
+
// --- Single-text provider implementations ---
|
|
1476
1470
|
async _embedFastembed(text) {
|
|
1477
1471
|
const embeddings = this.model.embed([text], 1);
|
|
1478
1472
|
for await (const batch of embeddings) {
|
|
@@ -1503,17 +1497,21 @@ function getEmbeddingService() {
|
|
|
1503
1497
|
}
|
|
1504
1498
|
|
|
1505
1499
|
// src/services/search/VectorSearch.ts
|
|
1500
|
+
var DEFAULT_MAX_CANDIDATES = 2e3;
|
|
1506
1501
|
function cosineSimilarity(a, b) {
|
|
1507
|
-
|
|
1502
|
+
const len = a.length;
|
|
1503
|
+
if (len !== b.length) return 0;
|
|
1508
1504
|
let dotProduct = 0;
|
|
1509
1505
|
let normA = 0;
|
|
1510
1506
|
let normB = 0;
|
|
1511
|
-
for (let i = 0; i <
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1507
|
+
for (let i = 0; i < len; i++) {
|
|
1508
|
+
const ai = a[i];
|
|
1509
|
+
const bi = b[i];
|
|
1510
|
+
dotProduct += ai * bi;
|
|
1511
|
+
normA += ai * ai;
|
|
1512
|
+
normB += bi * bi;
|
|
1513
|
+
}
|
|
1514
|
+
const denominator = Math.sqrt(normA * normB);
|
|
1517
1515
|
if (denominator === 0) return 0;
|
|
1518
1516
|
return dotProduct / denominator;
|
|
1519
1517
|
}
|
|
@@ -1526,23 +1524,36 @@ function bufferToFloat32(buf) {
|
|
|
1526
1524
|
}
|
|
1527
1525
|
var VectorSearch = class {
|
|
1528
1526
|
/**
|
|
1529
|
-
*
|
|
1527
|
+
* Semantic search with SQL pre-filtering for scalability.
|
|
1528
|
+
*
|
|
1529
|
+
* 2-phase strategy:
|
|
1530
|
+
* 1. SQL pre-filters by project + sorts by recency (loads max N candidates)
|
|
1531
|
+
* 2. JS computes cosine similarity only on filtered candidates
|
|
1532
|
+
*
|
|
1533
|
+
* With 50k observations and maxCandidates=2000, loads only ~4% of data.
|
|
1530
1534
|
*/
|
|
1531
1535
|
async search(db, queryEmbedding, options = {}) {
|
|
1532
1536
|
const limit = options.limit || 10;
|
|
1533
1537
|
const threshold = options.threshold || 0.3;
|
|
1538
|
+
const maxCandidates = options.maxCandidates || DEFAULT_MAX_CANDIDATES;
|
|
1534
1539
|
try {
|
|
1535
|
-
|
|
1540
|
+
const conditions = [];
|
|
1541
|
+
const params = [];
|
|
1542
|
+
if (options.project) {
|
|
1543
|
+
conditions.push("o.project = ?");
|
|
1544
|
+
params.push(options.project);
|
|
1545
|
+
}
|
|
1546
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1547
|
+
const sql = `
|
|
1536
1548
|
SELECT e.observation_id, e.embedding,
|
|
1537
1549
|
o.title, o.text, o.type, o.project, o.created_at, o.created_at_epoch
|
|
1538
1550
|
FROM observation_embeddings e
|
|
1539
1551
|
JOIN observations o ON o.id = e.observation_id
|
|
1552
|
+
${whereClause}
|
|
1553
|
+
ORDER BY o.created_at_epoch DESC
|
|
1554
|
+
LIMIT ?
|
|
1540
1555
|
`;
|
|
1541
|
-
|
|
1542
|
-
if (options.project) {
|
|
1543
|
-
sql += " WHERE o.project = ?";
|
|
1544
|
-
params.push(options.project);
|
|
1545
|
-
}
|
|
1556
|
+
params.push(maxCandidates);
|
|
1546
1557
|
const rows = db.query(sql).all(...params);
|
|
1547
1558
|
const scored = [];
|
|
1548
1559
|
for (const row of rows) {
|
|
@@ -1563,14 +1574,15 @@ var VectorSearch = class {
|
|
|
1563
1574
|
}
|
|
1564
1575
|
}
|
|
1565
1576
|
scored.sort((a, b) => b.similarity - a.similarity);
|
|
1577
|
+
logger.debug("VECTOR", `Search: ${rows.length} candidates \u2192 ${scored.length} above threshold \u2192 ${Math.min(scored.length, limit)} results`);
|
|
1566
1578
|
return scored.slice(0, limit);
|
|
1567
1579
|
} catch (error) {
|
|
1568
|
-
logger.error("VECTOR", `
|
|
1580
|
+
logger.error("VECTOR", `Vector search error: ${error}`);
|
|
1569
1581
|
return [];
|
|
1570
1582
|
}
|
|
1571
1583
|
}
|
|
1572
1584
|
/**
|
|
1573
|
-
*
|
|
1585
|
+
* Store embedding for an observation.
|
|
1574
1586
|
*/
|
|
1575
1587
|
async storeEmbedding(db, observationId, embedding, model) {
|
|
1576
1588
|
try {
|
|
@@ -1586,18 +1598,18 @@ var VectorSearch = class {
|
|
|
1586
1598
|
embedding.length,
|
|
1587
1599
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
1588
1600
|
);
|
|
1589
|
-
logger.debug("VECTOR", `Embedding
|
|
1601
|
+
logger.debug("VECTOR", `Embedding saved for observation ${observationId}`);
|
|
1590
1602
|
} catch (error) {
|
|
1591
|
-
logger.error("VECTOR", `
|
|
1603
|
+
logger.error("VECTOR", `Error saving embedding: ${error}`);
|
|
1592
1604
|
}
|
|
1593
1605
|
}
|
|
1594
1606
|
/**
|
|
1595
|
-
*
|
|
1607
|
+
* Generate embeddings for observations that don't have them yet.
|
|
1596
1608
|
*/
|
|
1597
1609
|
async backfillEmbeddings(db, batchSize = 50) {
|
|
1598
1610
|
const embeddingService2 = getEmbeddingService();
|
|
1599
1611
|
if (!await embeddingService2.initialize()) {
|
|
1600
|
-
logger.warn("VECTOR", "Embedding service
|
|
1612
|
+
logger.warn("VECTOR", "Embedding service not available, backfill skipped");
|
|
1601
1613
|
return 0;
|
|
1602
1614
|
}
|
|
1603
1615
|
const rows = db.query(`
|
|
@@ -1623,11 +1635,11 @@ var VectorSearch = class {
|
|
|
1623
1635
|
count++;
|
|
1624
1636
|
}
|
|
1625
1637
|
}
|
|
1626
|
-
logger.info("VECTOR", `Backfill
|
|
1638
|
+
logger.info("VECTOR", `Backfill completed: ${count}/${rows.length} embeddings generated`);
|
|
1627
1639
|
return count;
|
|
1628
1640
|
}
|
|
1629
1641
|
/**
|
|
1630
|
-
*
|
|
1642
|
+
* Embedding statistics.
|
|
1631
1643
|
*/
|
|
1632
1644
|
getStats(db) {
|
|
1633
1645
|
try {
|
|
@@ -1700,21 +1712,21 @@ function knowledgeTypeBoost(type) {
|
|
|
1700
1712
|
var HybridSearch = class {
|
|
1701
1713
|
embeddingInitialized = false;
|
|
1702
1714
|
/**
|
|
1703
|
-
*
|
|
1715
|
+
* Initialize the embedding service (lazy, non-blocking)
|
|
1704
1716
|
*/
|
|
1705
1717
|
async initialize() {
|
|
1706
1718
|
try {
|
|
1707
1719
|
const embeddingService2 = getEmbeddingService();
|
|
1708
1720
|
await embeddingService2.initialize();
|
|
1709
1721
|
this.embeddingInitialized = embeddingService2.isAvailable();
|
|
1710
|
-
logger.info("SEARCH", `HybridSearch
|
|
1722
|
+
logger.info("SEARCH", `HybridSearch initialized (embedding: ${this.embeddingInitialized ? "active" : "disabled"})`);
|
|
1711
1723
|
} catch (error) {
|
|
1712
|
-
logger.warn("SEARCH", "
|
|
1724
|
+
logger.warn("SEARCH", "Embedding initialization failed, using only FTS5", {}, error);
|
|
1713
1725
|
this.embeddingInitialized = false;
|
|
1714
1726
|
}
|
|
1715
1727
|
}
|
|
1716
1728
|
/**
|
|
1717
|
-
*
|
|
1729
|
+
* Hybrid search with 4-signal scoring
|
|
1718
1730
|
*/
|
|
1719
1731
|
async search(db, query, options = {}) {
|
|
1720
1732
|
const limit = options.limit || 10;
|
|
@@ -1730,7 +1742,7 @@ var HybridSearch = class {
|
|
|
1730
1742
|
const vectorResults = await vectorSearch2.search(db, queryEmbedding, {
|
|
1731
1743
|
project: options.project,
|
|
1732
1744
|
limit: limit * 2,
|
|
1733
|
-
//
|
|
1745
|
+
// Fetch more results for ranking
|
|
1734
1746
|
threshold: 0.3
|
|
1735
1747
|
});
|
|
1736
1748
|
for (const hit of vectorResults) {
|
|
@@ -1747,10 +1759,10 @@ var HybridSearch = class {
|
|
|
1747
1759
|
source: "vector"
|
|
1748
1760
|
});
|
|
1749
1761
|
}
|
|
1750
|
-
logger.debug("SEARCH", `Vector search: ${vectorResults.length}
|
|
1762
|
+
logger.debug("SEARCH", `Vector search: ${vectorResults.length} results`);
|
|
1751
1763
|
}
|
|
1752
1764
|
} catch (error) {
|
|
1753
|
-
logger.warn("SEARCH", "
|
|
1765
|
+
logger.warn("SEARCH", "Vector search failed, using only keyword", {}, error);
|
|
1754
1766
|
}
|
|
1755
1767
|
}
|
|
1756
1768
|
try {
|
|
@@ -1780,9 +1792,9 @@ var HybridSearch = class {
|
|
|
1780
1792
|
});
|
|
1781
1793
|
}
|
|
1782
1794
|
}
|
|
1783
|
-
logger.debug("SEARCH", `Keyword search: ${keywordResults.length}
|
|
1795
|
+
logger.debug("SEARCH", `Keyword search: ${keywordResults.length} results`);
|
|
1784
1796
|
} catch (error) {
|
|
1785
|
-
logger.error("SEARCH", "
|
|
1797
|
+
logger.error("SEARCH", "Keyword search failed", {}, error);
|
|
1786
1798
|
}
|
|
1787
1799
|
if (rawItems.size === 0) return [];
|
|
1788
1800
|
const allFTS5Ranks = Array.from(rawItems.values()).filter((item) => item.fts5Rank !== null).map((item) => item.fts5Rank);
|
|
@@ -1870,33 +1882,33 @@ var KiroMemorySDK = class {
|
|
|
1870
1882
|
};
|
|
1871
1883
|
}
|
|
1872
1884
|
/**
|
|
1873
|
-
*
|
|
1885
|
+
* Validate input for storeObservation
|
|
1874
1886
|
*/
|
|
1875
1887
|
validateObservationInput(data) {
|
|
1876
1888
|
if (!data.type || typeof data.type !== "string" || data.type.length > 100) {
|
|
1877
|
-
throw new Error("type
|
|
1889
|
+
throw new Error("type is required (string, max 100 chars)");
|
|
1878
1890
|
}
|
|
1879
1891
|
if (!data.title || typeof data.title !== "string" || data.title.length > 500) {
|
|
1880
|
-
throw new Error("title
|
|
1892
|
+
throw new Error("title is required (string, max 500 chars)");
|
|
1881
1893
|
}
|
|
1882
1894
|
if (!data.content || typeof data.content !== "string" || data.content.length > 1e5) {
|
|
1883
|
-
throw new Error("content
|
|
1895
|
+
throw new Error("content is required (string, max 100KB)");
|
|
1884
1896
|
}
|
|
1885
1897
|
}
|
|
1886
1898
|
/**
|
|
1887
|
-
*
|
|
1899
|
+
* Validate input for storeSummary
|
|
1888
1900
|
*/
|
|
1889
1901
|
validateSummaryInput(data) {
|
|
1890
1902
|
const MAX = 5e4;
|
|
1891
1903
|
for (const [key, val] of Object.entries(data)) {
|
|
1892
1904
|
if (val !== void 0 && val !== null) {
|
|
1893
|
-
if (typeof val !== "string") throw new Error(`${key}
|
|
1894
|
-
if (val.length > MAX) throw new Error(`${key}
|
|
1905
|
+
if (typeof val !== "string") throw new Error(`${key} must be a string`);
|
|
1906
|
+
if (val.length > MAX) throw new Error(`${key} too large (max 50KB)`);
|
|
1895
1907
|
}
|
|
1896
1908
|
}
|
|
1897
1909
|
}
|
|
1898
1910
|
/**
|
|
1899
|
-
*
|
|
1911
|
+
* Generate and store embedding for an observation (fire-and-forget, non-blocking)
|
|
1900
1912
|
*/
|
|
1901
1913
|
async generateEmbeddingAsync(observationId, title, content, concepts) {
|
|
1902
1914
|
try {
|
|
@@ -1916,39 +1928,39 @@ var KiroMemorySDK = class {
|
|
|
1916
1928
|
);
|
|
1917
1929
|
}
|
|
1918
1930
|
} catch (error) {
|
|
1919
|
-
logger.debug("SDK", `Embedding generation
|
|
1931
|
+
logger.debug("SDK", `Embedding generation failed for obs ${observationId}: ${error}`);
|
|
1920
1932
|
}
|
|
1921
1933
|
}
|
|
1922
1934
|
/**
|
|
1923
|
-
*
|
|
1924
|
-
*
|
|
1925
|
-
*
|
|
1935
|
+
* Generate SHA256 content hash for content-based deduplication.
|
|
1936
|
+
* Uses (project + type + title + narrative) as semantic identity tuple.
|
|
1937
|
+
* Does NOT include sessionId since it's unique per invocation.
|
|
1926
1938
|
*/
|
|
1927
1939
|
generateContentHash(type, title, narrative) {
|
|
1928
1940
|
const payload = `${this.project}|${type}|${title}|${narrative || ""}`;
|
|
1929
1941
|
return createHash("sha256").update(payload).digest("hex");
|
|
1930
1942
|
}
|
|
1931
1943
|
/**
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1944
|
+
* Deduplication windows per type (ms).
|
|
1945
|
+
* Types with many repetitions have wider windows.
|
|
1934
1946
|
*/
|
|
1935
1947
|
getDeduplicationWindow(type) {
|
|
1936
1948
|
switch (type) {
|
|
1937
1949
|
case "file-read":
|
|
1938
1950
|
return 6e4;
|
|
1939
|
-
// 60s —
|
|
1951
|
+
// 60s — frequent reads on the same files
|
|
1940
1952
|
case "file-write":
|
|
1941
1953
|
return 1e4;
|
|
1942
|
-
// 10s —
|
|
1954
|
+
// 10s — rapid consecutive writes
|
|
1943
1955
|
case "command":
|
|
1944
1956
|
return 3e4;
|
|
1945
1957
|
// 30s — standard
|
|
1946
1958
|
case "research":
|
|
1947
1959
|
return 12e4;
|
|
1948
|
-
// 120s — web search
|
|
1960
|
+
// 120s — repeated web search and fetch
|
|
1949
1961
|
case "delegation":
|
|
1950
1962
|
return 6e4;
|
|
1951
|
-
// 60s —
|
|
1963
|
+
// 60s — rapid delegations
|
|
1952
1964
|
default:
|
|
1953
1965
|
return 3e4;
|
|
1954
1966
|
}
|
|
@@ -1962,7 +1974,7 @@ var KiroMemorySDK = class {
|
|
|
1962
1974
|
const contentHash = this.generateContentHash(data.type, data.title, data.narrative);
|
|
1963
1975
|
const dedupWindow = this.getDeduplicationWindow(data.type);
|
|
1964
1976
|
if (isDuplicateObservation(this.db.db, contentHash, dedupWindow)) {
|
|
1965
|
-
logger.debug("SDK", `
|
|
1977
|
+
logger.debug("SDK", `Duplicate observation discarded (${data.type}, ${dedupWindow}ms): ${data.title}`);
|
|
1966
1978
|
return -1;
|
|
1967
1979
|
}
|
|
1968
1980
|
const filesRead = data.filesRead || (data.type === "file-read" ? data.files : void 0);
|
|
@@ -1990,12 +2002,12 @@ var KiroMemorySDK = class {
|
|
|
1990
2002
|
return observationId;
|
|
1991
2003
|
}
|
|
1992
2004
|
/**
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
2005
|
+
* Store structured knowledge (constraint, decision, heuristic, rejected).
|
|
2006
|
+
* Uses the `type` field for knowledgeType and `facts` for JSON metadata.
|
|
1995
2007
|
*/
|
|
1996
2008
|
async storeKnowledge(data) {
|
|
1997
2009
|
if (!KNOWLEDGE_TYPES.includes(data.knowledgeType)) {
|
|
1998
|
-
throw new Error(`knowledgeType
|
|
2010
|
+
throw new Error(`Invalid knowledgeType: ${data.knowledgeType}. Allowed values: ${KNOWLEDGE_TYPES.join(", ")}`);
|
|
1999
2011
|
}
|
|
2000
2012
|
this.validateObservationInput({ type: data.knowledgeType, title: data.title, content: data.content });
|
|
2001
2013
|
const metadata = (() => {
|
|
@@ -2027,9 +2039,9 @@ var KiroMemorySDK = class {
|
|
|
2027
2039
|
}
|
|
2028
2040
|
})();
|
|
2029
2041
|
const sessionId = "sdk-" + Date.now();
|
|
2030
|
-
const contentHash = this.generateContentHash(data.
|
|
2042
|
+
const contentHash = this.generateContentHash(data.knowledgeType, data.title);
|
|
2031
2043
|
if (isDuplicateObservation(this.db.db, contentHash)) {
|
|
2032
|
-
logger.debug("SDK", `
|
|
2044
|
+
logger.debug("SDK", `Duplicate knowledge discarded: ${data.title}`);
|
|
2033
2045
|
return -1;
|
|
2034
2046
|
}
|
|
2035
2047
|
const discoveryTokens = Math.ceil(data.content.length / 4);
|
|
@@ -2046,11 +2058,11 @@ var KiroMemorySDK = class {
|
|
|
2046
2058
|
null,
|
|
2047
2059
|
// narrative
|
|
2048
2060
|
JSON.stringify(metadata),
|
|
2049
|
-
// facts =
|
|
2061
|
+
// facts = JSON metadata
|
|
2050
2062
|
data.concepts?.join(", ") || null,
|
|
2051
2063
|
data.files?.join(", ") || null,
|
|
2052
2064
|
null,
|
|
2053
|
-
// filesModified: knowledge
|
|
2065
|
+
// filesModified: knowledge doesn't modify files
|
|
2054
2066
|
0,
|
|
2055
2067
|
// prompt_number
|
|
2056
2068
|
contentHash,
|
|
@@ -2161,8 +2173,8 @@ var KiroMemorySDK = class {
|
|
|
2161
2173
|
return this.project;
|
|
2162
2174
|
}
|
|
2163
2175
|
/**
|
|
2164
|
-
*
|
|
2165
|
-
*
|
|
2176
|
+
* Hybrid search: vector search + keyword FTS5
|
|
2177
|
+
* Requires HybridSearch initialization (embedding service)
|
|
2166
2178
|
*/
|
|
2167
2179
|
async hybridSearch(query, options = {}) {
|
|
2168
2180
|
const hybridSearch2 = getHybridSearch();
|
|
@@ -2172,8 +2184,8 @@ var KiroMemorySDK = class {
|
|
|
2172
2184
|
});
|
|
2173
2185
|
}
|
|
2174
2186
|
/**
|
|
2175
|
-
*
|
|
2176
|
-
*
|
|
2187
|
+
* Semantic-only search (vector search)
|
|
2188
|
+
* Returns results based on cosine similarity with embeddings
|
|
2177
2189
|
*/
|
|
2178
2190
|
async semanticSearch(query, options = {}) {
|
|
2179
2191
|
const embeddingService2 = getEmbeddingService();
|
|
@@ -2208,21 +2220,21 @@ var KiroMemorySDK = class {
|
|
|
2208
2220
|
}));
|
|
2209
2221
|
}
|
|
2210
2222
|
/**
|
|
2211
|
-
*
|
|
2223
|
+
* Generate embeddings for observations that don't have them yet
|
|
2212
2224
|
*/
|
|
2213
2225
|
async backfillEmbeddings(batchSize = 50) {
|
|
2214
2226
|
const vectorSearch2 = getVectorSearch();
|
|
2215
2227
|
return vectorSearch2.backfillEmbeddings(this.db.db, batchSize);
|
|
2216
2228
|
}
|
|
2217
2229
|
/**
|
|
2218
|
-
*
|
|
2230
|
+
* Embedding statistics in the database
|
|
2219
2231
|
*/
|
|
2220
2232
|
getEmbeddingStats() {
|
|
2221
2233
|
const vectorSearch2 = getVectorSearch();
|
|
2222
2234
|
return vectorSearch2.getStats(this.db.db);
|
|
2223
2235
|
}
|
|
2224
2236
|
/**
|
|
2225
|
-
*
|
|
2237
|
+
* Initialize the embedding service (lazy, call before hybridSearch)
|
|
2226
2238
|
*/
|
|
2227
2239
|
async initializeEmbeddings() {
|
|
2228
2240
|
const hybridSearch2 = getHybridSearch();
|
|
@@ -2230,10 +2242,10 @@ var KiroMemorySDK = class {
|
|
|
2230
2242
|
return getEmbeddingService().isAvailable();
|
|
2231
2243
|
}
|
|
2232
2244
|
/**
|
|
2233
|
-
*
|
|
2245
|
+
* Smart context with 4-signal ranking and token budget.
|
|
2234
2246
|
*
|
|
2235
|
-
*
|
|
2236
|
-
*
|
|
2247
|
+
* If query present: uses HybridSearch with SEARCH_WEIGHTS.
|
|
2248
|
+
* If no query: ranking by recency + project match (CONTEXT_WEIGHTS).
|
|
2237
2249
|
*/
|
|
2238
2250
|
async getSmartContext(options = {}) {
|
|
2239
2251
|
const tokenBudget = options.tokenBudget || parseInt(process.env.KIRO_MEMORY_CONTEXT_TOKENS || "0", 10) || 2e3;
|
|
@@ -2307,8 +2319,8 @@ var KiroMemorySDK = class {
|
|
|
2307
2319
|
};
|
|
2308
2320
|
}
|
|
2309
2321
|
/**
|
|
2310
|
-
*
|
|
2311
|
-
*
|
|
2322
|
+
* Detect stale observations (files modified after creation) and mark them in DB.
|
|
2323
|
+
* Returns the number of observations marked as stale.
|
|
2312
2324
|
*/
|
|
2313
2325
|
async detectStaleObservations() {
|
|
2314
2326
|
const staleObs = getStaleObservations(this.db.db, this.project);
|
|
@@ -2319,14 +2331,14 @@ var KiroMemorySDK = class {
|
|
|
2319
2331
|
return staleObs.length;
|
|
2320
2332
|
}
|
|
2321
2333
|
/**
|
|
2322
|
-
*
|
|
2323
|
-
*
|
|
2334
|
+
* Consolidate duplicate observations on the same file and type.
|
|
2335
|
+
* Groups by (project, type, files_modified), keeps the most recent.
|
|
2324
2336
|
*/
|
|
2325
2337
|
async consolidateObservations(options = {}) {
|
|
2326
2338
|
return consolidateObservations(this.db.db, this.project, options);
|
|
2327
2339
|
}
|
|
2328
2340
|
/**
|
|
2329
|
-
*
|
|
2341
|
+
* Decay statistics: total, stale, never accessed, recently accessed.
|
|
2330
2342
|
*/
|
|
2331
2343
|
async getDecayStats() {
|
|
2332
2344
|
const total = this.db.db.query(
|
|
@@ -2345,8 +2357,8 @@ var KiroMemorySDK = class {
|
|
|
2345
2357
|
return { total, stale, neverAccessed, recentlyAccessed };
|
|
2346
2358
|
}
|
|
2347
2359
|
/**
|
|
2348
|
-
*
|
|
2349
|
-
*
|
|
2360
|
+
* Create a structured checkpoint for session resume.
|
|
2361
|
+
* Automatically saves a context_snapshot with the last 10 observations.
|
|
2350
2362
|
*/
|
|
2351
2363
|
async createCheckpoint(sessionId, data) {
|
|
2352
2364
|
const recentObs = getObservationsByProject(this.db.db, this.project, 10);
|
|
@@ -2363,21 +2375,21 @@ var KiroMemorySDK = class {
|
|
|
2363
2375
|
});
|
|
2364
2376
|
}
|
|
2365
2377
|
/**
|
|
2366
|
-
*
|
|
2378
|
+
* Retrieve the latest checkpoint of a specific session.
|
|
2367
2379
|
*/
|
|
2368
2380
|
async getCheckpoint(sessionId) {
|
|
2369
2381
|
return getLatestCheckpoint(this.db.db, sessionId);
|
|
2370
2382
|
}
|
|
2371
2383
|
/**
|
|
2372
|
-
*
|
|
2373
|
-
*
|
|
2384
|
+
* Retrieve the latest checkpoint for the current project.
|
|
2385
|
+
* Useful for automatic resume without specifying session ID.
|
|
2374
2386
|
*/
|
|
2375
2387
|
async getLatestProjectCheckpoint() {
|
|
2376
2388
|
return getLatestCheckpointByProject(this.db.db, this.project);
|
|
2377
2389
|
}
|
|
2378
2390
|
/**
|
|
2379
|
-
*
|
|
2380
|
-
*
|
|
2391
|
+
* Generate an activity report for the current project.
|
|
2392
|
+
* Aggregates observations, sessions, summaries and files for a time period.
|
|
2381
2393
|
*/
|
|
2382
2394
|
async generateReport(options) {
|
|
2383
2395
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -2409,8 +2421,6 @@ var KiroMemorySDK = class {
|
|
|
2409
2421
|
function createKiroMemory(config) {
|
|
2410
2422
|
return new KiroMemorySDK(config);
|
|
2411
2423
|
}
|
|
2412
|
-
var ContextKitSDK = KiroMemorySDK;
|
|
2413
|
-
var createContextKit = createKiroMemory;
|
|
2414
2424
|
|
|
2415
2425
|
// src/index.ts
|
|
2416
2426
|
init_Search();
|
|
@@ -2461,7 +2471,7 @@ async function readStdin() {
|
|
|
2461
2471
|
}
|
|
2462
2472
|
resolve(JSON.parse(data));
|
|
2463
2473
|
} catch (err) {
|
|
2464
|
-
reject(new Error(`
|
|
2474
|
+
reject(new Error(`Error parsing stdin JSON: ${err}`));
|
|
2465
2475
|
}
|
|
2466
2476
|
});
|
|
2467
2477
|
process.stdin.on("error", (err) => {
|
|
@@ -2486,19 +2496,19 @@ function detectProject(cwd) {
|
|
|
2486
2496
|
function formatContext(data) {
|
|
2487
2497
|
let output = "";
|
|
2488
2498
|
if (data.summaries && data.summaries.length > 0) {
|
|
2489
|
-
output += "##
|
|
2499
|
+
output += "## Previous Sessions\n\n";
|
|
2490
2500
|
data.summaries.slice(0, 3).forEach((sum) => {
|
|
2491
|
-
if (sum.learned) output += `- **
|
|
2501
|
+
if (sum.learned) output += `- **Learned**: ${sum.learned}
|
|
2492
2502
|
`;
|
|
2493
|
-
if (sum.completed) output += `- **
|
|
2503
|
+
if (sum.completed) output += `- **Completed**: ${sum.completed}
|
|
2494
2504
|
`;
|
|
2495
|
-
if (sum.next_steps) output += `- **
|
|
2505
|
+
if (sum.next_steps) output += `- **Next steps**: ${sum.next_steps}
|
|
2496
2506
|
`;
|
|
2497
2507
|
output += "\n";
|
|
2498
2508
|
});
|
|
2499
2509
|
}
|
|
2500
2510
|
if (data.observations && data.observations.length > 0) {
|
|
2501
|
-
output += "##
|
|
2511
|
+
output += "## Recent Observations\n\n";
|
|
2502
2512
|
data.observations.slice(0, 10).forEach((obs) => {
|
|
2503
2513
|
const text = obs.text ? obs.text.substring(0, 150) : "";
|
|
2504
2514
|
output += `- **[${obs.type || "obs"}] ${obs.title}**: ${text}
|
|
@@ -2519,35 +2529,29 @@ async function runHook(name, handler) {
|
|
|
2519
2529
|
}
|
|
2520
2530
|
debugLog(name, "stdin", input);
|
|
2521
2531
|
await handler(input);
|
|
2522
|
-
debugLog(name, "
|
|
2532
|
+
debugLog(name, "completed", { success: true });
|
|
2523
2533
|
process.exit(0);
|
|
2524
2534
|
} catch (error) {
|
|
2525
|
-
debugLog(name, "
|
|
2526
|
-
process.stderr.write(`[kiro-memory:${name}]
|
|
2535
|
+
debugLog(name, "error", { error: String(error) });
|
|
2536
|
+
process.stderr.write(`[kiro-memory:${name}] Error: ${error}
|
|
2527
2537
|
`);
|
|
2528
2538
|
process.exit(0);
|
|
2529
2539
|
}
|
|
2530
2540
|
}
|
|
2531
2541
|
|
|
2532
2542
|
// src/index.ts
|
|
2533
|
-
var VERSION = "1.
|
|
2543
|
+
var VERSION = "2.1.0";
|
|
2534
2544
|
export {
|
|
2535
|
-
KiroMemoryDatabase as ContextKitDatabase,
|
|
2536
|
-
ContextKitSDK,
|
|
2537
|
-
DatabaseManager,
|
|
2538
2545
|
KiroMemoryDatabase,
|
|
2539
2546
|
KiroMemorySDK,
|
|
2540
2547
|
LogLevel,
|
|
2541
2548
|
VERSION,
|
|
2542
|
-
createContextKit,
|
|
2543
2549
|
createKiroMemory,
|
|
2544
2550
|
detectProject,
|
|
2545
2551
|
formatContext,
|
|
2546
|
-
getDatabase,
|
|
2547
2552
|
getObservationsByIds,
|
|
2548
2553
|
getProjectStats,
|
|
2549
2554
|
getTimeline,
|
|
2550
|
-
initializeDatabase,
|
|
2551
2555
|
logger,
|
|
2552
2556
|
readStdin,
|
|
2553
2557
|
runHook,
|