engrm 0.4.22 → 0.4.23
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 +18 -0
- package/dist/cli.js +308 -24
- package/dist/hooks/elicitation-result.js +223 -15
- package/dist/hooks/post-tool-use.js +257 -15
- package/dist/hooks/pre-compact.js +962 -17
- package/dist/hooks/sentinel.js +223 -15
- package/dist/hooks/session-start.js +1066 -92
- package/dist/hooks/stop.js +361 -33
- package/dist/hooks/user-prompt-submit.js +267 -15
- package/dist/server.js +863 -49
- package/package.json +1 -1
package/dist/hooks/sentinel.js
CHANGED
|
@@ -353,7 +353,7 @@ var MIGRATIONS = [
|
|
|
353
353
|
-- Sync outbox (offline-first queue)
|
|
354
354
|
CREATE TABLE sync_outbox (
|
|
355
355
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
356
|
-
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
356
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
357
357
|
record_id INTEGER NOT NULL,
|
|
358
358
|
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
359
359
|
'pending', 'syncing', 'synced', 'failed'
|
|
@@ -646,6 +646,18 @@ var MIGRATIONS = [
|
|
|
646
646
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
647
647
|
`
|
|
648
648
|
},
|
|
649
|
+
{
|
|
650
|
+
version: 11,
|
|
651
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
652
|
+
sql: `
|
|
653
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
654
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
655
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
656
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
657
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
658
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
659
|
+
`
|
|
660
|
+
},
|
|
649
661
|
{
|
|
650
662
|
version: 12,
|
|
651
663
|
description: "Add synced handoff metadata to session summaries",
|
|
@@ -657,15 +669,79 @@ var MIGRATIONS = [
|
|
|
657
669
|
`
|
|
658
670
|
},
|
|
659
671
|
{
|
|
660
|
-
version:
|
|
661
|
-
description: "Add
|
|
672
|
+
version: 13,
|
|
673
|
+
description: "Add current_thread to session summaries",
|
|
662
674
|
sql: `
|
|
663
|
-
ALTER TABLE
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
675
|
+
ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
|
|
676
|
+
`
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
version: 14,
|
|
680
|
+
description: "Add chat_messages lane for raw conversation recall",
|
|
681
|
+
sql: `
|
|
682
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
683
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
684
|
+
session_id TEXT NOT NULL,
|
|
685
|
+
project_id INTEGER REFERENCES projects(id),
|
|
686
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
687
|
+
content TEXT NOT NULL,
|
|
688
|
+
user_id TEXT NOT NULL,
|
|
689
|
+
device_id TEXT NOT NULL,
|
|
690
|
+
agent TEXT DEFAULT 'claude-code',
|
|
691
|
+
created_at_epoch INTEGER NOT NULL
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session
|
|
695
|
+
ON chat_messages(session_id, created_at_epoch DESC, id DESC);
|
|
696
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_project
|
|
697
|
+
ON chat_messages(project_id, created_at_epoch DESC, id DESC);
|
|
698
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created
|
|
699
|
+
ON chat_messages(created_at_epoch DESC, id DESC);
|
|
700
|
+
`
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
version: 15,
|
|
704
|
+
description: "Add remote_source_id for chat message sync deduplication",
|
|
705
|
+
sql: `
|
|
706
|
+
ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
|
|
707
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
|
|
708
|
+
ON chat_messages(remote_source_id)
|
|
709
|
+
WHERE remote_source_id IS NOT NULL;
|
|
710
|
+
`
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
version: 16,
|
|
714
|
+
description: "Allow chat_message records in sync_outbox",
|
|
715
|
+
sql: `
|
|
716
|
+
CREATE TABLE sync_outbox_new (
|
|
717
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
718
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
719
|
+
record_id INTEGER NOT NULL,
|
|
720
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
721
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
722
|
+
)),
|
|
723
|
+
retry_count INTEGER DEFAULT 0,
|
|
724
|
+
max_retries INTEGER DEFAULT 10,
|
|
725
|
+
last_error TEXT,
|
|
726
|
+
created_at_epoch INTEGER NOT NULL,
|
|
727
|
+
synced_at_epoch INTEGER,
|
|
728
|
+
next_retry_epoch INTEGER
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
INSERT INTO sync_outbox_new (
|
|
732
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
733
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
734
|
+
)
|
|
735
|
+
SELECT
|
|
736
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
737
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
738
|
+
FROM sync_outbox;
|
|
739
|
+
|
|
740
|
+
DROP TABLE sync_outbox;
|
|
741
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
742
|
+
|
|
743
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
744
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
669
745
|
`
|
|
670
746
|
}
|
|
671
747
|
];
|
|
@@ -725,6 +801,18 @@ function inferLegacySchemaVersion(db) {
|
|
|
725
801
|
if (columnExists(db, "session_summaries", "capture_state") && columnExists(db, "session_summaries", "recent_tool_names") && columnExists(db, "session_summaries", "hot_files") && columnExists(db, "session_summaries", "recent_outcomes")) {
|
|
726
802
|
version = Math.max(version, 12);
|
|
727
803
|
}
|
|
804
|
+
if (columnExists(db, "session_summaries", "current_thread")) {
|
|
805
|
+
version = Math.max(version, 13);
|
|
806
|
+
}
|
|
807
|
+
if (tableExists(db, "chat_messages")) {
|
|
808
|
+
version = Math.max(version, 14);
|
|
809
|
+
}
|
|
810
|
+
if (columnExists(db, "chat_messages", "remote_source_id")) {
|
|
811
|
+
version = Math.max(version, 15);
|
|
812
|
+
}
|
|
813
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
814
|
+
version = Math.max(version, 16);
|
|
815
|
+
}
|
|
728
816
|
return version;
|
|
729
817
|
}
|
|
730
818
|
function runMigrations(db) {
|
|
@@ -808,7 +896,8 @@ function ensureSessionSummaryColumns(db) {
|
|
|
808
896
|
"capture_state",
|
|
809
897
|
"recent_tool_names",
|
|
810
898
|
"hot_files",
|
|
811
|
-
"recent_outcomes"
|
|
899
|
+
"recent_outcomes",
|
|
900
|
+
"current_thread"
|
|
812
901
|
];
|
|
813
902
|
for (const column of required) {
|
|
814
903
|
if (columnExists(db, "session_summaries", column))
|
|
@@ -816,10 +905,75 @@ function ensureSessionSummaryColumns(db) {
|
|
|
816
905
|
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
817
906
|
}
|
|
818
907
|
const current = getSchemaVersion(db);
|
|
819
|
-
if (current <
|
|
820
|
-
db.exec("PRAGMA user_version =
|
|
908
|
+
if (current < 13) {
|
|
909
|
+
db.exec("PRAGMA user_version = 13");
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function ensureChatMessageColumns(db) {
|
|
913
|
+
if (!tableExists(db, "chat_messages"))
|
|
914
|
+
return;
|
|
915
|
+
if (!columnExists(db, "chat_messages", "remote_source_id")) {
|
|
916
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
917
|
+
}
|
|
918
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source ON chat_messages(remote_source_id) WHERE remote_source_id IS NOT NULL");
|
|
919
|
+
const current = getSchemaVersion(db);
|
|
920
|
+
if (current < 15) {
|
|
921
|
+
db.exec("PRAGMA user_version = 15");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
925
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
926
|
+
const current = getSchemaVersion(db);
|
|
927
|
+
if (current < 16) {
|
|
928
|
+
db.exec("PRAGMA user_version = 16");
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
db.exec("BEGIN TRANSACTION");
|
|
933
|
+
try {
|
|
934
|
+
db.exec(`
|
|
935
|
+
CREATE TABLE sync_outbox_new (
|
|
936
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
937
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
938
|
+
record_id INTEGER NOT NULL,
|
|
939
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
940
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
941
|
+
)),
|
|
942
|
+
retry_count INTEGER DEFAULT 0,
|
|
943
|
+
max_retries INTEGER DEFAULT 10,
|
|
944
|
+
last_error TEXT,
|
|
945
|
+
created_at_epoch INTEGER NOT NULL,
|
|
946
|
+
synced_at_epoch INTEGER,
|
|
947
|
+
next_retry_epoch INTEGER
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
INSERT INTO sync_outbox_new (
|
|
951
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
952
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
953
|
+
)
|
|
954
|
+
SELECT
|
|
955
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
956
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
957
|
+
FROM sync_outbox;
|
|
958
|
+
|
|
959
|
+
DROP TABLE sync_outbox;
|
|
960
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
961
|
+
|
|
962
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
963
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
964
|
+
`);
|
|
965
|
+
db.exec("PRAGMA user_version = 16");
|
|
966
|
+
db.exec("COMMIT");
|
|
967
|
+
} catch (error) {
|
|
968
|
+
db.exec("ROLLBACK");
|
|
969
|
+
throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
821
970
|
}
|
|
822
971
|
}
|
|
972
|
+
function syncOutboxSupportsChatMessages(db) {
|
|
973
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
|
|
974
|
+
const sql = row?.sql ?? "";
|
|
975
|
+
return sql.includes("'chat_message'");
|
|
976
|
+
}
|
|
823
977
|
function getSchemaVersion(db) {
|
|
824
978
|
const result = db.query("PRAGMA user_version").get();
|
|
825
979
|
return result.user_version;
|
|
@@ -979,6 +1133,8 @@ class MemDatabase {
|
|
|
979
1133
|
runMigrations(this.db);
|
|
980
1134
|
ensureObservationTypes(this.db);
|
|
981
1135
|
ensureSessionSummaryColumns(this.db);
|
|
1136
|
+
ensureChatMessageColumns(this.db);
|
|
1137
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
982
1138
|
}
|
|
983
1139
|
loadVecExtension() {
|
|
984
1140
|
try {
|
|
@@ -1204,6 +1360,7 @@ class MemDatabase {
|
|
|
1204
1360
|
p.name AS project_name,
|
|
1205
1361
|
ss.request AS request,
|
|
1206
1362
|
ss.completed AS completed,
|
|
1363
|
+
ss.current_thread AS current_thread,
|
|
1207
1364
|
ss.capture_state AS capture_state,
|
|
1208
1365
|
ss.recent_tool_names AS recent_tool_names,
|
|
1209
1366
|
ss.hot_files AS hot_files,
|
|
@@ -1222,6 +1379,7 @@ class MemDatabase {
|
|
|
1222
1379
|
p.name AS project_name,
|
|
1223
1380
|
ss.request AS request,
|
|
1224
1381
|
ss.completed AS completed,
|
|
1382
|
+
ss.current_thread AS current_thread,
|
|
1225
1383
|
ss.capture_state AS capture_state,
|
|
1226
1384
|
ss.recent_tool_names AS recent_tool_names,
|
|
1227
1385
|
ss.hot_files AS hot_files,
|
|
@@ -1312,6 +1470,54 @@ class MemDatabase {
|
|
|
1312
1470
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1313
1471
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1314
1472
|
}
|
|
1473
|
+
insertChatMessage(input) {
|
|
1474
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1475
|
+
const content = input.content.trim();
|
|
1476
|
+
const result = this.db.query(`INSERT INTO chat_messages (
|
|
1477
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
1478
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.role, content, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt, input.remote_source_id ?? null);
|
|
1479
|
+
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1480
|
+
}
|
|
1481
|
+
getChatMessageById(id) {
|
|
1482
|
+
return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
|
|
1483
|
+
}
|
|
1484
|
+
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1485
|
+
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1486
|
+
}
|
|
1487
|
+
getSessionChatMessages(sessionId, limit = 50) {
|
|
1488
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1489
|
+
WHERE session_id = ?
|
|
1490
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
1491
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1492
|
+
}
|
|
1493
|
+
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
1494
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1495
|
+
if (projectId !== null) {
|
|
1496
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1497
|
+
WHERE project_id = ?${visibilityClause}
|
|
1498
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1499
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1500
|
+
}
|
|
1501
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1502
|
+
WHERE 1 = 1${visibilityClause}
|
|
1503
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1504
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1505
|
+
}
|
|
1506
|
+
searchChatMessages(query, projectId, limit = 20, userId) {
|
|
1507
|
+
const needle = `%${query.toLowerCase()}%`;
|
|
1508
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1509
|
+
if (projectId !== null) {
|
|
1510
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1511
|
+
WHERE project_id = ?
|
|
1512
|
+
AND lower(content) LIKE ?${visibilityClause}
|
|
1513
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1514
|
+
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1515
|
+
}
|
|
1516
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1517
|
+
WHERE lower(content) LIKE ?${visibilityClause}
|
|
1518
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1519
|
+
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1520
|
+
}
|
|
1315
1521
|
addToOutbox(recordType, recordId) {
|
|
1316
1522
|
const now = Math.floor(Date.now() / 1000);
|
|
1317
1523
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1400,9 +1606,9 @@ class MemDatabase {
|
|
|
1400
1606
|
};
|
|
1401
1607
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1402
1608
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1403
|
-
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1609
|
+
current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1404
1610
|
)
|
|
1405
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
|
|
1611
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.current_thread ?? null, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
|
|
1406
1612
|
const id = Number(result.lastInsertRowid);
|
|
1407
1613
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1408
1614
|
}
|
|
@@ -1418,6 +1624,7 @@ class MemDatabase {
|
|
|
1418
1624
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1419
1625
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1420
1626
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1627
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1421
1628
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1422
1629
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1423
1630
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1431,12 +1638,13 @@ class MemDatabase {
|
|
|
1431
1638
|
learned = ?,
|
|
1432
1639
|
completed = ?,
|
|
1433
1640
|
next_steps = ?,
|
|
1641
|
+
current_thread = ?,
|
|
1434
1642
|
capture_state = ?,
|
|
1435
1643
|
recent_tool_names = ?,
|
|
1436
1644
|
hot_files = ?,
|
|
1437
1645
|
recent_outcomes = ?,
|
|
1438
1646
|
created_at_epoch = ?
|
|
1439
|
-
WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
|
|
1647
|
+
WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.current_thread, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
|
|
1440
1648
|
return this.getSessionSummary(summary.session_id);
|
|
1441
1649
|
}
|
|
1442
1650
|
getSessionSummary(sessionId) {
|