engrm 0.4.23 → 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 +47 -3
- package/dist/cli.js +103 -5
- package/dist/hooks/elicitation-result.js +90 -5
- package/dist/hooks/post-tool-use.js +680 -16
- package/dist/hooks/pre-compact.js +672 -16
- package/dist/hooks/sentinel.js +90 -5
- package/dist/hooks/session-start.js +546 -36
- package/dist/hooks/stop.js +546 -8
- package/dist/hooks/user-prompt-submit.js +1390 -6
- package/dist/server.js +546 -44
- 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 |
|
|
@@ -222,8 +222,12 @@ The MCP server exposes tools that supported agents can call directly:
|
|
|
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
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 |
|
|
225
226
|
| `recent_handoffs` | List recent saved handoffs for the current project or workspace |
|
|
226
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 |
|
|
227
231
|
| `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
|
|
228
232
|
| `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
|
|
229
233
|
| `capture_git_diff` | Reduce a git diff into a durable memory object and save it |
|
|
@@ -282,15 +286,55 @@ For long-running work across devices, Engrm now has an explicit handoff flow:
|
|
|
282
286
|
|
|
283
287
|
- `create_handoff`
|
|
284
288
|
- snapshot the active thread into a syncable handoff message
|
|
289
|
+
- `refresh_handoff`
|
|
290
|
+
- refresh the rolling live handoff draft for the current session
|
|
285
291
|
- `recent_handoffs`
|
|
286
292
|
- list the latest saved handoffs
|
|
287
293
|
- `load_handoff`
|
|
288
294
|
- reopen a saved handoff as a clear resume point in a new session
|
|
289
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
|
+
|
|
290
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.
|
|
291
320
|
|
|
292
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.
|
|
293
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
|
+
|
|
294
338
|
### Local Memory Inspection
|
|
295
339
|
|
|
296
340
|
For local testing, Engrm now exposes a small inspection set that lets you see
|
|
@@ -314,9 +358,9 @@ What each tool is good for:
|
|
|
314
358
|
|
|
315
359
|
- `capture_status` tells you whether prompt/tool hooks are live on this machine
|
|
316
360
|
- `memory_console` gives the quickest project snapshot
|
|
317
|
-
- `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
|
|
318
362
|
- `recent_sessions` helps you pick a session worth opening
|
|
319
|
-
- `session_story` reconstructs one session in detail
|
|
363
|
+
- `session_story` reconstructs one session in detail, including handoffs and chat recall
|
|
320
364
|
- `tool_memory_index` shows which tools and plugins are actually producing durable memory
|
|
321
365
|
- `session_tool_memory` shows which tool calls in one session turned into reusable memory and which did not
|
|
322
366
|
- `project_memory_index` shows typed memory by repo
|
package/dist/cli.js
CHANGED
|
@@ -695,6 +695,19 @@ var MIGRATIONS = [
|
|
|
695
695
|
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
696
696
|
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
697
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;
|
|
710
|
+
`
|
|
698
711
|
}
|
|
699
712
|
];
|
|
700
713
|
function isVecExtensionLoaded(db) {
|
|
@@ -765,6 +778,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
765
778
|
if (syncOutboxSupportsChatMessages(db)) {
|
|
766
779
|
version = Math.max(version, 16);
|
|
767
780
|
}
|
|
781
|
+
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
782
|
+
version = Math.max(version, 17);
|
|
783
|
+
}
|
|
768
784
|
return version;
|
|
769
785
|
}
|
|
770
786
|
function runMigrations(db) {
|
|
@@ -868,9 +884,17 @@ function ensureChatMessageColumns(db) {
|
|
|
868
884
|
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
869
885
|
}
|
|
870
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");
|
|
871
895
|
const current = getSchemaVersion(db);
|
|
872
|
-
if (current <
|
|
873
|
-
db.exec("PRAGMA user_version =
|
|
896
|
+
if (current < 17) {
|
|
897
|
+
db.exec("PRAGMA user_version = 17");
|
|
874
898
|
}
|
|
875
899
|
}
|
|
876
900
|
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
@@ -1155,6 +1179,22 @@ class MemDatabase {
|
|
|
1155
1179
|
getObservationById(id) {
|
|
1156
1180
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
1157
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
|
+
}
|
|
1158
1198
|
getObservationsByIds(ids, userId) {
|
|
1159
1199
|
if (ids.length === 0)
|
|
1160
1200
|
return [];
|
|
@@ -1426,8 +1466,8 @@ class MemDatabase {
|
|
|
1426
1466
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1427
1467
|
const content = input.content.trim();
|
|
1428
1468
|
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);
|
|
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);
|
|
1431
1471
|
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1432
1472
|
}
|
|
1433
1473
|
getChatMessageById(id) {
|
|
@@ -1439,7 +1479,17 @@ class MemDatabase {
|
|
|
1439
1479
|
getSessionChatMessages(sessionId, limit = 50) {
|
|
1440
1480
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1441
1481
|
WHERE session_id = ?
|
|
1442
|
-
|
|
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
|
|
1443
1493
|
LIMIT ?`).all(sessionId, limit);
|
|
1444
1494
|
}
|
|
1445
1495
|
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
@@ -1447,11 +1497,27 @@ class MemDatabase {
|
|
|
1447
1497
|
if (projectId !== null) {
|
|
1448
1498
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1449
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
|
+
)
|
|
1450
1508
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1451
1509
|
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1452
1510
|
}
|
|
1453
1511
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1454
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
|
+
)
|
|
1455
1521
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1456
1522
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1457
1523
|
}
|
|
@@ -1462,14 +1528,33 @@ class MemDatabase {
|
|
|
1462
1528
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1463
1529
|
WHERE project_id = ?
|
|
1464
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
|
+
)
|
|
1465
1539
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1466
1540
|
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1467
1541
|
}
|
|
1468
1542
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1469
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
|
+
)
|
|
1470
1552
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1471
1553
|
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1472
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
|
+
}
|
|
1473
1558
|
addToOutbox(recordType, recordId) {
|
|
1474
1559
|
const now = Math.floor(Date.now() / 1000);
|
|
1475
1560
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -2198,6 +2283,19 @@ var MIGRATIONS2 = [
|
|
|
2198
2283
|
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
2199
2284
|
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
2200
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;
|
|
2298
|
+
`
|
|
2201
2299
|
}
|
|
2202
2300
|
];
|
|
2203
2301
|
function isVecExtensionLoaded2(db) {
|
|
@@ -1528,6 +1528,19 @@ var MIGRATIONS = [
|
|
|
1528
1528
|
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
1529
1529
|
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
1530
1530
|
`
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
version: 17,
|
|
1534
|
+
description: "Track transcript-backed chat messages separately from hook chat",
|
|
1535
|
+
sql: `
|
|
1536
|
+
ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook';
|
|
1537
|
+
ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER;
|
|
1538
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_source_kind
|
|
1539
|
+
ON chat_messages(session_id, source_kind, transcript_index, created_at_epoch DESC, id DESC);
|
|
1540
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_session_transcript
|
|
1541
|
+
ON chat_messages(session_id, transcript_index)
|
|
1542
|
+
WHERE transcript_index IS NOT NULL;
|
|
1543
|
+
`
|
|
1531
1544
|
}
|
|
1532
1545
|
];
|
|
1533
1546
|
function isVecExtensionLoaded(db) {
|
|
@@ -1598,6 +1611,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
1598
1611
|
if (syncOutboxSupportsChatMessages(db)) {
|
|
1599
1612
|
version = Math.max(version, 16);
|
|
1600
1613
|
}
|
|
1614
|
+
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
1615
|
+
version = Math.max(version, 17);
|
|
1616
|
+
}
|
|
1601
1617
|
return version;
|
|
1602
1618
|
}
|
|
1603
1619
|
function runMigrations(db) {
|
|
@@ -1701,9 +1717,17 @@ function ensureChatMessageColumns(db) {
|
|
|
1701
1717
|
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
1702
1718
|
}
|
|
1703
1719
|
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");
|
|
1720
|
+
if (!columnExists(db, "chat_messages", "source_kind")) {
|
|
1721
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook'");
|
|
1722
|
+
}
|
|
1723
|
+
if (!columnExists(db, "chat_messages", "transcript_index")) {
|
|
1724
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER");
|
|
1725
|
+
}
|
|
1726
|
+
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)");
|
|
1727
|
+
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");
|
|
1704
1728
|
const current = getSchemaVersion(db);
|
|
1705
|
-
if (current <
|
|
1706
|
-
db.exec("PRAGMA user_version =
|
|
1729
|
+
if (current < 17) {
|
|
1730
|
+
db.exec("PRAGMA user_version = 17");
|
|
1707
1731
|
}
|
|
1708
1732
|
}
|
|
1709
1733
|
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
@@ -1988,6 +2012,22 @@ class MemDatabase {
|
|
|
1988
2012
|
getObservationById(id) {
|
|
1989
2013
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
1990
2014
|
}
|
|
2015
|
+
updateObservationContent(id, update) {
|
|
2016
|
+
const existing = this.getObservationById(id);
|
|
2017
|
+
if (!existing)
|
|
2018
|
+
return null;
|
|
2019
|
+
const createdAtEpoch = update.created_at_epoch ?? existing.created_at_epoch;
|
|
2020
|
+
const createdAt = new Date(createdAtEpoch * 1000).toISOString();
|
|
2021
|
+
this.db.query(`UPDATE observations
|
|
2022
|
+
SET title = ?, narrative = ?, facts = ?, concepts = ?, created_at = ?, created_at_epoch = ?
|
|
2023
|
+
WHERE id = ?`).run(update.title, update.narrative ?? null, update.facts ?? null, update.concepts ?? null, createdAt, createdAtEpoch, id);
|
|
2024
|
+
this.ftsDelete(existing);
|
|
2025
|
+
const refreshed = this.getObservationById(id);
|
|
2026
|
+
if (!refreshed)
|
|
2027
|
+
return null;
|
|
2028
|
+
this.ftsInsert(refreshed);
|
|
2029
|
+
return refreshed;
|
|
2030
|
+
}
|
|
1991
2031
|
getObservationsByIds(ids, userId) {
|
|
1992
2032
|
if (ids.length === 0)
|
|
1993
2033
|
return [];
|
|
@@ -2259,8 +2299,8 @@ class MemDatabase {
|
|
|
2259
2299
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
2260
2300
|
const content = input.content.trim();
|
|
2261
2301
|
const result = this.db.query(`INSERT INTO chat_messages (
|
|
2262
|
-
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
2263
|
-
) 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);
|
|
2302
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id, source_kind, transcript_index
|
|
2303
|
+
) 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);
|
|
2264
2304
|
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
2265
2305
|
}
|
|
2266
2306
|
getChatMessageById(id) {
|
|
@@ -2272,7 +2312,17 @@ class MemDatabase {
|
|
|
2272
2312
|
getSessionChatMessages(sessionId, limit = 50) {
|
|
2273
2313
|
return this.db.query(`SELECT * FROM chat_messages
|
|
2274
2314
|
WHERE session_id = ?
|
|
2275
|
-
|
|
2315
|
+
AND (
|
|
2316
|
+
source_kind = 'transcript'
|
|
2317
|
+
OR NOT EXISTS (
|
|
2318
|
+
SELECT 1 FROM chat_messages t2
|
|
2319
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
2320
|
+
AND t2.source_kind = 'transcript'
|
|
2321
|
+
)
|
|
2322
|
+
)
|
|
2323
|
+
ORDER BY
|
|
2324
|
+
CASE WHEN transcript_index IS NULL THEN created_at_epoch ELSE transcript_index END ASC,
|
|
2325
|
+
id ASC
|
|
2276
2326
|
LIMIT ?`).all(sessionId, limit);
|
|
2277
2327
|
}
|
|
2278
2328
|
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
@@ -2280,11 +2330,27 @@ class MemDatabase {
|
|
|
2280
2330
|
if (projectId !== null) {
|
|
2281
2331
|
return this.db.query(`SELECT * FROM chat_messages
|
|
2282
2332
|
WHERE project_id = ?${visibilityClause}
|
|
2333
|
+
AND (
|
|
2334
|
+
source_kind = 'transcript'
|
|
2335
|
+
OR NOT EXISTS (
|
|
2336
|
+
SELECT 1 FROM chat_messages t2
|
|
2337
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
2338
|
+
AND t2.source_kind = 'transcript'
|
|
2339
|
+
)
|
|
2340
|
+
)
|
|
2283
2341
|
ORDER BY created_at_epoch DESC, id DESC
|
|
2284
2342
|
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
2285
2343
|
}
|
|
2286
2344
|
return this.db.query(`SELECT * FROM chat_messages
|
|
2287
2345
|
WHERE 1 = 1${visibilityClause}
|
|
2346
|
+
AND (
|
|
2347
|
+
source_kind = 'transcript'
|
|
2348
|
+
OR NOT EXISTS (
|
|
2349
|
+
SELECT 1 FROM chat_messages t2
|
|
2350
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
2351
|
+
AND t2.source_kind = 'transcript'
|
|
2352
|
+
)
|
|
2353
|
+
)
|
|
2288
2354
|
ORDER BY created_at_epoch DESC, id DESC
|
|
2289
2355
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
2290
2356
|
}
|
|
@@ -2295,14 +2361,33 @@ class MemDatabase {
|
|
|
2295
2361
|
return this.db.query(`SELECT * FROM chat_messages
|
|
2296
2362
|
WHERE project_id = ?
|
|
2297
2363
|
AND lower(content) LIKE ?${visibilityClause}
|
|
2364
|
+
AND (
|
|
2365
|
+
source_kind = 'transcript'
|
|
2366
|
+
OR NOT EXISTS (
|
|
2367
|
+
SELECT 1 FROM chat_messages t2
|
|
2368
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
2369
|
+
AND t2.source_kind = 'transcript'
|
|
2370
|
+
)
|
|
2371
|
+
)
|
|
2298
2372
|
ORDER BY created_at_epoch DESC, id DESC
|
|
2299
2373
|
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
2300
2374
|
}
|
|
2301
2375
|
return this.db.query(`SELECT * FROM chat_messages
|
|
2302
2376
|
WHERE lower(content) LIKE ?${visibilityClause}
|
|
2377
|
+
AND (
|
|
2378
|
+
source_kind = 'transcript'
|
|
2379
|
+
OR NOT EXISTS (
|
|
2380
|
+
SELECT 1 FROM chat_messages t2
|
|
2381
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
2382
|
+
AND t2.source_kind = 'transcript'
|
|
2383
|
+
)
|
|
2384
|
+
)
|
|
2303
2385
|
ORDER BY created_at_epoch DESC, id DESC
|
|
2304
2386
|
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
2305
2387
|
}
|
|
2388
|
+
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
2389
|
+
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
2390
|
+
}
|
|
2306
2391
|
addToOutbox(recordType, recordId) {
|
|
2307
2392
|
const now = Math.floor(Date.now() / 1000);
|
|
2308
2393
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|