engrm 0.4.21 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/cli.js +326 -21
- package/dist/hooks/elicitation-result.js +245 -12
- package/dist/hooks/post-tool-use.js +333 -14
- package/dist/hooks/pre-compact.js +984 -14
- package/dist/hooks/sentinel.js +245 -12
- package/dist/hooks/session-start.js +1097 -90
- package/dist/hooks/stop.js +430 -75
- package/dist/hooks/user-prompt-submit.js +342 -13
- package/dist/server.js +923 -86
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -221,6 +221,9 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
221
221
|
| `recent_tools` | Inspect captured raw tool chronology |
|
|
222
222
|
| `recent_sessions` | List recent local sessions to inspect further |
|
|
223
223
|
| `session_story` | Show prompts, tools, observations, and summary for one session |
|
|
224
|
+
| `create_handoff` | Save an explicit syncable handoff so you can resume work on another device |
|
|
225
|
+
| `recent_handoffs` | List recent saved handoffs for the current project or workspace |
|
|
226
|
+
| `load_handoff` | Open a saved handoff as a resume point for a new session |
|
|
224
227
|
| `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
|
|
225
228
|
| `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
|
|
226
229
|
| `capture_git_diff` | Reduce a git diff into a durable memory object and save it |
|
|
@@ -273,6 +276,21 @@ These tools are intentionally small:
|
|
|
273
276
|
- reduced durable memory output
|
|
274
277
|
- visible in Engrm's local inspection tools so we can judge tool value honestly
|
|
275
278
|
|
|
279
|
+
### Explicit Handoffs
|
|
280
|
+
|
|
281
|
+
For long-running work across devices, Engrm now has an explicit handoff flow:
|
|
282
|
+
|
|
283
|
+
- `create_handoff`
|
|
284
|
+
- snapshot the active thread into a syncable handoff message
|
|
285
|
+
- `recent_handoffs`
|
|
286
|
+
- list the latest saved handoffs
|
|
287
|
+
- `load_handoff`
|
|
288
|
+
- reopen a saved handoff as a clear resume point in a new session
|
|
289
|
+
|
|
290
|
+
This is the deliberate version of multi-device continuity: useful when you want to move from laptop to home machine without waiting for an end-of-session summary.
|
|
291
|
+
|
|
292
|
+
The separate chat lane is still kept distinct from durable observations, but it can now sync too, so recent user/assistant conversation is recoverable on another machine without polluting the main memory feed.
|
|
293
|
+
|
|
276
294
|
### Local Memory Inspection
|
|
277
295
|
|
|
278
296
|
For local testing, Engrm now exposes a small inspection set that lets you see
|
package/dist/cli.js
CHANGED
|
@@ -305,7 +305,7 @@ var MIGRATIONS = [
|
|
|
305
305
|
-- Sync outbox (offline-first queue)
|
|
306
306
|
CREATE TABLE sync_outbox (
|
|
307
307
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
308
|
-
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
308
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
309
309
|
record_id INTEGER NOT NULL,
|
|
310
310
|
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
311
311
|
'pending', 'syncing', 'synced', 'failed'
|
|
@@ -600,6 +600,18 @@ var MIGRATIONS = [
|
|
|
600
600
|
},
|
|
601
601
|
{
|
|
602
602
|
version: 11,
|
|
603
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
604
|
+
sql: `
|
|
605
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
606
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
607
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
608
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
609
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
610
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
611
|
+
`
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
version: 12,
|
|
603
615
|
description: "Add synced handoff metadata to session summaries",
|
|
604
616
|
sql: `
|
|
605
617
|
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
@@ -609,15 +621,79 @@ var MIGRATIONS = [
|
|
|
609
621
|
`
|
|
610
622
|
},
|
|
611
623
|
{
|
|
612
|
-
version:
|
|
613
|
-
description: "Add
|
|
624
|
+
version: 13,
|
|
625
|
+
description: "Add current_thread to session summaries",
|
|
614
626
|
sql: `
|
|
615
|
-
ALTER TABLE
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
627
|
+
ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
|
|
628
|
+
`
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
version: 14,
|
|
632
|
+
description: "Add chat_messages lane for raw conversation recall",
|
|
633
|
+
sql: `
|
|
634
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
635
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
636
|
+
session_id TEXT NOT NULL,
|
|
637
|
+
project_id INTEGER REFERENCES projects(id),
|
|
638
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
639
|
+
content TEXT NOT NULL,
|
|
640
|
+
user_id TEXT NOT NULL,
|
|
641
|
+
device_id TEXT NOT NULL,
|
|
642
|
+
agent TEXT DEFAULT 'claude-code',
|
|
643
|
+
created_at_epoch INTEGER NOT NULL
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session
|
|
647
|
+
ON chat_messages(session_id, created_at_epoch DESC, id DESC);
|
|
648
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_project
|
|
649
|
+
ON chat_messages(project_id, created_at_epoch DESC, id DESC);
|
|
650
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created
|
|
651
|
+
ON chat_messages(created_at_epoch DESC, id DESC);
|
|
652
|
+
`
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
version: 15,
|
|
656
|
+
description: "Add remote_source_id for chat message sync deduplication",
|
|
657
|
+
sql: `
|
|
658
|
+
ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
|
|
659
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
|
|
660
|
+
ON chat_messages(remote_source_id)
|
|
661
|
+
WHERE remote_source_id IS NOT NULL;
|
|
662
|
+
`
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
version: 16,
|
|
666
|
+
description: "Allow chat_message records in sync_outbox",
|
|
667
|
+
sql: `
|
|
668
|
+
CREATE TABLE sync_outbox_new (
|
|
669
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
670
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
671
|
+
record_id INTEGER NOT NULL,
|
|
672
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
673
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
674
|
+
)),
|
|
675
|
+
retry_count INTEGER DEFAULT 0,
|
|
676
|
+
max_retries INTEGER DEFAULT 10,
|
|
677
|
+
last_error TEXT,
|
|
678
|
+
created_at_epoch INTEGER NOT NULL,
|
|
679
|
+
synced_at_epoch INTEGER,
|
|
680
|
+
next_retry_epoch INTEGER
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
INSERT INTO sync_outbox_new (
|
|
684
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
685
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
686
|
+
)
|
|
687
|
+
SELECT
|
|
688
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
689
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
690
|
+
FROM sync_outbox;
|
|
691
|
+
|
|
692
|
+
DROP TABLE sync_outbox;
|
|
693
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
694
|
+
|
|
695
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
696
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
621
697
|
`
|
|
622
698
|
}
|
|
623
699
|
];
|
|
@@ -674,6 +750,21 @@ function inferLegacySchemaVersion(db) {
|
|
|
674
750
|
version = Math.max(version, 10);
|
|
675
751
|
if (columnExists(db, "observations", "source_tool"))
|
|
676
752
|
version = Math.max(version, 11);
|
|
753
|
+
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")) {
|
|
754
|
+
version = Math.max(version, 12);
|
|
755
|
+
}
|
|
756
|
+
if (columnExists(db, "session_summaries", "current_thread")) {
|
|
757
|
+
version = Math.max(version, 13);
|
|
758
|
+
}
|
|
759
|
+
if (tableExists(db, "chat_messages")) {
|
|
760
|
+
version = Math.max(version, 14);
|
|
761
|
+
}
|
|
762
|
+
if (columnExists(db, "chat_messages", "remote_source_id")) {
|
|
763
|
+
version = Math.max(version, 15);
|
|
764
|
+
}
|
|
765
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
766
|
+
version = Math.max(version, 16);
|
|
767
|
+
}
|
|
677
768
|
return version;
|
|
678
769
|
}
|
|
679
770
|
function runMigrations(db) {
|
|
@@ -752,6 +843,89 @@ function ensureObservationTypes(db) {
|
|
|
752
843
|
}
|
|
753
844
|
}
|
|
754
845
|
}
|
|
846
|
+
function ensureSessionSummaryColumns(db) {
|
|
847
|
+
const required = [
|
|
848
|
+
"capture_state",
|
|
849
|
+
"recent_tool_names",
|
|
850
|
+
"hot_files",
|
|
851
|
+
"recent_outcomes",
|
|
852
|
+
"current_thread"
|
|
853
|
+
];
|
|
854
|
+
for (const column of required) {
|
|
855
|
+
if (columnExists(db, "session_summaries", column))
|
|
856
|
+
continue;
|
|
857
|
+
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
858
|
+
}
|
|
859
|
+
const current = getSchemaVersion(db);
|
|
860
|
+
if (current < 13) {
|
|
861
|
+
db.exec("PRAGMA user_version = 13");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function ensureChatMessageColumns(db) {
|
|
865
|
+
if (!tableExists(db, "chat_messages"))
|
|
866
|
+
return;
|
|
867
|
+
if (!columnExists(db, "chat_messages", "remote_source_id")) {
|
|
868
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
869
|
+
}
|
|
870
|
+
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");
|
|
871
|
+
const current = getSchemaVersion(db);
|
|
872
|
+
if (current < 15) {
|
|
873
|
+
db.exec("PRAGMA user_version = 15");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
877
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
878
|
+
const current = getSchemaVersion(db);
|
|
879
|
+
if (current < 16) {
|
|
880
|
+
db.exec("PRAGMA user_version = 16");
|
|
881
|
+
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
db.exec("BEGIN TRANSACTION");
|
|
885
|
+
try {
|
|
886
|
+
db.exec(`
|
|
887
|
+
CREATE TABLE sync_outbox_new (
|
|
888
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
889
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
890
|
+
record_id INTEGER NOT NULL,
|
|
891
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
892
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
893
|
+
)),
|
|
894
|
+
retry_count INTEGER DEFAULT 0,
|
|
895
|
+
max_retries INTEGER DEFAULT 10,
|
|
896
|
+
last_error TEXT,
|
|
897
|
+
created_at_epoch INTEGER NOT NULL,
|
|
898
|
+
synced_at_epoch INTEGER,
|
|
899
|
+
next_retry_epoch INTEGER
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
INSERT INTO sync_outbox_new (
|
|
903
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
904
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
905
|
+
)
|
|
906
|
+
SELECT
|
|
907
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
908
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
909
|
+
FROM sync_outbox;
|
|
910
|
+
|
|
911
|
+
DROP TABLE sync_outbox;
|
|
912
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
913
|
+
|
|
914
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
915
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
916
|
+
`);
|
|
917
|
+
db.exec("PRAGMA user_version = 16");
|
|
918
|
+
db.exec("COMMIT");
|
|
919
|
+
} catch (error) {
|
|
920
|
+
db.exec("ROLLBACK");
|
|
921
|
+
throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function syncOutboxSupportsChatMessages(db) {
|
|
925
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
|
|
926
|
+
const sql = row?.sql ?? "";
|
|
927
|
+
return sql.includes("'chat_message'");
|
|
928
|
+
}
|
|
755
929
|
function getSchemaVersion(db) {
|
|
756
930
|
const result = db.query("PRAGMA user_version").get();
|
|
757
931
|
return result.user_version;
|
|
@@ -910,6 +1084,9 @@ class MemDatabase {
|
|
|
910
1084
|
this.vecAvailable = this.loadVecExtension();
|
|
911
1085
|
runMigrations(this.db);
|
|
912
1086
|
ensureObservationTypes(this.db);
|
|
1087
|
+
ensureSessionSummaryColumns(this.db);
|
|
1088
|
+
ensureChatMessageColumns(this.db);
|
|
1089
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
913
1090
|
}
|
|
914
1091
|
loadVecExtension() {
|
|
915
1092
|
try {
|
|
@@ -1135,6 +1312,7 @@ class MemDatabase {
|
|
|
1135
1312
|
p.name AS project_name,
|
|
1136
1313
|
ss.request AS request,
|
|
1137
1314
|
ss.completed AS completed,
|
|
1315
|
+
ss.current_thread AS current_thread,
|
|
1138
1316
|
ss.capture_state AS capture_state,
|
|
1139
1317
|
ss.recent_tool_names AS recent_tool_names,
|
|
1140
1318
|
ss.hot_files AS hot_files,
|
|
@@ -1153,6 +1331,7 @@ class MemDatabase {
|
|
|
1153
1331
|
p.name AS project_name,
|
|
1154
1332
|
ss.request AS request,
|
|
1155
1333
|
ss.completed AS completed,
|
|
1334
|
+
ss.current_thread AS current_thread,
|
|
1156
1335
|
ss.capture_state AS capture_state,
|
|
1157
1336
|
ss.recent_tool_names AS recent_tool_names,
|
|
1158
1337
|
ss.hot_files AS hot_files,
|
|
@@ -1243,6 +1422,54 @@ class MemDatabase {
|
|
|
1243
1422
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1244
1423
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1245
1424
|
}
|
|
1425
|
+
insertChatMessage(input) {
|
|
1426
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1427
|
+
const content = input.content.trim();
|
|
1428
|
+
const result = this.db.query(`INSERT INTO chat_messages (
|
|
1429
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
1430
|
+
) 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);
|
|
1431
|
+
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1432
|
+
}
|
|
1433
|
+
getChatMessageById(id) {
|
|
1434
|
+
return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
|
|
1435
|
+
}
|
|
1436
|
+
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1437
|
+
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1438
|
+
}
|
|
1439
|
+
getSessionChatMessages(sessionId, limit = 50) {
|
|
1440
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1441
|
+
WHERE session_id = ?
|
|
1442
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
1443
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1444
|
+
}
|
|
1445
|
+
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
1446
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1447
|
+
if (projectId !== null) {
|
|
1448
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1449
|
+
WHERE project_id = ?${visibilityClause}
|
|
1450
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1451
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1452
|
+
}
|
|
1453
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1454
|
+
WHERE 1 = 1${visibilityClause}
|
|
1455
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1456
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1457
|
+
}
|
|
1458
|
+
searchChatMessages(query, projectId, limit = 20, userId) {
|
|
1459
|
+
const needle = `%${query.toLowerCase()}%`;
|
|
1460
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1461
|
+
if (projectId !== null) {
|
|
1462
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1463
|
+
WHERE project_id = ?
|
|
1464
|
+
AND lower(content) LIKE ?${visibilityClause}
|
|
1465
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1466
|
+
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1467
|
+
}
|
|
1468
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1469
|
+
WHERE lower(content) LIKE ?${visibilityClause}
|
|
1470
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1471
|
+
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1472
|
+
}
|
|
1246
1473
|
addToOutbox(recordType, recordId) {
|
|
1247
1474
|
const now = Math.floor(Date.now() / 1000);
|
|
1248
1475
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1331,9 +1558,9 @@ class MemDatabase {
|
|
|
1331
1558
|
};
|
|
1332
1559
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1333
1560
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1334
|
-
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1561
|
+
current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1335
1562
|
)
|
|
1336
|
-
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);
|
|
1563
|
+
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);
|
|
1337
1564
|
const id = Number(result.lastInsertRowid);
|
|
1338
1565
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1339
1566
|
}
|
|
@@ -1349,6 +1576,7 @@ class MemDatabase {
|
|
|
1349
1576
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1350
1577
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1351
1578
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1579
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1352
1580
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1353
1581
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1354
1582
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1362,12 +1590,13 @@ class MemDatabase {
|
|
|
1362
1590
|
learned = ?,
|
|
1363
1591
|
completed = ?,
|
|
1364
1592
|
next_steps = ?,
|
|
1593
|
+
current_thread = ?,
|
|
1365
1594
|
capture_state = ?,
|
|
1366
1595
|
recent_tool_names = ?,
|
|
1367
1596
|
hot_files = ?,
|
|
1368
1597
|
recent_outcomes = ?,
|
|
1369
1598
|
created_at_epoch = ?
|
|
1370
|
-
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);
|
|
1599
|
+
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);
|
|
1371
1600
|
return this.getSessionSummary(summary.session_id);
|
|
1372
1601
|
}
|
|
1373
1602
|
getSessionSummary(sessionId) {
|
|
@@ -1579,7 +1808,7 @@ var MIGRATIONS2 = [
|
|
|
1579
1808
|
-- Sync outbox (offline-first queue)
|
|
1580
1809
|
CREATE TABLE sync_outbox (
|
|
1581
1810
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1582
|
-
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
1811
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
1583
1812
|
record_id INTEGER NOT NULL,
|
|
1584
1813
|
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
1585
1814
|
'pending', 'syncing', 'synced', 'failed'
|
|
@@ -1874,6 +2103,18 @@ var MIGRATIONS2 = [
|
|
|
1874
2103
|
},
|
|
1875
2104
|
{
|
|
1876
2105
|
version: 11,
|
|
2106
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
2107
|
+
sql: `
|
|
2108
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
2109
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
2110
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
2111
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
2112
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
2113
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
2114
|
+
`
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
version: 12,
|
|
1877
2118
|
description: "Add synced handoff metadata to session summaries",
|
|
1878
2119
|
sql: `
|
|
1879
2120
|
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
@@ -1883,15 +2124,79 @@ var MIGRATIONS2 = [
|
|
|
1883
2124
|
`
|
|
1884
2125
|
},
|
|
1885
2126
|
{
|
|
1886
|
-
version:
|
|
1887
|
-
description: "Add
|
|
2127
|
+
version: 13,
|
|
2128
|
+
description: "Add current_thread to session summaries",
|
|
1888
2129
|
sql: `
|
|
1889
|
-
ALTER TABLE
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2130
|
+
ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
|
|
2131
|
+
`
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
version: 14,
|
|
2135
|
+
description: "Add chat_messages lane for raw conversation recall",
|
|
2136
|
+
sql: `
|
|
2137
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
2138
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2139
|
+
session_id TEXT NOT NULL,
|
|
2140
|
+
project_id INTEGER REFERENCES projects(id),
|
|
2141
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
2142
|
+
content TEXT NOT NULL,
|
|
2143
|
+
user_id TEXT NOT NULL,
|
|
2144
|
+
device_id TEXT NOT NULL,
|
|
2145
|
+
agent TEXT DEFAULT 'claude-code',
|
|
2146
|
+
created_at_epoch INTEGER NOT NULL
|
|
2147
|
+
);
|
|
2148
|
+
|
|
2149
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session
|
|
2150
|
+
ON chat_messages(session_id, created_at_epoch DESC, id DESC);
|
|
2151
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_project
|
|
2152
|
+
ON chat_messages(project_id, created_at_epoch DESC, id DESC);
|
|
2153
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created
|
|
2154
|
+
ON chat_messages(created_at_epoch DESC, id DESC);
|
|
2155
|
+
`
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
version: 15,
|
|
2159
|
+
description: "Add remote_source_id for chat message sync deduplication",
|
|
2160
|
+
sql: `
|
|
2161
|
+
ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
|
|
2162
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
|
|
2163
|
+
ON chat_messages(remote_source_id)
|
|
2164
|
+
WHERE remote_source_id IS NOT NULL;
|
|
2165
|
+
`
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
version: 16,
|
|
2169
|
+
description: "Allow chat_message records in sync_outbox",
|
|
2170
|
+
sql: `
|
|
2171
|
+
CREATE TABLE sync_outbox_new (
|
|
2172
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2173
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
2174
|
+
record_id INTEGER NOT NULL,
|
|
2175
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
2176
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
2177
|
+
)),
|
|
2178
|
+
retry_count INTEGER DEFAULT 0,
|
|
2179
|
+
max_retries INTEGER DEFAULT 10,
|
|
2180
|
+
last_error TEXT,
|
|
2181
|
+
created_at_epoch INTEGER NOT NULL,
|
|
2182
|
+
synced_at_epoch INTEGER,
|
|
2183
|
+
next_retry_epoch INTEGER
|
|
2184
|
+
);
|
|
2185
|
+
|
|
2186
|
+
INSERT INTO sync_outbox_new (
|
|
2187
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
2188
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
2189
|
+
)
|
|
2190
|
+
SELECT
|
|
2191
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
2192
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
2193
|
+
FROM sync_outbox;
|
|
2194
|
+
|
|
2195
|
+
DROP TABLE sync_outbox;
|
|
2196
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
2197
|
+
|
|
2198
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
2199
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
1895
2200
|
`
|
|
1896
2201
|
}
|
|
1897
2202
|
];
|