engrm 0.4.22 → 0.4.25
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 +65 -3
- package/dist/cli.js +406 -24
- package/dist/hooks/elicitation-result.js +308 -15
- package/dist/hooks/post-tool-use.js +932 -26
- package/dist/hooks/pre-compact.js +1660 -59
- package/dist/hooks/sentinel.js +308 -15
- package/dist/hooks/session-start.js +1594 -110
- package/dist/hooks/stop.js +899 -33
- package/dist/hooks/user-prompt-submit.js +1651 -15
- package/dist/server.js +1379 -63
- 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'
|
|
@@ -883,6 +883,18 @@ var MIGRATIONS = [
|
|
|
883
883
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
884
884
|
`
|
|
885
885
|
},
|
|
886
|
+
{
|
|
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
|
+
},
|
|
886
898
|
{
|
|
887
899
|
version: 12,
|
|
888
900
|
description: "Add synced handoff metadata to session summaries",
|
|
@@ -894,15 +906,92 @@ 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);
|
|
982
|
+
`
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
version: 17,
|
|
986
|
+
description: "Track transcript-backed chat messages separately from hook chat",
|
|
987
|
+
sql: `
|
|
988
|
+
ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook';
|
|
989
|
+
ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER;
|
|
990
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_source_kind
|
|
991
|
+
ON chat_messages(session_id, source_kind, transcript_index, created_at_epoch DESC, id DESC);
|
|
992
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_session_transcript
|
|
993
|
+
ON chat_messages(session_id, transcript_index)
|
|
994
|
+
WHERE transcript_index IS NOT NULL;
|
|
906
995
|
`
|
|
907
996
|
}
|
|
908
997
|
];
|
|
@@ -962,6 +1051,21 @@ function inferLegacySchemaVersion(db) {
|
|
|
962
1051
|
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")) {
|
|
963
1052
|
version = Math.max(version, 12);
|
|
964
1053
|
}
|
|
1054
|
+
if (columnExists(db, "session_summaries", "current_thread")) {
|
|
1055
|
+
version = Math.max(version, 13);
|
|
1056
|
+
}
|
|
1057
|
+
if (tableExists(db, "chat_messages")) {
|
|
1058
|
+
version = Math.max(version, 14);
|
|
1059
|
+
}
|
|
1060
|
+
if (columnExists(db, "chat_messages", "remote_source_id")) {
|
|
1061
|
+
version = Math.max(version, 15);
|
|
1062
|
+
}
|
|
1063
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
1064
|
+
version = Math.max(version, 16);
|
|
1065
|
+
}
|
|
1066
|
+
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
1067
|
+
version = Math.max(version, 17);
|
|
1068
|
+
}
|
|
965
1069
|
return version;
|
|
966
1070
|
}
|
|
967
1071
|
function runMigrations(db) {
|
|
@@ -1045,7 +1149,8 @@ function ensureSessionSummaryColumns(db) {
|
|
|
1045
1149
|
"capture_state",
|
|
1046
1150
|
"recent_tool_names",
|
|
1047
1151
|
"hot_files",
|
|
1048
|
-
"recent_outcomes"
|
|
1152
|
+
"recent_outcomes",
|
|
1153
|
+
"current_thread"
|
|
1049
1154
|
];
|
|
1050
1155
|
for (const column of required) {
|
|
1051
1156
|
if (columnExists(db, "session_summaries", column))
|
|
@@ -1053,10 +1158,83 @@ function ensureSessionSummaryColumns(db) {
|
|
|
1053
1158
|
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
1054
1159
|
}
|
|
1055
1160
|
const current = getSchemaVersion(db);
|
|
1056
|
-
if (current <
|
|
1057
|
-
db.exec("PRAGMA user_version =
|
|
1161
|
+
if (current < 13) {
|
|
1162
|
+
db.exec("PRAGMA user_version = 13");
|
|
1058
1163
|
}
|
|
1059
1164
|
}
|
|
1165
|
+
function ensureChatMessageColumns(db) {
|
|
1166
|
+
if (!tableExists(db, "chat_messages"))
|
|
1167
|
+
return;
|
|
1168
|
+
if (!columnExists(db, "chat_messages", "remote_source_id")) {
|
|
1169
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
1170
|
+
}
|
|
1171
|
+
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");
|
|
1172
|
+
if (!columnExists(db, "chat_messages", "source_kind")) {
|
|
1173
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook'");
|
|
1174
|
+
}
|
|
1175
|
+
if (!columnExists(db, "chat_messages", "transcript_index")) {
|
|
1176
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER");
|
|
1177
|
+
}
|
|
1178
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_chat_messages_source_kind ON chat_messages(session_id, source_kind, transcript_index, created_at_epoch DESC, id DESC)");
|
|
1179
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_session_transcript ON chat_messages(session_id, transcript_index) WHERE transcript_index IS NOT NULL");
|
|
1180
|
+
const current = getSchemaVersion(db);
|
|
1181
|
+
if (current < 17) {
|
|
1182
|
+
db.exec("PRAGMA user_version = 17");
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
1186
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
1187
|
+
const current = getSchemaVersion(db);
|
|
1188
|
+
if (current < 16) {
|
|
1189
|
+
db.exec("PRAGMA user_version = 16");
|
|
1190
|
+
}
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
db.exec("BEGIN TRANSACTION");
|
|
1194
|
+
try {
|
|
1195
|
+
db.exec(`
|
|
1196
|
+
CREATE TABLE sync_outbox_new (
|
|
1197
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1198
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
1199
|
+
record_id INTEGER NOT NULL,
|
|
1200
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
1201
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
1202
|
+
)),
|
|
1203
|
+
retry_count INTEGER DEFAULT 0,
|
|
1204
|
+
max_retries INTEGER DEFAULT 10,
|
|
1205
|
+
last_error TEXT,
|
|
1206
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1207
|
+
synced_at_epoch INTEGER,
|
|
1208
|
+
next_retry_epoch INTEGER
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
INSERT INTO sync_outbox_new (
|
|
1212
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
1213
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
1214
|
+
)
|
|
1215
|
+
SELECT
|
|
1216
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
1217
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
1218
|
+
FROM sync_outbox;
|
|
1219
|
+
|
|
1220
|
+
DROP TABLE sync_outbox;
|
|
1221
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
1222
|
+
|
|
1223
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
1224
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
1225
|
+
`);
|
|
1226
|
+
db.exec("PRAGMA user_version = 16");
|
|
1227
|
+
db.exec("COMMIT");
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
db.exec("ROLLBACK");
|
|
1230
|
+
throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function syncOutboxSupportsChatMessages(db) {
|
|
1234
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
|
|
1235
|
+
const sql = row?.sql ?? "";
|
|
1236
|
+
return sql.includes("'chat_message'");
|
|
1237
|
+
}
|
|
1060
1238
|
function getSchemaVersion(db) {
|
|
1061
1239
|
const result = db.query("PRAGMA user_version").get();
|
|
1062
1240
|
return result.user_version;
|
|
@@ -1136,6 +1314,8 @@ class MemDatabase {
|
|
|
1136
1314
|
runMigrations(this.db);
|
|
1137
1315
|
ensureObservationTypes(this.db);
|
|
1138
1316
|
ensureSessionSummaryColumns(this.db);
|
|
1317
|
+
ensureChatMessageColumns(this.db);
|
|
1318
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1139
1319
|
}
|
|
1140
1320
|
loadVecExtension() {
|
|
1141
1321
|
try {
|
|
@@ -1204,6 +1384,22 @@ class MemDatabase {
|
|
|
1204
1384
|
getObservationById(id) {
|
|
1205
1385
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
1206
1386
|
}
|
|
1387
|
+
updateObservationContent(id, update) {
|
|
1388
|
+
const existing = this.getObservationById(id);
|
|
1389
|
+
if (!existing)
|
|
1390
|
+
return null;
|
|
1391
|
+
const createdAtEpoch = update.created_at_epoch ?? existing.created_at_epoch;
|
|
1392
|
+
const createdAt = new Date(createdAtEpoch * 1000).toISOString();
|
|
1393
|
+
this.db.query(`UPDATE observations
|
|
1394
|
+
SET title = ?, narrative = ?, facts = ?, concepts = ?, created_at = ?, created_at_epoch = ?
|
|
1395
|
+
WHERE id = ?`).run(update.title, update.narrative ?? null, update.facts ?? null, update.concepts ?? null, createdAt, createdAtEpoch, id);
|
|
1396
|
+
this.ftsDelete(existing);
|
|
1397
|
+
const refreshed = this.getObservationById(id);
|
|
1398
|
+
if (!refreshed)
|
|
1399
|
+
return null;
|
|
1400
|
+
this.ftsInsert(refreshed);
|
|
1401
|
+
return refreshed;
|
|
1402
|
+
}
|
|
1207
1403
|
getObservationsByIds(ids, userId) {
|
|
1208
1404
|
if (ids.length === 0)
|
|
1209
1405
|
return [];
|
|
@@ -1361,6 +1557,7 @@ class MemDatabase {
|
|
|
1361
1557
|
p.name AS project_name,
|
|
1362
1558
|
ss.request AS request,
|
|
1363
1559
|
ss.completed AS completed,
|
|
1560
|
+
ss.current_thread AS current_thread,
|
|
1364
1561
|
ss.capture_state AS capture_state,
|
|
1365
1562
|
ss.recent_tool_names AS recent_tool_names,
|
|
1366
1563
|
ss.hot_files AS hot_files,
|
|
@@ -1379,6 +1576,7 @@ class MemDatabase {
|
|
|
1379
1576
|
p.name AS project_name,
|
|
1380
1577
|
ss.request AS request,
|
|
1381
1578
|
ss.completed AS completed,
|
|
1579
|
+
ss.current_thread AS current_thread,
|
|
1382
1580
|
ss.capture_state AS capture_state,
|
|
1383
1581
|
ss.recent_tool_names AS recent_tool_names,
|
|
1384
1582
|
ss.hot_files AS hot_files,
|
|
@@ -1469,6 +1667,99 @@ class MemDatabase {
|
|
|
1469
1667
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1470
1668
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1471
1669
|
}
|
|
1670
|
+
insertChatMessage(input) {
|
|
1671
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1672
|
+
const content = input.content.trim();
|
|
1673
|
+
const result = this.db.query(`INSERT INTO chat_messages (
|
|
1674
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id, source_kind, transcript_index
|
|
1675
|
+
) 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, input.source_kind ?? "hook", input.transcript_index ?? null);
|
|
1676
|
+
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1677
|
+
}
|
|
1678
|
+
getChatMessageById(id) {
|
|
1679
|
+
return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
|
|
1680
|
+
}
|
|
1681
|
+
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1682
|
+
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1683
|
+
}
|
|
1684
|
+
getSessionChatMessages(sessionId, limit = 50) {
|
|
1685
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1686
|
+
WHERE session_id = ?
|
|
1687
|
+
AND (
|
|
1688
|
+
source_kind = 'transcript'
|
|
1689
|
+
OR NOT EXISTS (
|
|
1690
|
+
SELECT 1 FROM chat_messages t2
|
|
1691
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1692
|
+
AND t2.source_kind = 'transcript'
|
|
1693
|
+
)
|
|
1694
|
+
)
|
|
1695
|
+
ORDER BY
|
|
1696
|
+
CASE WHEN transcript_index IS NULL THEN created_at_epoch ELSE transcript_index END ASC,
|
|
1697
|
+
id ASC
|
|
1698
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1699
|
+
}
|
|
1700
|
+
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
1701
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1702
|
+
if (projectId !== null) {
|
|
1703
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1704
|
+
WHERE project_id = ?${visibilityClause}
|
|
1705
|
+
AND (
|
|
1706
|
+
source_kind = 'transcript'
|
|
1707
|
+
OR NOT EXISTS (
|
|
1708
|
+
SELECT 1 FROM chat_messages t2
|
|
1709
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1710
|
+
AND t2.source_kind = 'transcript'
|
|
1711
|
+
)
|
|
1712
|
+
)
|
|
1713
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1714
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1715
|
+
}
|
|
1716
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1717
|
+
WHERE 1 = 1${visibilityClause}
|
|
1718
|
+
AND (
|
|
1719
|
+
source_kind = 'transcript'
|
|
1720
|
+
OR NOT EXISTS (
|
|
1721
|
+
SELECT 1 FROM chat_messages t2
|
|
1722
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1723
|
+
AND t2.source_kind = 'transcript'
|
|
1724
|
+
)
|
|
1725
|
+
)
|
|
1726
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1727
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1728
|
+
}
|
|
1729
|
+
searchChatMessages(query, projectId, limit = 20, userId) {
|
|
1730
|
+
const needle = `%${query.toLowerCase()}%`;
|
|
1731
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1732
|
+
if (projectId !== null) {
|
|
1733
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1734
|
+
WHERE project_id = ?
|
|
1735
|
+
AND lower(content) LIKE ?${visibilityClause}
|
|
1736
|
+
AND (
|
|
1737
|
+
source_kind = 'transcript'
|
|
1738
|
+
OR NOT EXISTS (
|
|
1739
|
+
SELECT 1 FROM chat_messages t2
|
|
1740
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1741
|
+
AND t2.source_kind = 'transcript'
|
|
1742
|
+
)
|
|
1743
|
+
)
|
|
1744
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1745
|
+
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1746
|
+
}
|
|
1747
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1748
|
+
WHERE lower(content) LIKE ?${visibilityClause}
|
|
1749
|
+
AND (
|
|
1750
|
+
source_kind = 'transcript'
|
|
1751
|
+
OR NOT EXISTS (
|
|
1752
|
+
SELECT 1 FROM chat_messages t2
|
|
1753
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1754
|
+
AND t2.source_kind = 'transcript'
|
|
1755
|
+
)
|
|
1756
|
+
)
|
|
1757
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1758
|
+
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1759
|
+
}
|
|
1760
|
+
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
1761
|
+
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
1762
|
+
}
|
|
1472
1763
|
addToOutbox(recordType, recordId) {
|
|
1473
1764
|
const now = Math.floor(Date.now() / 1000);
|
|
1474
1765
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1557,9 +1848,9 @@ class MemDatabase {
|
|
|
1557
1848
|
};
|
|
1558
1849
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1559
1850
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1560
|
-
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1851
|
+
current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1561
1852
|
)
|
|
1562
|
-
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);
|
|
1853
|
+
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);
|
|
1563
1854
|
const id = Number(result.lastInsertRowid);
|
|
1564
1855
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1565
1856
|
}
|
|
@@ -1575,6 +1866,7 @@ class MemDatabase {
|
|
|
1575
1866
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1576
1867
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1577
1868
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1869
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1578
1870
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1579
1871
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1580
1872
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1588,12 +1880,13 @@ class MemDatabase {
|
|
|
1588
1880
|
learned = ?,
|
|
1589
1881
|
completed = ?,
|
|
1590
1882
|
next_steps = ?,
|
|
1883
|
+
current_thread = ?,
|
|
1591
1884
|
capture_state = ?,
|
|
1592
1885
|
recent_tool_names = ?,
|
|
1593
1886
|
hot_files = ?,
|
|
1594
1887
|
recent_outcomes = ?,
|
|
1595
1888
|
created_at_epoch = ?
|
|
1596
|
-
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);
|
|
1889
|
+
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);
|
|
1597
1890
|
return this.getSessionSummary(summary.session_id);
|
|
1598
1891
|
}
|
|
1599
1892
|
getSessionSummary(sessionId) {
|
|
@@ -1854,22 +2147,27 @@ function buildSourceId(config, localId, type = "obs") {
|
|
|
1854
2147
|
return `${config.user_id}-${config.device_id}-${type}-${localId}`;
|
|
1855
2148
|
}
|
|
1856
2149
|
function parseSourceId(sourceId) {
|
|
1857
|
-
const
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2150
|
+
for (const type of ["obs", "summary", "chat"]) {
|
|
2151
|
+
const marker = `-${type}-`;
|
|
2152
|
+
const idx = sourceId.lastIndexOf(marker);
|
|
2153
|
+
if (idx === -1)
|
|
2154
|
+
continue;
|
|
2155
|
+
const prefix = sourceId.slice(0, idx);
|
|
2156
|
+
const localIdStr = sourceId.slice(idx + marker.length);
|
|
2157
|
+
const localId = parseInt(localIdStr, 10);
|
|
2158
|
+
if (isNaN(localId))
|
|
2159
|
+
return null;
|
|
2160
|
+
const firstDash = prefix.indexOf("-");
|
|
2161
|
+
if (firstDash === -1)
|
|
2162
|
+
return null;
|
|
2163
|
+
return {
|
|
2164
|
+
userId: prefix.slice(0, firstDash),
|
|
2165
|
+
deviceId: prefix.slice(firstDash + 1),
|
|
2166
|
+
localId,
|
|
2167
|
+
type
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
return null;
|
|
1873
2171
|
}
|
|
1874
2172
|
|
|
1875
2173
|
// src/sync/client.ts
|
|
@@ -2045,11 +2343,13 @@ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
|
2045
2343
|
return acc;
|
|
2046
2344
|
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
2047
2345
|
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
2346
|
+
const currentThread = buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames);
|
|
2048
2347
|
return {
|
|
2049
2348
|
prompt_count: prompts.length,
|
|
2050
2349
|
tool_event_count: toolEvents.length,
|
|
2051
2350
|
recent_request_prompts: recentRequestPrompts,
|
|
2052
2351
|
latest_request: latestRequest,
|
|
2352
|
+
current_thread: currentThread,
|
|
2053
2353
|
recent_tool_names: recentToolNames,
|
|
2054
2354
|
recent_tool_commands: recentToolCommands,
|
|
2055
2355
|
capture_state: captureState,
|
|
@@ -2059,6 +2359,37 @@ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
|
2059
2359
|
latest_observation_prompt_number: latestObservationPromptNumber
|
|
2060
2360
|
};
|
|
2061
2361
|
}
|
|
2362
|
+
function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames) {
|
|
2363
|
+
const request = compactLine(latestRequest);
|
|
2364
|
+
const outcome = recentOutcomes.map((item) => compactLine(item)).find(Boolean);
|
|
2365
|
+
const file = hotFiles[0] ? compactFileHint(hotFiles[0]) : null;
|
|
2366
|
+
const tools = recentToolNames.slice(0, 2).join("/");
|
|
2367
|
+
if (outcome && file) {
|
|
2368
|
+
return `${outcome} · ${file}${tools ? ` · ${tools}` : ""}`;
|
|
2369
|
+
}
|
|
2370
|
+
if (request && file) {
|
|
2371
|
+
return `${request} · ${file}${tools ? ` · ${tools}` : ""}`;
|
|
2372
|
+
}
|
|
2373
|
+
if (outcome) {
|
|
2374
|
+
return `${outcome}${tools ? ` · ${tools}` : ""}`;
|
|
2375
|
+
}
|
|
2376
|
+
if (request) {
|
|
2377
|
+
return `${request}${tools ? ` · ${tools}` : ""}`;
|
|
2378
|
+
}
|
|
2379
|
+
return null;
|
|
2380
|
+
}
|
|
2381
|
+
function compactLine(value) {
|
|
2382
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
2383
|
+
if (!trimmed)
|
|
2384
|
+
return null;
|
|
2385
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
2386
|
+
}
|
|
2387
|
+
function compactFileHint(value) {
|
|
2388
|
+
const parts = value.split("/");
|
|
2389
|
+
if (parts.length <= 2)
|
|
2390
|
+
return value;
|
|
2391
|
+
return parts.slice(-2).join("/");
|
|
2392
|
+
}
|
|
2062
2393
|
function parseJsonArray2(value) {
|
|
2063
2394
|
if (!value)
|
|
2064
2395
|
return [];
|
|
@@ -2071,6 +2402,30 @@ function parseJsonArray2(value) {
|
|
|
2071
2402
|
}
|
|
2072
2403
|
|
|
2073
2404
|
// src/sync/push.ts
|
|
2405
|
+
function buildChatVectorDocument(chat, config, project) {
|
|
2406
|
+
return {
|
|
2407
|
+
site_id: config.site_id,
|
|
2408
|
+
namespace: config.namespace,
|
|
2409
|
+
source_type: "chat",
|
|
2410
|
+
source_id: buildSourceId(config, chat.id, "chat"),
|
|
2411
|
+
content: chat.content,
|
|
2412
|
+
metadata: {
|
|
2413
|
+
project_canonical: project.canonical_id,
|
|
2414
|
+
project_name: project.name,
|
|
2415
|
+
user_id: chat.user_id,
|
|
2416
|
+
device_id: chat.device_id,
|
|
2417
|
+
device_name: __require("node:os").hostname(),
|
|
2418
|
+
agent: chat.agent,
|
|
2419
|
+
type: "chat",
|
|
2420
|
+
role: chat.role,
|
|
2421
|
+
session_id: chat.session_id,
|
|
2422
|
+
created_at_epoch: chat.created_at_epoch,
|
|
2423
|
+
local_id: chat.id,
|
|
2424
|
+
source_kind: chat.source_kind,
|
|
2425
|
+
transcript_index: chat.transcript_index
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2074
2429
|
function buildVectorDocument(obs, config, project) {
|
|
2075
2430
|
const parts = [obs.title];
|
|
2076
2431
|
if (obs.narrative)
|
|
@@ -2151,6 +2506,7 @@ function buildSummaryVectorDocument(summary, config, project, observations = [],
|
|
|
2151
2506
|
learned: summary.learned,
|
|
2152
2507
|
completed: summary.completed,
|
|
2153
2508
|
next_steps: summary.next_steps,
|
|
2509
|
+
current_thread: summary.current_thread,
|
|
2154
2510
|
summary_sections_present: countPresentSections(summary),
|
|
2155
2511
|
investigated_items: extractSectionItems(summary.investigated),
|
|
2156
2512
|
learned_items: extractSectionItems(summary.learned),
|
|
@@ -2161,6 +2517,7 @@ function buildSummaryVectorDocument(summary, config, project, observations = [],
|
|
|
2161
2517
|
capture_state: captureContext?.capture_state ?? "summary-only",
|
|
2162
2518
|
recent_request_prompts: captureContext?.recent_request_prompts ?? [],
|
|
2163
2519
|
latest_request: captureContext?.latest_request ?? null,
|
|
2520
|
+
current_thread: captureContext?.current_thread ?? null,
|
|
2164
2521
|
recent_tool_names: captureContext?.recent_tool_names ?? [],
|
|
2165
2522
|
recent_tool_commands: captureContext?.recent_tool_commands ?? [],
|
|
2166
2523
|
hot_files: captureContext?.hot_files ?? [],
|
|
@@ -2214,6 +2571,32 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
2214
2571
|
batch.push({ entryId: entry.id, doc: doc2 });
|
|
2215
2572
|
continue;
|
|
2216
2573
|
}
|
|
2574
|
+
if (entry.record_type === "chat_message") {
|
|
2575
|
+
const chat = db.getChatMessageById(entry.record_id);
|
|
2576
|
+
if (!chat || chat.remote_source_id) {
|
|
2577
|
+
markSynced(db, entry.id);
|
|
2578
|
+
skipped++;
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (!chat.project_id) {
|
|
2582
|
+
markSynced(db, entry.id);
|
|
2583
|
+
skipped++;
|
|
2584
|
+
continue;
|
|
2585
|
+
}
|
|
2586
|
+
const project2 = db.getProjectById(chat.project_id);
|
|
2587
|
+
if (!project2) {
|
|
2588
|
+
markSynced(db, entry.id);
|
|
2589
|
+
skipped++;
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
markSyncing(db, entry.id);
|
|
2593
|
+
const doc2 = buildChatVectorDocument(chat, config, {
|
|
2594
|
+
canonical_id: project2.canonical_id,
|
|
2595
|
+
name: project2.name
|
|
2596
|
+
});
|
|
2597
|
+
batch.push({ entryId: entry.id, doc: doc2 });
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2217
2600
|
if (entry.record_type !== "observation") {
|
|
2218
2601
|
skipped++;
|
|
2219
2602
|
continue;
|
|
@@ -2251,7 +2634,13 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
2251
2634
|
return { pushed, failed, skipped };
|
|
2252
2635
|
try {
|
|
2253
2636
|
await client.batchIngest(batch.map((b) => b.doc));
|
|
2254
|
-
for (const { entryId } of batch) {
|
|
2637
|
+
for (const { entryId, doc } of batch) {
|
|
2638
|
+
if (doc.source_type === "chat") {
|
|
2639
|
+
const localId = typeof doc.metadata?.local_id === "number" ? doc.metadata.local_id : null;
|
|
2640
|
+
if (localId !== null) {
|
|
2641
|
+
db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc.source_id, localId);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2255
2644
|
markSynced(db, entryId);
|
|
2256
2645
|
pushed++;
|
|
2257
2646
|
}
|
|
@@ -2259,6 +2648,12 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
2259
2648
|
for (const { entryId, doc } of batch) {
|
|
2260
2649
|
try {
|
|
2261
2650
|
await client.ingest(doc);
|
|
2651
|
+
if (doc.source_type === "chat") {
|
|
2652
|
+
const localId = typeof doc.metadata?.local_id === "number" ? doc.metadata.local_id : null;
|
|
2653
|
+
if (localId !== null) {
|
|
2654
|
+
db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc.source_id, localId);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2262
2657
|
markSynced(db, entryId);
|
|
2263
2658
|
pushed++;
|
|
2264
2659
|
} catch (err) {
|
|
@@ -2614,7 +3009,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
2614
3009
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
2615
3010
|
risk_score: riskScore,
|
|
2616
3011
|
stacks_detected: stacks,
|
|
2617
|
-
client_version: "0.4.
|
|
3012
|
+
client_version: "0.4.25",
|
|
2618
3013
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
2619
3014
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
2620
3015
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3515,6 +3910,40 @@ function readTranscript(sessionId, cwd, transcriptPath) {
|
|
|
3515
3910
|
}
|
|
3516
3911
|
return messages;
|
|
3517
3912
|
}
|
|
3913
|
+
function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
3914
|
+
const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
|
|
3915
|
+
...message,
|
|
3916
|
+
text: message.text.trim()
|
|
3917
|
+
})).filter((message) => message.text.length > 0);
|
|
3918
|
+
if (messages.length === 0)
|
|
3919
|
+
return { imported: 0, total: 0 };
|
|
3920
|
+
const session = db.getSessionById(sessionId);
|
|
3921
|
+
const projectId = session?.project_id ?? null;
|
|
3922
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3923
|
+
let imported = 0;
|
|
3924
|
+
for (let index = 0;index < messages.length; index++) {
|
|
3925
|
+
const transcriptIndex = index + 1;
|
|
3926
|
+
if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
|
|
3927
|
+
continue;
|
|
3928
|
+
const message = messages[index];
|
|
3929
|
+
const createdAtEpoch = Math.max(0, now - (messages.length - transcriptIndex));
|
|
3930
|
+
const row = db.insertChatMessage({
|
|
3931
|
+
session_id: sessionId,
|
|
3932
|
+
project_id: projectId,
|
|
3933
|
+
role: message.role,
|
|
3934
|
+
content: message.text,
|
|
3935
|
+
user_id: config.user_id,
|
|
3936
|
+
device_id: config.device_id,
|
|
3937
|
+
agent: "claude-code",
|
|
3938
|
+
created_at_epoch: createdAtEpoch,
|
|
3939
|
+
source_kind: "transcript",
|
|
3940
|
+
transcript_index: transcriptIndex
|
|
3941
|
+
});
|
|
3942
|
+
db.addToOutbox("chat_message", row.id);
|
|
3943
|
+
imported++;
|
|
3944
|
+
}
|
|
3945
|
+
return { imported, total: messages.length };
|
|
3946
|
+
}
|
|
3518
3947
|
function truncateTranscript(messages, maxBytes = 50000) {
|
|
3519
3948
|
const lines = [];
|
|
3520
3949
|
for (const msg of messages) {
|
|
@@ -3590,6 +4019,417 @@ async function saveTranscriptResults(db, config, results, sessionId, cwd) {
|
|
|
3590
4019
|
return saved;
|
|
3591
4020
|
}
|
|
3592
4021
|
|
|
4022
|
+
// src/tools/session-story.ts
|
|
4023
|
+
function getSessionStory(db, input) {
|
|
4024
|
+
const session = db.getSessionById(input.session_id);
|
|
4025
|
+
const summary = db.getSessionSummary(input.session_id);
|
|
4026
|
+
const prompts = db.getSessionUserPrompts(input.session_id, 50);
|
|
4027
|
+
const chatMessages = db.getSessionChatMessages(input.session_id, 50);
|
|
4028
|
+
const toolEvents = db.getSessionToolEvents(input.session_id, 100);
|
|
4029
|
+
const allObservations = db.getObservationsBySession(input.session_id);
|
|
4030
|
+
const handoffs = allObservations.filter((obs) => looksLikeHandoff(obs));
|
|
4031
|
+
const rollingHandoffDrafts = handoffs.filter((obs) => isDraftHandoff(obs));
|
|
4032
|
+
const savedHandoffs = handoffs.filter((obs) => !isDraftHandoff(obs));
|
|
4033
|
+
const observations = allObservations.filter((obs) => !looksLikeHandoff(obs));
|
|
4034
|
+
const metrics = db.getSessionMetrics(input.session_id);
|
|
4035
|
+
const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
|
|
4036
|
+
const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
|
|
4037
|
+
return {
|
|
4038
|
+
session,
|
|
4039
|
+
project_name: projectName,
|
|
4040
|
+
summary,
|
|
4041
|
+
prompts,
|
|
4042
|
+
chat_messages: chatMessages,
|
|
4043
|
+
tool_events: toolEvents,
|
|
4044
|
+
observations,
|
|
4045
|
+
handoffs,
|
|
4046
|
+
saved_handoffs: savedHandoffs,
|
|
4047
|
+
rolling_handoff_drafts: rollingHandoffDrafts,
|
|
4048
|
+
metrics,
|
|
4049
|
+
capture_state: classifyCaptureState({
|
|
4050
|
+
hasSummary: Boolean(summary?.request || summary?.completed),
|
|
4051
|
+
promptCount: prompts.length,
|
|
4052
|
+
toolEventCount: toolEvents.length
|
|
4053
|
+
}),
|
|
4054
|
+
capture_gaps: buildCaptureGaps({
|
|
4055
|
+
promptCount: prompts.length,
|
|
4056
|
+
toolEventCount: toolEvents.length,
|
|
4057
|
+
toolCallsCount: metrics?.tool_calls_count ?? 0,
|
|
4058
|
+
observationCount: observations.length,
|
|
4059
|
+
hasSummary: Boolean(summary?.request || summary?.completed)
|
|
4060
|
+
}),
|
|
4061
|
+
latest_request: latestRequest,
|
|
4062
|
+
recent_outcomes: collectRecentOutcomes(observations),
|
|
4063
|
+
hot_files: collectHotFiles(observations),
|
|
4064
|
+
provenance_summary: collectProvenanceSummary(observations)
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
function classifyCaptureState(input) {
|
|
4068
|
+
if (input.promptCount > 0 && input.toolEventCount > 0)
|
|
4069
|
+
return "rich";
|
|
4070
|
+
if (input.promptCount > 0 || input.toolEventCount > 0)
|
|
4071
|
+
return "partial";
|
|
4072
|
+
if (input.hasSummary)
|
|
4073
|
+
return "summary-only";
|
|
4074
|
+
return "legacy";
|
|
4075
|
+
}
|
|
4076
|
+
function buildCaptureGaps(input) {
|
|
4077
|
+
const gaps = [];
|
|
4078
|
+
if (input.promptCount === 0)
|
|
4079
|
+
gaps.push("missing prompts");
|
|
4080
|
+
if (input.toolCallsCount > 0 && input.toolEventCount === 0) {
|
|
4081
|
+
gaps.push("missing raw tool chronology");
|
|
4082
|
+
} else if (input.toolEventCount === 0) {
|
|
4083
|
+
gaps.push("no tool events");
|
|
4084
|
+
}
|
|
4085
|
+
if (input.observationCount === 0 && input.hasSummary) {
|
|
4086
|
+
gaps.push("summary without reusable observations");
|
|
4087
|
+
}
|
|
4088
|
+
return gaps;
|
|
4089
|
+
}
|
|
4090
|
+
function collectRecentOutcomes(observations) {
|
|
4091
|
+
const seen = new Set;
|
|
4092
|
+
const outcomes = [];
|
|
4093
|
+
for (const obs of observations) {
|
|
4094
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
4095
|
+
continue;
|
|
4096
|
+
const title = obs.title.trim();
|
|
4097
|
+
if (!title || looksLikeFileOperationTitle(title))
|
|
4098
|
+
continue;
|
|
4099
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
4100
|
+
if (seen.has(normalized))
|
|
4101
|
+
continue;
|
|
4102
|
+
seen.add(normalized);
|
|
4103
|
+
outcomes.push(title);
|
|
4104
|
+
if (outcomes.length >= 6)
|
|
4105
|
+
break;
|
|
4106
|
+
}
|
|
4107
|
+
return outcomes;
|
|
4108
|
+
}
|
|
4109
|
+
function collectHotFiles(observations) {
|
|
4110
|
+
const counts = new Map;
|
|
4111
|
+
for (const obs of observations) {
|
|
4112
|
+
for (const path of [...parseJsonArray3(obs.files_modified), ...parseJsonArray3(obs.files_read)]) {
|
|
4113
|
+
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
return Array.from(counts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || a.path.localeCompare(b.path)).slice(0, 8);
|
|
4117
|
+
}
|
|
4118
|
+
function parseJsonArray3(value) {
|
|
4119
|
+
if (!value)
|
|
4120
|
+
return [];
|
|
4121
|
+
try {
|
|
4122
|
+
const parsed = JSON.parse(value);
|
|
4123
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
4124
|
+
} catch {
|
|
4125
|
+
return [];
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
function looksLikeFileOperationTitle(value) {
|
|
4129
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
4130
|
+
}
|
|
4131
|
+
function collectProvenanceSummary(observations) {
|
|
4132
|
+
const counts = new Map;
|
|
4133
|
+
for (const obs of observations) {
|
|
4134
|
+
if (!obs.source_tool)
|
|
4135
|
+
continue;
|
|
4136
|
+
counts.set(obs.source_tool, (counts.get(obs.source_tool) ?? 0) + 1);
|
|
4137
|
+
}
|
|
4138
|
+
return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
// src/tools/handoffs.ts
|
|
4142
|
+
async function upsertRollingHandoff(db, config, input) {
|
|
4143
|
+
const resolved = resolveTargetSession(db, input.cwd, config.user_id, input.session_id);
|
|
4144
|
+
if (!resolved.session) {
|
|
4145
|
+
return {
|
|
4146
|
+
success: false,
|
|
4147
|
+
reason: "No recent session found to draft a handoff yet"
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
const story = getSessionStory(db, { session_id: resolved.session.session_id });
|
|
4151
|
+
if (!story.session) {
|
|
4152
|
+
return {
|
|
4153
|
+
success: false,
|
|
4154
|
+
reason: `Session ${resolved.session.session_id} not found`
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
const includeChat = input.include_chat === true || input.include_chat !== false && shouldAutoIncludeChat(story);
|
|
4158
|
+
const chatLimit = Math.max(1, Math.min(input.chat_limit ?? 3, 6));
|
|
4159
|
+
const title = `Handoff Draft: ${buildHandoffTitle(story.summary, story.latest_request)}`;
|
|
4160
|
+
const narrative = buildHandoffNarrative(story.summary, story, {
|
|
4161
|
+
includeChat,
|
|
4162
|
+
chatLimit
|
|
4163
|
+
});
|
|
4164
|
+
const facts = buildHandoffFacts(story.summary, story);
|
|
4165
|
+
const concepts = buildDraftHandoffConcepts(story.project_name, story.capture_state);
|
|
4166
|
+
const existing = getSessionRollingHandoff(db, story.session.session_id);
|
|
4167
|
+
const now = Math.floor(Date.now() / 1000);
|
|
4168
|
+
if (existing) {
|
|
4169
|
+
const nextFacts = JSON.stringify(facts);
|
|
4170
|
+
const nextConcepts = JSON.stringify(concepts);
|
|
4171
|
+
const shouldRefresh = existing.title !== title || (existing.narrative ?? null) !== narrative || (existing.facts ?? null) !== nextFacts || (existing.concepts ?? null) !== nextConcepts || now - existing.created_at_epoch >= 120;
|
|
4172
|
+
if (!shouldRefresh) {
|
|
4173
|
+
return {
|
|
4174
|
+
success: true,
|
|
4175
|
+
observation_id: existing.id,
|
|
4176
|
+
session_id: story.session.session_id,
|
|
4177
|
+
title: existing.title
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4180
|
+
const updated = db.updateObservationContent(existing.id, {
|
|
4181
|
+
title,
|
|
4182
|
+
narrative,
|
|
4183
|
+
facts: nextFacts,
|
|
4184
|
+
concepts: nextConcepts,
|
|
4185
|
+
created_at_epoch: now
|
|
4186
|
+
});
|
|
4187
|
+
if (!updated) {
|
|
4188
|
+
return {
|
|
4189
|
+
success: false,
|
|
4190
|
+
reason: "Failed to update rolling handoff draft"
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4193
|
+
db.addToOutbox("observation", updated.id);
|
|
4194
|
+
return {
|
|
4195
|
+
success: true,
|
|
4196
|
+
observation_id: updated.id,
|
|
4197
|
+
session_id: story.session.session_id,
|
|
4198
|
+
title: updated.title
|
|
4199
|
+
};
|
|
4200
|
+
}
|
|
4201
|
+
const result = await saveObservation(db, config, {
|
|
4202
|
+
type: "message",
|
|
4203
|
+
title,
|
|
4204
|
+
narrative,
|
|
4205
|
+
facts,
|
|
4206
|
+
concepts,
|
|
4207
|
+
session_id: story.session.session_id,
|
|
4208
|
+
cwd: input.cwd,
|
|
4209
|
+
agent: "engrm-handoff",
|
|
4210
|
+
source_tool: "rolling_handoff"
|
|
4211
|
+
});
|
|
4212
|
+
return {
|
|
4213
|
+
success: result.success,
|
|
4214
|
+
observation_id: result.observation_id,
|
|
4215
|
+
session_id: story.session.session_id,
|
|
4216
|
+
title,
|
|
4217
|
+
reason: result.reason
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
4220
|
+
function getRecentHandoffs(db, input) {
|
|
4221
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
|
|
4222
|
+
const queryLimit = input.current_device_id ? Math.max(limit, Math.min(limit * 5, 50)) : limit;
|
|
4223
|
+
const projectScoped = input.project_scoped !== false;
|
|
4224
|
+
let projectId = null;
|
|
4225
|
+
let projectName;
|
|
4226
|
+
if (projectScoped) {
|
|
4227
|
+
const cwd = input.cwd ?? process.cwd();
|
|
4228
|
+
const detected = detectProject(cwd);
|
|
4229
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
4230
|
+
if (project) {
|
|
4231
|
+
projectId = project.id;
|
|
4232
|
+
projectName = project.name;
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
const conditions = [
|
|
4236
|
+
"o.type = 'message'",
|
|
4237
|
+
"o.lifecycle IN ('active', 'aging', 'pinned')",
|
|
4238
|
+
"o.superseded_by IS NULL",
|
|
4239
|
+
`(o.title LIKE 'Handoff:%' OR o.concepts LIKE '%"handoff"%')`
|
|
4240
|
+
];
|
|
4241
|
+
const params = [];
|
|
4242
|
+
if (input.user_id) {
|
|
4243
|
+
conditions.push("(o.sensitivity != 'personal' OR o.user_id = ?)");
|
|
4244
|
+
params.push(input.user_id);
|
|
4245
|
+
}
|
|
4246
|
+
if (projectId !== null) {
|
|
4247
|
+
conditions.push("o.project_id = ?");
|
|
4248
|
+
params.push(projectId);
|
|
4249
|
+
}
|
|
4250
|
+
params.push(queryLimit);
|
|
4251
|
+
const handoffs = db.db.query(`SELECT o.*, p.name AS project_name
|
|
4252
|
+
FROM observations o
|
|
4253
|
+
LEFT JOIN projects p ON p.id = o.project_id
|
|
4254
|
+
WHERE ${conditions.join(" AND ")}
|
|
4255
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
4256
|
+
LIMIT ?`).all(...params);
|
|
4257
|
+
handoffs.sort((a, b) => compareHandoffs(a, b, input.current_device_id));
|
|
4258
|
+
return {
|
|
4259
|
+
handoffs: handoffs.slice(0, limit),
|
|
4260
|
+
project: projectName
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
function formatHandoffSource(handoff) {
|
|
4264
|
+
const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
|
|
4265
|
+
const ageLabel = ageSeconds < 3600 ? `${Math.max(1, Math.floor(ageSeconds / 60) || 1)}m ago` : ageSeconds < 86400 ? `${Math.floor(ageSeconds / 3600)}h ago` : `${Math.floor(ageSeconds / 86400)}d ago`;
|
|
4266
|
+
return `from ${handoff.device_id} · ${ageLabel}`;
|
|
4267
|
+
}
|
|
4268
|
+
function isDraftHandoff(obs) {
|
|
4269
|
+
if (obs.title.startsWith("Handoff Draft:"))
|
|
4270
|
+
return true;
|
|
4271
|
+
const concepts = parseJsonArray4(obs.concepts);
|
|
4272
|
+
return concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
|
|
4273
|
+
}
|
|
4274
|
+
function getSessionRollingHandoff(db, sessionId) {
|
|
4275
|
+
return db.db.query(`SELECT o.*, p.name AS project_name
|
|
4276
|
+
FROM observations o
|
|
4277
|
+
LEFT JOIN projects p ON p.id = o.project_id
|
|
4278
|
+
WHERE o.session_id = ?
|
|
4279
|
+
AND o.type = 'message'
|
|
4280
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
4281
|
+
AND o.superseded_by IS NULL
|
|
4282
|
+
AND (o.title LIKE 'Handoff Draft:%' OR o.concepts LIKE '%"draft-handoff"%')
|
|
4283
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
4284
|
+
LIMIT 1`).get(sessionId) ?? null;
|
|
4285
|
+
}
|
|
4286
|
+
function compareHandoffs(a, b, currentDeviceId) {
|
|
4287
|
+
const aDraft = isDraftHandoff(a) ? 1 : 0;
|
|
4288
|
+
const bDraft = isDraftHandoff(b) ? 1 : 0;
|
|
4289
|
+
if (aDraft !== bDraft)
|
|
4290
|
+
return aDraft - bDraft;
|
|
4291
|
+
if (currentDeviceId) {
|
|
4292
|
+
const aOther = a.device_id !== currentDeviceId ? 1 : 0;
|
|
4293
|
+
const bOther = b.device_id !== currentDeviceId ? 1 : 0;
|
|
4294
|
+
if (aOther !== bOther)
|
|
4295
|
+
return bOther - aOther;
|
|
4296
|
+
}
|
|
4297
|
+
if (b.created_at_epoch !== a.created_at_epoch) {
|
|
4298
|
+
return b.created_at_epoch - a.created_at_epoch;
|
|
4299
|
+
}
|
|
4300
|
+
return b.id - a.id;
|
|
4301
|
+
}
|
|
4302
|
+
function resolveTargetSession(db, cwd, userId, sessionId) {
|
|
4303
|
+
if (sessionId) {
|
|
4304
|
+
const session = db.getSessionById(sessionId);
|
|
4305
|
+
if (!session)
|
|
4306
|
+
return { session: null };
|
|
4307
|
+
const projectName = session.project_id ? db.getProjectById(session.project_id)?.name : undefined;
|
|
4308
|
+
return {
|
|
4309
|
+
session: {
|
|
4310
|
+
...session,
|
|
4311
|
+
project_name: projectName ?? null,
|
|
4312
|
+
request: db.getSessionSummary(sessionId)?.request ?? null,
|
|
4313
|
+
completed: db.getSessionSummary(sessionId)?.completed ?? null,
|
|
4314
|
+
current_thread: db.getSessionSummary(sessionId)?.current_thread ?? null,
|
|
4315
|
+
capture_state: db.getSessionSummary(sessionId)?.capture_state ?? null,
|
|
4316
|
+
recent_tool_names: db.getSessionSummary(sessionId)?.recent_tool_names ?? null,
|
|
4317
|
+
hot_files: db.getSessionSummary(sessionId)?.hot_files ?? null,
|
|
4318
|
+
recent_outcomes: db.getSessionSummary(sessionId)?.recent_outcomes ?? null,
|
|
4319
|
+
prompt_count: db.getSessionUserPrompts(sessionId, 200).length,
|
|
4320
|
+
tool_event_count: db.getSessionToolEvents(sessionId, 200).length
|
|
4321
|
+
},
|
|
4322
|
+
projectName: projectName ?? undefined
|
|
4323
|
+
};
|
|
4324
|
+
}
|
|
4325
|
+
const detected = detectProject(cwd ?? process.cwd());
|
|
4326
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
4327
|
+
const sessions = db.getRecentSessions(project?.id ?? null, 10, userId);
|
|
4328
|
+
return {
|
|
4329
|
+
session: sessions[0] ?? null,
|
|
4330
|
+
projectName: project?.name
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4333
|
+
function buildHandoffTitle(summary, latestRequest, explicit) {
|
|
4334
|
+
const chosen = explicit?.trim() || summary?.current_thread?.trim() || summary?.completed?.trim() || latestRequest?.trim() || "Current work";
|
|
4335
|
+
return compactLine2(chosen) ?? "Current work";
|
|
4336
|
+
}
|
|
4337
|
+
function buildHandoffNarrative(summary, story, options) {
|
|
4338
|
+
const sections = [];
|
|
4339
|
+
if (summary?.request || story.latest_request) {
|
|
4340
|
+
sections.push(`Request: ${summary?.request ?? story.latest_request}`);
|
|
4341
|
+
}
|
|
4342
|
+
if (summary?.current_thread) {
|
|
4343
|
+
sections.push(`Current thread: ${summary.current_thread}`);
|
|
4344
|
+
}
|
|
4345
|
+
if (summary?.investigated) {
|
|
4346
|
+
sections.push(`Investigated: ${summary.investigated}`);
|
|
4347
|
+
}
|
|
4348
|
+
if (summary?.learned) {
|
|
4349
|
+
sections.push(`Learned: ${summary.learned}`);
|
|
4350
|
+
}
|
|
4351
|
+
if (summary?.completed) {
|
|
4352
|
+
sections.push(`Completed: ${summary.completed}`);
|
|
4353
|
+
}
|
|
4354
|
+
if (summary?.next_steps) {
|
|
4355
|
+
sections.push(`Next Steps: ${summary.next_steps}`);
|
|
4356
|
+
}
|
|
4357
|
+
if (story.recent_outcomes.length > 0) {
|
|
4358
|
+
sections.push(`Recent outcomes:
|
|
4359
|
+
${story.recent_outcomes.slice(0, 5).map((item) => `- ${item}`).join(`
|
|
4360
|
+
`)}`);
|
|
4361
|
+
}
|
|
4362
|
+
if (story.hot_files.length > 0) {
|
|
4363
|
+
sections.push(`Hot files:
|
|
4364
|
+
${story.hot_files.slice(0, 5).map((file) => `- ${file.path}`).join(`
|
|
4365
|
+
`)}`);
|
|
4366
|
+
}
|
|
4367
|
+
if (story.provenance_summary.length > 0) {
|
|
4368
|
+
sections.push(`Tool trail:
|
|
4369
|
+
${story.provenance_summary.slice(0, 5).map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
4370
|
+
`)}`);
|
|
4371
|
+
}
|
|
4372
|
+
if (options.includeChat && story.chat_messages.length > 0) {
|
|
4373
|
+
const chatLines = story.chat_messages.slice(-options.chatLimit).map((msg) => `- [${msg.role}] ${compactLine2(msg.content) ?? msg.content.slice(0, 120)}`);
|
|
4374
|
+
sections.push(`Chat snippets:
|
|
4375
|
+
${chatLines.join(`
|
|
4376
|
+
`)}`);
|
|
4377
|
+
}
|
|
4378
|
+
return sections.filter(Boolean).join(`
|
|
4379
|
+
|
|
4380
|
+
`);
|
|
4381
|
+
}
|
|
4382
|
+
function shouldAutoIncludeChat(story) {
|
|
4383
|
+
if (story.chat_messages.length === 0)
|
|
4384
|
+
return false;
|
|
4385
|
+
const summary = story.summary;
|
|
4386
|
+
const thinSummary = !summary?.completed && !summary?.current_thread && story.recent_outcomes.length < 2;
|
|
4387
|
+
const thinChronology = story.capture_state !== "rich" || story.tool_events.length === 0;
|
|
4388
|
+
return thinSummary || thinChronology;
|
|
4389
|
+
}
|
|
4390
|
+
function buildHandoffFacts(summary, story) {
|
|
4391
|
+
const facts = [
|
|
4392
|
+
`session_id=${story.session?.session_id ?? "unknown"}`,
|
|
4393
|
+
`capture_state=${story.capture_state}`,
|
|
4394
|
+
story.project_name ? `project=${story.project_name}` : null,
|
|
4395
|
+
summary?.current_thread ? `current_thread=${summary.current_thread}` : null,
|
|
4396
|
+
story.hot_files[0] ? `hot_file=${story.hot_files[0].path}` : null,
|
|
4397
|
+
story.provenance_summary[0] ? `primary_tool=${story.provenance_summary[0].tool}` : null
|
|
4398
|
+
];
|
|
4399
|
+
return facts.filter((item) => Boolean(item));
|
|
4400
|
+
}
|
|
4401
|
+
function buildDraftHandoffConcepts(projectName, captureState) {
|
|
4402
|
+
return [
|
|
4403
|
+
"handoff",
|
|
4404
|
+
"draft-handoff",
|
|
4405
|
+
"auto-handoff",
|
|
4406
|
+
`capture:${captureState}`,
|
|
4407
|
+
...projectName ? [projectName] : []
|
|
4408
|
+
];
|
|
4409
|
+
}
|
|
4410
|
+
function looksLikeHandoff(obs) {
|
|
4411
|
+
if (obs.title.startsWith("Handoff:") || obs.title.startsWith("Handoff Draft:"))
|
|
4412
|
+
return true;
|
|
4413
|
+
const concepts = parseJsonArray4(obs.concepts);
|
|
4414
|
+
return concepts.includes("handoff") || concepts.includes("session-handoff") || concepts.includes("draft-handoff");
|
|
4415
|
+
}
|
|
4416
|
+
function parseJsonArray4(value) {
|
|
4417
|
+
if (!value)
|
|
4418
|
+
return [];
|
|
4419
|
+
try {
|
|
4420
|
+
const parsed = JSON.parse(value);
|
|
4421
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
4422
|
+
} catch {
|
|
4423
|
+
return [];
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
function compactLine2(value) {
|
|
4427
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
4428
|
+
if (!trimmed)
|
|
4429
|
+
return null;
|
|
4430
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
3593
4433
|
// hooks/stop.ts
|
|
3594
4434
|
function printRetrospective(summary) {
|
|
3595
4435
|
const lines = [];
|
|
@@ -3637,8 +4477,30 @@ async function main() {
|
|
|
3637
4477
|
try {
|
|
3638
4478
|
if (event.session_id) {
|
|
3639
4479
|
db.completeSession(event.session_id);
|
|
4480
|
+
syncTranscriptChat(db, config, event.session_id, event.cwd, event.transcript_path);
|
|
3640
4481
|
if (event.last_assistant_message) {
|
|
3641
4482
|
try {
|
|
4483
|
+
const detected = detectProject(event.cwd);
|
|
4484
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id) ?? db.upsertProject({
|
|
4485
|
+
canonical_id: detected.canonical_id,
|
|
4486
|
+
name: detected.name,
|
|
4487
|
+
local_path: event.cwd,
|
|
4488
|
+
remote_url: detected.remote_url ?? null
|
|
4489
|
+
});
|
|
4490
|
+
const compactAssistant = event.last_assistant_message.replace(/\s+/g, " ").trim();
|
|
4491
|
+
if (compactAssistant.length >= 24) {
|
|
4492
|
+
const chatMessage = db.insertChatMessage({
|
|
4493
|
+
session_id: event.session_id,
|
|
4494
|
+
project_id: project.id,
|
|
4495
|
+
role: "assistant",
|
|
4496
|
+
content: event.last_assistant_message,
|
|
4497
|
+
user_id: config.user_id,
|
|
4498
|
+
device_id: config.device_id,
|
|
4499
|
+
agent: "claude-code",
|
|
4500
|
+
source_kind: "hook"
|
|
4501
|
+
});
|
|
4502
|
+
db.addToOutbox("chat_message", chatMessage.id);
|
|
4503
|
+
}
|
|
3642
4504
|
createAssistantCheckpoint(db, event.session_id, event.cwd, event.last_assistant_message);
|
|
3643
4505
|
} catch {}
|
|
3644
4506
|
}
|
|
@@ -3652,6 +4514,10 @@ async function main() {
|
|
|
3652
4514
|
if (summary) {
|
|
3653
4515
|
const row = db.upsertSessionSummary(summary);
|
|
3654
4516
|
db.addToOutbox("summary", row.id);
|
|
4517
|
+
await upsertRollingHandoff(db, config, {
|
|
4518
|
+
session_id: event.session_id,
|
|
4519
|
+
cwd: event.cwd
|
|
4520
|
+
});
|
|
3655
4521
|
let securityFindings = [];
|
|
3656
4522
|
try {
|
|
3657
4523
|
if (session?.project_id) {
|