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/stop.js
CHANGED
|
@@ -590,7 +590,7 @@ var MIGRATIONS = [
|
|
|
590
590
|
-- Sync outbox (offline-first queue)
|
|
591
591
|
CREATE TABLE sync_outbox (
|
|
592
592
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
593
|
-
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
593
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
594
594
|
record_id INTEGER NOT NULL,
|
|
595
595
|
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
596
596
|
'pending', 'syncing', 'synced', 'failed'
|
|
@@ -885,6 +885,18 @@ var MIGRATIONS = [
|
|
|
885
885
|
},
|
|
886
886
|
{
|
|
887
887
|
version: 11,
|
|
888
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
889
|
+
sql: `
|
|
890
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
891
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
892
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
893
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
894
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
895
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
896
|
+
`
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
version: 12,
|
|
888
900
|
description: "Add synced handoff metadata to session summaries",
|
|
889
901
|
sql: `
|
|
890
902
|
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
@@ -894,15 +906,79 @@ var MIGRATIONS = [
|
|
|
894
906
|
`
|
|
895
907
|
},
|
|
896
908
|
{
|
|
897
|
-
version:
|
|
898
|
-
description: "Add
|
|
909
|
+
version: 13,
|
|
910
|
+
description: "Add current_thread to session summaries",
|
|
899
911
|
sql: `
|
|
900
|
-
ALTER TABLE
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
912
|
+
ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
|
|
913
|
+
`
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
version: 14,
|
|
917
|
+
description: "Add chat_messages lane for raw conversation recall",
|
|
918
|
+
sql: `
|
|
919
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
920
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
921
|
+
session_id TEXT NOT NULL,
|
|
922
|
+
project_id INTEGER REFERENCES projects(id),
|
|
923
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
924
|
+
content TEXT NOT NULL,
|
|
925
|
+
user_id TEXT NOT NULL,
|
|
926
|
+
device_id TEXT NOT NULL,
|
|
927
|
+
agent TEXT DEFAULT 'claude-code',
|
|
928
|
+
created_at_epoch INTEGER NOT NULL
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session
|
|
932
|
+
ON chat_messages(session_id, created_at_epoch DESC, id DESC);
|
|
933
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_project
|
|
934
|
+
ON chat_messages(project_id, created_at_epoch DESC, id DESC);
|
|
935
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created
|
|
936
|
+
ON chat_messages(created_at_epoch DESC, id DESC);
|
|
937
|
+
`
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
version: 15,
|
|
941
|
+
description: "Add remote_source_id for chat message sync deduplication",
|
|
942
|
+
sql: `
|
|
943
|
+
ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
|
|
944
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
|
|
945
|
+
ON chat_messages(remote_source_id)
|
|
946
|
+
WHERE remote_source_id IS NOT NULL;
|
|
947
|
+
`
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
version: 16,
|
|
951
|
+
description: "Allow chat_message records in sync_outbox",
|
|
952
|
+
sql: `
|
|
953
|
+
CREATE TABLE sync_outbox_new (
|
|
954
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
955
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
956
|
+
record_id INTEGER NOT NULL,
|
|
957
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
958
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
959
|
+
)),
|
|
960
|
+
retry_count INTEGER DEFAULT 0,
|
|
961
|
+
max_retries INTEGER DEFAULT 10,
|
|
962
|
+
last_error TEXT,
|
|
963
|
+
created_at_epoch INTEGER NOT NULL,
|
|
964
|
+
synced_at_epoch INTEGER,
|
|
965
|
+
next_retry_epoch INTEGER
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
INSERT INTO sync_outbox_new (
|
|
969
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
970
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
971
|
+
)
|
|
972
|
+
SELECT
|
|
973
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
974
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
975
|
+
FROM sync_outbox;
|
|
976
|
+
|
|
977
|
+
DROP TABLE sync_outbox;
|
|
978
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
979
|
+
|
|
980
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
981
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
906
982
|
`
|
|
907
983
|
}
|
|
908
984
|
];
|
|
@@ -959,6 +1035,21 @@ function inferLegacySchemaVersion(db) {
|
|
|
959
1035
|
version = Math.max(version, 10);
|
|
960
1036
|
if (columnExists(db, "observations", "source_tool"))
|
|
961
1037
|
version = Math.max(version, 11);
|
|
1038
|
+
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")) {
|
|
1039
|
+
version = Math.max(version, 12);
|
|
1040
|
+
}
|
|
1041
|
+
if (columnExists(db, "session_summaries", "current_thread")) {
|
|
1042
|
+
version = Math.max(version, 13);
|
|
1043
|
+
}
|
|
1044
|
+
if (tableExists(db, "chat_messages")) {
|
|
1045
|
+
version = Math.max(version, 14);
|
|
1046
|
+
}
|
|
1047
|
+
if (columnExists(db, "chat_messages", "remote_source_id")) {
|
|
1048
|
+
version = Math.max(version, 15);
|
|
1049
|
+
}
|
|
1050
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
1051
|
+
version = Math.max(version, 16);
|
|
1052
|
+
}
|
|
962
1053
|
return version;
|
|
963
1054
|
}
|
|
964
1055
|
function runMigrations(db) {
|
|
@@ -1037,6 +1128,93 @@ function ensureObservationTypes(db) {
|
|
|
1037
1128
|
}
|
|
1038
1129
|
}
|
|
1039
1130
|
}
|
|
1131
|
+
function ensureSessionSummaryColumns(db) {
|
|
1132
|
+
const required = [
|
|
1133
|
+
"capture_state",
|
|
1134
|
+
"recent_tool_names",
|
|
1135
|
+
"hot_files",
|
|
1136
|
+
"recent_outcomes",
|
|
1137
|
+
"current_thread"
|
|
1138
|
+
];
|
|
1139
|
+
for (const column of required) {
|
|
1140
|
+
if (columnExists(db, "session_summaries", column))
|
|
1141
|
+
continue;
|
|
1142
|
+
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
1143
|
+
}
|
|
1144
|
+
const current = getSchemaVersion(db);
|
|
1145
|
+
if (current < 13) {
|
|
1146
|
+
db.exec("PRAGMA user_version = 13");
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function ensureChatMessageColumns(db) {
|
|
1150
|
+
if (!tableExists(db, "chat_messages"))
|
|
1151
|
+
return;
|
|
1152
|
+
if (!columnExists(db, "chat_messages", "remote_source_id")) {
|
|
1153
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
1154
|
+
}
|
|
1155
|
+
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");
|
|
1156
|
+
const current = getSchemaVersion(db);
|
|
1157
|
+
if (current < 15) {
|
|
1158
|
+
db.exec("PRAGMA user_version = 15");
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
1162
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
1163
|
+
const current = getSchemaVersion(db);
|
|
1164
|
+
if (current < 16) {
|
|
1165
|
+
db.exec("PRAGMA user_version = 16");
|
|
1166
|
+
}
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
db.exec("BEGIN TRANSACTION");
|
|
1170
|
+
try {
|
|
1171
|
+
db.exec(`
|
|
1172
|
+
CREATE TABLE sync_outbox_new (
|
|
1173
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1174
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
1175
|
+
record_id INTEGER NOT NULL,
|
|
1176
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
1177
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
1178
|
+
)),
|
|
1179
|
+
retry_count INTEGER DEFAULT 0,
|
|
1180
|
+
max_retries INTEGER DEFAULT 10,
|
|
1181
|
+
last_error TEXT,
|
|
1182
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1183
|
+
synced_at_epoch INTEGER,
|
|
1184
|
+
next_retry_epoch INTEGER
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
INSERT INTO sync_outbox_new (
|
|
1188
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
1189
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
1190
|
+
)
|
|
1191
|
+
SELECT
|
|
1192
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
1193
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
1194
|
+
FROM sync_outbox;
|
|
1195
|
+
|
|
1196
|
+
DROP TABLE sync_outbox;
|
|
1197
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
1198
|
+
|
|
1199
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
1200
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
1201
|
+
`);
|
|
1202
|
+
db.exec("PRAGMA user_version = 16");
|
|
1203
|
+
db.exec("COMMIT");
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
db.exec("ROLLBACK");
|
|
1206
|
+
throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function syncOutboxSupportsChatMessages(db) {
|
|
1210
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
|
|
1211
|
+
const sql = row?.sql ?? "";
|
|
1212
|
+
return sql.includes("'chat_message'");
|
|
1213
|
+
}
|
|
1214
|
+
function getSchemaVersion(db) {
|
|
1215
|
+
const result = db.query("PRAGMA user_version").get();
|
|
1216
|
+
return result.user_version;
|
|
1217
|
+
}
|
|
1040
1218
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
1041
1219
|
|
|
1042
1220
|
// src/storage/sqlite.ts
|
|
@@ -1111,6 +1289,9 @@ class MemDatabase {
|
|
|
1111
1289
|
this.vecAvailable = this.loadVecExtension();
|
|
1112
1290
|
runMigrations(this.db);
|
|
1113
1291
|
ensureObservationTypes(this.db);
|
|
1292
|
+
ensureSessionSummaryColumns(this.db);
|
|
1293
|
+
ensureChatMessageColumns(this.db);
|
|
1294
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1114
1295
|
}
|
|
1115
1296
|
loadVecExtension() {
|
|
1116
1297
|
try {
|
|
@@ -1336,6 +1517,7 @@ class MemDatabase {
|
|
|
1336
1517
|
p.name AS project_name,
|
|
1337
1518
|
ss.request AS request,
|
|
1338
1519
|
ss.completed AS completed,
|
|
1520
|
+
ss.current_thread AS current_thread,
|
|
1339
1521
|
ss.capture_state AS capture_state,
|
|
1340
1522
|
ss.recent_tool_names AS recent_tool_names,
|
|
1341
1523
|
ss.hot_files AS hot_files,
|
|
@@ -1354,6 +1536,7 @@ class MemDatabase {
|
|
|
1354
1536
|
p.name AS project_name,
|
|
1355
1537
|
ss.request AS request,
|
|
1356
1538
|
ss.completed AS completed,
|
|
1539
|
+
ss.current_thread AS current_thread,
|
|
1357
1540
|
ss.capture_state AS capture_state,
|
|
1358
1541
|
ss.recent_tool_names AS recent_tool_names,
|
|
1359
1542
|
ss.hot_files AS hot_files,
|
|
@@ -1444,6 +1627,54 @@ class MemDatabase {
|
|
|
1444
1627
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1445
1628
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1446
1629
|
}
|
|
1630
|
+
insertChatMessage(input) {
|
|
1631
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1632
|
+
const content = input.content.trim();
|
|
1633
|
+
const result = this.db.query(`INSERT INTO chat_messages (
|
|
1634
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
1635
|
+
) 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);
|
|
1636
|
+
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1637
|
+
}
|
|
1638
|
+
getChatMessageById(id) {
|
|
1639
|
+
return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
|
|
1640
|
+
}
|
|
1641
|
+
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1642
|
+
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1643
|
+
}
|
|
1644
|
+
getSessionChatMessages(sessionId, limit = 50) {
|
|
1645
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1646
|
+
WHERE session_id = ?
|
|
1647
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
1648
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1649
|
+
}
|
|
1650
|
+
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
1651
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1652
|
+
if (projectId !== null) {
|
|
1653
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1654
|
+
WHERE project_id = ?${visibilityClause}
|
|
1655
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1656
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1657
|
+
}
|
|
1658
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1659
|
+
WHERE 1 = 1${visibilityClause}
|
|
1660
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1661
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1662
|
+
}
|
|
1663
|
+
searchChatMessages(query, projectId, limit = 20, userId) {
|
|
1664
|
+
const needle = `%${query.toLowerCase()}%`;
|
|
1665
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1666
|
+
if (projectId !== null) {
|
|
1667
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1668
|
+
WHERE project_id = ?
|
|
1669
|
+
AND lower(content) LIKE ?${visibilityClause}
|
|
1670
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1671
|
+
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1672
|
+
}
|
|
1673
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1674
|
+
WHERE lower(content) LIKE ?${visibilityClause}
|
|
1675
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1676
|
+
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1677
|
+
}
|
|
1447
1678
|
addToOutbox(recordType, recordId) {
|
|
1448
1679
|
const now = Math.floor(Date.now() / 1000);
|
|
1449
1680
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1532,9 +1763,9 @@ class MemDatabase {
|
|
|
1532
1763
|
};
|
|
1533
1764
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1534
1765
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1535
|
-
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1766
|
+
current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1536
1767
|
)
|
|
1537
|
-
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);
|
|
1768
|
+
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);
|
|
1538
1769
|
const id = Number(result.lastInsertRowid);
|
|
1539
1770
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1540
1771
|
}
|
|
@@ -1550,6 +1781,7 @@ class MemDatabase {
|
|
|
1550
1781
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1551
1782
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1552
1783
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1784
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1553
1785
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1554
1786
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1555
1787
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1563,12 +1795,13 @@ class MemDatabase {
|
|
|
1563
1795
|
learned = ?,
|
|
1564
1796
|
completed = ?,
|
|
1565
1797
|
next_steps = ?,
|
|
1798
|
+
current_thread = ?,
|
|
1566
1799
|
capture_state = ?,
|
|
1567
1800
|
recent_tool_names = ?,
|
|
1568
1801
|
hot_files = ?,
|
|
1569
1802
|
recent_outcomes = ?,
|
|
1570
1803
|
created_at_epoch = ?
|
|
1571
|
-
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);
|
|
1804
|
+
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);
|
|
1572
1805
|
return this.getSessionSummary(summary.session_id);
|
|
1573
1806
|
}
|
|
1574
1807
|
getSessionSummary(sessionId) {
|
|
@@ -1829,22 +2062,27 @@ function buildSourceId(config, localId, type = "obs") {
|
|
|
1829
2062
|
return `${config.user_id}-${config.device_id}-${type}-${localId}`;
|
|
1830
2063
|
}
|
|
1831
2064
|
function parseSourceId(sourceId) {
|
|
1832
|
-
const
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2065
|
+
for (const type of ["obs", "summary", "chat"]) {
|
|
2066
|
+
const marker = `-${type}-`;
|
|
2067
|
+
const idx = sourceId.lastIndexOf(marker);
|
|
2068
|
+
if (idx === -1)
|
|
2069
|
+
continue;
|
|
2070
|
+
const prefix = sourceId.slice(0, idx);
|
|
2071
|
+
const localIdStr = sourceId.slice(idx + marker.length);
|
|
2072
|
+
const localId = parseInt(localIdStr, 10);
|
|
2073
|
+
if (isNaN(localId))
|
|
2074
|
+
return null;
|
|
2075
|
+
const firstDash = prefix.indexOf("-");
|
|
2076
|
+
if (firstDash === -1)
|
|
2077
|
+
return null;
|
|
2078
|
+
return {
|
|
2079
|
+
userId: prefix.slice(0, firstDash),
|
|
2080
|
+
deviceId: prefix.slice(firstDash + 1),
|
|
2081
|
+
localId,
|
|
2082
|
+
type
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
return null;
|
|
1848
2086
|
}
|
|
1849
2087
|
|
|
1850
2088
|
// src/sync/client.ts
|
|
@@ -2001,7 +2239,106 @@ function computeSessionValueSignals(observations, securityFindings = []) {
|
|
|
2001
2239
|
};
|
|
2002
2240
|
}
|
|
2003
2241
|
|
|
2242
|
+
// src/capture/session-handoff.ts
|
|
2243
|
+
function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
2244
|
+
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
2245
|
+
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
2246
|
+
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
2247
|
+
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
2248
|
+
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
2249
|
+
...parseJsonArray2(obs.files_modified),
|
|
2250
|
+
...parseJsonArray2(obs.files_read)
|
|
2251
|
+
]).filter(Boolean))].slice(0, 6);
|
|
2252
|
+
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);
|
|
2253
|
+
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
2254
|
+
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
2255
|
+
if (!obs.source_tool)
|
|
2256
|
+
return acc;
|
|
2257
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
2258
|
+
return acc;
|
|
2259
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
2260
|
+
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
2261
|
+
const currentThread = buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames);
|
|
2262
|
+
return {
|
|
2263
|
+
prompt_count: prompts.length,
|
|
2264
|
+
tool_event_count: toolEvents.length,
|
|
2265
|
+
recent_request_prompts: recentRequestPrompts,
|
|
2266
|
+
latest_request: latestRequest,
|
|
2267
|
+
current_thread: currentThread,
|
|
2268
|
+
recent_tool_names: recentToolNames,
|
|
2269
|
+
recent_tool_commands: recentToolCommands,
|
|
2270
|
+
capture_state: captureState,
|
|
2271
|
+
hot_files: hotFiles,
|
|
2272
|
+
recent_outcomes: recentOutcomes,
|
|
2273
|
+
observation_source_tools: observationSourceTools,
|
|
2274
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames) {
|
|
2278
|
+
const request = compactLine(latestRequest);
|
|
2279
|
+
const outcome = recentOutcomes.map((item) => compactLine(item)).find(Boolean);
|
|
2280
|
+
const file = hotFiles[0] ? compactFileHint(hotFiles[0]) : null;
|
|
2281
|
+
const tools = recentToolNames.slice(0, 2).join("/");
|
|
2282
|
+
if (outcome && file) {
|
|
2283
|
+
return `${outcome} · ${file}${tools ? ` · ${tools}` : ""}`;
|
|
2284
|
+
}
|
|
2285
|
+
if (request && file) {
|
|
2286
|
+
return `${request} · ${file}${tools ? ` · ${tools}` : ""}`;
|
|
2287
|
+
}
|
|
2288
|
+
if (outcome) {
|
|
2289
|
+
return `${outcome}${tools ? ` · ${tools}` : ""}`;
|
|
2290
|
+
}
|
|
2291
|
+
if (request) {
|
|
2292
|
+
return `${request}${tools ? ` · ${tools}` : ""}`;
|
|
2293
|
+
}
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
2296
|
+
function compactLine(value) {
|
|
2297
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
2298
|
+
if (!trimmed)
|
|
2299
|
+
return null;
|
|
2300
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
2301
|
+
}
|
|
2302
|
+
function compactFileHint(value) {
|
|
2303
|
+
const parts = value.split("/");
|
|
2304
|
+
if (parts.length <= 2)
|
|
2305
|
+
return value;
|
|
2306
|
+
return parts.slice(-2).join("/");
|
|
2307
|
+
}
|
|
2308
|
+
function parseJsonArray2(value) {
|
|
2309
|
+
if (!value)
|
|
2310
|
+
return [];
|
|
2311
|
+
try {
|
|
2312
|
+
const parsed = JSON.parse(value);
|
|
2313
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2314
|
+
} catch {
|
|
2315
|
+
return [];
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2004
2319
|
// src/sync/push.ts
|
|
2320
|
+
function buildChatVectorDocument(chat, config, project) {
|
|
2321
|
+
return {
|
|
2322
|
+
site_id: config.site_id,
|
|
2323
|
+
namespace: config.namespace,
|
|
2324
|
+
source_type: "chat",
|
|
2325
|
+
source_id: buildSourceId(config, chat.id, "chat"),
|
|
2326
|
+
content: chat.content,
|
|
2327
|
+
metadata: {
|
|
2328
|
+
project_canonical: project.canonical_id,
|
|
2329
|
+
project_name: project.name,
|
|
2330
|
+
user_id: chat.user_id,
|
|
2331
|
+
device_id: chat.device_id,
|
|
2332
|
+
device_name: __require("node:os").hostname(),
|
|
2333
|
+
agent: chat.agent,
|
|
2334
|
+
type: "chat",
|
|
2335
|
+
role: chat.role,
|
|
2336
|
+
session_id: chat.session_id,
|
|
2337
|
+
created_at_epoch: chat.created_at_epoch,
|
|
2338
|
+
local_id: chat.id
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2005
2342
|
function buildVectorDocument(obs, config, project) {
|
|
2006
2343
|
const parts = [obs.title];
|
|
2007
2344
|
if (obs.narrative)
|
|
@@ -2082,6 +2419,7 @@ function buildSummaryVectorDocument(summary, config, project, observations = [],
|
|
|
2082
2419
|
learned: summary.learned,
|
|
2083
2420
|
completed: summary.completed,
|
|
2084
2421
|
next_steps: summary.next_steps,
|
|
2422
|
+
current_thread: summary.current_thread,
|
|
2085
2423
|
summary_sections_present: countPresentSections(summary),
|
|
2086
2424
|
investigated_items: extractSectionItems(summary.investigated),
|
|
2087
2425
|
learned_items: extractSectionItems(summary.learned),
|
|
@@ -2092,6 +2430,7 @@ function buildSummaryVectorDocument(summary, config, project, observations = [],
|
|
|
2092
2430
|
capture_state: captureContext?.capture_state ?? "summary-only",
|
|
2093
2431
|
recent_request_prompts: captureContext?.recent_request_prompts ?? [],
|
|
2094
2432
|
latest_request: captureContext?.latest_request ?? null,
|
|
2433
|
+
current_thread: captureContext?.current_thread ?? null,
|
|
2095
2434
|
recent_tool_names: captureContext?.recent_tool_names ?? [],
|
|
2096
2435
|
recent_tool_commands: captureContext?.recent_tool_commands ?? [],
|
|
2097
2436
|
hot_files: captureContext?.hot_files ?? [],
|
|
@@ -2141,7 +2480,33 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
2141
2480
|
const doc2 = buildSummaryVectorDocument(summary, config, {
|
|
2142
2481
|
canonical_id: project2.canonical_id,
|
|
2143
2482
|
name: project2.name
|
|
2144
|
-
}, summaryObservations,
|
|
2483
|
+
}, summaryObservations, buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, summaryObservations));
|
|
2484
|
+
batch.push({ entryId: entry.id, doc: doc2 });
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
if (entry.record_type === "chat_message") {
|
|
2488
|
+
const chat = db.getChatMessageById(entry.record_id);
|
|
2489
|
+
if (!chat || chat.remote_source_id) {
|
|
2490
|
+
markSynced(db, entry.id);
|
|
2491
|
+
skipped++;
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
if (!chat.project_id) {
|
|
2495
|
+
markSynced(db, entry.id);
|
|
2496
|
+
skipped++;
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
const project2 = db.getProjectById(chat.project_id);
|
|
2500
|
+
if (!project2) {
|
|
2501
|
+
markSynced(db, entry.id);
|
|
2502
|
+
skipped++;
|
|
2503
|
+
continue;
|
|
2504
|
+
}
|
|
2505
|
+
markSyncing(db, entry.id);
|
|
2506
|
+
const doc2 = buildChatVectorDocument(chat, config, {
|
|
2507
|
+
canonical_id: project2.canonical_id,
|
|
2508
|
+
name: project2.name
|
|
2509
|
+
});
|
|
2145
2510
|
batch.push({ entryId: entry.id, doc: doc2 });
|
|
2146
2511
|
continue;
|
|
2147
2512
|
}
|
|
@@ -2182,7 +2547,13 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
2182
2547
|
return { pushed, failed, skipped };
|
|
2183
2548
|
try {
|
|
2184
2549
|
await client.batchIngest(batch.map((b) => b.doc));
|
|
2185
|
-
for (const { entryId } of batch) {
|
|
2550
|
+
for (const { entryId, doc } of batch) {
|
|
2551
|
+
if (doc.source_type === "chat") {
|
|
2552
|
+
const localId = typeof doc.metadata?.local_id === "number" ? doc.metadata.local_id : null;
|
|
2553
|
+
if (localId !== null) {
|
|
2554
|
+
db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc.source_id, localId);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2186
2557
|
markSynced(db, entryId);
|
|
2187
2558
|
pushed++;
|
|
2188
2559
|
}
|
|
@@ -2190,6 +2561,12 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
2190
2561
|
for (const { entryId, doc } of batch) {
|
|
2191
2562
|
try {
|
|
2192
2563
|
await client.ingest(doc);
|
|
2564
|
+
if (doc.source_type === "chat") {
|
|
2565
|
+
const localId = typeof doc.metadata?.local_id === "number" ? doc.metadata.local_id : null;
|
|
2566
|
+
if (localId !== null) {
|
|
2567
|
+
db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc.source_id, localId);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2193
2570
|
markSynced(db, entryId);
|
|
2194
2571
|
pushed++;
|
|
2195
2572
|
} catch (err) {
|
|
@@ -2212,48 +2589,6 @@ function countPresentSections(summary) {
|
|
|
2212
2589
|
function extractSectionItems(section) {
|
|
2213
2590
|
return extractSummaryItems(section, 4);
|
|
2214
2591
|
}
|
|
2215
|
-
function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
2216
|
-
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
2217
|
-
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
2218
|
-
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
2219
|
-
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
2220
|
-
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
2221
|
-
...parseJsonArray2(obs.files_modified),
|
|
2222
|
-
...parseJsonArray2(obs.files_read)
|
|
2223
|
-
]).filter(Boolean))].slice(0, 6);
|
|
2224
|
-
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);
|
|
2225
|
-
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
2226
|
-
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
2227
|
-
if (!obs.source_tool)
|
|
2228
|
-
return acc;
|
|
2229
|
-
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
2230
|
-
return acc;
|
|
2231
|
-
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
2232
|
-
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
2233
|
-
return {
|
|
2234
|
-
prompt_count: prompts.length,
|
|
2235
|
-
tool_event_count: toolEvents.length,
|
|
2236
|
-
recent_request_prompts: recentRequestPrompts,
|
|
2237
|
-
latest_request: latestRequest,
|
|
2238
|
-
recent_tool_names: recentToolNames,
|
|
2239
|
-
recent_tool_commands: recentToolCommands,
|
|
2240
|
-
capture_state: captureState,
|
|
2241
|
-
hot_files: hotFiles,
|
|
2242
|
-
recent_outcomes: recentOutcomes,
|
|
2243
|
-
observation_source_tools: observationSourceTools,
|
|
2244
|
-
latest_observation_prompt_number: latestObservationPromptNumber
|
|
2245
|
-
};
|
|
2246
|
-
}
|
|
2247
|
-
function parseJsonArray2(value) {
|
|
2248
|
-
if (!value)
|
|
2249
|
-
return [];
|
|
2250
|
-
try {
|
|
2251
|
-
const parsed = JSON.parse(value);
|
|
2252
|
-
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2253
|
-
} catch {
|
|
2254
|
-
return [];
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
2592
|
|
|
2258
2593
|
// src/embeddings/embedder.ts
|
|
2259
2594
|
var _available = null;
|
|
@@ -2587,7 +2922,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
2587
2922
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
2588
2923
|
risk_score: riskScore,
|
|
2589
2924
|
stacks_detected: stacks,
|
|
2590
|
-
client_version: "0.4.
|
|
2925
|
+
client_version: "0.4.23",
|
|
2591
2926
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
2592
2927
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
2593
2928
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3612,6 +3947,26 @@ async function main() {
|
|
|
3612
3947
|
db.completeSession(event.session_id);
|
|
3613
3948
|
if (event.last_assistant_message) {
|
|
3614
3949
|
try {
|
|
3950
|
+
const detected = detectProject(event.cwd);
|
|
3951
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id) ?? db.upsertProject({
|
|
3952
|
+
canonical_id: detected.canonical_id,
|
|
3953
|
+
name: detected.name,
|
|
3954
|
+
local_path: event.cwd,
|
|
3955
|
+
remote_url: detected.remote_url ?? null
|
|
3956
|
+
});
|
|
3957
|
+
const compactAssistant = event.last_assistant_message.replace(/\s+/g, " ").trim();
|
|
3958
|
+
if (compactAssistant.length >= 24) {
|
|
3959
|
+
const chatMessage = db.insertChatMessage({
|
|
3960
|
+
session_id: event.session_id,
|
|
3961
|
+
project_id: project.id,
|
|
3962
|
+
role: "assistant",
|
|
3963
|
+
content: event.last_assistant_message,
|
|
3964
|
+
user_id: config.user_id,
|
|
3965
|
+
device_id: config.device_id,
|
|
3966
|
+
agent: "claude-code"
|
|
3967
|
+
});
|
|
3968
|
+
db.addToOutbox("chat_message", chatMessage.id);
|
|
3969
|
+
}
|
|
3615
3970
|
createAssistantCheckpoint(db, event.session_id, event.cwd, event.last_assistant_message);
|
|
3616
3971
|
} catch {}
|
|
3617
3972
|
}
|
|
@@ -3623,7 +3978,7 @@ async function main() {
|
|
|
3623
3978
|
const assistantSections = extractAssistantSummarySections(event.last_assistant_message);
|
|
3624
3979
|
const summary = mergeSessionSummary(retrospective, assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? mergeSessionSummary(buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message), assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message);
|
|
3625
3980
|
if (summary) {
|
|
3626
|
-
const row = db.
|
|
3981
|
+
const row = db.upsertSessionSummary(summary);
|
|
3627
3982
|
db.addToOutbox("summary", row.id);
|
|
3628
3983
|
let securityFindings = [];
|
|
3629
3984
|
try {
|
|
@@ -3969,7 +4324,7 @@ function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
|
|
|
3969
4324
|
}
|
|
3970
4325
|
function isGenericCheckpointLine(value) {
|
|
3971
4326
|
const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3972
|
-
return normalized === "here's where things stand:" || normalized === "here's where things stand" || normalized === "where things stand:" || normalized === "where things stand" || normalized === "current status:" || normalized === "current status" || normalized === "status update:" || normalized === "status update";
|
|
4327
|
+
return normalized === "here's where things stand:" || normalized === "here's where things stand" || normalized === "where things stand:" || normalized === "where things stand" || normalized === "current status:" || normalized === "current status" || normalized === "status update:" || normalized === "status update" || normalized.startsWith("all clean. here's a summary of what was fixed") || normalized.startsWith("all clean, here's a summary of what was fixed") || normalized.startsWith("now i have enough to give a clear, accurate assessment") || normalized.startsWith("here's the real picture") || normalized === "tl;dr:" || normalized.startsWith("tl;dr:");
|
|
3973
4328
|
}
|
|
3974
4329
|
function detectUnsavedPlans(message) {
|
|
3975
4330
|
const hints = [];
|