engrm 0.4.21 → 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 +326 -21
- package/dist/hooks/elicitation-result.js +245 -12
- package/dist/hooks/post-tool-use.js +333 -14
- package/dist/hooks/pre-compact.js +984 -14
- package/dist/hooks/sentinel.js +245 -12
- package/dist/hooks/session-start.js +1097 -90
- package/dist/hooks/stop.js +430 -75
- package/dist/hooks/user-prompt-submit.js +342 -13
- package/dist/server.js +923 -86
- 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'
|
|
@@ -648,6 +648,18 @@ var MIGRATIONS = [
|
|
|
648
648
|
},
|
|
649
649
|
{
|
|
650
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
|
+
},
|
|
661
|
+
{
|
|
662
|
+
version: 12,
|
|
651
663
|
description: "Add synced handoff metadata to session summaries",
|
|
652
664
|
sql: `
|
|
653
665
|
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
@@ -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
|
];
|
|
@@ -722,6 +798,21 @@ function inferLegacySchemaVersion(db) {
|
|
|
722
798
|
version = Math.max(version, 10);
|
|
723
799
|
if (columnExists(db, "observations", "source_tool"))
|
|
724
800
|
version = Math.max(version, 11);
|
|
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")) {
|
|
802
|
+
version = Math.max(version, 12);
|
|
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
|
+
}
|
|
725
816
|
return version;
|
|
726
817
|
}
|
|
727
818
|
function runMigrations(db) {
|
|
@@ -800,6 +891,93 @@ function ensureObservationTypes(db) {
|
|
|
800
891
|
}
|
|
801
892
|
}
|
|
802
893
|
}
|
|
894
|
+
function ensureSessionSummaryColumns(db) {
|
|
895
|
+
const required = [
|
|
896
|
+
"capture_state",
|
|
897
|
+
"recent_tool_names",
|
|
898
|
+
"hot_files",
|
|
899
|
+
"recent_outcomes",
|
|
900
|
+
"current_thread"
|
|
901
|
+
];
|
|
902
|
+
for (const column of required) {
|
|
903
|
+
if (columnExists(db, "session_summaries", column))
|
|
904
|
+
continue;
|
|
905
|
+
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
906
|
+
}
|
|
907
|
+
const current = getSchemaVersion(db);
|
|
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)}`);
|
|
970
|
+
}
|
|
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
|
+
}
|
|
977
|
+
function getSchemaVersion(db) {
|
|
978
|
+
const result = db.query("PRAGMA user_version").get();
|
|
979
|
+
return result.user_version;
|
|
980
|
+
}
|
|
803
981
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
804
982
|
|
|
805
983
|
// src/storage/sqlite.ts
|
|
@@ -954,6 +1132,9 @@ class MemDatabase {
|
|
|
954
1132
|
this.vecAvailable = this.loadVecExtension();
|
|
955
1133
|
runMigrations(this.db);
|
|
956
1134
|
ensureObservationTypes(this.db);
|
|
1135
|
+
ensureSessionSummaryColumns(this.db);
|
|
1136
|
+
ensureChatMessageColumns(this.db);
|
|
1137
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
957
1138
|
}
|
|
958
1139
|
loadVecExtension() {
|
|
959
1140
|
try {
|
|
@@ -1179,6 +1360,7 @@ class MemDatabase {
|
|
|
1179
1360
|
p.name AS project_name,
|
|
1180
1361
|
ss.request AS request,
|
|
1181
1362
|
ss.completed AS completed,
|
|
1363
|
+
ss.current_thread AS current_thread,
|
|
1182
1364
|
ss.capture_state AS capture_state,
|
|
1183
1365
|
ss.recent_tool_names AS recent_tool_names,
|
|
1184
1366
|
ss.hot_files AS hot_files,
|
|
@@ -1197,6 +1379,7 @@ class MemDatabase {
|
|
|
1197
1379
|
p.name AS project_name,
|
|
1198
1380
|
ss.request AS request,
|
|
1199
1381
|
ss.completed AS completed,
|
|
1382
|
+
ss.current_thread AS current_thread,
|
|
1200
1383
|
ss.capture_state AS capture_state,
|
|
1201
1384
|
ss.recent_tool_names AS recent_tool_names,
|
|
1202
1385
|
ss.hot_files AS hot_files,
|
|
@@ -1287,6 +1470,54 @@ class MemDatabase {
|
|
|
1287
1470
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1288
1471
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1289
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
|
+
}
|
|
1290
1521
|
addToOutbox(recordType, recordId) {
|
|
1291
1522
|
const now = Math.floor(Date.now() / 1000);
|
|
1292
1523
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1375,9 +1606,9 @@ class MemDatabase {
|
|
|
1375
1606
|
};
|
|
1376
1607
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1377
1608
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1378
|
-
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
|
|
1379
1610
|
)
|
|
1380
|
-
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);
|
|
1381
1612
|
const id = Number(result.lastInsertRowid);
|
|
1382
1613
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1383
1614
|
}
|
|
@@ -1393,6 +1624,7 @@ class MemDatabase {
|
|
|
1393
1624
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1394
1625
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1395
1626
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1627
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1396
1628
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1397
1629
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1398
1630
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1406,12 +1638,13 @@ class MemDatabase {
|
|
|
1406
1638
|
learned = ?,
|
|
1407
1639
|
completed = ?,
|
|
1408
1640
|
next_steps = ?,
|
|
1641
|
+
current_thread = ?,
|
|
1409
1642
|
capture_state = ?,
|
|
1410
1643
|
recent_tool_names = ?,
|
|
1411
1644
|
hot_files = ?,
|
|
1412
1645
|
recent_outcomes = ?,
|
|
1413
1646
|
created_at_epoch = ?
|
|
1414
|
-
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);
|
|
1415
1648
|
return this.getSessionSummary(summary.session_id);
|
|
1416
1649
|
}
|
|
1417
1650
|
getSessionSummary(sessionId) {
|