engrm 0.4.27 → 0.4.29
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 +4 -2
- package/dist/cli.js +84 -0
- package/dist/hooks/elicitation-result.js +73 -0
- package/dist/hooks/post-tool-use.js +218 -12
- package/dist/hooks/pre-compact.js +218 -12
- package/dist/hooks/sentinel.js +70 -0
- package/dist/hooks/session-start.js +89 -3
- package/dist/hooks/stop.js +225 -13
- package/dist/hooks/user-prompt-submit.js +283 -71
- package/dist/server.js +432 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
227
227
|
| `load_handoff` | Open a saved handoff as a resume point for a new session |
|
|
228
228
|
| `refresh_chat_recall` | Rehydrate the separate chat lane from a Claude transcript when a long session feels under-captured |
|
|
229
229
|
| `recent_chat` | Inspect the separate synced chat lane without mixing it into durable memory |
|
|
230
|
-
| `search_chat` | Search recent chat recall separately from reusable memory observations |
|
|
230
|
+
| `search_chat` | Search recent chat recall with hybrid lexical + semantic matching, separately from reusable memory observations |
|
|
231
231
|
| `search_recall` | Search durable memory and chat recall together when you do not want to guess the right lane |
|
|
232
232
|
| `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
|
|
233
233
|
| `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
|
|
@@ -362,6 +362,8 @@ What each tool is good for:
|
|
|
362
362
|
- `memory_console` gives the quickest project snapshot, including whether continuity is `fresh`, `thin`, or `cold`
|
|
363
363
|
- `memory_console`, `project_memory_index`, and `session_context` now also show whether project chat recall is transcript-backed or only hook-captured
|
|
364
364
|
- when chat continuity is only hook-captured, the workbench and startup hints now prefer `refresh_chat_recall`
|
|
365
|
+
- the workbench and startup hints now also prefer `search_recall` as the first “what were we just talking about?” path when recent prompts/chat/observations exist
|
|
366
|
+
- `search_chat` now uses hybrid lexical + semantic ranking when sqlite-vec and local embeddings are available, so recent conversation recall is less dependent on exact wording
|
|
365
367
|
- `activity_feed` shows the merged chronology across prompts, tools, chat, handoffs, observations, and summaries
|
|
366
368
|
- `recent_sessions` helps you pick a session worth opening
|
|
367
369
|
- `session_story` reconstructs one session in detail, including handoffs and chat recall
|
|
@@ -369,7 +371,7 @@ What each tool is good for:
|
|
|
369
371
|
- `session_tool_memory` shows which tool calls in one session turned into reusable memory and which did not
|
|
370
372
|
- `project_memory_index` shows typed memory by repo, including continuity state and hot files
|
|
371
373
|
- `workspace_memory_index` shows coverage across all repos on the machine
|
|
372
|
-
- `recent_chat` / `search_chat` now report transcript-vs-hook coverage too, so weak OpenClaw recall is easier to diagnose and refresh
|
|
374
|
+
- `recent_chat` / `search_chat` now report transcript-vs-hook coverage too, and `search_chat` will also mark when semantic ranking was available, so weak OpenClaw recall is easier to diagnose and refresh
|
|
373
375
|
|
|
374
376
|
### Thin Tool Workflow
|
|
375
377
|
|
package/dist/cli.js
CHANGED
|
@@ -708,6 +708,17 @@ var MIGRATIONS = [
|
|
|
708
708
|
ON chat_messages(session_id, transcript_index)
|
|
709
709
|
WHERE transcript_index IS NOT NULL;
|
|
710
710
|
`
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
version: 18,
|
|
714
|
+
description: "Add sqlite-vec semantic search for chat recall",
|
|
715
|
+
condition: (db) => isVecExtensionLoaded(db),
|
|
716
|
+
sql: `
|
|
717
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
718
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
719
|
+
embedding FLOAT[384]
|
|
720
|
+
);
|
|
721
|
+
`
|
|
711
722
|
}
|
|
712
723
|
];
|
|
713
724
|
function isVecExtensionLoaded(db) {
|
|
@@ -781,6 +792,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
781
792
|
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
782
793
|
version = Math.max(version, 17);
|
|
783
794
|
}
|
|
795
|
+
if (tableExists(db, "vec_chat_messages")) {
|
|
796
|
+
version = Math.max(version, 18);
|
|
797
|
+
}
|
|
784
798
|
return version;
|
|
785
799
|
}
|
|
786
800
|
function runMigrations(db) {
|
|
@@ -897,6 +911,20 @@ function ensureChatMessageColumns(db) {
|
|
|
897
911
|
db.exec("PRAGMA user_version = 17");
|
|
898
912
|
}
|
|
899
913
|
}
|
|
914
|
+
function ensureChatVectorTable(db) {
|
|
915
|
+
if (!isVecExtensionLoaded(db))
|
|
916
|
+
return;
|
|
917
|
+
db.exec(`
|
|
918
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
919
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
920
|
+
embedding FLOAT[384]
|
|
921
|
+
);
|
|
922
|
+
`);
|
|
923
|
+
const current = getSchemaVersion(db);
|
|
924
|
+
if (current < 18) {
|
|
925
|
+
db.exec("PRAGMA user_version = 18");
|
|
926
|
+
}
|
|
927
|
+
}
|
|
900
928
|
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
901
929
|
if (syncOutboxSupportsChatMessages(db)) {
|
|
902
930
|
const current = getSchemaVersion(db);
|
|
@@ -1110,6 +1138,7 @@ class MemDatabase {
|
|
|
1110
1138
|
ensureObservationTypes(this.db);
|
|
1111
1139
|
ensureSessionSummaryColumns(this.db);
|
|
1112
1140
|
ensureChatMessageColumns(this.db);
|
|
1141
|
+
ensureChatVectorTable(this.db);
|
|
1113
1142
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1114
1143
|
}
|
|
1115
1144
|
loadVecExtension() {
|
|
@@ -1476,6 +1505,14 @@ class MemDatabase {
|
|
|
1476
1505
|
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1477
1506
|
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1478
1507
|
}
|
|
1508
|
+
getChatMessagesByIds(ids) {
|
|
1509
|
+
if (ids.length === 0)
|
|
1510
|
+
return [];
|
|
1511
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1512
|
+
const rows = this.db.query(`SELECT * FROM chat_messages WHERE id IN (${placeholders})`).all(...ids);
|
|
1513
|
+
const order = new Map(ids.map((id, index) => [id, index]));
|
|
1514
|
+
return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
|
|
1515
|
+
}
|
|
1479
1516
|
getSessionChatMessages(sessionId, limit = 50) {
|
|
1480
1517
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1481
1518
|
WHERE session_id = ?
|
|
@@ -1552,6 +1589,39 @@ class MemDatabase {
|
|
|
1552
1589
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1553
1590
|
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1554
1591
|
}
|
|
1592
|
+
vecChatInsert(chatMessageId, embedding) {
|
|
1593
|
+
if (!this.vecAvailable)
|
|
1594
|
+
return;
|
|
1595
|
+
this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
|
|
1596
|
+
}
|
|
1597
|
+
searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
|
|
1598
|
+
if (!this.vecAvailable)
|
|
1599
|
+
return [];
|
|
1600
|
+
const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
|
|
1601
|
+
const visibilityClause = userId ? " AND c.user_id = ?" : "";
|
|
1602
|
+
const transcriptPreference = `
|
|
1603
|
+
AND (
|
|
1604
|
+
c.source_kind = 'transcript'
|
|
1605
|
+
OR NOT EXISTS (
|
|
1606
|
+
SELECT 1 FROM chat_messages t2
|
|
1607
|
+
WHERE t2.session_id = c.session_id
|
|
1608
|
+
AND t2.source_kind = 'transcript'
|
|
1609
|
+
)
|
|
1610
|
+
)`;
|
|
1611
|
+
if (projectId !== null) {
|
|
1612
|
+
return this.db.query(`SELECT v.chat_message_id, v.distance
|
|
1613
|
+
FROM vec_chat_messages v
|
|
1614
|
+
JOIN chat_messages c ON c.id = v.chat_message_id
|
|
1615
|
+
WHERE v.embedding MATCH ?
|
|
1616
|
+
AND k = ?
|
|
1617
|
+
AND c.project_id = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, projectId, ...userId ? [userId] : []);
|
|
1618
|
+
}
|
|
1619
|
+
return this.db.query(`SELECT v.chat_message_id, v.distance
|
|
1620
|
+
FROM vec_chat_messages v
|
|
1621
|
+
JOIN chat_messages c ON c.id = v.chat_message_id
|
|
1622
|
+
WHERE v.embedding MATCH ?
|
|
1623
|
+
AND k = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, ...userId ? [userId] : []);
|
|
1624
|
+
}
|
|
1555
1625
|
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
1556
1626
|
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
1557
1627
|
}
|
|
@@ -2296,6 +2366,17 @@ var MIGRATIONS2 = [
|
|
|
2296
2366
|
ON chat_messages(session_id, transcript_index)
|
|
2297
2367
|
WHERE transcript_index IS NOT NULL;
|
|
2298
2368
|
`
|
|
2369
|
+
},
|
|
2370
|
+
{
|
|
2371
|
+
version: 18,
|
|
2372
|
+
description: "Add sqlite-vec semantic search for chat recall",
|
|
2373
|
+
condition: (db) => isVecExtensionLoaded2(db),
|
|
2374
|
+
sql: `
|
|
2375
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
2376
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
2377
|
+
embedding FLOAT[384]
|
|
2378
|
+
);
|
|
2379
|
+
`
|
|
2299
2380
|
}
|
|
2300
2381
|
];
|
|
2301
2382
|
function isVecExtensionLoaded2(db) {
|
|
@@ -3288,6 +3369,9 @@ function composeEmbeddingText(obs) {
|
|
|
3288
3369
|
|
|
3289
3370
|
`);
|
|
3290
3371
|
}
|
|
3372
|
+
function composeChatEmbeddingText(text) {
|
|
3373
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 2000);
|
|
3374
|
+
}
|
|
3291
3375
|
async function getPipeline() {
|
|
3292
3376
|
if (_pipeline)
|
|
3293
3377
|
return _pipeline;
|
|
@@ -510,6 +510,9 @@ function composeEmbeddingText(obs) {
|
|
|
510
510
|
|
|
511
511
|
`);
|
|
512
512
|
}
|
|
513
|
+
function composeChatEmbeddingText(text) {
|
|
514
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 2000);
|
|
515
|
+
}
|
|
513
516
|
async function getPipeline() {
|
|
514
517
|
if (_pipeline)
|
|
515
518
|
return _pipeline;
|
|
@@ -1541,6 +1544,17 @@ var MIGRATIONS = [
|
|
|
1541
1544
|
ON chat_messages(session_id, transcript_index)
|
|
1542
1545
|
WHERE transcript_index IS NOT NULL;
|
|
1543
1546
|
`
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
version: 18,
|
|
1550
|
+
description: "Add sqlite-vec semantic search for chat recall",
|
|
1551
|
+
condition: (db) => isVecExtensionLoaded(db),
|
|
1552
|
+
sql: `
|
|
1553
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
1554
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
1555
|
+
embedding FLOAT[384]
|
|
1556
|
+
);
|
|
1557
|
+
`
|
|
1544
1558
|
}
|
|
1545
1559
|
];
|
|
1546
1560
|
function isVecExtensionLoaded(db) {
|
|
@@ -1614,6 +1628,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
1614
1628
|
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
1615
1629
|
version = Math.max(version, 17);
|
|
1616
1630
|
}
|
|
1631
|
+
if (tableExists(db, "vec_chat_messages")) {
|
|
1632
|
+
version = Math.max(version, 18);
|
|
1633
|
+
}
|
|
1617
1634
|
return version;
|
|
1618
1635
|
}
|
|
1619
1636
|
function runMigrations(db) {
|
|
@@ -1730,6 +1747,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1730
1747
|
db.exec("PRAGMA user_version = 17");
|
|
1731
1748
|
}
|
|
1732
1749
|
}
|
|
1750
|
+
function ensureChatVectorTable(db) {
|
|
1751
|
+
if (!isVecExtensionLoaded(db))
|
|
1752
|
+
return;
|
|
1753
|
+
db.exec(`
|
|
1754
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
1755
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
1756
|
+
embedding FLOAT[384]
|
|
1757
|
+
);
|
|
1758
|
+
`);
|
|
1759
|
+
const current = getSchemaVersion(db);
|
|
1760
|
+
if (current < 18) {
|
|
1761
|
+
db.exec("PRAGMA user_version = 18");
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1733
1764
|
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
1734
1765
|
if (syncOutboxSupportsChatMessages(db)) {
|
|
1735
1766
|
const current = getSchemaVersion(db);
|
|
@@ -1943,6 +1974,7 @@ class MemDatabase {
|
|
|
1943
1974
|
ensureObservationTypes(this.db);
|
|
1944
1975
|
ensureSessionSummaryColumns(this.db);
|
|
1945
1976
|
ensureChatMessageColumns(this.db);
|
|
1977
|
+
ensureChatVectorTable(this.db);
|
|
1946
1978
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1947
1979
|
}
|
|
1948
1980
|
loadVecExtension() {
|
|
@@ -2309,6 +2341,14 @@ class MemDatabase {
|
|
|
2309
2341
|
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
2310
2342
|
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
2311
2343
|
}
|
|
2344
|
+
getChatMessagesByIds(ids) {
|
|
2345
|
+
if (ids.length === 0)
|
|
2346
|
+
return [];
|
|
2347
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
2348
|
+
const rows = this.db.query(`SELECT * FROM chat_messages WHERE id IN (${placeholders})`).all(...ids);
|
|
2349
|
+
const order = new Map(ids.map((id, index) => [id, index]));
|
|
2350
|
+
return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
|
|
2351
|
+
}
|
|
2312
2352
|
getSessionChatMessages(sessionId, limit = 50) {
|
|
2313
2353
|
return this.db.query(`SELECT * FROM chat_messages
|
|
2314
2354
|
WHERE session_id = ?
|
|
@@ -2385,6 +2425,39 @@ class MemDatabase {
|
|
|
2385
2425
|
ORDER BY created_at_epoch DESC, id DESC
|
|
2386
2426
|
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
2387
2427
|
}
|
|
2428
|
+
vecChatInsert(chatMessageId, embedding) {
|
|
2429
|
+
if (!this.vecAvailable)
|
|
2430
|
+
return;
|
|
2431
|
+
this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
|
|
2432
|
+
}
|
|
2433
|
+
searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
|
|
2434
|
+
if (!this.vecAvailable)
|
|
2435
|
+
return [];
|
|
2436
|
+
const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
|
|
2437
|
+
const visibilityClause = userId ? " AND c.user_id = ?" : "";
|
|
2438
|
+
const transcriptPreference = `
|
|
2439
|
+
AND (
|
|
2440
|
+
c.source_kind = 'transcript'
|
|
2441
|
+
OR NOT EXISTS (
|
|
2442
|
+
SELECT 1 FROM chat_messages t2
|
|
2443
|
+
WHERE t2.session_id = c.session_id
|
|
2444
|
+
AND t2.source_kind = 'transcript'
|
|
2445
|
+
)
|
|
2446
|
+
)`;
|
|
2447
|
+
if (projectId !== null) {
|
|
2448
|
+
return this.db.query(`SELECT v.chat_message_id, v.distance
|
|
2449
|
+
FROM vec_chat_messages v
|
|
2450
|
+
JOIN chat_messages c ON c.id = v.chat_message_id
|
|
2451
|
+
WHERE v.embedding MATCH ?
|
|
2452
|
+
AND k = ?
|
|
2453
|
+
AND c.project_id = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, projectId, ...userId ? [userId] : []);
|
|
2454
|
+
}
|
|
2455
|
+
return this.db.query(`SELECT v.chat_message_id, v.distance
|
|
2456
|
+
FROM vec_chat_messages v
|
|
2457
|
+
JOIN chat_messages c ON c.id = v.chat_message_id
|
|
2458
|
+
WHERE v.embedding MATCH ?
|
|
2459
|
+
AND k = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, ...userId ? [userId] : []);
|
|
2460
|
+
}
|
|
2388
2461
|
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
2389
2462
|
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
2390
2463
|
}
|
|
@@ -886,6 +886,17 @@ var MIGRATIONS = [
|
|
|
886
886
|
ON chat_messages(session_id, transcript_index)
|
|
887
887
|
WHERE transcript_index IS NOT NULL;
|
|
888
888
|
`
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
version: 18,
|
|
892
|
+
description: "Add sqlite-vec semantic search for chat recall",
|
|
893
|
+
condition: (db) => isVecExtensionLoaded(db),
|
|
894
|
+
sql: `
|
|
895
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
896
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
897
|
+
embedding FLOAT[384]
|
|
898
|
+
);
|
|
899
|
+
`
|
|
889
900
|
}
|
|
890
901
|
];
|
|
891
902
|
function isVecExtensionLoaded(db) {
|
|
@@ -959,6 +970,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
959
970
|
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
960
971
|
version = Math.max(version, 17);
|
|
961
972
|
}
|
|
973
|
+
if (tableExists(db, "vec_chat_messages")) {
|
|
974
|
+
version = Math.max(version, 18);
|
|
975
|
+
}
|
|
962
976
|
return version;
|
|
963
977
|
}
|
|
964
978
|
function runMigrations(db) {
|
|
@@ -1075,6 +1089,20 @@ function ensureChatMessageColumns(db) {
|
|
|
1075
1089
|
db.exec("PRAGMA user_version = 17");
|
|
1076
1090
|
}
|
|
1077
1091
|
}
|
|
1092
|
+
function ensureChatVectorTable(db) {
|
|
1093
|
+
if (!isVecExtensionLoaded(db))
|
|
1094
|
+
return;
|
|
1095
|
+
db.exec(`
|
|
1096
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
|
|
1097
|
+
chat_message_id INTEGER PRIMARY KEY,
|
|
1098
|
+
embedding FLOAT[384]
|
|
1099
|
+
);
|
|
1100
|
+
`);
|
|
1101
|
+
const current = getSchemaVersion(db);
|
|
1102
|
+
if (current < 18) {
|
|
1103
|
+
db.exec("PRAGMA user_version = 18");
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1078
1106
|
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
1079
1107
|
if (syncOutboxSupportsChatMessages(db)) {
|
|
1080
1108
|
const current = getSchemaVersion(db);
|
|
@@ -1288,6 +1316,7 @@ class MemDatabase {
|
|
|
1288
1316
|
ensureObservationTypes(this.db);
|
|
1289
1317
|
ensureSessionSummaryColumns(this.db);
|
|
1290
1318
|
ensureChatMessageColumns(this.db);
|
|
1319
|
+
ensureChatVectorTable(this.db);
|
|
1291
1320
|
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1292
1321
|
}
|
|
1293
1322
|
loadVecExtension() {
|
|
@@ -1654,6 +1683,14 @@ class MemDatabase {
|
|
|
1654
1683
|
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1655
1684
|
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1656
1685
|
}
|
|
1686
|
+
getChatMessagesByIds(ids) {
|
|
1687
|
+
if (ids.length === 0)
|
|
1688
|
+
return [];
|
|
1689
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1690
|
+
const rows = this.db.query(`SELECT * FROM chat_messages WHERE id IN (${placeholders})`).all(...ids);
|
|
1691
|
+
const order = new Map(ids.map((id, index) => [id, index]));
|
|
1692
|
+
return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
|
|
1693
|
+
}
|
|
1657
1694
|
getSessionChatMessages(sessionId, limit = 50) {
|
|
1658
1695
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1659
1696
|
WHERE session_id = ?
|
|
@@ -1730,6 +1767,39 @@ class MemDatabase {
|
|
|
1730
1767
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1731
1768
|
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1732
1769
|
}
|
|
1770
|
+
vecChatInsert(chatMessageId, embedding) {
|
|
1771
|
+
if (!this.vecAvailable)
|
|
1772
|
+
return;
|
|
1773
|
+
this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
|
|
1774
|
+
}
|
|
1775
|
+
searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
|
|
1776
|
+
if (!this.vecAvailable)
|
|
1777
|
+
return [];
|
|
1778
|
+
const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
|
|
1779
|
+
const visibilityClause = userId ? " AND c.user_id = ?" : "";
|
|
1780
|
+
const transcriptPreference = `
|
|
1781
|
+
AND (
|
|
1782
|
+
c.source_kind = 'transcript'
|
|
1783
|
+
OR NOT EXISTS (
|
|
1784
|
+
SELECT 1 FROM chat_messages t2
|
|
1785
|
+
WHERE t2.session_id = c.session_id
|
|
1786
|
+
AND t2.source_kind = 'transcript'
|
|
1787
|
+
)
|
|
1788
|
+
)`;
|
|
1789
|
+
if (projectId !== null) {
|
|
1790
|
+
return this.db.query(`SELECT v.chat_message_id, v.distance
|
|
1791
|
+
FROM vec_chat_messages v
|
|
1792
|
+
JOIN chat_messages c ON c.id = v.chat_message_id
|
|
1793
|
+
WHERE v.embedding MATCH ?
|
|
1794
|
+
AND k = ?
|
|
1795
|
+
AND c.project_id = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, projectId, ...userId ? [userId] : []);
|
|
1796
|
+
}
|
|
1797
|
+
return this.db.query(`SELECT v.chat_message_id, v.distance
|
|
1798
|
+
FROM vec_chat_messages v
|
|
1799
|
+
JOIN chat_messages c ON c.id = v.chat_message_id
|
|
1800
|
+
WHERE v.embedding MATCH ?
|
|
1801
|
+
AND k = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, ...userId ? [userId] : []);
|
|
1802
|
+
}
|
|
1733
1803
|
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
1734
1804
|
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
1735
1805
|
}
|
|
@@ -2514,6 +2584,9 @@ function composeEmbeddingText(obs) {
|
|
|
2514
2584
|
|
|
2515
2585
|
`);
|
|
2516
2586
|
}
|
|
2587
|
+
function composeChatEmbeddingText(text) {
|
|
2588
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 2000);
|
|
2589
|
+
}
|
|
2517
2590
|
async function getPipeline() {
|
|
2518
2591
|
if (_pipeline)
|
|
2519
2592
|
return _pipeline;
|
|
@@ -3648,6 +3721,7 @@ function parseJsonArray(value) {
|
|
|
3648
3721
|
}
|
|
3649
3722
|
|
|
3650
3723
|
// src/capture/transcript.ts
|
|
3724
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3651
3725
|
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
3652
3726
|
import { join as join4 } from "node:path";
|
|
3653
3727
|
import { homedir as homedir3 } from "node:os";
|
|
@@ -3701,23 +3775,109 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
3701
3775
|
}
|
|
3702
3776
|
return messages;
|
|
3703
3777
|
}
|
|
3704
|
-
function
|
|
3705
|
-
|
|
3778
|
+
function resolveHistoryPath(historyPath) {
|
|
3779
|
+
if (historyPath)
|
|
3780
|
+
return historyPath;
|
|
3781
|
+
const override = process.env["ENGRM_CLAUDE_HISTORY_PATH"];
|
|
3782
|
+
if (override)
|
|
3783
|
+
return override;
|
|
3784
|
+
return join4(homedir3(), ".claude", "history.jsonl");
|
|
3785
|
+
}
|
|
3786
|
+
function readHistoryFallback(sessionId, cwd, opts) {
|
|
3787
|
+
const path = resolveHistoryPath(opts?.historyPath);
|
|
3788
|
+
if (!existsSync4(path))
|
|
3789
|
+
return [];
|
|
3790
|
+
let raw;
|
|
3791
|
+
try {
|
|
3792
|
+
raw = readFileSync4(path, "utf-8");
|
|
3793
|
+
} catch {
|
|
3794
|
+
return [];
|
|
3795
|
+
}
|
|
3796
|
+
const targetCanonical = detectProject(cwd).canonical_id;
|
|
3797
|
+
const windowStart = Math.max(0, (opts?.startedAtEpoch ?? Math.floor(Date.now() / 1000) - 6 * 3600) - 600);
|
|
3798
|
+
const windowEnd = (opts?.completedAtEpoch ?? Math.floor(Date.now() / 1000)) + 600;
|
|
3799
|
+
const entries = [];
|
|
3800
|
+
for (const line of raw.split(`
|
|
3801
|
+
`)) {
|
|
3802
|
+
if (!line.trim())
|
|
3803
|
+
continue;
|
|
3804
|
+
let entry;
|
|
3805
|
+
try {
|
|
3806
|
+
entry = JSON.parse(line);
|
|
3807
|
+
} catch {
|
|
3808
|
+
continue;
|
|
3809
|
+
}
|
|
3810
|
+
if (typeof entry?.display !== "string" || typeof entry?.timestamp !== "number")
|
|
3811
|
+
continue;
|
|
3812
|
+
const createdAtEpoch = Math.floor(entry.timestamp / 1000);
|
|
3813
|
+
entries.push({
|
|
3814
|
+
display: entry.display.trim(),
|
|
3815
|
+
project: typeof entry.project === "string" ? entry.project : "",
|
|
3816
|
+
sessionId: typeof entry.sessionId === "string" ? entry.sessionId : "",
|
|
3817
|
+
timestamp: createdAtEpoch
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
const bySession = entries.filter((entry) => entry.display.length > 0 && entry.sessionId === sessionId).sort((a, b) => a.timestamp - b.timestamp);
|
|
3821
|
+
if (bySession.length > 0) {
|
|
3822
|
+
return dedupeHistoryMessages(bySession.map((entry) => ({
|
|
3823
|
+
role: "user",
|
|
3824
|
+
text: entry.display,
|
|
3825
|
+
createdAtEpoch: entry.timestamp
|
|
3826
|
+
})));
|
|
3827
|
+
}
|
|
3828
|
+
const byProjectAndWindow = entries.filter((entry) => {
|
|
3829
|
+
if (entry.display.length === 0)
|
|
3830
|
+
return false;
|
|
3831
|
+
if (entry.timestamp < windowStart || entry.timestamp > windowEnd)
|
|
3832
|
+
return false;
|
|
3833
|
+
if (!entry.project)
|
|
3834
|
+
return false;
|
|
3835
|
+
return detectProject(entry.project).canonical_id === targetCanonical;
|
|
3836
|
+
}).sort((a, b) => a.timestamp - b.timestamp);
|
|
3837
|
+
return dedupeHistoryMessages(byProjectAndWindow.map((entry) => ({
|
|
3838
|
+
role: "user",
|
|
3839
|
+
text: entry.display,
|
|
3840
|
+
createdAtEpoch: entry.timestamp
|
|
3841
|
+
})));
|
|
3842
|
+
}
|
|
3843
|
+
async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
3844
|
+
const session = db.getSessionById(sessionId);
|
|
3845
|
+
const transcriptMessages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
|
|
3706
3846
|
...message,
|
|
3707
3847
|
text: message.text.trim()
|
|
3708
3848
|
})).filter((message) => message.text.length > 0);
|
|
3849
|
+
const messages = transcriptMessages.length > 0 ? transcriptMessages.map((message, index) => ({
|
|
3850
|
+
...message,
|
|
3851
|
+
sourceKind: "transcript",
|
|
3852
|
+
transcriptIndex: index + 1,
|
|
3853
|
+
createdAtEpoch: null,
|
|
3854
|
+
remoteSourceId: null
|
|
3855
|
+
})) : readHistoryFallback(sessionId, cwd, {
|
|
3856
|
+
startedAtEpoch: session?.started_at_epoch ?? null,
|
|
3857
|
+
completedAtEpoch: session?.completed_at_epoch ?? null
|
|
3858
|
+
}).map((message) => ({
|
|
3859
|
+
role: message.role,
|
|
3860
|
+
text: message.text,
|
|
3861
|
+
sourceKind: "hook",
|
|
3862
|
+
transcriptIndex: null,
|
|
3863
|
+
createdAtEpoch: message.createdAtEpoch,
|
|
3864
|
+
remoteSourceId: buildHistorySourceId(sessionId, message.createdAtEpoch, message.text)
|
|
3865
|
+
}));
|
|
3709
3866
|
if (messages.length === 0)
|
|
3710
3867
|
return { imported: 0, total: 0 };
|
|
3711
|
-
const session = db.getSessionById(sessionId);
|
|
3712
3868
|
const projectId = session?.project_id ?? null;
|
|
3713
3869
|
const now = Math.floor(Date.now() / 1000);
|
|
3714
3870
|
let imported = 0;
|
|
3715
3871
|
for (let index = 0;index < messages.length; index++) {
|
|
3716
|
-
const transcriptIndex = index + 1;
|
|
3717
|
-
if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
|
|
3718
|
-
continue;
|
|
3719
3872
|
const message = messages[index];
|
|
3720
|
-
const
|
|
3873
|
+
const transcriptIndex = message.transcriptIndex ?? index + 1;
|
|
3874
|
+
if (message.sourceKind === "transcript" && db.getTranscriptChatMessage(sessionId, transcriptIndex)) {
|
|
3875
|
+
continue;
|
|
3876
|
+
}
|
|
3877
|
+
if (message.remoteSourceId && db.getChatMessageByRemoteSourceId(message.remoteSourceId)) {
|
|
3878
|
+
continue;
|
|
3879
|
+
}
|
|
3880
|
+
const createdAtEpoch = message.createdAtEpoch ?? Math.max(0, now - (messages.length - transcriptIndex));
|
|
3721
3881
|
const row = db.insertChatMessage({
|
|
3722
3882
|
session_id: sessionId,
|
|
3723
3883
|
project_id: projectId,
|
|
@@ -3727,14 +3887,50 @@ function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
|
3727
3887
|
device_id: config.device_id,
|
|
3728
3888
|
agent: "claude-code",
|
|
3729
3889
|
created_at_epoch: createdAtEpoch,
|
|
3730
|
-
|
|
3731
|
-
|
|
3890
|
+
remote_source_id: message.remoteSourceId,
|
|
3891
|
+
source_kind: message.sourceKind,
|
|
3892
|
+
transcript_index: message.transcriptIndex
|
|
3732
3893
|
});
|
|
3733
3894
|
db.addToOutbox("chat_message", row.id);
|
|
3895
|
+
if (message.role === "user") {
|
|
3896
|
+
db.insertUserPrompt({
|
|
3897
|
+
session_id: sessionId,
|
|
3898
|
+
project_id: projectId,
|
|
3899
|
+
prompt: message.text,
|
|
3900
|
+
cwd,
|
|
3901
|
+
user_id: config.user_id,
|
|
3902
|
+
device_id: config.device_id,
|
|
3903
|
+
agent: "claude-code",
|
|
3904
|
+
created_at_epoch: createdAtEpoch
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
if (db.vecAvailable) {
|
|
3908
|
+
const embedding = await embedText(composeChatEmbeddingText(message.text));
|
|
3909
|
+
if (embedding) {
|
|
3910
|
+
db.vecChatInsert(row.id, embedding);
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3734
3913
|
imported++;
|
|
3735
3914
|
}
|
|
3736
3915
|
return { imported, total: messages.length };
|
|
3737
3916
|
}
|
|
3917
|
+
function dedupeHistoryMessages(messages) {
|
|
3918
|
+
const deduped = [];
|
|
3919
|
+
for (const message of messages) {
|
|
3920
|
+
const compact = message.text.replace(/\s+/g, " ").trim();
|
|
3921
|
+
if (!compact)
|
|
3922
|
+
continue;
|
|
3923
|
+
const previous = deduped[deduped.length - 1];
|
|
3924
|
+
if (previous && previous.text.replace(/\s+/g, " ").trim() === compact)
|
|
3925
|
+
continue;
|
|
3926
|
+
deduped.push({ ...message, text: compact });
|
|
3927
|
+
}
|
|
3928
|
+
return deduped;
|
|
3929
|
+
}
|
|
3930
|
+
function buildHistorySourceId(sessionId, createdAtEpoch, text) {
|
|
3931
|
+
const digest = createHash3("sha1").update(text).digest("hex").slice(0, 12);
|
|
3932
|
+
return `history:${sessionId}:${createdAtEpoch}:${digest}`;
|
|
3933
|
+
}
|
|
3738
3934
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
3739
3935
|
const lines = [];
|
|
3740
3936
|
for (const msg of messages) {
|
|
@@ -3810,6 +4006,16 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
|
|
|
3810
4006
|
return saved;
|
|
3811
4007
|
}
|
|
3812
4008
|
|
|
4009
|
+
// src/tools/recent-chat.ts
|
|
4010
|
+
function getChatCaptureOrigin(message) {
|
|
4011
|
+
if (message.source_kind === "transcript")
|
|
4012
|
+
return "transcript";
|
|
4013
|
+
if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
|
|
4014
|
+
return "history";
|
|
4015
|
+
}
|
|
4016
|
+
return "hook";
|
|
4017
|
+
}
|
|
4018
|
+
|
|
3813
4019
|
// src/tools/session-story.ts
|
|
3814
4020
|
function getSessionStory(db, input) {
|
|
3815
4021
|
const session = db.getSessionById(input.session_id);
|
|
@@ -3932,9 +4138,9 @@ function collectProvenanceSummary(observations) {
|
|
|
3932
4138
|
}
|
|
3933
4139
|
function summarizeChatSources(messages) {
|
|
3934
4140
|
return messages.reduce((summary, message) => {
|
|
3935
|
-
summary[message
|
|
4141
|
+
summary[getChatCaptureOrigin(message)] += 1;
|
|
3936
4142
|
return summary;
|
|
3937
|
-
}, { transcript: 0, hook: 0 });
|
|
4143
|
+
}, { transcript: 0, history: 0, hook: 0 });
|
|
3938
4144
|
}
|
|
3939
4145
|
|
|
3940
4146
|
// src/tools/handoffs.ts
|
|
@@ -4256,7 +4462,7 @@ async function main() {
|
|
|
4256
4462
|
try {
|
|
4257
4463
|
if (event.session_id) {
|
|
4258
4464
|
persistRawToolChronology(event, config.user_id, config.device_id);
|
|
4259
|
-
syncTranscriptChat(db, config, event.session_id, event.cwd);
|
|
4465
|
+
await syncTranscriptChat(db, config, event.session_id, event.cwd);
|
|
4260
4466
|
}
|
|
4261
4467
|
const textToScan = extractScanText(event);
|
|
4262
4468
|
if (textToScan) {
|