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
|
@@ -421,7 +421,7 @@ var MIGRATIONS = [
|
|
|
421
421
|
-- Sync outbox (offline-first queue)
|
|
422
422
|
CREATE TABLE sync_outbox (
|
|
423
423
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
424
|
-
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
424
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
425
425
|
record_id INTEGER NOT NULL,
|
|
426
426
|
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
427
427
|
'pending', 'syncing', 'synced', 'failed'
|
|
@@ -716,6 +716,18 @@ var MIGRATIONS = [
|
|
|
716
716
|
},
|
|
717
717
|
{
|
|
718
718
|
version: 11,
|
|
719
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
720
|
+
sql: `
|
|
721
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
722
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
723
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
724
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
725
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
726
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
727
|
+
`
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
version: 12,
|
|
719
731
|
description: "Add synced handoff metadata to session summaries",
|
|
720
732
|
sql: `
|
|
721
733
|
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
@@ -725,15 +737,79 @@ var MIGRATIONS = [
|
|
|
725
737
|
`
|
|
726
738
|
},
|
|
727
739
|
{
|
|
728
|
-
version:
|
|
729
|
-
description: "Add
|
|
740
|
+
version: 13,
|
|
741
|
+
description: "Add current_thread to session summaries",
|
|
730
742
|
sql: `
|
|
731
|
-
ALTER TABLE
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
743
|
+
ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
|
|
744
|
+
`
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
version: 14,
|
|
748
|
+
description: "Add chat_messages lane for raw conversation recall",
|
|
749
|
+
sql: `
|
|
750
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
751
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
752
|
+
session_id TEXT NOT NULL,
|
|
753
|
+
project_id INTEGER REFERENCES projects(id),
|
|
754
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
755
|
+
content TEXT NOT NULL,
|
|
756
|
+
user_id TEXT NOT NULL,
|
|
757
|
+
device_id TEXT NOT NULL,
|
|
758
|
+
agent TEXT DEFAULT 'claude-code',
|
|
759
|
+
created_at_epoch INTEGER NOT NULL
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session
|
|
763
|
+
ON chat_messages(session_id, created_at_epoch DESC, id DESC);
|
|
764
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_project
|
|
765
|
+
ON chat_messages(project_id, created_at_epoch DESC, id DESC);
|
|
766
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created
|
|
767
|
+
ON chat_messages(created_at_epoch DESC, id DESC);
|
|
768
|
+
`
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
version: 15,
|
|
772
|
+
description: "Add remote_source_id for chat message sync deduplication",
|
|
773
|
+
sql: `
|
|
774
|
+
ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
|
|
775
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
|
|
776
|
+
ON chat_messages(remote_source_id)
|
|
777
|
+
WHERE remote_source_id IS NOT NULL;
|
|
778
|
+
`
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
version: 16,
|
|
782
|
+
description: "Allow chat_message records in sync_outbox",
|
|
783
|
+
sql: `
|
|
784
|
+
CREATE TABLE sync_outbox_new (
|
|
785
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
786
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
787
|
+
record_id INTEGER NOT NULL,
|
|
788
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
789
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
790
|
+
)),
|
|
791
|
+
retry_count INTEGER DEFAULT 0,
|
|
792
|
+
max_retries INTEGER DEFAULT 10,
|
|
793
|
+
last_error TEXT,
|
|
794
|
+
created_at_epoch INTEGER NOT NULL,
|
|
795
|
+
synced_at_epoch INTEGER,
|
|
796
|
+
next_retry_epoch INTEGER
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
INSERT INTO sync_outbox_new (
|
|
800
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
801
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
802
|
+
)
|
|
803
|
+
SELECT
|
|
804
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
805
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
806
|
+
FROM sync_outbox;
|
|
807
|
+
|
|
808
|
+
DROP TABLE sync_outbox;
|
|
809
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
810
|
+
|
|
811
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
812
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
737
813
|
`
|
|
738
814
|
}
|
|
739
815
|
];
|
|
@@ -790,6 +866,21 @@ function inferLegacySchemaVersion(db) {
|
|
|
790
866
|
version = Math.max(version, 10);
|
|
791
867
|
if (columnExists(db, "observations", "source_tool"))
|
|
792
868
|
version = Math.max(version, 11);
|
|
869
|
+
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")) {
|
|
870
|
+
version = Math.max(version, 12);
|
|
871
|
+
}
|
|
872
|
+
if (columnExists(db, "session_summaries", "current_thread")) {
|
|
873
|
+
version = Math.max(version, 13);
|
|
874
|
+
}
|
|
875
|
+
if (tableExists(db, "chat_messages")) {
|
|
876
|
+
version = Math.max(version, 14);
|
|
877
|
+
}
|
|
878
|
+
if (columnExists(db, "chat_messages", "remote_source_id")) {
|
|
879
|
+
version = Math.max(version, 15);
|
|
880
|
+
}
|
|
881
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
882
|
+
version = Math.max(version, 16);
|
|
883
|
+
}
|
|
793
884
|
return version;
|
|
794
885
|
}
|
|
795
886
|
function runMigrations(db) {
|
|
@@ -868,6 +959,93 @@ function ensureObservationTypes(db) {
|
|
|
868
959
|
}
|
|
869
960
|
}
|
|
870
961
|
}
|
|
962
|
+
function ensureSessionSummaryColumns(db) {
|
|
963
|
+
const required = [
|
|
964
|
+
"capture_state",
|
|
965
|
+
"recent_tool_names",
|
|
966
|
+
"hot_files",
|
|
967
|
+
"recent_outcomes",
|
|
968
|
+
"current_thread"
|
|
969
|
+
];
|
|
970
|
+
for (const column of required) {
|
|
971
|
+
if (columnExists(db, "session_summaries", column))
|
|
972
|
+
continue;
|
|
973
|
+
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
974
|
+
}
|
|
975
|
+
const current = getSchemaVersion(db);
|
|
976
|
+
if (current < 13) {
|
|
977
|
+
db.exec("PRAGMA user_version = 13");
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function ensureChatMessageColumns(db) {
|
|
981
|
+
if (!tableExists(db, "chat_messages"))
|
|
982
|
+
return;
|
|
983
|
+
if (!columnExists(db, "chat_messages", "remote_source_id")) {
|
|
984
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
985
|
+
}
|
|
986
|
+
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");
|
|
987
|
+
const current = getSchemaVersion(db);
|
|
988
|
+
if (current < 15) {
|
|
989
|
+
db.exec("PRAGMA user_version = 15");
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
993
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
994
|
+
const current = getSchemaVersion(db);
|
|
995
|
+
if (current < 16) {
|
|
996
|
+
db.exec("PRAGMA user_version = 16");
|
|
997
|
+
}
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
db.exec("BEGIN TRANSACTION");
|
|
1001
|
+
try {
|
|
1002
|
+
db.exec(`
|
|
1003
|
+
CREATE TABLE sync_outbox_new (
|
|
1004
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1005
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
1006
|
+
record_id INTEGER NOT NULL,
|
|
1007
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
1008
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
1009
|
+
)),
|
|
1010
|
+
retry_count INTEGER DEFAULT 0,
|
|
1011
|
+
max_retries INTEGER DEFAULT 10,
|
|
1012
|
+
last_error TEXT,
|
|
1013
|
+
created_at_epoch INTEGER NOT NULL,
|
|
1014
|
+
synced_at_epoch INTEGER,
|
|
1015
|
+
next_retry_epoch INTEGER
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
INSERT INTO sync_outbox_new (
|
|
1019
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
1020
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
1021
|
+
)
|
|
1022
|
+
SELECT
|
|
1023
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
1024
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
1025
|
+
FROM sync_outbox;
|
|
1026
|
+
|
|
1027
|
+
DROP TABLE sync_outbox;
|
|
1028
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
1029
|
+
|
|
1030
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
1031
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
1032
|
+
`);
|
|
1033
|
+
db.exec("PRAGMA user_version = 16");
|
|
1034
|
+
db.exec("COMMIT");
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
db.exec("ROLLBACK");
|
|
1037
|
+
throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function syncOutboxSupportsChatMessages(db) {
|
|
1041
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
|
|
1042
|
+
const sql = row?.sql ?? "";
|
|
1043
|
+
return sql.includes("'chat_message'");
|
|
1044
|
+
}
|
|
1045
|
+
function getSchemaVersion(db) {
|
|
1046
|
+
const result = db.query("PRAGMA user_version").get();
|
|
1047
|
+
return result.user_version;
|
|
1048
|
+
}
|
|
871
1049
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
872
1050
|
|
|
873
1051
|
// src/storage/sqlite.ts
|
|
@@ -1022,6 +1200,9 @@ class MemDatabase {
|
|
|
1022
1200
|
this.vecAvailable = this.loadVecExtension();
|
|
1023
1201
|
runMigrations(this.db);
|
|
1024
1202
|
ensureObservationTypes(this.db);
|
|
1203
|
+
ensureSessionSummaryColumns(this.db);
|
|
1204
|
+
ensureChatMessageColumns(this.db);
|
|
1205
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
1025
1206
|
}
|
|
1026
1207
|
loadVecExtension() {
|
|
1027
1208
|
try {
|
|
@@ -1247,6 +1428,7 @@ class MemDatabase {
|
|
|
1247
1428
|
p.name AS project_name,
|
|
1248
1429
|
ss.request AS request,
|
|
1249
1430
|
ss.completed AS completed,
|
|
1431
|
+
ss.current_thread AS current_thread,
|
|
1250
1432
|
ss.capture_state AS capture_state,
|
|
1251
1433
|
ss.recent_tool_names AS recent_tool_names,
|
|
1252
1434
|
ss.hot_files AS hot_files,
|
|
@@ -1265,6 +1447,7 @@ class MemDatabase {
|
|
|
1265
1447
|
p.name AS project_name,
|
|
1266
1448
|
ss.request AS request,
|
|
1267
1449
|
ss.completed AS completed,
|
|
1450
|
+
ss.current_thread AS current_thread,
|
|
1268
1451
|
ss.capture_state AS capture_state,
|
|
1269
1452
|
ss.recent_tool_names AS recent_tool_names,
|
|
1270
1453
|
ss.hot_files AS hot_files,
|
|
@@ -1355,6 +1538,54 @@ class MemDatabase {
|
|
|
1355
1538
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1356
1539
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1357
1540
|
}
|
|
1541
|
+
insertChatMessage(input) {
|
|
1542
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1543
|
+
const content = input.content.trim();
|
|
1544
|
+
const result = this.db.query(`INSERT INTO chat_messages (
|
|
1545
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
1546
|
+
) 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);
|
|
1547
|
+
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1548
|
+
}
|
|
1549
|
+
getChatMessageById(id) {
|
|
1550
|
+
return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
|
|
1551
|
+
}
|
|
1552
|
+
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1553
|
+
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1554
|
+
}
|
|
1555
|
+
getSessionChatMessages(sessionId, limit = 50) {
|
|
1556
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1557
|
+
WHERE session_id = ?
|
|
1558
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
1559
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1560
|
+
}
|
|
1561
|
+
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
1562
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1563
|
+
if (projectId !== null) {
|
|
1564
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1565
|
+
WHERE project_id = ?${visibilityClause}
|
|
1566
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1567
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1568
|
+
}
|
|
1569
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1570
|
+
WHERE 1 = 1${visibilityClause}
|
|
1571
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1572
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1573
|
+
}
|
|
1574
|
+
searchChatMessages(query, projectId, limit = 20, userId) {
|
|
1575
|
+
const needle = `%${query.toLowerCase()}%`;
|
|
1576
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1577
|
+
if (projectId !== null) {
|
|
1578
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1579
|
+
WHERE project_id = ?
|
|
1580
|
+
AND lower(content) LIKE ?${visibilityClause}
|
|
1581
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1582
|
+
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1583
|
+
}
|
|
1584
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1585
|
+
WHERE lower(content) LIKE ?${visibilityClause}
|
|
1586
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1587
|
+
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1588
|
+
}
|
|
1358
1589
|
addToOutbox(recordType, recordId) {
|
|
1359
1590
|
const now = Math.floor(Date.now() / 1000);
|
|
1360
1591
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1443,9 +1674,9 @@ class MemDatabase {
|
|
|
1443
1674
|
};
|
|
1444
1675
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1445
1676
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1446
|
-
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1677
|
+
current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1447
1678
|
)
|
|
1448
|
-
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);
|
|
1679
|
+
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);
|
|
1449
1680
|
const id = Number(result.lastInsertRowid);
|
|
1450
1681
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1451
1682
|
}
|
|
@@ -1461,6 +1692,7 @@ class MemDatabase {
|
|
|
1461
1692
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1462
1693
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1463
1694
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1695
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1464
1696
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1465
1697
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1466
1698
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1474,12 +1706,13 @@ class MemDatabase {
|
|
|
1474
1706
|
learned = ?,
|
|
1475
1707
|
completed = ?,
|
|
1476
1708
|
next_steps = ?,
|
|
1709
|
+
current_thread = ?,
|
|
1477
1710
|
capture_state = ?,
|
|
1478
1711
|
recent_tool_names = ?,
|
|
1479
1712
|
hot_files = ?,
|
|
1480
1713
|
recent_outcomes = ?,
|
|
1481
1714
|
created_at_epoch = ?
|
|
1482
|
-
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);
|
|
1715
|
+
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);
|
|
1483
1716
|
return this.getSessionSummary(summary.session_id);
|
|
1484
1717
|
}
|
|
1485
1718
|
getSessionSummary(sessionId) {
|
|
@@ -1626,6 +1859,83 @@ function runHook(hookName, fn) {
|
|
|
1626
1859
|
});
|
|
1627
1860
|
}
|
|
1628
1861
|
|
|
1862
|
+
// src/capture/session-handoff.ts
|
|
1863
|
+
function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
1864
|
+
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
1865
|
+
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
1866
|
+
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
1867
|
+
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
1868
|
+
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
1869
|
+
...parseJsonArray(obs.files_modified),
|
|
1870
|
+
...parseJsonArray(obs.files_read)
|
|
1871
|
+
]).filter(Boolean))].slice(0, 6);
|
|
1872
|
+
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);
|
|
1873
|
+
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
1874
|
+
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
1875
|
+
if (!obs.source_tool)
|
|
1876
|
+
return acc;
|
|
1877
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
1878
|
+
return acc;
|
|
1879
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
1880
|
+
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
1881
|
+
const currentThread = buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames);
|
|
1882
|
+
return {
|
|
1883
|
+
prompt_count: prompts.length,
|
|
1884
|
+
tool_event_count: toolEvents.length,
|
|
1885
|
+
recent_request_prompts: recentRequestPrompts,
|
|
1886
|
+
latest_request: latestRequest,
|
|
1887
|
+
current_thread: currentThread,
|
|
1888
|
+
recent_tool_names: recentToolNames,
|
|
1889
|
+
recent_tool_commands: recentToolCommands,
|
|
1890
|
+
capture_state: captureState,
|
|
1891
|
+
hot_files: hotFiles,
|
|
1892
|
+
recent_outcomes: recentOutcomes,
|
|
1893
|
+
observation_source_tools: observationSourceTools,
|
|
1894
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames) {
|
|
1898
|
+
const request = compactLine(latestRequest);
|
|
1899
|
+
const outcome = recentOutcomes.map((item) => compactLine(item)).find(Boolean);
|
|
1900
|
+
const file = hotFiles[0] ? compactFileHint(hotFiles[0]) : null;
|
|
1901
|
+
const tools = recentToolNames.slice(0, 2).join("/");
|
|
1902
|
+
if (outcome && file) {
|
|
1903
|
+
return `${outcome} · ${file}${tools ? ` · ${tools}` : ""}`;
|
|
1904
|
+
}
|
|
1905
|
+
if (request && file) {
|
|
1906
|
+
return `${request} · ${file}${tools ? ` · ${tools}` : ""}`;
|
|
1907
|
+
}
|
|
1908
|
+
if (outcome) {
|
|
1909
|
+
return `${outcome}${tools ? ` · ${tools}` : ""}`;
|
|
1910
|
+
}
|
|
1911
|
+
if (request) {
|
|
1912
|
+
return `${request}${tools ? ` · ${tools}` : ""}`;
|
|
1913
|
+
}
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
function compactLine(value) {
|
|
1917
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
1918
|
+
if (!trimmed)
|
|
1919
|
+
return null;
|
|
1920
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
1921
|
+
}
|
|
1922
|
+
function compactFileHint(value) {
|
|
1923
|
+
const parts = value.split("/");
|
|
1924
|
+
if (parts.length <= 2)
|
|
1925
|
+
return value;
|
|
1926
|
+
return parts.slice(-2).join("/");
|
|
1927
|
+
}
|
|
1928
|
+
function parseJsonArray(value) {
|
|
1929
|
+
if (!value)
|
|
1930
|
+
return [];
|
|
1931
|
+
try {
|
|
1932
|
+
const parsed = JSON.parse(value);
|
|
1933
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1934
|
+
} catch {
|
|
1935
|
+
return [];
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1629
1939
|
// hooks/user-prompt-submit.ts
|
|
1630
1940
|
async function main() {
|
|
1631
1941
|
const event = await parseStdinJson();
|
|
@@ -1653,8 +1963,22 @@ async function main() {
|
|
|
1653
1963
|
device_id: config.device_id,
|
|
1654
1964
|
agent: "claude-code"
|
|
1655
1965
|
});
|
|
1966
|
+
const chatMessage = db.insertChatMessage({
|
|
1967
|
+
session_id: event.session_id,
|
|
1968
|
+
project_id: project.id,
|
|
1969
|
+
role: "user",
|
|
1970
|
+
content: event.prompt,
|
|
1971
|
+
user_id: config.user_id,
|
|
1972
|
+
device_id: config.device_id,
|
|
1973
|
+
agent: "claude-code"
|
|
1974
|
+
});
|
|
1975
|
+
db.addToOutbox("chat_message", chatMessage.id);
|
|
1656
1976
|
const compactPrompt = event.prompt.replace(/\s+/g, " ").trim();
|
|
1657
1977
|
if (compactPrompt.length >= 8) {
|
|
1978
|
+
const sessionPrompts = db.getSessionUserPrompts(event.session_id, 20);
|
|
1979
|
+
const sessionToolEvents = db.getSessionToolEvents(event.session_id, 20);
|
|
1980
|
+
const sessionObservations = db.getObservationsBySession(event.session_id);
|
|
1981
|
+
const handoff = buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, sessionObservations);
|
|
1658
1982
|
const summary = db.upsertSessionSummary({
|
|
1659
1983
|
session_id: event.session_id,
|
|
1660
1984
|
project_id: project.id,
|
|
@@ -1663,7 +1987,12 @@ async function main() {
|
|
|
1663
1987
|
investigated: null,
|
|
1664
1988
|
learned: null,
|
|
1665
1989
|
completed: null,
|
|
1666
|
-
next_steps: null
|
|
1990
|
+
next_steps: null,
|
|
1991
|
+
current_thread: handoff.current_thread,
|
|
1992
|
+
capture_state: handoff.capture_state,
|
|
1993
|
+
recent_tool_names: JSON.stringify(handoff.recent_tool_names),
|
|
1994
|
+
hot_files: JSON.stringify(handoff.hot_files),
|
|
1995
|
+
recent_outcomes: JSON.stringify(handoff.recent_outcomes)
|
|
1667
1996
|
});
|
|
1668
1997
|
db.addToOutbox("summary", summary.id);
|
|
1669
1998
|
}
|