kiro-memory 1.6.0 → 1.7.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.
- package/README.md +105 -99
- package/package.json +14 -7
- package/plugin/dist/cli/contextkit.js +2661 -497
- package/plugin/dist/hooks/agentSpawn.js +1455 -189
- package/plugin/dist/hooks/kiro-hooks.js +1389 -156
- package/plugin/dist/hooks/postToolUse.js +1451 -174
- package/plugin/dist/hooks/stop.js +1426 -170
- package/plugin/dist/hooks/userPromptSubmit.js +1418 -170
- package/plugin/dist/index.js +1406 -172
- package/plugin/dist/sdk/index.js +1389 -155
- package/plugin/dist/servers/mcp-server.js +203 -2
- package/plugin/dist/services/search/EmbeddingService.js +363 -0
- package/plugin/dist/services/search/HybridSearch.js +703 -151
- package/plugin/dist/services/search/ScoringEngine.js +75 -0
- package/plugin/dist/services/search/VectorSearch.js +512 -0
- package/plugin/dist/services/search/index.js +776 -64
- package/plugin/dist/services/sqlite/Database.js +49 -0
- package/plugin/dist/services/sqlite/Observations.js +70 -6
- package/plugin/dist/services/sqlite/Search.js +92 -8
- package/plugin/dist/services/sqlite/Summaries.js +8 -5
- package/plugin/dist/services/sqlite/index.js +384 -18
- package/plugin/dist/types/worker-types.js +6 -0
- package/plugin/dist/viewer.js +369 -69
- package/plugin/dist/worker-service.js +1496 -148
|
@@ -5,9 +5,16 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __commonJS = (cb, mod) => function __require() {
|
|
9
12
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
13
|
};
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
11
18
|
var __copyProps = (to, from, except, desc) => {
|
|
12
19
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
20
|
for (let key of __getOwnPropNames(from))
|
|
@@ -1555,6 +1562,363 @@ var require_ip_address = __commonJS({
|
|
|
1555
1562
|
}
|
|
1556
1563
|
});
|
|
1557
1564
|
|
|
1565
|
+
// src/services/sqlite/Observations.ts
|
|
1566
|
+
var Observations_exports = {};
|
|
1567
|
+
__export(Observations_exports, {
|
|
1568
|
+
consolidateObservations: () => consolidateObservations,
|
|
1569
|
+
createObservation: () => createObservation,
|
|
1570
|
+
deleteObservation: () => deleteObservation,
|
|
1571
|
+
getObservationsByProject: () => getObservationsByProject,
|
|
1572
|
+
getObservationsBySession: () => getObservationsBySession,
|
|
1573
|
+
searchObservations: () => searchObservations,
|
|
1574
|
+
updateLastAccessed: () => updateLastAccessed
|
|
1575
|
+
});
|
|
1576
|
+
function escapeLikePattern(input) {
|
|
1577
|
+
return input.replace(/[%_\\]/g, "\\$&");
|
|
1578
|
+
}
|
|
1579
|
+
function createObservation(db2, memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber) {
|
|
1580
|
+
const now = /* @__PURE__ */ new Date();
|
|
1581
|
+
const result = db2.run(
|
|
1582
|
+
`INSERT INTO observations
|
|
1583
|
+
(memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
|
1584
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1585
|
+
[memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
|
|
1586
|
+
);
|
|
1587
|
+
return Number(result.lastInsertRowid);
|
|
1588
|
+
}
|
|
1589
|
+
function getObservationsBySession(db2, memorySessionId) {
|
|
1590
|
+
const query = db2.query(
|
|
1591
|
+
"SELECT * FROM observations WHERE memory_session_id = ? ORDER BY prompt_number ASC"
|
|
1592
|
+
);
|
|
1593
|
+
return query.all(memorySessionId);
|
|
1594
|
+
}
|
|
1595
|
+
function getObservationsByProject(db2, project, limit = 100) {
|
|
1596
|
+
const query = db2.query(
|
|
1597
|
+
"SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
|
|
1598
|
+
);
|
|
1599
|
+
return query.all(project, limit);
|
|
1600
|
+
}
|
|
1601
|
+
function searchObservations(db2, searchTerm, project) {
|
|
1602
|
+
const sql = project ? `SELECT * FROM observations
|
|
1603
|
+
WHERE project = ? AND (title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\')
|
|
1604
|
+
ORDER BY created_at_epoch DESC` : `SELECT * FROM observations
|
|
1605
|
+
WHERE title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\'
|
|
1606
|
+
ORDER BY created_at_epoch DESC`;
|
|
1607
|
+
const pattern = `%${escapeLikePattern(searchTerm)}%`;
|
|
1608
|
+
const query = db2.query(sql);
|
|
1609
|
+
if (project) {
|
|
1610
|
+
return query.all(project, pattern, pattern, pattern);
|
|
1611
|
+
}
|
|
1612
|
+
return query.all(pattern, pattern, pattern);
|
|
1613
|
+
}
|
|
1614
|
+
function deleteObservation(db2, id) {
|
|
1615
|
+
db2.run("DELETE FROM observations WHERE id = ?", [id]);
|
|
1616
|
+
}
|
|
1617
|
+
function updateLastAccessed(db2, ids) {
|
|
1618
|
+
if (!Array.isArray(ids) || ids.length === 0) return;
|
|
1619
|
+
const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
|
|
1620
|
+
if (validIds.length === 0) return;
|
|
1621
|
+
const now = Date.now();
|
|
1622
|
+
const placeholders = validIds.map(() => "?").join(",");
|
|
1623
|
+
db2.run(
|
|
1624
|
+
`UPDATE observations SET last_accessed_epoch = ? WHERE id IN (${placeholders})`,
|
|
1625
|
+
[now, ...validIds]
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
function consolidateObservations(db2, project, options = {}) {
|
|
1629
|
+
const minGroupSize = options.minGroupSize || 3;
|
|
1630
|
+
const groups = db2.query(`
|
|
1631
|
+
SELECT type, files_modified, COUNT(*) as cnt, GROUP_CONCAT(id) as ids
|
|
1632
|
+
FROM observations
|
|
1633
|
+
WHERE project = ? AND files_modified IS NOT NULL AND files_modified != ''
|
|
1634
|
+
GROUP BY type, files_modified
|
|
1635
|
+
HAVING cnt >= ?
|
|
1636
|
+
ORDER BY cnt DESC
|
|
1637
|
+
`).all(project, minGroupSize);
|
|
1638
|
+
if (groups.length === 0) return { merged: 0, removed: 0 };
|
|
1639
|
+
let totalMerged = 0;
|
|
1640
|
+
let totalRemoved = 0;
|
|
1641
|
+
for (const group of groups) {
|
|
1642
|
+
const obsIds = group.ids.split(",").map(Number);
|
|
1643
|
+
const placeholders = obsIds.map(() => "?").join(",");
|
|
1644
|
+
const observations = db2.query(
|
|
1645
|
+
`SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`
|
|
1646
|
+
).all(...obsIds);
|
|
1647
|
+
if (observations.length < minGroupSize) continue;
|
|
1648
|
+
if (options.dryRun) {
|
|
1649
|
+
totalMerged += 1;
|
|
1650
|
+
totalRemoved += observations.length - 1;
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const keeper = observations[0];
|
|
1654
|
+
const others = observations.slice(1);
|
|
1655
|
+
const uniqueTexts = /* @__PURE__ */ new Set();
|
|
1656
|
+
if (keeper.text) uniqueTexts.add(keeper.text);
|
|
1657
|
+
for (const obs of others) {
|
|
1658
|
+
if (obs.text && !uniqueTexts.has(obs.text)) {
|
|
1659
|
+
uniqueTexts.add(obs.text);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
const consolidatedText = Array.from(uniqueTexts).join("\n---\n").substring(0, 1e5);
|
|
1663
|
+
db2.run(
|
|
1664
|
+
"UPDATE observations SET text = ?, title = ? WHERE id = ?",
|
|
1665
|
+
[consolidatedText, `[consolidato x${observations.length}] ${keeper.title}`, keeper.id]
|
|
1666
|
+
);
|
|
1667
|
+
const removeIds = others.map((o) => o.id);
|
|
1668
|
+
const removePlaceholders = removeIds.map(() => "?").join(",");
|
|
1669
|
+
db2.run(`DELETE FROM observations WHERE id IN (${removePlaceholders})`, removeIds);
|
|
1670
|
+
db2.run(`DELETE FROM observation_embeddings WHERE observation_id IN (${removePlaceholders})`, removeIds);
|
|
1671
|
+
totalMerged += 1;
|
|
1672
|
+
totalRemoved += removeIds.length;
|
|
1673
|
+
}
|
|
1674
|
+
return { merged: totalMerged, removed: totalRemoved };
|
|
1675
|
+
}
|
|
1676
|
+
var init_Observations = __esm({
|
|
1677
|
+
"src/services/sqlite/Observations.ts"() {
|
|
1678
|
+
"use strict";
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// src/services/sqlite/Search.ts
|
|
1683
|
+
var Search_exports = {};
|
|
1684
|
+
__export(Search_exports, {
|
|
1685
|
+
getObservationsByIds: () => getObservationsByIds,
|
|
1686
|
+
getProjectStats: () => getProjectStats,
|
|
1687
|
+
getStaleObservations: () => getStaleObservations,
|
|
1688
|
+
getTimeline: () => getTimeline,
|
|
1689
|
+
markObservationsStale: () => markObservationsStale,
|
|
1690
|
+
searchObservationsFTS: () => searchObservationsFTS,
|
|
1691
|
+
searchObservationsFTSWithRank: () => searchObservationsFTSWithRank,
|
|
1692
|
+
searchObservationsLIKE: () => searchObservationsLIKE,
|
|
1693
|
+
searchSummariesFiltered: () => searchSummariesFiltered
|
|
1694
|
+
});
|
|
1695
|
+
import { existsSync as existsSync3, statSync } from "fs";
|
|
1696
|
+
function escapeLikePattern2(input) {
|
|
1697
|
+
return input.replace(/[%_\\]/g, "\\$&");
|
|
1698
|
+
}
|
|
1699
|
+
function sanitizeFTS5Query(query) {
|
|
1700
|
+
const trimmed = query.length > 1e4 ? query.substring(0, 1e4) : query;
|
|
1701
|
+
const terms = trimmed.replace(/[""]/g, "").split(/\s+/).filter((t) => t.length > 0).slice(0, 100).map((t) => `"${t}"`);
|
|
1702
|
+
return terms.join(" ");
|
|
1703
|
+
}
|
|
1704
|
+
function searchObservationsFTS(db2, query, filters = {}) {
|
|
1705
|
+
const limit = filters.limit || 50;
|
|
1706
|
+
try {
|
|
1707
|
+
const safeQuery = sanitizeFTS5Query(query);
|
|
1708
|
+
if (!safeQuery) return searchObservationsLIKE(db2, query, filters);
|
|
1709
|
+
let sql = `
|
|
1710
|
+
SELECT o.* FROM observations o
|
|
1711
|
+
JOIN observations_fts fts ON o.id = fts.rowid
|
|
1712
|
+
WHERE observations_fts MATCH ?
|
|
1713
|
+
`;
|
|
1714
|
+
const params = [safeQuery];
|
|
1715
|
+
if (filters.project) {
|
|
1716
|
+
sql += " AND o.project = ?";
|
|
1717
|
+
params.push(filters.project);
|
|
1718
|
+
}
|
|
1719
|
+
if (filters.type) {
|
|
1720
|
+
sql += " AND o.type = ?";
|
|
1721
|
+
params.push(filters.type);
|
|
1722
|
+
}
|
|
1723
|
+
if (filters.dateStart) {
|
|
1724
|
+
sql += " AND o.created_at_epoch >= ?";
|
|
1725
|
+
params.push(filters.dateStart);
|
|
1726
|
+
}
|
|
1727
|
+
if (filters.dateEnd) {
|
|
1728
|
+
sql += " AND o.created_at_epoch <= ?";
|
|
1729
|
+
params.push(filters.dateEnd);
|
|
1730
|
+
}
|
|
1731
|
+
sql += " ORDER BY rank LIMIT ?";
|
|
1732
|
+
params.push(limit);
|
|
1733
|
+
const stmt = db2.query(sql);
|
|
1734
|
+
return stmt.all(...params);
|
|
1735
|
+
} catch {
|
|
1736
|
+
return searchObservationsLIKE(db2, query, filters);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function searchObservationsFTSWithRank(db2, query, filters = {}) {
|
|
1740
|
+
const limit = filters.limit || 50;
|
|
1741
|
+
try {
|
|
1742
|
+
const safeQuery = sanitizeFTS5Query(query);
|
|
1743
|
+
if (!safeQuery) return [];
|
|
1744
|
+
let sql = `
|
|
1745
|
+
SELECT o.*, rank as fts5_rank FROM observations o
|
|
1746
|
+
JOIN observations_fts fts ON o.id = fts.rowid
|
|
1747
|
+
WHERE observations_fts MATCH ?
|
|
1748
|
+
`;
|
|
1749
|
+
const params = [safeQuery];
|
|
1750
|
+
if (filters.project) {
|
|
1751
|
+
sql += " AND o.project = ?";
|
|
1752
|
+
params.push(filters.project);
|
|
1753
|
+
}
|
|
1754
|
+
if (filters.type) {
|
|
1755
|
+
sql += " AND o.type = ?";
|
|
1756
|
+
params.push(filters.type);
|
|
1757
|
+
}
|
|
1758
|
+
if (filters.dateStart) {
|
|
1759
|
+
sql += " AND o.created_at_epoch >= ?";
|
|
1760
|
+
params.push(filters.dateStart);
|
|
1761
|
+
}
|
|
1762
|
+
if (filters.dateEnd) {
|
|
1763
|
+
sql += " AND o.created_at_epoch <= ?";
|
|
1764
|
+
params.push(filters.dateEnd);
|
|
1765
|
+
}
|
|
1766
|
+
sql += " ORDER BY rank LIMIT ?";
|
|
1767
|
+
params.push(limit);
|
|
1768
|
+
const stmt = db2.query(sql);
|
|
1769
|
+
return stmt.all(...params);
|
|
1770
|
+
} catch {
|
|
1771
|
+
return [];
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function searchObservationsLIKE(db2, query, filters = {}) {
|
|
1775
|
+
const limit = filters.limit || 50;
|
|
1776
|
+
const pattern = `%${escapeLikePattern2(query)}%`;
|
|
1777
|
+
let sql = `
|
|
1778
|
+
SELECT * FROM observations
|
|
1779
|
+
WHERE (title LIKE ? ESCAPE '\\' OR text LIKE ? ESCAPE '\\' OR narrative LIKE ? ESCAPE '\\' OR concepts LIKE ? ESCAPE '\\')
|
|
1780
|
+
`;
|
|
1781
|
+
const params = [pattern, pattern, pattern, pattern];
|
|
1782
|
+
if (filters.project) {
|
|
1783
|
+
sql += " AND project = ?";
|
|
1784
|
+
params.push(filters.project);
|
|
1785
|
+
}
|
|
1786
|
+
if (filters.type) {
|
|
1787
|
+
sql += " AND type = ?";
|
|
1788
|
+
params.push(filters.type);
|
|
1789
|
+
}
|
|
1790
|
+
if (filters.dateStart) {
|
|
1791
|
+
sql += " AND created_at_epoch >= ?";
|
|
1792
|
+
params.push(filters.dateStart);
|
|
1793
|
+
}
|
|
1794
|
+
if (filters.dateEnd) {
|
|
1795
|
+
sql += " AND created_at_epoch <= ?";
|
|
1796
|
+
params.push(filters.dateEnd);
|
|
1797
|
+
}
|
|
1798
|
+
sql += " ORDER BY created_at_epoch DESC LIMIT ?";
|
|
1799
|
+
params.push(limit);
|
|
1800
|
+
const stmt = db2.query(sql);
|
|
1801
|
+
return stmt.all(...params);
|
|
1802
|
+
}
|
|
1803
|
+
function searchSummariesFiltered(db2, query, filters = {}) {
|
|
1804
|
+
const limit = filters.limit || 20;
|
|
1805
|
+
const pattern = `%${escapeLikePattern2(query)}%`;
|
|
1806
|
+
let sql = `
|
|
1807
|
+
SELECT * FROM summaries
|
|
1808
|
+
WHERE (request LIKE ? ESCAPE '\\' OR learned LIKE ? ESCAPE '\\' OR completed LIKE ? ESCAPE '\\' OR notes LIKE ? ESCAPE '\\' OR next_steps LIKE ? ESCAPE '\\')
|
|
1809
|
+
`;
|
|
1810
|
+
const params = [pattern, pattern, pattern, pattern, pattern];
|
|
1811
|
+
if (filters.project) {
|
|
1812
|
+
sql += " AND project = ?";
|
|
1813
|
+
params.push(filters.project);
|
|
1814
|
+
}
|
|
1815
|
+
if (filters.dateStart) {
|
|
1816
|
+
sql += " AND created_at_epoch >= ?";
|
|
1817
|
+
params.push(filters.dateStart);
|
|
1818
|
+
}
|
|
1819
|
+
if (filters.dateEnd) {
|
|
1820
|
+
sql += " AND created_at_epoch <= ?";
|
|
1821
|
+
params.push(filters.dateEnd);
|
|
1822
|
+
}
|
|
1823
|
+
sql += " ORDER BY created_at_epoch DESC LIMIT ?";
|
|
1824
|
+
params.push(limit);
|
|
1825
|
+
const stmt = db2.query(sql);
|
|
1826
|
+
return stmt.all(...params);
|
|
1827
|
+
}
|
|
1828
|
+
function getObservationsByIds(db2, ids) {
|
|
1829
|
+
if (!Array.isArray(ids) || ids.length === 0) return [];
|
|
1830
|
+
const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
|
|
1831
|
+
if (validIds.length === 0) return [];
|
|
1832
|
+
const placeholders = validIds.map(() => "?").join(",");
|
|
1833
|
+
const sql = `SELECT * FROM observations WHERE id IN (${placeholders}) ORDER BY created_at_epoch DESC`;
|
|
1834
|
+
const stmt = db2.query(sql);
|
|
1835
|
+
return stmt.all(...validIds);
|
|
1836
|
+
}
|
|
1837
|
+
function getTimeline(db2, anchorId, depthBefore = 5, depthAfter = 5) {
|
|
1838
|
+
const anchorStmt = db2.query("SELECT created_at_epoch FROM observations WHERE id = ?");
|
|
1839
|
+
const anchor = anchorStmt.get(anchorId);
|
|
1840
|
+
if (!anchor) return [];
|
|
1841
|
+
const anchorEpoch = anchor.created_at_epoch;
|
|
1842
|
+
const beforeStmt = db2.query(`
|
|
1843
|
+
SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
|
|
1844
|
+
FROM observations
|
|
1845
|
+
WHERE created_at_epoch < ?
|
|
1846
|
+
ORDER BY created_at_epoch DESC
|
|
1847
|
+
LIMIT ?
|
|
1848
|
+
`);
|
|
1849
|
+
const before = beforeStmt.all(anchorEpoch, depthBefore).reverse();
|
|
1850
|
+
const selfStmt = db2.query(`
|
|
1851
|
+
SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
|
|
1852
|
+
FROM observations WHERE id = ?
|
|
1853
|
+
`);
|
|
1854
|
+
const self = selfStmt.all(anchorId);
|
|
1855
|
+
const afterStmt = db2.query(`
|
|
1856
|
+
SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
|
|
1857
|
+
FROM observations
|
|
1858
|
+
WHERE created_at_epoch > ?
|
|
1859
|
+
ORDER BY created_at_epoch ASC
|
|
1860
|
+
LIMIT ?
|
|
1861
|
+
`);
|
|
1862
|
+
const after = afterStmt.all(anchorEpoch, depthAfter);
|
|
1863
|
+
return [...before, ...self, ...after];
|
|
1864
|
+
}
|
|
1865
|
+
function getProjectStats(db2, project) {
|
|
1866
|
+
const obsStmt = db2.query("SELECT COUNT(*) as count FROM observations WHERE project = ?");
|
|
1867
|
+
const sumStmt = db2.query("SELECT COUNT(*) as count FROM summaries WHERE project = ?");
|
|
1868
|
+
const sesStmt = db2.query("SELECT COUNT(*) as count FROM sessions WHERE project = ?");
|
|
1869
|
+
const prmStmt = db2.query("SELECT COUNT(*) as count FROM prompts WHERE project = ?");
|
|
1870
|
+
return {
|
|
1871
|
+
observations: obsStmt.get(project)?.count || 0,
|
|
1872
|
+
summaries: sumStmt.get(project)?.count || 0,
|
|
1873
|
+
sessions: sesStmt.get(project)?.count || 0,
|
|
1874
|
+
prompts: prmStmt.get(project)?.count || 0
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function getStaleObservations(db2, project) {
|
|
1878
|
+
const rows = db2.query(`
|
|
1879
|
+
SELECT * FROM observations
|
|
1880
|
+
WHERE project = ? AND files_modified IS NOT NULL AND files_modified != ''
|
|
1881
|
+
ORDER BY created_at_epoch DESC
|
|
1882
|
+
LIMIT 500
|
|
1883
|
+
`).all(project);
|
|
1884
|
+
const staleObs = [];
|
|
1885
|
+
for (const obs of rows) {
|
|
1886
|
+
if (!obs.files_modified) continue;
|
|
1887
|
+
const files = obs.files_modified.split(",").map((f) => f.trim()).filter(Boolean);
|
|
1888
|
+
let isStale = false;
|
|
1889
|
+
for (const filepath of files) {
|
|
1890
|
+
try {
|
|
1891
|
+
if (!existsSync3(filepath)) continue;
|
|
1892
|
+
const stat = statSync(filepath);
|
|
1893
|
+
if (stat.mtimeMs > obs.created_at_epoch) {
|
|
1894
|
+
isStale = true;
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
} catch {
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (isStale) {
|
|
1901
|
+
staleObs.push(obs);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return staleObs;
|
|
1905
|
+
}
|
|
1906
|
+
function markObservationsStale(db2, ids, stale) {
|
|
1907
|
+
if (!Array.isArray(ids) || ids.length === 0) return;
|
|
1908
|
+
const validIds = ids.filter((id) => typeof id === "number" && Number.isInteger(id) && id > 0).slice(0, 500);
|
|
1909
|
+
if (validIds.length === 0) return;
|
|
1910
|
+
const placeholders = validIds.map(() => "?").join(",");
|
|
1911
|
+
db2.run(
|
|
1912
|
+
`UPDATE observations SET is_stale = ? WHERE id IN (${placeholders})`,
|
|
1913
|
+
[stale ? 1 : 0, ...validIds]
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
var init_Search = __esm({
|
|
1917
|
+
"src/services/sqlite/Search.ts"() {
|
|
1918
|
+
"use strict";
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1558
1922
|
// src/services/worker-service.ts
|
|
1559
1923
|
import express from "express";
|
|
1560
1924
|
import cors from "cors";
|
|
@@ -3002,7 +3366,7 @@ var rate_limit_default = rateLimit;
|
|
|
3002
3366
|
// src/services/worker-service.ts
|
|
3003
3367
|
import crypto from "crypto";
|
|
3004
3368
|
import { join as join3, dirname as dirname2 } from "path";
|
|
3005
|
-
import { existsSync as
|
|
3369
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync, unlinkSync, chmodSync } from "fs";
|
|
3006
3370
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3007
3371
|
|
|
3008
3372
|
// src/shims/bun-sqlite.ts
|
|
@@ -3532,31 +3896,65 @@ var MigrationRunner = class {
|
|
|
3532
3896
|
`);
|
|
3533
3897
|
db2.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_project_aliases_name ON project_aliases(project_name)");
|
|
3534
3898
|
}
|
|
3899
|
+
},
|
|
3900
|
+
{
|
|
3901
|
+
version: 4,
|
|
3902
|
+
up: (db2) => {
|
|
3903
|
+
db2.run(`
|
|
3904
|
+
CREATE TABLE IF NOT EXISTS observation_embeddings (
|
|
3905
|
+
observation_id INTEGER PRIMARY KEY,
|
|
3906
|
+
embedding BLOB NOT NULL,
|
|
3907
|
+
model TEXT NOT NULL,
|
|
3908
|
+
dimensions INTEGER NOT NULL,
|
|
3909
|
+
created_at TEXT NOT NULL,
|
|
3910
|
+
FOREIGN KEY (observation_id) REFERENCES observations(id) ON DELETE CASCADE
|
|
3911
|
+
)
|
|
3912
|
+
`);
|
|
3913
|
+
db2.run("CREATE INDEX IF NOT EXISTS idx_embeddings_model ON observation_embeddings(model)");
|
|
3914
|
+
}
|
|
3915
|
+
},
|
|
3916
|
+
{
|
|
3917
|
+
version: 5,
|
|
3918
|
+
up: (db2) => {
|
|
3919
|
+
db2.run("ALTER TABLE observations ADD COLUMN last_accessed_epoch INTEGER");
|
|
3920
|
+
db2.run("ALTER TABLE observations ADD COLUMN is_stale INTEGER DEFAULT 0");
|
|
3921
|
+
db2.run("CREATE INDEX IF NOT EXISTS idx_observations_last_accessed ON observations(last_accessed_epoch)");
|
|
3922
|
+
db2.run("CREATE INDEX IF NOT EXISTS idx_observations_stale ON observations(is_stale)");
|
|
3923
|
+
}
|
|
3924
|
+
},
|
|
3925
|
+
{
|
|
3926
|
+
version: 6,
|
|
3927
|
+
up: (db2) => {
|
|
3928
|
+
db2.run(`
|
|
3929
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
3930
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3931
|
+
session_id INTEGER NOT NULL,
|
|
3932
|
+
project TEXT NOT NULL,
|
|
3933
|
+
task TEXT NOT NULL,
|
|
3934
|
+
progress TEXT,
|
|
3935
|
+
next_steps TEXT,
|
|
3936
|
+
open_questions TEXT,
|
|
3937
|
+
relevant_files TEXT,
|
|
3938
|
+
context_snapshot TEXT,
|
|
3939
|
+
created_at TEXT NOT NULL,
|
|
3940
|
+
created_at_epoch INTEGER NOT NULL,
|
|
3941
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
3942
|
+
)
|
|
3943
|
+
`);
|
|
3944
|
+
db2.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id)");
|
|
3945
|
+
db2.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_project ON checkpoints(project)");
|
|
3946
|
+
db2.run("CREATE INDEX IF NOT EXISTS idx_checkpoints_epoch ON checkpoints(created_at_epoch)");
|
|
3947
|
+
}
|
|
3535
3948
|
}
|
|
3536
3949
|
];
|
|
3537
3950
|
}
|
|
3538
3951
|
};
|
|
3539
3952
|
|
|
3540
|
-
// src/services/
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
(memory_session_id, project, type, title, subtitle, text, narrative, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch)
|
|
3546
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3547
|
-
[memorySessionId, project, type, title, subtitle, text, narrative, facts, concepts, filesRead, filesModified, promptNumber, now.toISOString(), now.getTime()]
|
|
3548
|
-
);
|
|
3549
|
-
return Number(result.lastInsertRowid);
|
|
3550
|
-
}
|
|
3551
|
-
function getObservationsByProject(db2, project, limit = 100) {
|
|
3552
|
-
const query = db2.query(
|
|
3553
|
-
"SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC LIMIT ?"
|
|
3554
|
-
);
|
|
3555
|
-
return query.all(project, limit);
|
|
3556
|
-
}
|
|
3557
|
-
|
|
3558
|
-
// src/services/sqlite/Summaries.ts
|
|
3559
|
-
function createSummary(db2, sessionId, project, request, investigated, learned, completed, nextSteps, notes) {
|
|
3953
|
+
// src/services/worker-service.ts
|
|
3954
|
+
init_Observations();
|
|
3955
|
+
|
|
3956
|
+
// src/services/sqlite/Summaries.ts
|
|
3957
|
+
function createSummary(db2, sessionId, project, request, investigated, learned, completed, nextSteps, notes) {
|
|
3560
3958
|
const now = /* @__PURE__ */ new Date();
|
|
3561
3959
|
const result = db2.run(
|
|
3562
3960
|
`INSERT INTO summaries
|
|
@@ -3573,148 +3971,791 @@ function getSummariesByProject(db2, project, limit = 50) {
|
|
|
3573
3971
|
return query.all(project, limit);
|
|
3574
3972
|
}
|
|
3575
3973
|
|
|
3576
|
-
// src/services/
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3974
|
+
// src/services/worker-service.ts
|
|
3975
|
+
init_Search();
|
|
3976
|
+
|
|
3977
|
+
// src/services/search/EmbeddingService.ts
|
|
3978
|
+
var EmbeddingService = class {
|
|
3979
|
+
provider = null;
|
|
3980
|
+
model = null;
|
|
3981
|
+
initialized = false;
|
|
3982
|
+
initializing = null;
|
|
3983
|
+
/**
|
|
3984
|
+
* Inizializza il servizio di embedding.
|
|
3985
|
+
* Tenta fastembed, poi @huggingface/transformers, poi fallback a null.
|
|
3986
|
+
*/
|
|
3987
|
+
async initialize() {
|
|
3988
|
+
if (this.initialized) return this.provider !== null;
|
|
3989
|
+
if (this.initializing) return this.initializing;
|
|
3990
|
+
this.initializing = this._doInitialize();
|
|
3991
|
+
const result = await this.initializing;
|
|
3992
|
+
this.initializing = null;
|
|
3993
|
+
return result;
|
|
3994
|
+
}
|
|
3995
|
+
async _doInitialize() {
|
|
3996
|
+
try {
|
|
3997
|
+
const fastembed = await import("fastembed");
|
|
3998
|
+
const EmbeddingModel = fastembed.EmbeddingModel || fastembed.default?.EmbeddingModel;
|
|
3999
|
+
const FlagEmbedding = fastembed.FlagEmbedding || fastembed.default?.FlagEmbedding;
|
|
4000
|
+
if (FlagEmbedding && EmbeddingModel) {
|
|
4001
|
+
this.model = await FlagEmbedding.init({
|
|
4002
|
+
model: EmbeddingModel.BGESmallENV15
|
|
4003
|
+
});
|
|
4004
|
+
this.provider = "fastembed";
|
|
4005
|
+
this.initialized = true;
|
|
4006
|
+
logger.info("EMBEDDING", "Inizializzato con fastembed (BGE-small-en-v1.5)");
|
|
4007
|
+
return true;
|
|
4008
|
+
}
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
logger.debug("EMBEDDING", `fastembed non disponibile: ${error}`);
|
|
3595
4011
|
}
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
4012
|
+
try {
|
|
4013
|
+
const transformers = await import("@huggingface/transformers");
|
|
4014
|
+
const pipeline = transformers.pipeline || transformers.default?.pipeline;
|
|
4015
|
+
if (pipeline) {
|
|
4016
|
+
this.model = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
|
|
4017
|
+
quantized: true
|
|
4018
|
+
});
|
|
4019
|
+
this.provider = "transformers";
|
|
4020
|
+
this.initialized = true;
|
|
4021
|
+
logger.info("EMBEDDING", "Inizializzato con @huggingface/transformers (all-MiniLM-L6-v2)");
|
|
4022
|
+
return true;
|
|
4023
|
+
}
|
|
4024
|
+
} catch (error) {
|
|
4025
|
+
logger.debug("EMBEDDING", `@huggingface/transformers non disponibile: ${error}`);
|
|
3599
4026
|
}
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
4027
|
+
this.provider = null;
|
|
4028
|
+
this.initialized = true;
|
|
4029
|
+
logger.warn("EMBEDDING", "Nessun provider embedding disponibile, ricerca semantica disabilitata");
|
|
4030
|
+
return false;
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Genera embedding per un singolo testo.
|
|
4034
|
+
* Ritorna Float32Array con 384 dimensioni, o null se non disponibile.
|
|
4035
|
+
*/
|
|
4036
|
+
async embed(text) {
|
|
4037
|
+
if (!this.initialized) await this.initialize();
|
|
4038
|
+
if (!this.provider || !this.model) return null;
|
|
4039
|
+
try {
|
|
4040
|
+
const truncated = text.substring(0, 2e3);
|
|
4041
|
+
if (this.provider === "fastembed") {
|
|
4042
|
+
return await this._embedFastembed(truncated);
|
|
4043
|
+
} else if (this.provider === "transformers") {
|
|
4044
|
+
return await this._embedTransformers(truncated);
|
|
4045
|
+
}
|
|
4046
|
+
} catch (error) {
|
|
4047
|
+
logger.error("EMBEDDING", `Errore generazione embedding: ${error}`);
|
|
3603
4048
|
}
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
4049
|
+
return null;
|
|
4050
|
+
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Genera embeddings in batch.
|
|
4053
|
+
*/
|
|
4054
|
+
async embedBatch(texts) {
|
|
4055
|
+
if (!this.initialized) await this.initialize();
|
|
4056
|
+
if (!this.provider || !this.model) return texts.map(() => null);
|
|
4057
|
+
const results = [];
|
|
4058
|
+
for (const text of texts) {
|
|
4059
|
+
try {
|
|
4060
|
+
const embedding = await this.embed(text);
|
|
4061
|
+
results.push(embedding);
|
|
4062
|
+
} catch {
|
|
4063
|
+
results.push(null);
|
|
4064
|
+
}
|
|
3607
4065
|
}
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
4066
|
+
return results;
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* Verifica se il servizio è disponibile.
|
|
4070
|
+
*/
|
|
4071
|
+
isAvailable() {
|
|
4072
|
+
return this.initialized && this.provider !== null;
|
|
4073
|
+
}
|
|
4074
|
+
/**
|
|
4075
|
+
* Nome del provider attivo.
|
|
4076
|
+
*/
|
|
4077
|
+
getProvider() {
|
|
4078
|
+
return this.provider;
|
|
4079
|
+
}
|
|
4080
|
+
/**
|
|
4081
|
+
* Dimensioni del vettore embedding.
|
|
4082
|
+
*/
|
|
4083
|
+
getDimensions() {
|
|
4084
|
+
return 384;
|
|
4085
|
+
}
|
|
4086
|
+
// --- Provider specifici ---
|
|
4087
|
+
async _embedFastembed(text) {
|
|
4088
|
+
const embeddings = this.model.embed([text], 1);
|
|
4089
|
+
for await (const batch of embeddings) {
|
|
4090
|
+
if (batch && batch.length > 0) {
|
|
4091
|
+
const vec = batch[0];
|
|
4092
|
+
return vec instanceof Float32Array ? vec : new Float32Array(vec);
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
return null;
|
|
3614
4096
|
}
|
|
4097
|
+
async _embedTransformers(text) {
|
|
4098
|
+
const output = await this.model(text, {
|
|
4099
|
+
pooling: "mean",
|
|
4100
|
+
normalize: true
|
|
4101
|
+
});
|
|
4102
|
+
if (output?.data) {
|
|
4103
|
+
return output.data instanceof Float32Array ? output.data : new Float32Array(output.data);
|
|
4104
|
+
}
|
|
4105
|
+
return null;
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
var embeddingService = null;
|
|
4109
|
+
function getEmbeddingService() {
|
|
4110
|
+
if (!embeddingService) {
|
|
4111
|
+
embeddingService = new EmbeddingService();
|
|
4112
|
+
}
|
|
4113
|
+
return embeddingService;
|
|
3615
4114
|
}
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
4115
|
+
|
|
4116
|
+
// src/services/search/VectorSearch.ts
|
|
4117
|
+
function cosineSimilarity(a, b) {
|
|
4118
|
+
if (a.length !== b.length) return 0;
|
|
4119
|
+
let dotProduct = 0;
|
|
4120
|
+
let normA = 0;
|
|
4121
|
+
let normB = 0;
|
|
4122
|
+
for (let i = 0; i < a.length; i++) {
|
|
4123
|
+
dotProduct += a[i] * b[i];
|
|
4124
|
+
normA += a[i] * a[i];
|
|
4125
|
+
normB += b[i] * b[i];
|
|
4126
|
+
}
|
|
4127
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
4128
|
+
if (denominator === 0) return 0;
|
|
4129
|
+
return dotProduct / denominator;
|
|
4130
|
+
}
|
|
4131
|
+
function float32ToBuffer(arr) {
|
|
4132
|
+
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
4133
|
+
}
|
|
4134
|
+
function bufferToFloat32(buf) {
|
|
4135
|
+
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
4136
|
+
return new Float32Array(arrayBuffer);
|
|
4137
|
+
}
|
|
4138
|
+
var VectorSearch = class {
|
|
4139
|
+
/**
|
|
4140
|
+
* Ricerca semantica: calcola cosine similarity tra query e tutti gli embeddings.
|
|
4141
|
+
*/
|
|
4142
|
+
async search(db2, queryEmbedding, options = {}) {
|
|
4143
|
+
const limit = options.limit || 10;
|
|
4144
|
+
const threshold = options.threshold || 0.3;
|
|
4145
|
+
try {
|
|
4146
|
+
let sql = `
|
|
4147
|
+
SELECT e.observation_id, e.embedding,
|
|
4148
|
+
o.title, o.text, o.type, o.project, o.created_at, o.created_at_epoch
|
|
4149
|
+
FROM observation_embeddings e
|
|
4150
|
+
JOIN observations o ON o.id = e.observation_id
|
|
4151
|
+
`;
|
|
4152
|
+
const params = [];
|
|
4153
|
+
if (options.project) {
|
|
4154
|
+
sql += " WHERE o.project = ?";
|
|
4155
|
+
params.push(options.project);
|
|
4156
|
+
}
|
|
4157
|
+
const rows = db2.query(sql).all(...params);
|
|
4158
|
+
const scored = [];
|
|
4159
|
+
for (const row of rows) {
|
|
4160
|
+
const embedding = bufferToFloat32(row.embedding);
|
|
4161
|
+
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
4162
|
+
if (similarity >= threshold) {
|
|
4163
|
+
scored.push({
|
|
4164
|
+
id: row.observation_id,
|
|
4165
|
+
observationId: row.observation_id,
|
|
4166
|
+
similarity,
|
|
4167
|
+
title: row.title,
|
|
4168
|
+
text: row.text,
|
|
4169
|
+
type: row.type,
|
|
4170
|
+
project: row.project,
|
|
4171
|
+
created_at: row.created_at,
|
|
4172
|
+
created_at_epoch: row.created_at_epoch
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
4177
|
+
return scored.slice(0, limit);
|
|
4178
|
+
} catch (error) {
|
|
4179
|
+
logger.error("VECTOR", `Errore ricerca vettoriale: ${error}`);
|
|
4180
|
+
return [];
|
|
4181
|
+
}
|
|
3627
4182
|
}
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
4183
|
+
/**
|
|
4184
|
+
* Salva embedding per un'osservazione.
|
|
4185
|
+
*/
|
|
4186
|
+
async storeEmbedding(db2, observationId, embedding, model) {
|
|
4187
|
+
try {
|
|
4188
|
+
const blob = float32ToBuffer(embedding);
|
|
4189
|
+
db2.query(`
|
|
4190
|
+
INSERT OR REPLACE INTO observation_embeddings
|
|
4191
|
+
(observation_id, embedding, model, dimensions, created_at)
|
|
4192
|
+
VALUES (?, ?, ?, ?, ?)
|
|
4193
|
+
`).run(
|
|
4194
|
+
observationId,
|
|
4195
|
+
blob,
|
|
4196
|
+
model,
|
|
4197
|
+
embedding.length,
|
|
4198
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
4199
|
+
);
|
|
4200
|
+
logger.debug("VECTOR", `Embedding salvato per osservazione ${observationId}`);
|
|
4201
|
+
} catch (error) {
|
|
4202
|
+
logger.error("VECTOR", `Errore salvataggio embedding: ${error}`);
|
|
4203
|
+
}
|
|
3631
4204
|
}
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
4205
|
+
/**
|
|
4206
|
+
* Genera embeddings per osservazioni che non li hanno ancora.
|
|
4207
|
+
*/
|
|
4208
|
+
async backfillEmbeddings(db2, batchSize = 50) {
|
|
4209
|
+
const embeddingService2 = getEmbeddingService();
|
|
4210
|
+
if (!await embeddingService2.initialize()) {
|
|
4211
|
+
logger.warn("VECTOR", "Embedding service non disponibile, backfill saltato");
|
|
4212
|
+
return 0;
|
|
4213
|
+
}
|
|
4214
|
+
const rows = db2.query(`
|
|
4215
|
+
SELECT o.id, o.title, o.text, o.narrative, o.concepts
|
|
4216
|
+
FROM observations o
|
|
4217
|
+
LEFT JOIN observation_embeddings e ON e.observation_id = o.id
|
|
4218
|
+
WHERE e.observation_id IS NULL
|
|
4219
|
+
ORDER BY o.created_at_epoch DESC
|
|
4220
|
+
LIMIT ?
|
|
4221
|
+
`).all(batchSize);
|
|
4222
|
+
if (rows.length === 0) return 0;
|
|
4223
|
+
let count = 0;
|
|
4224
|
+
const model = embeddingService2.getProvider() || "unknown";
|
|
4225
|
+
for (const row of rows) {
|
|
4226
|
+
const parts = [row.title];
|
|
4227
|
+
if (row.text) parts.push(row.text);
|
|
4228
|
+
if (row.narrative) parts.push(row.narrative);
|
|
4229
|
+
if (row.concepts) parts.push(row.concepts);
|
|
4230
|
+
const fullText = parts.join(" ").substring(0, 2e3);
|
|
4231
|
+
const embedding = await embeddingService2.embed(fullText);
|
|
4232
|
+
if (embedding) {
|
|
4233
|
+
await this.storeEmbedding(db2, row.id, embedding, model);
|
|
4234
|
+
count++;
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
logger.info("VECTOR", `Backfill completato: ${count}/${rows.length} embeddings generati`);
|
|
4238
|
+
return count;
|
|
3635
4239
|
}
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
4240
|
+
/**
|
|
4241
|
+
* Statistiche sugli embeddings.
|
|
4242
|
+
*/
|
|
4243
|
+
getStats(db2) {
|
|
4244
|
+
try {
|
|
4245
|
+
const totalRow = db2.query("SELECT COUNT(*) as count FROM observations").get();
|
|
4246
|
+
const embeddedRow = db2.query("SELECT COUNT(*) as count FROM observation_embeddings").get();
|
|
4247
|
+
const total = totalRow?.count || 0;
|
|
4248
|
+
const embedded = embeddedRow?.count || 0;
|
|
4249
|
+
const percentage = total > 0 ? Math.round(embedded / total * 100) : 0;
|
|
4250
|
+
return { total, embedded, percentage };
|
|
4251
|
+
} catch {
|
|
4252
|
+
return { total: 0, embedded: 0, percentage: 0 };
|
|
4253
|
+
}
|
|
3639
4254
|
}
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
4255
|
+
};
|
|
4256
|
+
var vectorSearch = null;
|
|
4257
|
+
function getVectorSearch() {
|
|
4258
|
+
if (!vectorSearch) {
|
|
4259
|
+
vectorSearch = new VectorSearch();
|
|
4260
|
+
}
|
|
4261
|
+
return vectorSearch;
|
|
3644
4262
|
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4263
|
+
|
|
4264
|
+
// src/services/search/ScoringEngine.ts
|
|
4265
|
+
var SEARCH_WEIGHTS = {
|
|
4266
|
+
semantic: 0.4,
|
|
4267
|
+
fts5: 0.3,
|
|
4268
|
+
recency: 0.2,
|
|
4269
|
+
projectMatch: 0.1
|
|
4270
|
+
};
|
|
4271
|
+
function recencyScore(createdAtEpoch, halfLifeHours = 168) {
|
|
4272
|
+
if (!createdAtEpoch || createdAtEpoch <= 0) return 0;
|
|
4273
|
+
const nowMs = Date.now();
|
|
4274
|
+
const ageMs = nowMs - createdAtEpoch;
|
|
4275
|
+
if (ageMs <= 0) return 1;
|
|
4276
|
+
const ageHours = ageMs / (1e3 * 60 * 60);
|
|
4277
|
+
return Math.exp(-ageHours * Math.LN2 / halfLifeHours);
|
|
4278
|
+
}
|
|
4279
|
+
function normalizeFTS5Rank(rank, allRanks) {
|
|
4280
|
+
if (allRanks.length === 0) return 0;
|
|
4281
|
+
if (allRanks.length === 1) return 1;
|
|
4282
|
+
const minRank = Math.min(...allRanks);
|
|
4283
|
+
const maxRank = Math.max(...allRanks);
|
|
4284
|
+
if (minRank === maxRank) return 1;
|
|
4285
|
+
return (maxRank - rank) / (maxRank - minRank);
|
|
4286
|
+
}
|
|
4287
|
+
function projectMatchScore(itemProject, targetProject) {
|
|
4288
|
+
if (!itemProject || !targetProject) return 0;
|
|
4289
|
+
return itemProject.toLowerCase() === targetProject.toLowerCase() ? 1 : 0;
|
|
4290
|
+
}
|
|
4291
|
+
function computeCompositeScore(signals, weights) {
|
|
4292
|
+
return signals.semantic * weights.semantic + signals.fts5 * weights.fts5 + signals.recency * weights.recency + signals.projectMatch * weights.projectMatch;
|
|
4293
|
+
}
|
|
4294
|
+
var KNOWLEDGE_TYPE_BOOST = {
|
|
4295
|
+
constraint: 1.3,
|
|
4296
|
+
decision: 1.25,
|
|
4297
|
+
heuristic: 1.15,
|
|
4298
|
+
rejected: 1.1
|
|
4299
|
+
};
|
|
4300
|
+
function knowledgeTypeBoost(type) {
|
|
4301
|
+
return KNOWLEDGE_TYPE_BOOST[type] ?? 1;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
// src/services/search/HybridSearch.ts
|
|
4305
|
+
var HybridSearch = class {
|
|
4306
|
+
embeddingInitialized = false;
|
|
4307
|
+
/**
|
|
4308
|
+
* Inizializza il servizio di embedding (lazy, non bloccante)
|
|
4309
|
+
*/
|
|
4310
|
+
async initialize() {
|
|
4311
|
+
try {
|
|
4312
|
+
const embeddingService2 = getEmbeddingService();
|
|
4313
|
+
await embeddingService2.initialize();
|
|
4314
|
+
this.embeddingInitialized = embeddingService2.isAvailable();
|
|
4315
|
+
logger.info("SEARCH", `HybridSearch inizializzato (embedding: ${this.embeddingInitialized ? "attivo" : "disattivato"})`);
|
|
4316
|
+
} catch (error) {
|
|
4317
|
+
logger.warn("SEARCH", "Inizializzazione embedding fallita, uso solo FTS5", {}, error);
|
|
4318
|
+
this.embeddingInitialized = false;
|
|
4319
|
+
}
|
|
3656
4320
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
4321
|
+
/**
|
|
4322
|
+
* Ricerca ibrida con scoring a 4 segnali
|
|
4323
|
+
*/
|
|
4324
|
+
async search(db2, query, options = {}) {
|
|
4325
|
+
const limit = options.limit || 10;
|
|
4326
|
+
const weights = options.weights || SEARCH_WEIGHTS;
|
|
4327
|
+
const targetProject = options.project || "";
|
|
4328
|
+
const rawItems = /* @__PURE__ */ new Map();
|
|
4329
|
+
if (this.embeddingInitialized) {
|
|
4330
|
+
try {
|
|
4331
|
+
const embeddingService2 = getEmbeddingService();
|
|
4332
|
+
const queryEmbedding = await embeddingService2.embed(query);
|
|
4333
|
+
if (queryEmbedding) {
|
|
4334
|
+
const vectorSearch2 = getVectorSearch();
|
|
4335
|
+
const vectorResults = await vectorSearch2.search(db2, queryEmbedding, {
|
|
4336
|
+
project: options.project,
|
|
4337
|
+
limit: limit * 2,
|
|
4338
|
+
// Prendiamo piu risultati per il ranking
|
|
4339
|
+
threshold: 0.3
|
|
4340
|
+
});
|
|
4341
|
+
for (const hit of vectorResults) {
|
|
4342
|
+
rawItems.set(String(hit.observationId), {
|
|
4343
|
+
id: String(hit.observationId),
|
|
4344
|
+
title: hit.title,
|
|
4345
|
+
content: hit.text || "",
|
|
4346
|
+
type: hit.type,
|
|
4347
|
+
project: hit.project,
|
|
4348
|
+
created_at: hit.created_at,
|
|
4349
|
+
created_at_epoch: hit.created_at_epoch,
|
|
4350
|
+
semanticScore: hit.similarity,
|
|
4351
|
+
fts5Rank: null,
|
|
4352
|
+
source: "vector"
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
4355
|
+
logger.debug("SEARCH", `Vector search: ${vectorResults.length} risultati`);
|
|
4356
|
+
}
|
|
4357
|
+
} catch (error) {
|
|
4358
|
+
logger.warn("SEARCH", "Ricerca vettoriale fallita, uso solo keyword", {}, error);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
try {
|
|
4362
|
+
const { searchObservationsFTSWithRank: searchObservationsFTSWithRank2 } = await Promise.resolve().then(() => (init_Search(), Search_exports));
|
|
4363
|
+
const keywordResults = searchObservationsFTSWithRank2(db2, query, {
|
|
4364
|
+
project: options.project,
|
|
4365
|
+
limit: limit * 2
|
|
4366
|
+
});
|
|
4367
|
+
for (const obs of keywordResults) {
|
|
4368
|
+
const id = String(obs.id);
|
|
4369
|
+
const existing = rawItems.get(id);
|
|
4370
|
+
if (existing) {
|
|
4371
|
+
existing.fts5Rank = obs.fts5_rank;
|
|
4372
|
+
existing.source = "vector";
|
|
4373
|
+
} else {
|
|
4374
|
+
rawItems.set(id, {
|
|
4375
|
+
id,
|
|
4376
|
+
title: obs.title,
|
|
4377
|
+
content: obs.text || obs.narrative || "",
|
|
4378
|
+
type: obs.type,
|
|
4379
|
+
project: obs.project,
|
|
4380
|
+
created_at: obs.created_at,
|
|
4381
|
+
created_at_epoch: obs.created_at_epoch,
|
|
4382
|
+
semanticScore: 0,
|
|
4383
|
+
fts5Rank: obs.fts5_rank,
|
|
4384
|
+
source: "keyword"
|
|
4385
|
+
});
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
logger.debug("SEARCH", `Keyword search: ${keywordResults.length} risultati`);
|
|
4389
|
+
} catch (error) {
|
|
4390
|
+
logger.error("SEARCH", "Ricerca keyword fallita", {}, error);
|
|
4391
|
+
}
|
|
4392
|
+
if (rawItems.size === 0) return [];
|
|
4393
|
+
const allFTS5Ranks = Array.from(rawItems.values()).filter((item) => item.fts5Rank !== null).map((item) => item.fts5Rank);
|
|
4394
|
+
const scored = [];
|
|
4395
|
+
for (const item of rawItems.values()) {
|
|
4396
|
+
const signals = {
|
|
4397
|
+
semantic: item.semanticScore,
|
|
4398
|
+
fts5: item.fts5Rank !== null ? normalizeFTS5Rank(item.fts5Rank, allFTS5Ranks) : 0,
|
|
4399
|
+
recency: recencyScore(item.created_at_epoch),
|
|
4400
|
+
projectMatch: targetProject ? projectMatchScore(item.project, targetProject) : 0
|
|
4401
|
+
};
|
|
4402
|
+
const score = computeCompositeScore(signals, weights);
|
|
4403
|
+
const isHybrid = item.semanticScore > 0 && item.fts5Rank !== null;
|
|
4404
|
+
const hybridBoost = isHybrid ? 1.15 : 1;
|
|
4405
|
+
const finalScore = Math.min(1, score * hybridBoost * knowledgeTypeBoost(item.type));
|
|
4406
|
+
scored.push({
|
|
4407
|
+
id: item.id,
|
|
4408
|
+
title: item.title,
|
|
4409
|
+
content: item.content,
|
|
4410
|
+
type: item.type,
|
|
4411
|
+
project: item.project,
|
|
4412
|
+
created_at: item.created_at,
|
|
4413
|
+
created_at_epoch: item.created_at_epoch,
|
|
4414
|
+
score: finalScore,
|
|
4415
|
+
source: isHybrid ? "hybrid" : item.source,
|
|
4416
|
+
signals
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
scored.sort((a, b) => b.score - a.score);
|
|
4420
|
+
const finalResults = scored.slice(0, limit);
|
|
4421
|
+
if (finalResults.length > 0) {
|
|
4422
|
+
try {
|
|
4423
|
+
const { updateLastAccessed: updateLastAccessed2 } = await Promise.resolve().then(() => (init_Observations(), Observations_exports));
|
|
4424
|
+
const ids = finalResults.map((r) => parseInt(r.id, 10)).filter((id) => id > 0);
|
|
4425
|
+
if (ids.length > 0) {
|
|
4426
|
+
updateLastAccessed2(db2, ids);
|
|
4427
|
+
}
|
|
4428
|
+
} catch {
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
return finalResults;
|
|
3660
4432
|
}
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
4433
|
+
};
|
|
4434
|
+
var hybridSearch = null;
|
|
4435
|
+
function getHybridSearch() {
|
|
4436
|
+
if (!hybridSearch) {
|
|
4437
|
+
hybridSearch = new HybridSearch();
|
|
3664
4438
|
}
|
|
3665
|
-
|
|
3666
|
-
|
|
4439
|
+
return hybridSearch;
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
// src/services/sqlite/Analytics.ts
|
|
4443
|
+
function getObservationsTimeline(db2, project, days = 30) {
|
|
4444
|
+
const cutoffEpoch = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
4445
|
+
const sql = project ? `SELECT DATE(datetime(created_at_epoch / 1000, 'unixepoch')) as day, COUNT(*) as count
|
|
4446
|
+
FROM observations
|
|
4447
|
+
WHERE project = ? AND created_at_epoch >= ?
|
|
4448
|
+
GROUP BY day
|
|
4449
|
+
ORDER BY day ASC` : `SELECT DATE(datetime(created_at_epoch / 1000, 'unixepoch')) as day, COUNT(*) as count
|
|
4450
|
+
FROM observations
|
|
4451
|
+
WHERE created_at_epoch >= ?
|
|
4452
|
+
GROUP BY day
|
|
4453
|
+
ORDER BY day ASC`;
|
|
3667
4454
|
const stmt = db2.query(sql);
|
|
3668
|
-
|
|
4455
|
+
const rows = project ? stmt.all(project, cutoffEpoch) : stmt.all(cutoffEpoch);
|
|
4456
|
+
return rows;
|
|
3669
4457
|
}
|
|
3670
|
-
function
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
4458
|
+
function getTypeDistribution(db2, project) {
|
|
4459
|
+
const sql = project ? `SELECT type, COUNT(*) as count
|
|
4460
|
+
FROM observations
|
|
4461
|
+
WHERE project = ?
|
|
4462
|
+
GROUP BY type
|
|
4463
|
+
ORDER BY count DESC` : `SELECT type, COUNT(*) as count
|
|
4464
|
+
FROM observations
|
|
4465
|
+
GROUP BY type
|
|
4466
|
+
ORDER BY count DESC`;
|
|
3674
4467
|
const stmt = db2.query(sql);
|
|
3675
|
-
|
|
4468
|
+
const rows = project ? stmt.all(project) : stmt.all();
|
|
4469
|
+
return rows;
|
|
3676
4470
|
}
|
|
3677
|
-
function
|
|
3678
|
-
const
|
|
3679
|
-
const
|
|
3680
|
-
|
|
3681
|
-
const
|
|
3682
|
-
const
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
const
|
|
3690
|
-
const
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
`);
|
|
3694
|
-
const self = selfStmt.all(anchorId);
|
|
3695
|
-
const afterStmt = db2.query(`
|
|
3696
|
-
SELECT id, 'observation' as type, title, text as content, project, created_at, created_at_epoch
|
|
3697
|
-
FROM observations
|
|
3698
|
-
WHERE created_at_epoch > ?
|
|
3699
|
-
ORDER BY created_at_epoch ASC
|
|
3700
|
-
LIMIT ?
|
|
3701
|
-
`);
|
|
3702
|
-
const after = afterStmt.all(anchorEpoch, depthAfter);
|
|
3703
|
-
return [...before, ...self, ...after];
|
|
4471
|
+
function getSessionStats(db2, project) {
|
|
4472
|
+
const totalSql = project ? "SELECT COUNT(*) as count FROM sessions WHERE project = ?" : "SELECT COUNT(*) as count FROM sessions";
|
|
4473
|
+
const totalStmt = db2.query(totalSql);
|
|
4474
|
+
const total = project ? totalStmt.get(project)?.count || 0 : totalStmt.get()?.count || 0;
|
|
4475
|
+
const completedSql = project ? `SELECT COUNT(*) as count FROM sessions WHERE project = ? AND status = 'completed'` : `SELECT COUNT(*) as count FROM sessions WHERE status = 'completed'`;
|
|
4476
|
+
const completedStmt = db2.query(completedSql);
|
|
4477
|
+
const completed = project ? completedStmt.get(project)?.count || 0 : completedStmt.get()?.count || 0;
|
|
4478
|
+
const avgSql = project ? `SELECT AVG((completed_at_epoch - started_at_epoch) / 1000.0 / 60.0) as avg_min
|
|
4479
|
+
FROM sessions
|
|
4480
|
+
WHERE project = ? AND status = 'completed' AND completed_at_epoch IS NOT NULL AND completed_at_epoch > started_at_epoch` : `SELECT AVG((completed_at_epoch - started_at_epoch) / 1000.0 / 60.0) as avg_min
|
|
4481
|
+
FROM sessions
|
|
4482
|
+
WHERE status = 'completed' AND completed_at_epoch IS NOT NULL AND completed_at_epoch > started_at_epoch`;
|
|
4483
|
+
const avgStmt = db2.query(avgSql);
|
|
4484
|
+
const avgRow = project ? avgStmt.get(project) : avgStmt.get();
|
|
4485
|
+
const avgDurationMinutes = Math.round((avgRow?.avg_min || 0) * 10) / 10;
|
|
4486
|
+
return { total, completed, avgDurationMinutes };
|
|
3704
4487
|
}
|
|
3705
|
-
function
|
|
3706
|
-
const
|
|
3707
|
-
const
|
|
3708
|
-
const
|
|
3709
|
-
const
|
|
4488
|
+
function getAnalyticsOverview(db2, project) {
|
|
4489
|
+
const now = Date.now();
|
|
4490
|
+
const todayStart = now - now % (24 * 60 * 60 * 1e3);
|
|
4491
|
+
const weekStart = now - 7 * 24 * 60 * 60 * 1e3;
|
|
4492
|
+
const countQuery = (table) => {
|
|
4493
|
+
const sql = project ? `SELECT COUNT(*) as count FROM ${table} WHERE project = ?` : `SELECT COUNT(*) as count FROM ${table}`;
|
|
4494
|
+
const stmt = db2.query(sql);
|
|
4495
|
+
return project ? stmt.get(project)?.count || 0 : stmt.get()?.count || 0;
|
|
4496
|
+
};
|
|
4497
|
+
const observations = countQuery("observations");
|
|
4498
|
+
const summaries = countQuery("summaries");
|
|
4499
|
+
const sessions = countQuery("sessions");
|
|
4500
|
+
const prompts = countQuery("prompts");
|
|
4501
|
+
const todaySql = project ? "SELECT COUNT(*) as count FROM observations WHERE project = ? AND created_at_epoch >= ?" : "SELECT COUNT(*) as count FROM observations WHERE created_at_epoch >= ?";
|
|
4502
|
+
const todayStmt = db2.query(todaySql);
|
|
4503
|
+
const observationsToday = project ? todayStmt.get(project, todayStart)?.count || 0 : todayStmt.get(todayStart)?.count || 0;
|
|
4504
|
+
const weekSql = project ? "SELECT COUNT(*) as count FROM observations WHERE project = ? AND created_at_epoch >= ?" : "SELECT COUNT(*) as count FROM observations WHERE created_at_epoch >= ?";
|
|
4505
|
+
const weekStmt = db2.query(weekSql);
|
|
4506
|
+
const observationsThisWeek = project ? weekStmt.get(project, weekStart)?.count || 0 : weekStmt.get(weekStart)?.count || 0;
|
|
4507
|
+
const staleSql = project ? "SELECT COUNT(*) as count FROM observations WHERE project = ? AND is_stale = 1" : "SELECT COUNT(*) as count FROM observations WHERE is_stale = 1";
|
|
4508
|
+
const staleStmt = db2.query(staleSql);
|
|
4509
|
+
const staleCount = project ? staleStmt.get(project)?.count || 0 : staleStmt.get()?.count || 0;
|
|
4510
|
+
const knowledgeSql = project ? `SELECT COUNT(*) as count FROM observations WHERE project = ? AND type IN ('constraint', 'decision', 'heuristic', 'rejected')` : `SELECT COUNT(*) as count FROM observations WHERE type IN ('constraint', 'decision', 'heuristic', 'rejected')`;
|
|
4511
|
+
const knowledgeStmt = db2.query(knowledgeSql);
|
|
4512
|
+
const knowledgeCount = project ? knowledgeStmt.get(project)?.count || 0 : knowledgeStmt.get()?.count || 0;
|
|
3710
4513
|
return {
|
|
3711
|
-
observations
|
|
3712
|
-
summaries
|
|
3713
|
-
sessions
|
|
3714
|
-
prompts
|
|
4514
|
+
observations,
|
|
4515
|
+
summaries,
|
|
4516
|
+
sessions,
|
|
4517
|
+
prompts,
|
|
4518
|
+
observationsToday,
|
|
4519
|
+
observationsThisWeek,
|
|
4520
|
+
staleCount,
|
|
4521
|
+
knowledgeCount
|
|
4522
|
+
};
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
// src/services/sqlite/Checkpoints.ts
|
|
4526
|
+
function getLatestCheckpoint(db2, sessionId) {
|
|
4527
|
+
const query = db2.query(
|
|
4528
|
+
"SELECT * FROM checkpoints WHERE session_id = ? ORDER BY created_at_epoch DESC LIMIT 1"
|
|
4529
|
+
);
|
|
4530
|
+
return query.get(sessionId);
|
|
4531
|
+
}
|
|
4532
|
+
function getLatestCheckpointByProject(db2, project) {
|
|
4533
|
+
const query = db2.query(
|
|
4534
|
+
"SELECT * FROM checkpoints WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 1"
|
|
4535
|
+
);
|
|
4536
|
+
return query.get(project);
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
// src/services/sqlite/Sessions.ts
|
|
4540
|
+
function getActiveSessions(db2) {
|
|
4541
|
+
const query = db2.query("SELECT * FROM sessions WHERE status = 'active' ORDER BY started_at_epoch DESC");
|
|
4542
|
+
return query.all();
|
|
4543
|
+
}
|
|
4544
|
+
function getSessionsByProject(db2, project, limit = 100) {
|
|
4545
|
+
const query = db2.query("SELECT * FROM sessions WHERE project = ? ORDER BY started_at_epoch DESC LIMIT ?");
|
|
4546
|
+
return query.all(project, limit);
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
// src/services/sqlite/Reports.ts
|
|
4550
|
+
function getReportData(db2, project, startEpoch, endEpoch) {
|
|
4551
|
+
const startDate = new Date(startEpoch);
|
|
4552
|
+
const endDate = new Date(endEpoch);
|
|
4553
|
+
const days = Math.ceil((endEpoch - startEpoch) / (24 * 60 * 60 * 1e3));
|
|
4554
|
+
const label = days <= 7 ? "Weekly" : days <= 31 ? "Monthly" : "Custom";
|
|
4555
|
+
const countInRange = (table, epochCol = "created_at_epoch") => {
|
|
4556
|
+
const sql = project ? `SELECT COUNT(*) as count FROM ${table} WHERE project = ? AND ${epochCol} >= ? AND ${epochCol} <= ?` : `SELECT COUNT(*) as count FROM ${table} WHERE ${epochCol} >= ? AND ${epochCol} <= ?`;
|
|
4557
|
+
const stmt = db2.query(sql);
|
|
4558
|
+
const row = project ? stmt.get(project, startEpoch, endEpoch) : stmt.get(startEpoch, endEpoch);
|
|
4559
|
+
return row?.count || 0;
|
|
4560
|
+
};
|
|
4561
|
+
const observations = countInRange("observations");
|
|
4562
|
+
const summaries = countInRange("summaries");
|
|
4563
|
+
const prompts = countInRange("prompts");
|
|
4564
|
+
const sessions = countInRange("sessions", "started_at_epoch");
|
|
4565
|
+
const timelineSql = project ? `SELECT DATE(datetime(created_at_epoch / 1000, 'unixepoch')) as day, COUNT(*) as count
|
|
4566
|
+
FROM observations
|
|
4567
|
+
WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4568
|
+
GROUP BY day ORDER BY day ASC` : `SELECT DATE(datetime(created_at_epoch / 1000, 'unixepoch')) as day, COUNT(*) as count
|
|
4569
|
+
FROM observations
|
|
4570
|
+
WHERE created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4571
|
+
GROUP BY day ORDER BY day ASC`;
|
|
4572
|
+
const timelineStmt = db2.query(timelineSql);
|
|
4573
|
+
const timeline = project ? timelineStmt.all(project, startEpoch, endEpoch) : timelineStmt.all(startEpoch, endEpoch);
|
|
4574
|
+
const typeSql = project ? `SELECT type, COUNT(*) as count FROM observations
|
|
4575
|
+
WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4576
|
+
GROUP BY type ORDER BY count DESC` : `SELECT type, COUNT(*) as count FROM observations
|
|
4577
|
+
WHERE created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4578
|
+
GROUP BY type ORDER BY count DESC`;
|
|
4579
|
+
const typeStmt = db2.query(typeSql);
|
|
4580
|
+
const typeDistribution = project ? typeStmt.all(project, startEpoch, endEpoch) : typeStmt.all(startEpoch, endEpoch);
|
|
4581
|
+
const sessionTotalSql = project ? `SELECT COUNT(*) as count FROM sessions WHERE project = ? AND started_at_epoch >= ? AND started_at_epoch <= ?` : `SELECT COUNT(*) as count FROM sessions WHERE started_at_epoch >= ? AND started_at_epoch <= ?`;
|
|
4582
|
+
const sessionTotal = (project ? db2.query(sessionTotalSql).get(project, startEpoch, endEpoch)?.count : db2.query(sessionTotalSql).get(startEpoch, endEpoch)?.count) || 0;
|
|
4583
|
+
const sessionCompletedSql = project ? `SELECT COUNT(*) as count FROM sessions WHERE project = ? AND started_at_epoch >= ? AND started_at_epoch <= ? AND status = 'completed'` : `SELECT COUNT(*) as count FROM sessions WHERE started_at_epoch >= ? AND started_at_epoch <= ? AND status = 'completed'`;
|
|
4584
|
+
const sessionCompleted = (project ? db2.query(sessionCompletedSql).get(project, startEpoch, endEpoch)?.count : db2.query(sessionCompletedSql).get(startEpoch, endEpoch)?.count) || 0;
|
|
4585
|
+
const sessionAvgSql = project ? `SELECT AVG((completed_at_epoch - started_at_epoch) / 1000.0 / 60.0) as avg_min
|
|
4586
|
+
FROM sessions
|
|
4587
|
+
WHERE project = ? AND started_at_epoch >= ? AND started_at_epoch <= ?
|
|
4588
|
+
AND status = 'completed' AND completed_at_epoch IS NOT NULL AND completed_at_epoch > started_at_epoch` : `SELECT AVG((completed_at_epoch - started_at_epoch) / 1000.0 / 60.0) as avg_min
|
|
4589
|
+
FROM sessions
|
|
4590
|
+
WHERE started_at_epoch >= ? AND started_at_epoch <= ?
|
|
4591
|
+
AND status = 'completed' AND completed_at_epoch IS NOT NULL AND completed_at_epoch > started_at_epoch`;
|
|
4592
|
+
const avgRow = project ? db2.query(sessionAvgSql).get(project, startEpoch, endEpoch) : db2.query(sessionAvgSql).get(startEpoch, endEpoch);
|
|
4593
|
+
const avgDurationMinutes = Math.round((avgRow?.avg_min || 0) * 10) / 10;
|
|
4594
|
+
const knowledgeSql = project ? `SELECT COUNT(*) as count FROM observations
|
|
4595
|
+
WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4596
|
+
AND type IN ('constraint', 'decision', 'heuristic', 'rejected')` : `SELECT COUNT(*) as count FROM observations
|
|
4597
|
+
WHERE created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4598
|
+
AND type IN ('constraint', 'decision', 'heuristic', 'rejected')`;
|
|
4599
|
+
const knowledgeCount = (project ? db2.query(knowledgeSql).get(project, startEpoch, endEpoch)?.count : db2.query(knowledgeSql).get(startEpoch, endEpoch)?.count) || 0;
|
|
4600
|
+
const staleSql = project ? `SELECT COUNT(*) as count FROM observations
|
|
4601
|
+
WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ? AND is_stale = 1` : `SELECT COUNT(*) as count FROM observations
|
|
4602
|
+
WHERE created_at_epoch >= ? AND created_at_epoch <= ? AND is_stale = 1`;
|
|
4603
|
+
const staleCount = (project ? db2.query(staleSql).get(project, startEpoch, endEpoch)?.count : db2.query(staleSql).get(startEpoch, endEpoch)?.count) || 0;
|
|
4604
|
+
const summarySql = project ? `SELECT learned, completed, next_steps FROM summaries
|
|
4605
|
+
WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4606
|
+
ORDER BY created_at_epoch DESC` : `SELECT learned, completed, next_steps FROM summaries
|
|
4607
|
+
WHERE created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4608
|
+
ORDER BY created_at_epoch DESC`;
|
|
4609
|
+
const summaryRows = project ? db2.query(summarySql).all(project, startEpoch, endEpoch) : db2.query(summarySql).all(startEpoch, endEpoch);
|
|
4610
|
+
const topLearnings = [];
|
|
4611
|
+
const completedTasks = [];
|
|
4612
|
+
const nextStepsArr = [];
|
|
4613
|
+
for (const row of summaryRows) {
|
|
4614
|
+
if (row.learned) {
|
|
4615
|
+
const parts = row.learned.split("; ").filter(Boolean);
|
|
4616
|
+
topLearnings.push(...parts);
|
|
4617
|
+
}
|
|
4618
|
+
if (row.completed) {
|
|
4619
|
+
const parts = row.completed.split("; ").filter(Boolean);
|
|
4620
|
+
completedTasks.push(...parts);
|
|
4621
|
+
}
|
|
4622
|
+
if (row.next_steps) {
|
|
4623
|
+
const parts = row.next_steps.split("; ").filter(Boolean);
|
|
4624
|
+
nextStepsArr.push(...parts);
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
const filesSql = project ? `SELECT files_modified FROM observations
|
|
4628
|
+
WHERE project = ? AND created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4629
|
+
AND files_modified IS NOT NULL AND files_modified != ''` : `SELECT files_modified FROM observations
|
|
4630
|
+
WHERE created_at_epoch >= ? AND created_at_epoch <= ?
|
|
4631
|
+
AND files_modified IS NOT NULL AND files_modified != ''`;
|
|
4632
|
+
const fileRows = project ? db2.query(filesSql).all(project, startEpoch, endEpoch) : db2.query(filesSql).all(startEpoch, endEpoch);
|
|
4633
|
+
const fileCounts = /* @__PURE__ */ new Map();
|
|
4634
|
+
for (const row of fileRows) {
|
|
4635
|
+
const files = row.files_modified.split(",").map((f) => f.trim()).filter(Boolean);
|
|
4636
|
+
for (const file of files) {
|
|
4637
|
+
fileCounts.set(file, (fileCounts.get(file) || 0) + 1);
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
const fileHotspots = Array.from(fileCounts.entries()).map(([file, count]) => ({ file, count })).sort((a, b) => b.count - a.count).slice(0, 15);
|
|
4641
|
+
return {
|
|
4642
|
+
period: {
|
|
4643
|
+
start: startDate.toISOString().split("T")[0],
|
|
4644
|
+
end: endDate.toISOString().split("T")[0],
|
|
4645
|
+
days,
|
|
4646
|
+
label
|
|
4647
|
+
},
|
|
4648
|
+
overview: {
|
|
4649
|
+
observations,
|
|
4650
|
+
summaries,
|
|
4651
|
+
sessions,
|
|
4652
|
+
prompts,
|
|
4653
|
+
knowledgeCount,
|
|
4654
|
+
staleCount
|
|
4655
|
+
},
|
|
4656
|
+
timeline,
|
|
4657
|
+
typeDistribution,
|
|
4658
|
+
sessionStats: {
|
|
4659
|
+
total: sessionTotal,
|
|
4660
|
+
completed: sessionCompleted,
|
|
4661
|
+
avgDurationMinutes
|
|
4662
|
+
},
|
|
4663
|
+
topLearnings: [...new Set(topLearnings)].slice(0, 10),
|
|
4664
|
+
completedTasks: [...new Set(completedTasks)].slice(0, 10),
|
|
4665
|
+
nextSteps: [...new Set(nextStepsArr)].slice(0, 10),
|
|
4666
|
+
fileHotspots
|
|
3715
4667
|
};
|
|
3716
4668
|
}
|
|
3717
4669
|
|
|
4670
|
+
// src/services/report-formatter.ts
|
|
4671
|
+
function formatReportMarkdown(data) {
|
|
4672
|
+
const lines = [];
|
|
4673
|
+
lines.push(`# Kiro Memory Report \u2014 ${data.period.label}`);
|
|
4674
|
+
lines.push("");
|
|
4675
|
+
lines.push(`**Period**: ${data.period.start} \u2192 ${data.period.end} (${data.period.days} days)`);
|
|
4676
|
+
lines.push("");
|
|
4677
|
+
lines.push("## Overview");
|
|
4678
|
+
lines.push("");
|
|
4679
|
+
lines.push("| Metric | Count |");
|
|
4680
|
+
lines.push("|--------|------:|");
|
|
4681
|
+
lines.push(`| Observations | ${data.overview.observations} |`);
|
|
4682
|
+
lines.push(`| Summaries | ${data.overview.summaries} |`);
|
|
4683
|
+
lines.push(`| Sessions | ${data.overview.sessions} |`);
|
|
4684
|
+
lines.push(`| Prompts | ${data.overview.prompts} |`);
|
|
4685
|
+
lines.push(`| Knowledge items | ${data.overview.knowledgeCount} |`);
|
|
4686
|
+
if (data.overview.staleCount > 0) {
|
|
4687
|
+
lines.push(`| Stale observations | ${data.overview.staleCount} |`);
|
|
4688
|
+
}
|
|
4689
|
+
lines.push("");
|
|
4690
|
+
if (data.sessionStats.total > 0) {
|
|
4691
|
+
const completionPct = Math.round(data.sessionStats.completed / data.sessionStats.total * 100);
|
|
4692
|
+
lines.push("## Sessions");
|
|
4693
|
+
lines.push("");
|
|
4694
|
+
lines.push(`- **Total**: ${data.sessionStats.total}`);
|
|
4695
|
+
lines.push(`- **Completed**: ${data.sessionStats.completed} (${completionPct}%)`);
|
|
4696
|
+
if (data.sessionStats.avgDurationMinutes > 0) {
|
|
4697
|
+
lines.push(`- **Avg duration**: ${data.sessionStats.avgDurationMinutes} min`);
|
|
4698
|
+
}
|
|
4699
|
+
lines.push("");
|
|
4700
|
+
}
|
|
4701
|
+
if (data.timeline.length > 0) {
|
|
4702
|
+
lines.push("## Activity Timeline");
|
|
4703
|
+
lines.push("");
|
|
4704
|
+
lines.push("| Date | Observations |");
|
|
4705
|
+
lines.push("|------|------------:|");
|
|
4706
|
+
for (const entry of data.timeline) {
|
|
4707
|
+
lines.push(`| ${entry.day} | ${entry.count} |`);
|
|
4708
|
+
}
|
|
4709
|
+
lines.push("");
|
|
4710
|
+
}
|
|
4711
|
+
if (data.typeDistribution.length > 0) {
|
|
4712
|
+
lines.push("## Observation Types");
|
|
4713
|
+
lines.push("");
|
|
4714
|
+
for (const entry of data.typeDistribution) {
|
|
4715
|
+
lines.push(`- **${entry.type}**: ${entry.count}`);
|
|
4716
|
+
}
|
|
4717
|
+
lines.push("");
|
|
4718
|
+
}
|
|
4719
|
+
if (data.topLearnings.length > 0) {
|
|
4720
|
+
lines.push("## Key Learnings");
|
|
4721
|
+
lines.push("");
|
|
4722
|
+
for (const learning of data.topLearnings) {
|
|
4723
|
+
lines.push(`- ${learning}`);
|
|
4724
|
+
}
|
|
4725
|
+
lines.push("");
|
|
4726
|
+
}
|
|
4727
|
+
if (data.completedTasks.length > 0) {
|
|
4728
|
+
lines.push("## Completed");
|
|
4729
|
+
lines.push("");
|
|
4730
|
+
for (const task of data.completedTasks) {
|
|
4731
|
+
lines.push(`- ${task}`);
|
|
4732
|
+
}
|
|
4733
|
+
lines.push("");
|
|
4734
|
+
}
|
|
4735
|
+
if (data.nextSteps.length > 0) {
|
|
4736
|
+
lines.push("## Next Steps");
|
|
4737
|
+
lines.push("");
|
|
4738
|
+
for (const step of data.nextSteps) {
|
|
4739
|
+
lines.push(`- ${step}`);
|
|
4740
|
+
}
|
|
4741
|
+
lines.push("");
|
|
4742
|
+
}
|
|
4743
|
+
if (data.fileHotspots.length > 0) {
|
|
4744
|
+
lines.push("## File Hotspots");
|
|
4745
|
+
lines.push("");
|
|
4746
|
+
lines.push("| File | Modifications |");
|
|
4747
|
+
lines.push("|------|-------------:|");
|
|
4748
|
+
for (const entry of data.fileHotspots.slice(0, 10)) {
|
|
4749
|
+
lines.push(`| \`${entry.file}\` | ${entry.count} |`);
|
|
4750
|
+
}
|
|
4751
|
+
lines.push("");
|
|
4752
|
+
}
|
|
4753
|
+
return lines.join("\n");
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
// src/types/worker-types.ts
|
|
4757
|
+
var KNOWLEDGE_TYPES = ["constraint", "decision", "heuristic", "rejected"];
|
|
4758
|
+
|
|
3718
4759
|
// src/services/worker-service.ts
|
|
3719
4760
|
var __worker_dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
3720
4761
|
var PORT = process.env.KIRO_MEMORY_WORKER_PORT || process.env.CONTEXTKIT_WORKER_PORT || 3001;
|
|
@@ -3722,17 +4763,23 @@ var HOST = process.env.KIRO_MEMORY_WORKER_HOST || process.env.CONTEXTKIT_WORKER_
|
|
|
3722
4763
|
var PID_FILE = join3(DATA_DIR, "worker.pid");
|
|
3723
4764
|
var TOKEN_FILE = join3(DATA_DIR, "worker.token");
|
|
3724
4765
|
var MAX_SSE_CLIENTS = 50;
|
|
3725
|
-
if (!
|
|
4766
|
+
if (!existsSync4(DATA_DIR)) {
|
|
3726
4767
|
mkdirSync3(DATA_DIR, { recursive: true });
|
|
3727
4768
|
}
|
|
3728
4769
|
var WORKER_TOKEN = crypto.randomBytes(32).toString("hex");
|
|
3729
4770
|
writeFileSync(TOKEN_FILE, WORKER_TOKEN, "utf-8");
|
|
3730
4771
|
try {
|
|
3731
4772
|
chmodSync(TOKEN_FILE, 384);
|
|
3732
|
-
} catch {
|
|
4773
|
+
} catch (err) {
|
|
4774
|
+
if (process.platform !== "win32") {
|
|
4775
|
+
logger.warn("WORKER", `chmod 600 fallito su ${TOKEN_FILE}`, {}, err);
|
|
4776
|
+
}
|
|
3733
4777
|
}
|
|
3734
4778
|
var db = new KiroMemoryDatabase();
|
|
3735
4779
|
logger.info("WORKER", "Database initialized");
|
|
4780
|
+
getHybridSearch().initialize().catch((err) => {
|
|
4781
|
+
logger.warn("WORKER", "Inizializzazione embedding fallita, ricerca solo FTS5", {}, err);
|
|
4782
|
+
});
|
|
3736
4783
|
function parseIntSafe(value, defaultVal, min, max) {
|
|
3737
4784
|
if (!value) return defaultVal;
|
|
3738
4785
|
const parsed = parseInt(value, 10);
|
|
@@ -3740,7 +4787,7 @@ function parseIntSafe(value, defaultVal, min, max) {
|
|
|
3740
4787
|
return parsed;
|
|
3741
4788
|
}
|
|
3742
4789
|
function isValidProject(project) {
|
|
3743
|
-
return typeof project === "string" && project.length > 0 && project.length <= 200 && /^[\w\-\.\/@ ]+$/.test(project);
|
|
4790
|
+
return typeof project === "string" && project.length > 0 && project.length <= 200 && /^[\w\-\.\/@ ]+$/.test(project) && !project.includes("..");
|
|
3744
4791
|
}
|
|
3745
4792
|
function isValidString(val, maxLen) {
|
|
3746
4793
|
return typeof val === "string" && val.length <= maxLen;
|
|
@@ -3750,11 +4797,11 @@ app.use(helmet({
|
|
|
3750
4797
|
contentSecurityPolicy: {
|
|
3751
4798
|
directives: {
|
|
3752
4799
|
defaultSrc: ["'self'"],
|
|
3753
|
-
scriptSrc: ["'self'", "'unsafe-inline'"],
|
|
3754
|
-
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.tailwindcss.com"],
|
|
4800
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://cdn.tailwindcss.com"],
|
|
4801
|
+
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.tailwindcss.com"],
|
|
3755
4802
|
imgSrc: ["'self'", "data:"],
|
|
3756
4803
|
connectSrc: ["'self'"],
|
|
3757
|
-
fontSrc: ["'self'", "https://fonts.
|
|
4804
|
+
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
|
3758
4805
|
frameSrc: ["'none'"]
|
|
3759
4806
|
}
|
|
3760
4807
|
}
|
|
@@ -3849,6 +4896,10 @@ data: ${JSON.stringify({ timestamp: Date.now() })}
|
|
|
3849
4896
|
});
|
|
3850
4897
|
app.get("/api/context/:project", (req, res) => {
|
|
3851
4898
|
const { project } = req.params;
|
|
4899
|
+
if (!isValidProject(project)) {
|
|
4900
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
3852
4903
|
try {
|
|
3853
4904
|
const context = {
|
|
3854
4905
|
project,
|
|
@@ -3900,12 +4951,95 @@ app.post("/api/observations", (req, res) => {
|
|
|
3900
4951
|
0
|
|
3901
4952
|
);
|
|
3902
4953
|
broadcast("observation-created", { id, project, title });
|
|
4954
|
+
generateEmbeddingForObservation(id, title, content, concepts).catch(() => {
|
|
4955
|
+
});
|
|
3903
4956
|
res.json({ id, success: true });
|
|
3904
4957
|
} catch (error) {
|
|
3905
4958
|
logger.error("WORKER", "Failed to store observation", {}, error);
|
|
3906
4959
|
res.status(500).json({ error: "Failed to store observation" });
|
|
3907
4960
|
}
|
|
3908
4961
|
});
|
|
4962
|
+
app.post("/api/knowledge", (req, res) => {
|
|
4963
|
+
const {
|
|
4964
|
+
project,
|
|
4965
|
+
knowledge_type,
|
|
4966
|
+
title,
|
|
4967
|
+
content,
|
|
4968
|
+
concepts,
|
|
4969
|
+
files,
|
|
4970
|
+
severity,
|
|
4971
|
+
alternatives,
|
|
4972
|
+
reason,
|
|
4973
|
+
context: metaContext,
|
|
4974
|
+
confidence
|
|
4975
|
+
} = req.body;
|
|
4976
|
+
if (!isValidProject(project)) {
|
|
4977
|
+
res.status(400).json({ error: 'Invalid or missing "project"' });
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
if (!knowledge_type || !KNOWLEDGE_TYPES.includes(knowledge_type)) {
|
|
4981
|
+
res.status(400).json({ error: `Invalid "knowledge_type". Must be one of: ${KNOWLEDGE_TYPES.join(", ")}` });
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
if (!isValidString(title, 500)) {
|
|
4985
|
+
res.status(400).json({ error: 'Invalid or missing "title" (max 500 chars)' });
|
|
4986
|
+
return;
|
|
4987
|
+
}
|
|
4988
|
+
if (!isValidString(content, 1e5)) {
|
|
4989
|
+
res.status(400).json({ error: 'Invalid or missing "content" (max 100KB)' });
|
|
4990
|
+
return;
|
|
4991
|
+
}
|
|
4992
|
+
if (concepts && !Array.isArray(concepts)) {
|
|
4993
|
+
res.status(400).json({ error: '"concepts" must be an array' });
|
|
4994
|
+
return;
|
|
4995
|
+
}
|
|
4996
|
+
if (files && !Array.isArray(files)) {
|
|
4997
|
+
res.status(400).json({ error: '"files" must be an array' });
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
try {
|
|
5001
|
+
let metadata;
|
|
5002
|
+
switch (knowledge_type) {
|
|
5003
|
+
case "constraint":
|
|
5004
|
+
metadata = { knowledgeType: "constraint", severity: severity === "hard" ? "hard" : "soft", reason };
|
|
5005
|
+
break;
|
|
5006
|
+
case "decision":
|
|
5007
|
+
metadata = { knowledgeType: "decision", alternatives, reason };
|
|
5008
|
+
break;
|
|
5009
|
+
case "heuristic":
|
|
5010
|
+
metadata = { knowledgeType: "heuristic", context: metaContext, confidence: ["high", "medium", "low"].includes(confidence) ? confidence : void 0 };
|
|
5011
|
+
break;
|
|
5012
|
+
case "rejected":
|
|
5013
|
+
metadata = { knowledgeType: "rejected", reason: reason || "", alternatives };
|
|
5014
|
+
break;
|
|
5015
|
+
default:
|
|
5016
|
+
res.status(400).json({ error: "Invalid knowledge_type" });
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
const id = createObservation(
|
|
5020
|
+
db.db,
|
|
5021
|
+
"api-" + Date.now(),
|
|
5022
|
+
project,
|
|
5023
|
+
knowledge_type,
|
|
5024
|
+
title,
|
|
5025
|
+
null,
|
|
5026
|
+
content,
|
|
5027
|
+
null,
|
|
5028
|
+
JSON.stringify(metadata),
|
|
5029
|
+
concepts?.join(", ") || null,
|
|
5030
|
+
files?.join(", ") || null,
|
|
5031
|
+
null,
|
|
5032
|
+
0
|
|
5033
|
+
);
|
|
5034
|
+
broadcast("observation-created", { id, project, title, type: knowledge_type });
|
|
5035
|
+
generateEmbeddingForObservation(id, title, content, concepts).catch(() => {
|
|
5036
|
+
});
|
|
5037
|
+
res.json({ id, success: true, knowledge_type });
|
|
5038
|
+
} catch (error) {
|
|
5039
|
+
logger.error("WORKER", "Failed to store knowledge", {}, error);
|
|
5040
|
+
res.status(500).json({ error: "Failed to store knowledge" });
|
|
5041
|
+
}
|
|
5042
|
+
});
|
|
3909
5043
|
app.post("/api/summaries", (req, res) => {
|
|
3910
5044
|
const { sessionId, project, request, learned, completed, nextSteps } = req.body;
|
|
3911
5045
|
if (!isValidProject(project)) {
|
|
@@ -4014,6 +5148,10 @@ app.post("/api/observations/batch", (req, res) => {
|
|
|
4014
5148
|
});
|
|
4015
5149
|
app.get("/api/stats/:project", (req, res) => {
|
|
4016
5150
|
const { project } = req.params;
|
|
5151
|
+
if (!isValidProject(project)) {
|
|
5152
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
5153
|
+
return;
|
|
5154
|
+
}
|
|
4017
5155
|
try {
|
|
4018
5156
|
const stats = getProjectStats(db.db, project);
|
|
4019
5157
|
res.json(stats);
|
|
@@ -4022,6 +5160,76 @@ app.get("/api/stats/:project", (req, res) => {
|
|
|
4022
5160
|
res.status(500).json({ error: "Stats failed" });
|
|
4023
5161
|
}
|
|
4024
5162
|
});
|
|
5163
|
+
async function generateEmbeddingForObservation(observationId, title, content, concepts) {
|
|
5164
|
+
try {
|
|
5165
|
+
const embeddingService2 = getEmbeddingService();
|
|
5166
|
+
if (!embeddingService2.isAvailable()) return;
|
|
5167
|
+
const parts = [title];
|
|
5168
|
+
if (content) parts.push(content);
|
|
5169
|
+
if (concepts?.length) parts.push(concepts.join(", "));
|
|
5170
|
+
const fullText = parts.join(" ").substring(0, 2e3);
|
|
5171
|
+
const embedding = await embeddingService2.embed(fullText);
|
|
5172
|
+
if (embedding) {
|
|
5173
|
+
const vectorSearch2 = getVectorSearch();
|
|
5174
|
+
await vectorSearch2.storeEmbedding(
|
|
5175
|
+
db.db,
|
|
5176
|
+
observationId,
|
|
5177
|
+
embedding,
|
|
5178
|
+
embeddingService2.getProvider() || "unknown"
|
|
5179
|
+
);
|
|
5180
|
+
}
|
|
5181
|
+
} catch (error) {
|
|
5182
|
+
logger.debug("WORKER", `Embedding generation fallita per obs ${observationId}: ${error}`);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
app.get("/api/hybrid-search", async (req, res) => {
|
|
5186
|
+
const { q, project, limit } = req.query;
|
|
5187
|
+
if (!q) {
|
|
5188
|
+
res.status(400).json({ error: 'Query parameter "q" is required' });
|
|
5189
|
+
return;
|
|
5190
|
+
}
|
|
5191
|
+
try {
|
|
5192
|
+
const hybridSearch2 = getHybridSearch();
|
|
5193
|
+
const results = await hybridSearch2.search(db.db, q, {
|
|
5194
|
+
project: project || void 0,
|
|
5195
|
+
limit: parseIntSafe(limit, 10, 1, 100)
|
|
5196
|
+
});
|
|
5197
|
+
res.json({ results, count: results.length });
|
|
5198
|
+
} catch (error) {
|
|
5199
|
+
logger.error("WORKER", "Ricerca ibrida fallita", { query: q }, error);
|
|
5200
|
+
res.status(500).json({ error: "Hybrid search failed" });
|
|
5201
|
+
}
|
|
5202
|
+
});
|
|
5203
|
+
app.post("/api/embeddings/backfill", async (req, res) => {
|
|
5204
|
+
const { batchSize } = req.body || {};
|
|
5205
|
+
try {
|
|
5206
|
+
const vectorSearch2 = getVectorSearch();
|
|
5207
|
+
const count = await vectorSearch2.backfillEmbeddings(
|
|
5208
|
+
db.db,
|
|
5209
|
+
parseIntSafe(String(batchSize), 50, 1, 500)
|
|
5210
|
+
);
|
|
5211
|
+
res.json({ success: true, generated: count });
|
|
5212
|
+
} catch (error) {
|
|
5213
|
+
logger.error("WORKER", "Backfill embeddings fallito", {}, error);
|
|
5214
|
+
res.status(500).json({ error: "Backfill failed" });
|
|
5215
|
+
}
|
|
5216
|
+
});
|
|
5217
|
+
app.get("/api/embeddings/stats", (_req, res) => {
|
|
5218
|
+
try {
|
|
5219
|
+
const vectorSearch2 = getVectorSearch();
|
|
5220
|
+
const stats = vectorSearch2.getStats(db.db);
|
|
5221
|
+
const embeddingService2 = getEmbeddingService();
|
|
5222
|
+
res.json({
|
|
5223
|
+
...stats,
|
|
5224
|
+
provider: embeddingService2.getProvider(),
|
|
5225
|
+
dimensions: embeddingService2.getDimensions(),
|
|
5226
|
+
available: embeddingService2.isAvailable()
|
|
5227
|
+
});
|
|
5228
|
+
} catch (error) {
|
|
5229
|
+
logger.error("WORKER", "Embedding stats fallite", {}, error);
|
|
5230
|
+
res.status(500).json({ error: "Stats failed" });
|
|
5231
|
+
}
|
|
5232
|
+
});
|
|
4025
5233
|
app.get("/api/observations", (req, res) => {
|
|
4026
5234
|
const { offset, limit, project } = req.query;
|
|
4027
5235
|
const _offset = parseIntSafe(offset, 0, 0, 1e6);
|
|
@@ -4129,13 +5337,153 @@ app.get("/api/projects", (_req, res) => {
|
|
|
4129
5337
|
res.status(500).json({ error: "Failed to list projects" });
|
|
4130
5338
|
}
|
|
4131
5339
|
});
|
|
5340
|
+
app.get("/api/analytics/overview", (req, res) => {
|
|
5341
|
+
const { project } = req.query;
|
|
5342
|
+
if (project && !isValidProject(project)) {
|
|
5343
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
5344
|
+
return;
|
|
5345
|
+
}
|
|
5346
|
+
try {
|
|
5347
|
+
const overview = getAnalyticsOverview(db.db, project || void 0);
|
|
5348
|
+
res.json(overview);
|
|
5349
|
+
} catch (error) {
|
|
5350
|
+
logger.error("WORKER", "Analytics overview fallita", { project }, error);
|
|
5351
|
+
res.status(500).json({ error: "Analytics overview failed" });
|
|
5352
|
+
}
|
|
5353
|
+
});
|
|
5354
|
+
app.get("/api/analytics/timeline", (req, res) => {
|
|
5355
|
+
const { project, days } = req.query;
|
|
5356
|
+
if (project && !isValidProject(project)) {
|
|
5357
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
5358
|
+
return;
|
|
5359
|
+
}
|
|
5360
|
+
try {
|
|
5361
|
+
const timeline = getObservationsTimeline(
|
|
5362
|
+
db.db,
|
|
5363
|
+
project || void 0,
|
|
5364
|
+
parseIntSafe(days, 30, 1, 365)
|
|
5365
|
+
);
|
|
5366
|
+
res.json(timeline);
|
|
5367
|
+
} catch (error) {
|
|
5368
|
+
logger.error("WORKER", "Analytics timeline fallita", { project }, error);
|
|
5369
|
+
res.status(500).json({ error: "Analytics timeline failed" });
|
|
5370
|
+
}
|
|
5371
|
+
});
|
|
5372
|
+
app.get("/api/analytics/types", (req, res) => {
|
|
5373
|
+
const { project } = req.query;
|
|
5374
|
+
if (project && !isValidProject(project)) {
|
|
5375
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
5376
|
+
return;
|
|
5377
|
+
}
|
|
5378
|
+
try {
|
|
5379
|
+
const distribution = getTypeDistribution(db.db, project || void 0);
|
|
5380
|
+
res.json(distribution);
|
|
5381
|
+
} catch (error) {
|
|
5382
|
+
logger.error("WORKER", "Analytics types fallita", { project }, error);
|
|
5383
|
+
res.status(500).json({ error: "Analytics types failed" });
|
|
5384
|
+
}
|
|
5385
|
+
});
|
|
5386
|
+
app.get("/api/analytics/sessions", (req, res) => {
|
|
5387
|
+
const { project } = req.query;
|
|
5388
|
+
if (project && !isValidProject(project)) {
|
|
5389
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
5390
|
+
return;
|
|
5391
|
+
}
|
|
5392
|
+
try {
|
|
5393
|
+
const stats = getSessionStats(db.db, project || void 0);
|
|
5394
|
+
res.json(stats);
|
|
5395
|
+
} catch (error) {
|
|
5396
|
+
logger.error("WORKER", "Analytics sessions fallita", { project }, error);
|
|
5397
|
+
res.status(500).json({ error: "Analytics sessions failed" });
|
|
5398
|
+
}
|
|
5399
|
+
});
|
|
5400
|
+
app.get("/api/sessions", (req, res) => {
|
|
5401
|
+
const { project } = req.query;
|
|
5402
|
+
if (project && !isValidProject(project)) {
|
|
5403
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
5404
|
+
return;
|
|
5405
|
+
}
|
|
5406
|
+
try {
|
|
5407
|
+
const sessions = project ? getSessionsByProject(db.db, project, 50) : getActiveSessions(db.db);
|
|
5408
|
+
res.json(sessions);
|
|
5409
|
+
} catch (error) {
|
|
5410
|
+
logger.error("WORKER", "Lista sessioni fallita", { project }, error);
|
|
5411
|
+
res.status(500).json({ error: "Sessions list failed" });
|
|
5412
|
+
}
|
|
5413
|
+
});
|
|
5414
|
+
app.get("/api/sessions/:id/checkpoint", (req, res) => {
|
|
5415
|
+
const sessionId = parseInt(req.params.id, 10);
|
|
5416
|
+
if (isNaN(sessionId) || sessionId <= 0) {
|
|
5417
|
+
res.status(400).json({ error: "Invalid session ID" });
|
|
5418
|
+
return;
|
|
5419
|
+
}
|
|
5420
|
+
try {
|
|
5421
|
+
const checkpoint = getLatestCheckpoint(db.db, sessionId);
|
|
5422
|
+
if (!checkpoint) {
|
|
5423
|
+
res.status(404).json({ error: "No checkpoint found for this session" });
|
|
5424
|
+
return;
|
|
5425
|
+
}
|
|
5426
|
+
res.json(checkpoint);
|
|
5427
|
+
} catch (error) {
|
|
5428
|
+
logger.error("WORKER", "Checkpoint fetch fallito", { sessionId }, error);
|
|
5429
|
+
res.status(500).json({ error: "Checkpoint fetch failed" });
|
|
5430
|
+
}
|
|
5431
|
+
});
|
|
5432
|
+
app.get("/api/checkpoint", (req, res) => {
|
|
5433
|
+
const { project } = req.query;
|
|
5434
|
+
if (!project) {
|
|
5435
|
+
res.status(400).json({ error: "Project parameter is required" });
|
|
5436
|
+
return;
|
|
5437
|
+
}
|
|
5438
|
+
if (!isValidProject(project)) {
|
|
5439
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
5440
|
+
return;
|
|
5441
|
+
}
|
|
5442
|
+
try {
|
|
5443
|
+
const checkpoint = getLatestCheckpointByProject(db.db, project);
|
|
5444
|
+
if (!checkpoint) {
|
|
5445
|
+
res.status(404).json({ error: "No checkpoint found for this project" });
|
|
5446
|
+
return;
|
|
5447
|
+
}
|
|
5448
|
+
res.json(checkpoint);
|
|
5449
|
+
} catch (error) {
|
|
5450
|
+
logger.error("WORKER", "Checkpoint per progetto fallito", { project }, error);
|
|
5451
|
+
res.status(500).json({ error: "Project checkpoint fetch failed" });
|
|
5452
|
+
}
|
|
5453
|
+
});
|
|
5454
|
+
app.get("/api/report", (req, res) => {
|
|
5455
|
+
const { project, period, format } = req.query;
|
|
5456
|
+
if (project && !isValidProject(project)) {
|
|
5457
|
+
res.status(400).json({ error: "Invalid project name" });
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
const validPeriods = ["weekly", "monthly"];
|
|
5461
|
+
const reportPeriod = validPeriods.includes(period || "") ? period : "weekly";
|
|
5462
|
+
const daysBack = reportPeriod === "monthly" ? 30 : 7;
|
|
5463
|
+
const now = Date.now();
|
|
5464
|
+
const startEpoch = now - daysBack * 24 * 60 * 60 * 1e3;
|
|
5465
|
+
try {
|
|
5466
|
+
const data = getReportData(db.db, project || void 0, startEpoch, now);
|
|
5467
|
+
const outputFormat = format || "json";
|
|
5468
|
+
if (outputFormat === "markdown" || outputFormat === "md") {
|
|
5469
|
+
res.type("text/markdown").send(formatReportMarkdown(data));
|
|
5470
|
+
} else if (outputFormat === "text") {
|
|
5471
|
+
res.type("text/plain").send(JSON.stringify(data, null, 2));
|
|
5472
|
+
} else {
|
|
5473
|
+
res.json(data);
|
|
5474
|
+
}
|
|
5475
|
+
} catch (error) {
|
|
5476
|
+
logger.error("WORKER", "Report generation fallita", { project, period }, error);
|
|
5477
|
+
res.status(500).json({ error: "Report generation failed" });
|
|
5478
|
+
}
|
|
5479
|
+
});
|
|
4132
5480
|
app.use(express.static(__worker_dirname, {
|
|
4133
5481
|
index: false,
|
|
4134
5482
|
maxAge: "1h"
|
|
4135
5483
|
}));
|
|
4136
5484
|
app.get("/", (_req, res) => {
|
|
4137
5485
|
const viewerPath = join3(__worker_dirname, "viewer.html");
|
|
4138
|
-
if (
|
|
5486
|
+
if (existsSync4(viewerPath)) {
|
|
4139
5487
|
res.sendFile(viewerPath);
|
|
4140
5488
|
} else {
|
|
4141
5489
|
res.status(404).json({ error: "Viewer not found. Run npm run build first." });
|
|
@@ -4149,7 +5497,7 @@ function shutdown(signal) {
|
|
|
4149
5497
|
logger.info("WORKER", `Received ${signal}, shutting down gracefully...`);
|
|
4150
5498
|
server.close(() => {
|
|
4151
5499
|
logger.info("WORKER", "Server closed");
|
|
4152
|
-
if (
|
|
5500
|
+
if (existsSync4(PID_FILE)) {
|
|
4153
5501
|
unlinkSync(PID_FILE);
|
|
4154
5502
|
}
|
|
4155
5503
|
db.close();
|