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/README.md
CHANGED
|
@@ -211,7 +211,7 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
211
211
|
| `recent_activity` | Inspect what Engrm captured most recently |
|
|
212
212
|
| `memory_stats` | View high-level capture and sync health |
|
|
213
213
|
| `capture_status` | Check whether local hooks are registered and raw prompt/tool chronology is actually being captured |
|
|
214
|
-
| `activity_feed` | Inspect one chronological local feed across prompts, tools, observations, and summaries |
|
|
214
|
+
| `activity_feed` | Inspect one chronological local feed across prompts, tools, chat, handoffs, observations, and summaries |
|
|
215
215
|
| `memory_console` | Show a high-signal local memory console for the current project |
|
|
216
216
|
| `project_memory_index` | Show typed local memory by project, including hot files and recent sessions |
|
|
217
217
|
| `workspace_memory_index` | Show cross-project local memory coverage across the whole workspace |
|
|
@@ -221,6 +221,13 @@ 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
|
+
| `refresh_handoff` | Refresh the rolling live handoff draft for the current session without creating a new saved handoff |
|
|
226
|
+
| `recent_handoffs` | List recent saved handoffs for the current project or workspace |
|
|
227
|
+
| `load_handoff` | Open a saved handoff as a resume point for a new session |
|
|
228
|
+
| `refresh_chat_recall` | Rehydrate the separate chat lane from a Claude transcript when a long session feels under-captured |
|
|
229
|
+
| `recent_chat` | Inspect the separate synced chat lane without mixing it into durable memory |
|
|
230
|
+
| `search_chat` | Search recent chat recall separately from reusable memory observations |
|
|
224
231
|
| `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
|
|
225
232
|
| `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
|
|
226
233
|
| `capture_git_diff` | Reduce a git diff into a durable memory object and save it |
|
|
@@ -273,6 +280,61 @@ These tools are intentionally small:
|
|
|
273
280
|
- reduced durable memory output
|
|
274
281
|
- visible in Engrm's local inspection tools so we can judge tool value honestly
|
|
275
282
|
|
|
283
|
+
### Explicit Handoffs
|
|
284
|
+
|
|
285
|
+
For long-running work across devices, Engrm now has an explicit handoff flow:
|
|
286
|
+
|
|
287
|
+
- `create_handoff`
|
|
288
|
+
- snapshot the active thread into a syncable handoff message
|
|
289
|
+
- `refresh_handoff`
|
|
290
|
+
- refresh the rolling live handoff draft for the current session
|
|
291
|
+
- `recent_handoffs`
|
|
292
|
+
- list the latest saved handoffs
|
|
293
|
+
- `load_handoff`
|
|
294
|
+
- reopen a saved handoff as a clear resume point in a new session
|
|
295
|
+
|
|
296
|
+
Recent handoffs now carry:
|
|
297
|
+
|
|
298
|
+
- source machine
|
|
299
|
+
- freshness
|
|
300
|
+
- current thread / recent outcomes
|
|
301
|
+
- optional chat snippets when the session is still thin
|
|
302
|
+
|
|
303
|
+
Rolling handoff drafts:
|
|
304
|
+
|
|
305
|
+
- are kept as one updatable syncable draft per session
|
|
306
|
+
- refresh during prompt-time and tool-time summary updates
|
|
307
|
+
- let another machine resume live work even before you save a deliberate handoff
|
|
308
|
+
- are refreshed again before Claude compacts, so the active thread survives context compression better
|
|
309
|
+
|
|
310
|
+
The local workbench now shows handoff split too:
|
|
311
|
+
|
|
312
|
+
- saved handoffs
|
|
313
|
+
- rolling drafts
|
|
314
|
+
|
|
315
|
+
`activity_feed` and `session_story` now keep that distinction visible too, so a live rolling draft does not masquerade as a deliberate saved handoff.
|
|
316
|
+
|
|
317
|
+
When Engrm knows your current device, handoff tools also prefer resume points from another machine before showing the newest local handoff again.
|
|
318
|
+
|
|
319
|
+
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.
|
|
320
|
+
|
|
321
|
+
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.
|
|
322
|
+
|
|
323
|
+
For long sessions, Engrm now also supports transcript-backed chat hydration:
|
|
324
|
+
|
|
325
|
+
- `refresh_chat_recall`
|
|
326
|
+
- reads the Claude transcript for the current session
|
|
327
|
+
- fills gaps in the separate chat lane with transcript-backed messages
|
|
328
|
+
- keeps those rows marked separately from hook-edge chat so recall can prefer the fuller thread
|
|
329
|
+
|
|
330
|
+
Before Claude compacts, Engrm now also:
|
|
331
|
+
|
|
332
|
+
- refreshes transcript-backed chat recall for the active session
|
|
333
|
+
- refreshes the rolling handoff draft
|
|
334
|
+
- then injects the preserved thread into the compacted context
|
|
335
|
+
|
|
336
|
+
So compaction should reduce prompt-window pressure without making the memory layer act like the conversation never happened.
|
|
337
|
+
|
|
276
338
|
### Local Memory Inspection
|
|
277
339
|
|
|
278
340
|
For local testing, Engrm now exposes a small inspection set that lets you see
|
|
@@ -296,9 +358,9 @@ What each tool is good for:
|
|
|
296
358
|
|
|
297
359
|
- `capture_status` tells you whether prompt/tool hooks are live on this machine
|
|
298
360
|
- `memory_console` gives the quickest project snapshot
|
|
299
|
-
- `activity_feed` shows the merged chronology across prompts, tools, observations, and summaries
|
|
361
|
+
- `activity_feed` shows the merged chronology across prompts, tools, chat, handoffs, observations, and summaries
|
|
300
362
|
- `recent_sessions` helps you pick a session worth opening
|
|
301
|
-
- `session_story` reconstructs one session in detail
|
|
363
|
+
- `session_story` reconstructs one session in detail, including handoffs and chat recall
|
|
302
364
|
- `tool_memory_index` shows which tools and plugins are actually producing durable memory
|
|
303
365
|
- `session_tool_memory` shows which tool calls in one session turned into reusable memory and which did not
|
|
304
366
|
- `project_memory_index` shows typed memory by repo
|
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'
|
|
@@ -598,6 +598,18 @@ var MIGRATIONS = [
|
|
|
598
598
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
599
599
|
`
|
|
600
600
|
},
|
|
601
|
+
{
|
|
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
|
+
},
|
|
601
613
|
{
|
|
602
614
|
version: 12,
|
|
603
615
|
description: "Add synced handoff metadata to session summaries",
|
|
@@ -609,15 +621,92 @@ 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);
|
|
697
|
+
`
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
version: 17,
|
|
701
|
+
description: "Track transcript-backed chat messages separately from hook chat",
|
|
702
|
+
sql: `
|
|
703
|
+
ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook';
|
|
704
|
+
ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER;
|
|
705
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_source_kind
|
|
706
|
+
ON chat_messages(session_id, source_kind, transcript_index, created_at_epoch DESC, id DESC);
|
|
707
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_session_transcript
|
|
708
|
+
ON chat_messages(session_id, transcript_index)
|
|
709
|
+
WHERE transcript_index IS NOT NULL;
|
|
621
710
|
`
|
|
622
711
|
}
|
|
623
712
|
];
|
|
@@ -677,6 +766,21 @@ function inferLegacySchemaVersion(db) {
|
|
|
677
766
|
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")) {
|
|
678
767
|
version = Math.max(version, 12);
|
|
679
768
|
}
|
|
769
|
+
if (columnExists(db, "session_summaries", "current_thread")) {
|
|
770
|
+
version = Math.max(version, 13);
|
|
771
|
+
}
|
|
772
|
+
if (tableExists(db, "chat_messages")) {
|
|
773
|
+
version = Math.max(version, 14);
|
|
774
|
+
}
|
|
775
|
+
if (columnExists(db, "chat_messages", "remote_source_id")) {
|
|
776
|
+
version = Math.max(version, 15);
|
|
777
|
+
}
|
|
778
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
779
|
+
version = Math.max(version, 16);
|
|
780
|
+
}
|
|
781
|
+
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
782
|
+
version = Math.max(version, 17);
|
|
783
|
+
}
|
|
680
784
|
return version;
|
|
681
785
|
}
|
|
682
786
|
function runMigrations(db) {
|
|
@@ -760,7 +864,8 @@ function ensureSessionSummaryColumns(db) {
|
|
|
760
864
|
"capture_state",
|
|
761
865
|
"recent_tool_names",
|
|
762
866
|
"hot_files",
|
|
763
|
-
"recent_outcomes"
|
|
867
|
+
"recent_outcomes",
|
|
868
|
+
"current_thread"
|
|
764
869
|
];
|
|
765
870
|
for (const column of required) {
|
|
766
871
|
if (columnExists(db, "session_summaries", column))
|
|
@@ -768,10 +873,83 @@ function ensureSessionSummaryColumns(db) {
|
|
|
768
873
|
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
769
874
|
}
|
|
770
875
|
const current = getSchemaVersion(db);
|
|
771
|
-
if (current <
|
|
772
|
-
db.exec("PRAGMA user_version =
|
|
876
|
+
if (current < 13) {
|
|
877
|
+
db.exec("PRAGMA user_version = 13");
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function ensureChatMessageColumns(db) {
|
|
881
|
+
if (!tableExists(db, "chat_messages"))
|
|
882
|
+
return;
|
|
883
|
+
if (!columnExists(db, "chat_messages", "remote_source_id")) {
|
|
884
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
885
|
+
}
|
|
886
|
+
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");
|
|
887
|
+
if (!columnExists(db, "chat_messages", "source_kind")) {
|
|
888
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook'");
|
|
889
|
+
}
|
|
890
|
+
if (!columnExists(db, "chat_messages", "transcript_index")) {
|
|
891
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER");
|
|
892
|
+
}
|
|
893
|
+
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)");
|
|
894
|
+
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");
|
|
895
|
+
const current = getSchemaVersion(db);
|
|
896
|
+
if (current < 17) {
|
|
897
|
+
db.exec("PRAGMA user_version = 17");
|
|
773
898
|
}
|
|
774
899
|
}
|
|
900
|
+
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
901
|
+
if (syncOutboxSupportsChatMessages(db)) {
|
|
902
|
+
const current = getSchemaVersion(db);
|
|
903
|
+
if (current < 16) {
|
|
904
|
+
db.exec("PRAGMA user_version = 16");
|
|
905
|
+
}
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
db.exec("BEGIN TRANSACTION");
|
|
909
|
+
try {
|
|
910
|
+
db.exec(`
|
|
911
|
+
CREATE TABLE sync_outbox_new (
|
|
912
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
913
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
914
|
+
record_id INTEGER NOT NULL,
|
|
915
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
916
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
917
|
+
)),
|
|
918
|
+
retry_count INTEGER DEFAULT 0,
|
|
919
|
+
max_retries INTEGER DEFAULT 10,
|
|
920
|
+
last_error TEXT,
|
|
921
|
+
created_at_epoch INTEGER NOT NULL,
|
|
922
|
+
synced_at_epoch INTEGER,
|
|
923
|
+
next_retry_epoch INTEGER
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
INSERT INTO sync_outbox_new (
|
|
927
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
928
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
929
|
+
)
|
|
930
|
+
SELECT
|
|
931
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
932
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
933
|
+
FROM sync_outbox;
|
|
934
|
+
|
|
935
|
+
DROP TABLE sync_outbox;
|
|
936
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
937
|
+
|
|
938
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
939
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
940
|
+
`);
|
|
941
|
+
db.exec("PRAGMA user_version = 16");
|
|
942
|
+
db.exec("COMMIT");
|
|
943
|
+
} catch (error) {
|
|
944
|
+
db.exec("ROLLBACK");
|
|
945
|
+
throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function syncOutboxSupportsChatMessages(db) {
|
|
949
|
+
const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
|
|
950
|
+
const sql = row?.sql ?? "";
|
|
951
|
+
return sql.includes("'chat_message'");
|
|
952
|
+
}
|
|
775
953
|
function getSchemaVersion(db) {
|
|
776
954
|
const result = db.query("PRAGMA user_version").get();
|
|
777
955
|
return result.user_version;
|
|
@@ -931,6 +1109,8 @@ class MemDatabase {
|
|
|
931
1109
|
runMigrations(this.db);
|
|
932
1110
|
ensureObservationTypes(this.db);
|
|
933
1111
|
ensureSessionSummaryColumns(this.db);
|
|
1112
|
+
ensureChatMessageColumns(this.db);
|
|
1113
|
+
ensureSyncOutboxSupportsChatMessages(this.db);
|
|
934
1114
|
}
|
|
935
1115
|
loadVecExtension() {
|
|
936
1116
|
try {
|
|
@@ -999,6 +1179,22 @@ class MemDatabase {
|
|
|
999
1179
|
getObservationById(id) {
|
|
1000
1180
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
1001
1181
|
}
|
|
1182
|
+
updateObservationContent(id, update) {
|
|
1183
|
+
const existing = this.getObservationById(id);
|
|
1184
|
+
if (!existing)
|
|
1185
|
+
return null;
|
|
1186
|
+
const createdAtEpoch = update.created_at_epoch ?? existing.created_at_epoch;
|
|
1187
|
+
const createdAt = new Date(createdAtEpoch * 1000).toISOString();
|
|
1188
|
+
this.db.query(`UPDATE observations
|
|
1189
|
+
SET title = ?, narrative = ?, facts = ?, concepts = ?, created_at = ?, created_at_epoch = ?
|
|
1190
|
+
WHERE id = ?`).run(update.title, update.narrative ?? null, update.facts ?? null, update.concepts ?? null, createdAt, createdAtEpoch, id);
|
|
1191
|
+
this.ftsDelete(existing);
|
|
1192
|
+
const refreshed = this.getObservationById(id);
|
|
1193
|
+
if (!refreshed)
|
|
1194
|
+
return null;
|
|
1195
|
+
this.ftsInsert(refreshed);
|
|
1196
|
+
return refreshed;
|
|
1197
|
+
}
|
|
1002
1198
|
getObservationsByIds(ids, userId) {
|
|
1003
1199
|
if (ids.length === 0)
|
|
1004
1200
|
return [];
|
|
@@ -1156,6 +1352,7 @@ class MemDatabase {
|
|
|
1156
1352
|
p.name AS project_name,
|
|
1157
1353
|
ss.request AS request,
|
|
1158
1354
|
ss.completed AS completed,
|
|
1355
|
+
ss.current_thread AS current_thread,
|
|
1159
1356
|
ss.capture_state AS capture_state,
|
|
1160
1357
|
ss.recent_tool_names AS recent_tool_names,
|
|
1161
1358
|
ss.hot_files AS hot_files,
|
|
@@ -1174,6 +1371,7 @@ class MemDatabase {
|
|
|
1174
1371
|
p.name AS project_name,
|
|
1175
1372
|
ss.request AS request,
|
|
1176
1373
|
ss.completed AS completed,
|
|
1374
|
+
ss.current_thread AS current_thread,
|
|
1177
1375
|
ss.capture_state AS capture_state,
|
|
1178
1376
|
ss.recent_tool_names AS recent_tool_names,
|
|
1179
1377
|
ss.hot_files AS hot_files,
|
|
@@ -1264,6 +1462,99 @@ class MemDatabase {
|
|
|
1264
1462
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1265
1463
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1266
1464
|
}
|
|
1465
|
+
insertChatMessage(input) {
|
|
1466
|
+
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1467
|
+
const content = input.content.trim();
|
|
1468
|
+
const result = this.db.query(`INSERT INTO chat_messages (
|
|
1469
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id, source_kind, transcript_index
|
|
1470
|
+
) 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);
|
|
1471
|
+
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1472
|
+
}
|
|
1473
|
+
getChatMessageById(id) {
|
|
1474
|
+
return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
|
|
1475
|
+
}
|
|
1476
|
+
getChatMessageByRemoteSourceId(remoteSourceId) {
|
|
1477
|
+
return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
|
|
1478
|
+
}
|
|
1479
|
+
getSessionChatMessages(sessionId, limit = 50) {
|
|
1480
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1481
|
+
WHERE session_id = ?
|
|
1482
|
+
AND (
|
|
1483
|
+
source_kind = 'transcript'
|
|
1484
|
+
OR NOT EXISTS (
|
|
1485
|
+
SELECT 1 FROM chat_messages t2
|
|
1486
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1487
|
+
AND t2.source_kind = 'transcript'
|
|
1488
|
+
)
|
|
1489
|
+
)
|
|
1490
|
+
ORDER BY
|
|
1491
|
+
CASE WHEN transcript_index IS NULL THEN created_at_epoch ELSE transcript_index END ASC,
|
|
1492
|
+
id ASC
|
|
1493
|
+
LIMIT ?`).all(sessionId, limit);
|
|
1494
|
+
}
|
|
1495
|
+
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
1496
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1497
|
+
if (projectId !== null) {
|
|
1498
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1499
|
+
WHERE project_id = ?${visibilityClause}
|
|
1500
|
+
AND (
|
|
1501
|
+
source_kind = 'transcript'
|
|
1502
|
+
OR NOT EXISTS (
|
|
1503
|
+
SELECT 1 FROM chat_messages t2
|
|
1504
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1505
|
+
AND t2.source_kind = 'transcript'
|
|
1506
|
+
)
|
|
1507
|
+
)
|
|
1508
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1509
|
+
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1510
|
+
}
|
|
1511
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1512
|
+
WHERE 1 = 1${visibilityClause}
|
|
1513
|
+
AND (
|
|
1514
|
+
source_kind = 'transcript'
|
|
1515
|
+
OR NOT EXISTS (
|
|
1516
|
+
SELECT 1 FROM chat_messages t2
|
|
1517
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1518
|
+
AND t2.source_kind = 'transcript'
|
|
1519
|
+
)
|
|
1520
|
+
)
|
|
1521
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1522
|
+
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1523
|
+
}
|
|
1524
|
+
searchChatMessages(query, projectId, limit = 20, userId) {
|
|
1525
|
+
const needle = `%${query.toLowerCase()}%`;
|
|
1526
|
+
const visibilityClause = userId ? " AND user_id = ?" : "";
|
|
1527
|
+
if (projectId !== null) {
|
|
1528
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1529
|
+
WHERE project_id = ?
|
|
1530
|
+
AND lower(content) LIKE ?${visibilityClause}
|
|
1531
|
+
AND (
|
|
1532
|
+
source_kind = 'transcript'
|
|
1533
|
+
OR NOT EXISTS (
|
|
1534
|
+
SELECT 1 FROM chat_messages t2
|
|
1535
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1536
|
+
AND t2.source_kind = 'transcript'
|
|
1537
|
+
)
|
|
1538
|
+
)
|
|
1539
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1540
|
+
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1541
|
+
}
|
|
1542
|
+
return this.db.query(`SELECT * FROM chat_messages
|
|
1543
|
+
WHERE lower(content) LIKE ?${visibilityClause}
|
|
1544
|
+
AND (
|
|
1545
|
+
source_kind = 'transcript'
|
|
1546
|
+
OR NOT EXISTS (
|
|
1547
|
+
SELECT 1 FROM chat_messages t2
|
|
1548
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1549
|
+
AND t2.source_kind = 'transcript'
|
|
1550
|
+
)
|
|
1551
|
+
)
|
|
1552
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
1553
|
+
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1554
|
+
}
|
|
1555
|
+
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
1556
|
+
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
1557
|
+
}
|
|
1267
1558
|
addToOutbox(recordType, recordId) {
|
|
1268
1559
|
const now = Math.floor(Date.now() / 1000);
|
|
1269
1560
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -1352,9 +1643,9 @@ class MemDatabase {
|
|
|
1352
1643
|
};
|
|
1353
1644
|
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1354
1645
|
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1355
|
-
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1646
|
+
current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1356
1647
|
)
|
|
1357
|
-
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);
|
|
1648
|
+
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);
|
|
1358
1649
|
const id = Number(result.lastInsertRowid);
|
|
1359
1650
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1360
1651
|
}
|
|
@@ -1370,6 +1661,7 @@ class MemDatabase {
|
|
|
1370
1661
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1371
1662
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1372
1663
|
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1664
|
+
current_thread: summary.current_thread ?? existing.current_thread,
|
|
1373
1665
|
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1374
1666
|
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1375
1667
|
hot_files: summary.hot_files ?? existing.hot_files,
|
|
@@ -1383,12 +1675,13 @@ class MemDatabase {
|
|
|
1383
1675
|
learned = ?,
|
|
1384
1676
|
completed = ?,
|
|
1385
1677
|
next_steps = ?,
|
|
1678
|
+
current_thread = ?,
|
|
1386
1679
|
capture_state = ?,
|
|
1387
1680
|
recent_tool_names = ?,
|
|
1388
1681
|
hot_files = ?,
|
|
1389
1682
|
recent_outcomes = ?,
|
|
1390
1683
|
created_at_epoch = ?
|
|
1391
|
-
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);
|
|
1684
|
+
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);
|
|
1392
1685
|
return this.getSessionSummary(summary.session_id);
|
|
1393
1686
|
}
|
|
1394
1687
|
getSessionSummary(sessionId) {
|
|
@@ -1600,7 +1893,7 @@ var MIGRATIONS2 = [
|
|
|
1600
1893
|
-- Sync outbox (offline-first queue)
|
|
1601
1894
|
CREATE TABLE sync_outbox (
|
|
1602
1895
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1603
|
-
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
|
|
1896
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
1604
1897
|
record_id INTEGER NOT NULL,
|
|
1605
1898
|
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
1606
1899
|
'pending', 'syncing', 'synced', 'failed'
|
|
@@ -1893,6 +2186,18 @@ var MIGRATIONS2 = [
|
|
|
1893
2186
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
1894
2187
|
`
|
|
1895
2188
|
},
|
|
2189
|
+
{
|
|
2190
|
+
version: 11,
|
|
2191
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
2192
|
+
sql: `
|
|
2193
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
2194
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
2195
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
2196
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
2197
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
2198
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
2199
|
+
`
|
|
2200
|
+
},
|
|
1896
2201
|
{
|
|
1897
2202
|
version: 12,
|
|
1898
2203
|
description: "Add synced handoff metadata to session summaries",
|
|
@@ -1904,15 +2209,92 @@ var MIGRATIONS2 = [
|
|
|
1904
2209
|
`
|
|
1905
2210
|
},
|
|
1906
2211
|
{
|
|
1907
|
-
version:
|
|
1908
|
-
description: "Add
|
|
2212
|
+
version: 13,
|
|
2213
|
+
description: "Add current_thread to session summaries",
|
|
1909
2214
|
sql: `
|
|
1910
|
-
ALTER TABLE
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2215
|
+
ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
|
|
2216
|
+
`
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
version: 14,
|
|
2220
|
+
description: "Add chat_messages lane for raw conversation recall",
|
|
2221
|
+
sql: `
|
|
2222
|
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
2223
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2224
|
+
session_id TEXT NOT NULL,
|
|
2225
|
+
project_id INTEGER REFERENCES projects(id),
|
|
2226
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
2227
|
+
content TEXT NOT NULL,
|
|
2228
|
+
user_id TEXT NOT NULL,
|
|
2229
|
+
device_id TEXT NOT NULL,
|
|
2230
|
+
agent TEXT DEFAULT 'claude-code',
|
|
2231
|
+
created_at_epoch INTEGER NOT NULL
|
|
2232
|
+
);
|
|
2233
|
+
|
|
2234
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_session
|
|
2235
|
+
ON chat_messages(session_id, created_at_epoch DESC, id DESC);
|
|
2236
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_project
|
|
2237
|
+
ON chat_messages(project_id, created_at_epoch DESC, id DESC);
|
|
2238
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_created
|
|
2239
|
+
ON chat_messages(created_at_epoch DESC, id DESC);
|
|
2240
|
+
`
|
|
2241
|
+
},
|
|
2242
|
+
{
|
|
2243
|
+
version: 15,
|
|
2244
|
+
description: "Add remote_source_id for chat message sync deduplication",
|
|
2245
|
+
sql: `
|
|
2246
|
+
ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
|
|
2247
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
|
|
2248
|
+
ON chat_messages(remote_source_id)
|
|
2249
|
+
WHERE remote_source_id IS NOT NULL;
|
|
2250
|
+
`
|
|
2251
|
+
},
|
|
2252
|
+
{
|
|
2253
|
+
version: 16,
|
|
2254
|
+
description: "Allow chat_message records in sync_outbox",
|
|
2255
|
+
sql: `
|
|
2256
|
+
CREATE TABLE sync_outbox_new (
|
|
2257
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2258
|
+
record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
|
|
2259
|
+
record_id INTEGER NOT NULL,
|
|
2260
|
+
status TEXT DEFAULT 'pending' CHECK (status IN (
|
|
2261
|
+
'pending', 'syncing', 'synced', 'failed'
|
|
2262
|
+
)),
|
|
2263
|
+
retry_count INTEGER DEFAULT 0,
|
|
2264
|
+
max_retries INTEGER DEFAULT 10,
|
|
2265
|
+
last_error TEXT,
|
|
2266
|
+
created_at_epoch INTEGER NOT NULL,
|
|
2267
|
+
synced_at_epoch INTEGER,
|
|
2268
|
+
next_retry_epoch INTEGER
|
|
2269
|
+
);
|
|
2270
|
+
|
|
2271
|
+
INSERT INTO sync_outbox_new (
|
|
2272
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
2273
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
2274
|
+
)
|
|
2275
|
+
SELECT
|
|
2276
|
+
id, record_type, record_id, status, retry_count, max_retries,
|
|
2277
|
+
last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
|
|
2278
|
+
FROM sync_outbox;
|
|
2279
|
+
|
|
2280
|
+
DROP TABLE sync_outbox;
|
|
2281
|
+
ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
|
|
2282
|
+
|
|
2283
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
2284
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
2285
|
+
`
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
version: 17,
|
|
2289
|
+
description: "Track transcript-backed chat messages separately from hook chat",
|
|
2290
|
+
sql: `
|
|
2291
|
+
ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook';
|
|
2292
|
+
ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER;
|
|
2293
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_source_kind
|
|
2294
|
+
ON chat_messages(session_id, source_kind, transcript_index, created_at_epoch DESC, id DESC);
|
|
2295
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_session_transcript
|
|
2296
|
+
ON chat_messages(session_id, transcript_index)
|
|
2297
|
+
WHERE transcript_index IS NOT NULL;
|
|
1916
2298
|
`
|
|
1917
2299
|
}
|
|
1918
2300
|
];
|