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
|
@@ -483,7 +483,7 @@ var MIGRATIONS = [
|
|
|
483
483
|
-- Sync outbox (offline-first queue)
|
|
484
484
|
CREATE TABLE sync_outbox (
|
|
485
485
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
486
|
-
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
486
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
487
487
|
record_id INTEGER NOT NULL,
|
|
488
488
|
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
489
489
|
'pending', 'syncing', 'synced', 'failed'
|
|
@@ -778,6 +778,18 @@ var MIGRATIONS = [
|
|
|
778
778
|
},
|
|
779
779
|
{
|
|
780
780
|
version: 11,
|
|
781
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
782
|
+
sql: `
|
|
783
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
784
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
785
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
786
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
787
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
788
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
789
|
+
`
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
version: 12,
|
|
781
793
|
description: "Add synced handoff metadata to session summaries",
|
|
782
794
|
sql: `
|
|
783
795
|
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
@@ -787,15 +799,79 @@ var MIGRATIONS = [
|
|
|
787
799
|
`
|
|
788
800
|
},
|
|
789
801
|
{
|
|
790
|
-
version:
|
|
791
|
-
description: "Add
|
|
802
|
+
version: 13,
|
|
803
|
+
description: "Add current_thread to session summaries",
|
|
792
804
|
sql: `
|
|
793
|
-
ALTER TABLE
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
805
|
+
ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
|
|
806
|
+
`
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
version: 14,
|
|
810
|
+
description: "Add chat_messages lane for raw conversation recall",
|
|
811
|
+
sql: `
|
|
812
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
813
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
814
|
+
session_id TEXT NOT NULL,
|
|
815
|
+
project_id INTEGER REFERENCES projects(id),
|
|
816
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
817
|
+
content TEXT NOT NULL,
|
|
818
|
+
user_id TEXT NOT NULL,
|
|
819
|
+
device_id TEXT NOT NULL,
|
|
820
|
+
agent TEXT DEFAULT 'claude-code',
|
|
821
|
+
created_at_epoch INTEGER NOT NULL
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session
|
|
825
|
+
ON chat_messages(session_id, created_at_epoch DESC, id DESC);
|
|
826
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_project
|
|
827
|
+
ON chat_messages(project_id, created_at_epoch DESC, id DESC);
|
|
828
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created
|
|
829
|
+
ON chat_messages(created_at_epoch DESC, id DESC);
|
|
830
|
+
`
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
version: 15,
|
|
834
|
+
description: "Add remote_source_id for chat message sync deduplication",
|
|
835
|
+
sql: `
|
|
836
|
+
ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
|
|
837
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
|
|
838
|
+
ON chat_messages(remote_source_id)
|
|
839
|
+
WHERE remote_source_id IS NOT NULL;
|
|
840
|
+
`
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
version: 16,
|
|
844
|
+
description: "Allow chat_message records in sync_outbox",
|
|
845
|
+
sql: `
|
|
846
|
+
CREATE TABLE sync_outbox_new (
|
|
847
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
848
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
849
|
+
record_id INTEGER NOT NULL,
|
|
850
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
851
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
852
|
+
)),
|
|
853
|
+
retry_count INTEGER DEFAULT 0,
|
|
854
|
+
max_retries INTEGER DEFAULT 10,
|
|
855
|
+
last_error TEXT,
|
|
856
|
+
created_at_epoch INTEGER NOT NULL,
|
|
857
|
+
synced_at_epoch INTEGER,
|
|
858
|
+
next_retry_epoch INTEGER
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
INSERT INTO sync_outbox_new (
|
|
862
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
863
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
864
|
+
)
|
|
865
|
+
SELECT
|
|
866
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
867
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
868
|
+
FROM sync_outbox;
|
|
869
|
+
|
|
870
|
+
DROP TABLE sync_outbox;
|
|
871
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
872
|
+
|
|
873
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
874
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
799
875
|
`
|
|
800
876
|
}
|
|
801
877
|
];
|
|
@@ -852,6 +928,21 @@ function inferLegacySchemaVersion(db) {
|
|
|
852
928
|
version = Math.max(version, 10);
|
|
853
929
|
if (columnExists(db, "observations", "source_tool"))
|
|
854
930
|
version = Math.max(version, 11);
|
|
931
|
+
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")) {
|
|
932
|
+
version = Math.max(version, 12);
|
|
933
|
+
}
|
|
934
|
+
if (columnExists(db, "session_summaries", "current_thread")) {
|
|
935
|
+
version = Math.max(version, 13);
|
|
936
|
+
}
|
|
937
|
+
if (tableExists(db, "chat_messages")) {
|
|
938
|
+
version = Math.max(version, 14);
|
|
939
|
+
}
|
|
940
|
+
if (columnExists(db, "chat_messages", "remote_source_id")) {
|
|
941
|
+
version = Math.max(version, 15);
|
|
942
|
+
}
|
|
943
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
944
|
+
version = Math.max(version, 16);
|
|
945
|
+
}
|
|
855
946
|
return version;
|
|
856
947
|
}
|
|
857
948
|
function runMigrations(db) {
|
|
@@ -930,6 +1021,93 @@ function ensureObservationTypes(db) {
|
|
|
930
1021
|
}
|
|
931
1022
|
}
|
|
932
1023
|
}
|
|
1024
|
+
function ensureSessionSummaryColumns(db) {
|
|
1025
|
+
const required = [
|
|
1026
|
+
"capture_state",
|
|
1027
|
+
"recent_tool_names",
|
|
1028
|
+
"hot_files",
|
|
1029
|
+
"recent_outcomes",
|
|
1030
|
+
"current_thread"
|
|
1031
|
+
];
|
|
1032
|
+
for (const column of required) {
|
|
1033
|
+
if (columnExists(db, "session_summaries", column))
|
|
1034
|
+
continue;
|
|
1035
|
+
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
1036
|
+
}
|
|
1037
|
+
const current = getSchemaVersion(db);
|
|
1038
|
+
if (current < 13) {
|
|
1039
|
+
db.exec("PRAGMA user_version = 13");
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
function ensureChatMessageColumns(db) {
|
|
1043
|
+
if (!tableExists(db, "chat_messages"))
|
|
1044
|
+
return;
|
|
1045
|
+
if (!columnExists(db, "chat_messages", "remote_source_id")) {
|
|
1046
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
1047
|
+
}
|
|
1048
|
+
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");
|
|
1049
|
+
const current = getSchemaVersion(db);
|
|
1050
|
+
if (current < 15) {
|
|
1051
|
+
db.exec("PRAGMA user_version = 15");
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
1055
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
1056
|
+
const current = getSchemaVersion(db);
|
|
1057
|
+
if (current < 16) {
|
|
1058
|
+
db.exec("PRAGMA user_version = 16");
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
db.exec("BEGIN TRANSACTION");
|
|
1063
|
+
try {
|
|
1064
|
+
db.exec(`
|
|
1065
|
+
CREATE TABLE sync_outbox_new (
|
|
1066
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1067
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
1068
|
+
record_id INTEGER NOT NULL,
|
|
1069
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
1070
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
1071
|
+
)),
|
|
1072
|
+
retry_count INTEGER DEFAULT 0,
|
|
1073
|
+
max_retries INTEGER DEFAULT 10,
|
|
1074
|
+
last_error TEXT,
|
|
1075
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1076
|
+
synced_at_epoch INTEGER,
|
|
1077
|
+
next_retry_epoch INTEGER
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
INSERT INTO sync_outbox_new (
|
|
1081
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
1082
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
1083
|
+
)
|
|
1084
|
+
SELECT
|
|
1085
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
1086
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
1087
|
+
FROM sync_outbox;
|
|
1088
|
+
|
|
1089
|
+
DROP TABLE sync_outbox;
|
|
1090
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
1091
|
+
|
|
1092
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
1093
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
1094
|
+
`);
|
|
1095
|
+
db.exec("PRAGMA user_version = 16");
|
|
1096
|
+
db.exec("COMMIT");
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
db.exec("ROLLBACK");
|
|
1099
|
+
throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function syncOutboxSupportsChatMessages(db) {
|
|
1103
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
|
|
1104
|
+
const sql = row?.sql ?? "";
|
|
1105
|
+
return sql.includes("'chat_message'");
|
|
1106
|
+
}
|
|
1107
|
+
function getSchemaVersion(db) {
|
|
1108
|
+
const result = db.query("PRAGMA user_version").get();
|
|
1109
|
+
return result.user_version;
|
|
1110
|
+
}
|
|
933
1111
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
934
1112
|
|
|
935
1113
|
// src/storage/sqlite.ts
|
|
@@ -1084,6 +1262,9 @@ class MemDatabase {
|
|
|
1084
1262
|
this.vecAvailable = this.loadVecExtension();
|
|
1085
1263
|
runMigrations(this.db);
|
|
1086
1264
|
ensureObservationTypes(this.db);
|
|
1265
|
+
ensureSessionSummaryColumns(this.db);
|
|
1266
|
+
ensureChatMessageColumns(this.db);
|
|
1267
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1087
1268
|
}
|
|
1088
1269
|
loadVecExtension() {
|
|
1089
1270
|
try {
|
|
@@ -1309,6 +1490,7 @@ class MemDatabase {
|
|
|
1309
1490
|
p.name AS project_name,
|
|
1310
1491
|
ss.request AS request,
|
|
1311
1492
|
ss.completed AS completed,
|
|
1493
|
+
ss.current_thread AS current_thread,
|
|
1312
1494
|
ss.capture_state AS capture_state,
|
|
1313
1495
|
ss.recent_tool_names AS recent_tool_names,
|
|
1314
1496
|
ss.hot_files AS hot_files,
|
|
@@ -1327,6 +1509,7 @@ class MemDatabase {
|
|
|
1327
1509
|
p.name AS project_name,
|
|
1328
1510
|
ss.request AS request,
|
|
1329
1511
|
ss.completed AS completed,
|
|
1512
|
+
ss.current_thread AS current_thread,
|
|
1330
1513
|
ss.capture_state AS capture_state,
|
|
1331
1514
|
ss.recent_tool_names AS recent_tool_names,
|
|
1332
1515
|
ss.hot_files AS hot_files,
|
|
@@ -1417,6 +1600,54 @@ class MemDatabase {
|
|
|
1417
1600
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1418
1601
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1419
1602
|
}
|
|
1603
|
+
insertChatMessage(input) {
|
|
1604
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1605
|
+
const content = input.content.trim();
|
|
1606
|
+
const result = this.db.query(`INSERT INTO chat_messages (
|
|
1607
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
1608
|
+
) 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);
|
|
1609
|
+
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1610
|
+
}
|
|
1611
|
+
getChatMessageById(id) {
|
|
1612
|
+
return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
|
|
1613
|
+
}
|
|
1614
|
+
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1615
|
+
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1616
|
+
}
|
|
1617
|
+
getSessionChatMessages(sessionId, limit = 50) {
|
|
1618
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1619
|
+
WHERE session_id = ?
|
|
1620
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
1621
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1622
|
+
}
|
|
1623
|
+
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
1624
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1625
|
+
if (projectId !== null) {
|
|
1626
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1627
|
+
WHERE project_id = ?${visibilityClause}
|
|
1628
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1629
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1630
|
+
}
|
|
1631
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1632
|
+
WHERE 1 = 1${visibilityClause}
|
|
1633
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1634
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1635
|
+
}
|
|
1636
|
+
searchChatMessages(query, projectId, limit = 20, userId) {
|
|
1637
|
+
const needle = `%${query.toLowerCase()}%`;
|
|
1638
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1639
|
+
if (projectId !== null) {
|
|
1640
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1641
|
+
WHERE project_id = ?
|
|
1642
|
+
AND lower(content) LIKE ?${visibilityClause}
|
|
1643
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1644
|
+
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1645
|
+
}
|
|
1646
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1647
|
+
WHERE lower(content) LIKE ?${visibilityClause}
|
|
1648
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1649
|
+
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1650
|
+
}
|
|
1420
1651
|
addToOutbox(recordType, recordId) {
|
|
1421
1652
|
const now = Math.floor(Date.now() / 1000);
|
|
1422
1653
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1505,9 +1736,9 @@ class MemDatabase {
|
|
|
1505
1736
|
};
|
|
1506
1737
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1507
1738
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1508
|
-
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1739
|
+
current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1509
1740
|
)
|
|
1510
|
-
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);
|
|
1741
|
+
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);
|
|
1511
1742
|
const id = Number(result.lastInsertRowid);
|
|
1512
1743
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1513
1744
|
}
|
|
@@ -1523,6 +1754,7 @@ class MemDatabase {
|
|
|
1523
1754
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1524
1755
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1525
1756
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1757
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1526
1758
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1527
1759
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1528
1760
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1536,12 +1768,13 @@ class MemDatabase {
|
|
|
1536
1768
|
learned = ?,
|
|
1537
1769
|
completed = ?,
|
|
1538
1770
|
next_steps = ?,
|
|
1771
|
+
current_thread = ?,
|
|
1539
1772
|
capture_state = ?,
|
|
1540
1773
|
recent_tool_names = ?,
|
|
1541
1774
|
hot_files = ?,
|
|
1542
1775
|
recent_outcomes = ?,
|
|
1543
1776
|
created_at_epoch = ?
|
|
1544
|
-
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);
|
|
1777
|
+
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);
|
|
1545
1778
|
return this.getSessionSummary(summary.session_id);
|
|
1546
1779
|
}
|
|
1547
1780
|
getSessionSummary(sessionId) {
|
|
@@ -3252,6 +3485,83 @@ function mergeSectionItem(existing, item) {
|
|
|
3252
3485
|
- ${normalizedItem}`;
|
|
3253
3486
|
}
|
|
3254
3487
|
|
|
3488
|
+
// src/capture/session-handoff.ts
|
|
3489
|
+
function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
3490
|
+
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
3491
|
+
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
3492
|
+
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
3493
|
+
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
3494
|
+
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
3495
|
+
...parseJsonArray(obs.files_modified),
|
|
3496
|
+
...parseJsonArray(obs.files_read)
|
|
3497
|
+
]).filter(Boolean))].slice(0, 6);
|
|
3498
|
+
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
|
|
3499
|
+
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
3500
|
+
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
3501
|
+
if (!obs.source_tool)
|
|
3502
|
+
return acc;
|
|
3503
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
3504
|
+
return acc;
|
|
3505
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
3506
|
+
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
3507
|
+
const currentThread = buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames);
|
|
3508
|
+
return {
|
|
3509
|
+
prompt_count: prompts.length,
|
|
3510
|
+
tool_event_count: toolEvents.length,
|
|
3511
|
+
recent_request_prompts: recentRequestPrompts,
|
|
3512
|
+
latest_request: latestRequest,
|
|
3513
|
+
current_thread: currentThread,
|
|
3514
|
+
recent_tool_names: recentToolNames,
|
|
3515
|
+
recent_tool_commands: recentToolCommands,
|
|
3516
|
+
capture_state: captureState,
|
|
3517
|
+
hot_files: hotFiles,
|
|
3518
|
+
recent_outcomes: recentOutcomes,
|
|
3519
|
+
observation_source_tools: observationSourceTools,
|
|
3520
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames) {
|
|
3524
|
+
const request = compactLine(latestRequest);
|
|
3525
|
+
const outcome = recentOutcomes.map((item) => compactLine(item)).find(Boolean);
|
|
3526
|
+
const file = hotFiles[0] ? compactFileHint(hotFiles[0]) : null;
|
|
3527
|
+
const tools = recentToolNames.slice(0, 2).join("/");
|
|
3528
|
+
if (outcome && file) {
|
|
3529
|
+
return `${outcome} · ${file}${tools ? ` · ${tools}` : ""}`;
|
|
3530
|
+
}
|
|
3531
|
+
if (request && file) {
|
|
3532
|
+
return `${request} · ${file}${tools ? ` · ${tools}` : ""}`;
|
|
3533
|
+
}
|
|
3534
|
+
if (outcome) {
|
|
3535
|
+
return `${outcome}${tools ? ` · ${tools}` : ""}`;
|
|
3536
|
+
}
|
|
3537
|
+
if (request) {
|
|
3538
|
+
return `${request}${tools ? ` · ${tools}` : ""}`;
|
|
3539
|
+
}
|
|
3540
|
+
return null;
|
|
3541
|
+
}
|
|
3542
|
+
function compactLine(value) {
|
|
3543
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
3544
|
+
if (!trimmed)
|
|
3545
|
+
return null;
|
|
3546
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
3547
|
+
}
|
|
3548
|
+
function compactFileHint(value) {
|
|
3549
|
+
const parts = value.split("/");
|
|
3550
|
+
if (parts.length <= 2)
|
|
3551
|
+
return value;
|
|
3552
|
+
return parts.slice(-2).join("/");
|
|
3553
|
+
}
|
|
3554
|
+
function parseJsonArray(value) {
|
|
3555
|
+
if (!value)
|
|
3556
|
+
return [];
|
|
3557
|
+
try {
|
|
3558
|
+
const parsed = JSON.parse(value);
|
|
3559
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
3560
|
+
} catch {
|
|
3561
|
+
return [];
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3255
3565
|
// hooks/post-tool-use.ts
|
|
3256
3566
|
async function main() {
|
|
3257
3567
|
const raw = await readStdin();
|
|
@@ -3450,8 +3760,12 @@ function updateRollingSummaryFromObservation(db, observationId, event, userId) {
|
|
|
3450
3760
|
if (!update)
|
|
3451
3761
|
return;
|
|
3452
3762
|
const existing = db.getSessionSummary(event.session_id);
|
|
3763
|
+
const sessionPrompts = db.getSessionUserPrompts(event.session_id, 20);
|
|
3764
|
+
const sessionToolEvents = db.getSessionToolEvents(event.session_id, 20);
|
|
3765
|
+
const sessionObservations = db.getObservationsBySession(event.session_id);
|
|
3453
3766
|
const merged = mergeLiveSummarySections(existing, update);
|
|
3454
|
-
const
|
|
3767
|
+
const handoff = buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, sessionObservations);
|
|
3768
|
+
const currentRequest = existing?.request ?? handoff.latest_request ?? null;
|
|
3455
3769
|
const summary = db.upsertSessionSummary({
|
|
3456
3770
|
session_id: event.session_id,
|
|
3457
3771
|
project_id: observation.project_id,
|
|
@@ -3460,7 +3774,12 @@ function updateRollingSummaryFromObservation(db, observationId, event, userId) {
|
|
|
3460
3774
|
investigated: merged.investigated,
|
|
3461
3775
|
learned: merged.learned,
|
|
3462
3776
|
completed: merged.completed,
|
|
3463
|
-
next_steps: existing?.next_steps ?? null
|
|
3777
|
+
next_steps: existing?.next_steps ?? null,
|
|
3778
|
+
current_thread: handoff.current_thread,
|
|
3779
|
+
capture_state: handoff.capture_state,
|
|
3780
|
+
recent_tool_names: JSON.stringify(handoff.recent_tool_names),
|
|
3781
|
+
hot_files: JSON.stringify(handoff.hot_files),
|
|
3782
|
+
recent_outcomes: JSON.stringify(handoff.recent_outcomes)
|
|
3464
3783
|
});
|
|
3465
3784
|
db.addToOutbox("summary", summary.id);
|
|
3466
3785
|
}
|