engrm 0.4.22 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/cli.js +308 -24
- package/dist/hooks/elicitation-result.js +223 -15
- package/dist/hooks/post-tool-use.js +257 -15
- package/dist/hooks/pre-compact.js +962 -17
- package/dist/hooks/sentinel.js +223 -15
- package/dist/hooks/session-start.js +1066 -92
- package/dist/hooks/stop.js +361 -33
- package/dist/hooks/user-prompt-submit.js +267 -15
- package/dist/server.js +863 -49
- package/package.json +1 -1
|
@@ -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'
|
|
@@ -776,6 +776,18 @@ var MIGRATIONS = [
|
|
|
776
776
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
777
777
|
`
|
|
778
778
|
},
|
|
779
|
+
{
|
|
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
|
+
},
|
|
779
791
|
{
|
|
780
792
|
version: 12,
|
|
781
793
|
description: "Add synced handoff metadata to session summaries",
|
|
@@ -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
|
];
|
|
@@ -855,6 +931,18 @@ function inferLegacySchemaVersion(db) {
|
|
|
855
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")) {
|
|
856
932
|
version = Math.max(version, 12);
|
|
857
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
|
+
}
|
|
858
946
|
return version;
|
|
859
947
|
}
|
|
860
948
|
function runMigrations(db) {
|
|
@@ -938,7 +1026,8 @@ function ensureSessionSummaryColumns(db) {
|
|
|
938
1026
|
"capture_state",
|
|
939
1027
|
"recent_tool_names",
|
|
940
1028
|
"hot_files",
|
|
941
|
-
"recent_outcomes"
|
|
1029
|
+
"recent_outcomes",
|
|
1030
|
+
"current_thread"
|
|
942
1031
|
];
|
|
943
1032
|
for (const column of required) {
|
|
944
1033
|
if (columnExists(db, "session_summaries", column))
|
|
@@ -946,10 +1035,75 @@ function ensureSessionSummaryColumns(db) {
|
|
|
946
1035
|
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
947
1036
|
}
|
|
948
1037
|
const current = getSchemaVersion(db);
|
|
949
|
-
if (current <
|
|
950
|
-
db.exec("PRAGMA user_version =
|
|
1038
|
+
if (current < 13) {
|
|
1039
|
+
db.exec("PRAGMA user_version = 13");
|
|
951
1040
|
}
|
|
952
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
|
+
}
|
|
953
1107
|
function getSchemaVersion(db) {
|
|
954
1108
|
const result = db.query("PRAGMA user_version").get();
|
|
955
1109
|
return result.user_version;
|
|
@@ -1109,6 +1263,8 @@ class MemDatabase {
|
|
|
1109
1263
|
runMigrations(this.db);
|
|
1110
1264
|
ensureObservationTypes(this.db);
|
|
1111
1265
|
ensureSessionSummaryColumns(this.db);
|
|
1266
|
+
ensureChatMessageColumns(this.db);
|
|
1267
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1112
1268
|
}
|
|
1113
1269
|
loadVecExtension() {
|
|
1114
1270
|
try {
|
|
@@ -1334,6 +1490,7 @@ class MemDatabase {
|
|
|
1334
1490
|
p.name AS project_name,
|
|
1335
1491
|
ss.request AS request,
|
|
1336
1492
|
ss.completed AS completed,
|
|
1493
|
+
ss.current_thread AS current_thread,
|
|
1337
1494
|
ss.capture_state AS capture_state,
|
|
1338
1495
|
ss.recent_tool_names AS recent_tool_names,
|
|
1339
1496
|
ss.hot_files AS hot_files,
|
|
@@ -1352,6 +1509,7 @@ class MemDatabase {
|
|
|
1352
1509
|
p.name AS project_name,
|
|
1353
1510
|
ss.request AS request,
|
|
1354
1511
|
ss.completed AS completed,
|
|
1512
|
+
ss.current_thread AS current_thread,
|
|
1355
1513
|
ss.capture_state AS capture_state,
|
|
1356
1514
|
ss.recent_tool_names AS recent_tool_names,
|
|
1357
1515
|
ss.hot_files AS hot_files,
|
|
@@ -1442,6 +1600,54 @@ class MemDatabase {
|
|
|
1442
1600
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1443
1601
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1444
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
|
+
}
|
|
1445
1651
|
addToOutbox(recordType, recordId) {
|
|
1446
1652
|
const now = Math.floor(Date.now() / 1000);
|
|
1447
1653
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1530,9 +1736,9 @@ class MemDatabase {
|
|
|
1530
1736
|
};
|
|
1531
1737
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1532
1738
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1533
|
-
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
|
|
1534
1740
|
)
|
|
1535
|
-
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);
|
|
1536
1742
|
const id = Number(result.lastInsertRowid);
|
|
1537
1743
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1538
1744
|
}
|
|
@@ -1548,6 +1754,7 @@ class MemDatabase {
|
|
|
1548
1754
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1549
1755
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1550
1756
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1757
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1551
1758
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1552
1759
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1553
1760
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1561,12 +1768,13 @@ class MemDatabase {
|
|
|
1561
1768
|
learned = ?,
|
|
1562
1769
|
completed = ?,
|
|
1563
1770
|
next_steps = ?,
|
|
1771
|
+
current_thread = ?,
|
|
1564
1772
|
capture_state = ?,
|
|
1565
1773
|
recent_tool_names = ?,
|
|
1566
1774
|
hot_files = ?,
|
|
1567
1775
|
recent_outcomes = ?,
|
|
1568
1776
|
created_at_epoch = ?
|
|
1569
|
-
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);
|
|
1570
1778
|
return this.getSessionSummary(summary.session_id);
|
|
1571
1779
|
}
|
|
1572
1780
|
getSessionSummary(sessionId) {
|
|
@@ -3296,11 +3504,13 @@ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
|
3296
3504
|
return acc;
|
|
3297
3505
|
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
3298
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);
|
|
3299
3508
|
return {
|
|
3300
3509
|
prompt_count: prompts.length,
|
|
3301
3510
|
tool_event_count: toolEvents.length,
|
|
3302
3511
|
recent_request_prompts: recentRequestPrompts,
|
|
3303
3512
|
latest_request: latestRequest,
|
|
3513
|
+
current_thread: currentThread,
|
|
3304
3514
|
recent_tool_names: recentToolNames,
|
|
3305
3515
|
recent_tool_commands: recentToolCommands,
|
|
3306
3516
|
capture_state: captureState,
|
|
@@ -3310,6 +3520,37 @@ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
|
3310
3520
|
latest_observation_prompt_number: latestObservationPromptNumber
|
|
3311
3521
|
};
|
|
3312
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
|
+
}
|
|
3313
3554
|
function parseJsonArray(value) {
|
|
3314
3555
|
if (!value)
|
|
3315
3556
|
return [];
|
|
@@ -3534,6 +3775,7 @@ function updateRollingSummaryFromObservation(db, observationId, event, userId) {
|
|
|
3534
3775
|
learned: merged.learned,
|
|
3535
3776
|
completed: merged.completed,
|
|
3536
3777
|
next_steps: existing?.next_steps ?? null,
|
|
3778
|
+
current_thread: handoff.current_thread,
|
|
3537
3779
|
capture_state: handoff.capture_state,
|
|
3538
3780
|
recent_tool_names: JSON.stringify(handoff.recent_tool_names),
|
|
3539
3781
|
hot_files: JSON.stringify(handoff.hot_files),
|