engrm 0.4.23 → 0.4.26
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 +53 -7
- 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 +719 -27
- package/dist/hooks/sentinel.js +90 -5
- package/dist/hooks/session-start.js +1442 -170
- package/dist/hooks/stop.js +546 -8
- package/dist/hooks/user-prompt-submit.js +1390 -6
- package/dist/server.js +738 -76
- package/package.json +1 -1
|
@@ -873,6 +873,19 @@ var MIGRATIONS = [
|
|
|
873
873
|
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
874
874
|
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
875
875
|
`
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
version: 17,
|
|
879
|
+
description: "Track transcript-backed chat messages separately from hook chat",
|
|
880
|
+
sql: `
|
|
881
|
+
ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook';
|
|
882
|
+
ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER;
|
|
883
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_source_kind
|
|
884
|
+
ON chat_messages(session_id, source_kind, transcript_index, created_at_epoch DESC, id DESC);
|
|
885
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_session_transcript
|
|
886
|
+
ON chat_messages(session_id, transcript_index)
|
|
887
|
+
WHERE transcript_index IS NOT NULL;
|
|
888
|
+
`
|
|
876
889
|
}
|
|
877
890
|
];
|
|
878
891
|
function isVecExtensionLoaded(db) {
|
|
@@ -943,6 +956,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
943
956
|
if (syncOutboxSupportsChatMessages(db)) {
|
|
944
957
|
version = Math.max(version, 16);
|
|
945
958
|
}
|
|
959
|
+
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
960
|
+
version = Math.max(version, 17);
|
|
961
|
+
}
|
|
946
962
|
return version;
|
|
947
963
|
}
|
|
948
964
|
function runMigrations(db) {
|
|
@@ -1046,9 +1062,17 @@ function ensureChatMessageColumns(db) {
|
|
|
1046
1062
|
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
1047
1063
|
}
|
|
1048
1064
|
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");
|
|
1065
|
+
if (!columnExists(db, "chat_messages", "source_kind")) {
|
|
1066
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook'");
|
|
1067
|
+
}
|
|
1068
|
+
if (!columnExists(db, "chat_messages", "transcript_index")) {
|
|
1069
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER");
|
|
1070
|
+
}
|
|
1071
|
+
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)");
|
|
1072
|
+
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");
|
|
1049
1073
|
const current = getSchemaVersion(db);
|
|
1050
|
-
if (current <
|
|
1051
|
-
db.exec("PRAGMA user_version =
|
|
1074
|
+
if (current < 17) {
|
|
1075
|
+
db.exec("PRAGMA user_version = 17");
|
|
1052
1076
|
}
|
|
1053
1077
|
}
|
|
1054
1078
|
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
@@ -1333,6 +1357,22 @@ class MemDatabase {
|
|
|
1333
1357
|
getObservationById(id) {
|
|
1334
1358
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
1335
1359
|
}
|
|
1360
|
+
updateObservationContent(id, update) {
|
|
1361
|
+
const existing = this.getObservationById(id);
|
|
1362
|
+
if (!existing)
|
|
1363
|
+
return null;
|
|
1364
|
+
const createdAtEpoch = update.created_at_epoch ?? existing.created_at_epoch;
|
|
1365
|
+
const createdAt = new Date(createdAtEpoch * 1000).toISOString();
|
|
1366
|
+
this.db.query(`UPDATE observations
|
|
1367
|
+
SET title = ?, narrative = ?, facts = ?, concepts = ?, created_at = ?, created_at_epoch = ?
|
|
1368
|
+
WHERE id = ?`).run(update.title, update.narrative ?? null, update.facts ?? null, update.concepts ?? null, createdAt, createdAtEpoch, id);
|
|
1369
|
+
this.ftsDelete(existing);
|
|
1370
|
+
const refreshed = this.getObservationById(id);
|
|
1371
|
+
if (!refreshed)
|
|
1372
|
+
return null;
|
|
1373
|
+
this.ftsInsert(refreshed);
|
|
1374
|
+
return refreshed;
|
|
1375
|
+
}
|
|
1336
1376
|
getObservationsByIds(ids, userId) {
|
|
1337
1377
|
if (ids.length === 0)
|
|
1338
1378
|
return [];
|
|
@@ -1604,8 +1644,8 @@ class MemDatabase {
|
|
|
1604
1644
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1605
1645
|
const content = input.content.trim();
|
|
1606
1646
|
const result = this.db.query(`INSERT INTO chat_messages (
|
|
1607
|
-
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
1608
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.role, content, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt, input.remote_source_id ?? null);
|
|
1647
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id, source_kind, transcript_index
|
|
1648
|
+
) 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);
|
|
1609
1649
|
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
1610
1650
|
}
|
|
1611
1651
|
getChatMessageById(id) {
|
|
@@ -1617,7 +1657,17 @@ class MemDatabase {
|
|
|
1617
1657
|
getSessionChatMessages(sessionId, limit = 50) {
|
|
1618
1658
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1619
1659
|
WHERE session_id = ?
|
|
1620
|
-
|
|
1660
|
+
AND (
|
|
1661
|
+
source_kind = 'transcript'
|
|
1662
|
+
OR NOT EXISTS (
|
|
1663
|
+
SELECT 1 FROM chat_messages t2
|
|
1664
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1665
|
+
AND t2.source_kind = 'transcript'
|
|
1666
|
+
)
|
|
1667
|
+
)
|
|
1668
|
+
ORDER BY
|
|
1669
|
+
CASE WHEN transcript_index IS NULL THEN created_at_epoch ELSE transcript_index END ASC,
|
|
1670
|
+
id ASC
|
|
1621
1671
|
LIMIT ?`).all(sessionId, limit);
|
|
1622
1672
|
}
|
|
1623
1673
|
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
@@ -1625,11 +1675,27 @@ class MemDatabase {
|
|
|
1625
1675
|
if (projectId !== null) {
|
|
1626
1676
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1627
1677
|
WHERE project_id = ?${visibilityClause}
|
|
1678
|
+
AND (
|
|
1679
|
+
source_kind = 'transcript'
|
|
1680
|
+
OR NOT EXISTS (
|
|
1681
|
+
SELECT 1 FROM chat_messages t2
|
|
1682
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1683
|
+
AND t2.source_kind = 'transcript'
|
|
1684
|
+
)
|
|
1685
|
+
)
|
|
1628
1686
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1629
1687
|
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
1630
1688
|
}
|
|
1631
1689
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1632
1690
|
WHERE 1 = 1${visibilityClause}
|
|
1691
|
+
AND (
|
|
1692
|
+
source_kind = 'transcript'
|
|
1693
|
+
OR NOT EXISTS (
|
|
1694
|
+
SELECT 1 FROM chat_messages t2
|
|
1695
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1696
|
+
AND t2.source_kind = 'transcript'
|
|
1697
|
+
)
|
|
1698
|
+
)
|
|
1633
1699
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1634
1700
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
1635
1701
|
}
|
|
@@ -1640,14 +1706,33 @@ class MemDatabase {
|
|
|
1640
1706
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1641
1707
|
WHERE project_id = ?
|
|
1642
1708
|
AND lower(content) LIKE ?${visibilityClause}
|
|
1709
|
+
AND (
|
|
1710
|
+
source_kind = 'transcript'
|
|
1711
|
+
OR NOT EXISTS (
|
|
1712
|
+
SELECT 1 FROM chat_messages t2
|
|
1713
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1714
|
+
AND t2.source_kind = 'transcript'
|
|
1715
|
+
)
|
|
1716
|
+
)
|
|
1643
1717
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1644
1718
|
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
1645
1719
|
}
|
|
1646
1720
|
return this.db.query(`SELECT * FROM chat_messages
|
|
1647
1721
|
WHERE lower(content) LIKE ?${visibilityClause}
|
|
1722
|
+
AND (
|
|
1723
|
+
source_kind = 'transcript'
|
|
1724
|
+
OR NOT EXISTS (
|
|
1725
|
+
SELECT 1 FROM chat_messages t2
|
|
1726
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
1727
|
+
AND t2.source_kind = 'transcript'
|
|
1728
|
+
)
|
|
1729
|
+
)
|
|
1648
1730
|
ORDER BY created_at_epoch DESC, id DESC
|
|
1649
1731
|
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
1650
1732
|
}
|
|
1733
|
+
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
1734
|
+
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
1735
|
+
}
|
|
1651
1736
|
addToOutbox(recordType, recordId) {
|
|
1652
1737
|
const now = Math.floor(Date.now() / 1000);
|
|
1653
1738
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -3562,6 +3647,580 @@ function parseJsonArray(value) {
|
|
|
3562
3647
|
}
|
|
3563
3648
|
}
|
|
3564
3649
|
|
|
3650
|
+
// src/capture/transcript.ts
|
|
3651
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
3652
|
+
import { join as join4 } from "node:path";
|
|
3653
|
+
import { homedir as homedir3 } from "node:os";
|
|
3654
|
+
function resolveTranscriptPath(sessionId, cwd, transcriptPath) {
|
|
3655
|
+
if (transcriptPath)
|
|
3656
|
+
return transcriptPath;
|
|
3657
|
+
const encodedCwd = cwd.replace(/\//g, "-");
|
|
3658
|
+
return join4(homedir3(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
|
|
3659
|
+
}
|
|
3660
|
+
function readTranscript(sessionId, cwd, transcriptPath) {
|
|
3661
|
+
const path = resolveTranscriptPath(sessionId, cwd, transcriptPath);
|
|
3662
|
+
if (!existsSync4(path))
|
|
3663
|
+
return [];
|
|
3664
|
+
let raw;
|
|
3665
|
+
try {
|
|
3666
|
+
raw = readFileSync4(path, "utf-8");
|
|
3667
|
+
} catch {
|
|
3668
|
+
return [];
|
|
3669
|
+
}
|
|
3670
|
+
const messages = [];
|
|
3671
|
+
for (const line of raw.split(`
|
|
3672
|
+
`)) {
|
|
3673
|
+
if (!line.trim())
|
|
3674
|
+
continue;
|
|
3675
|
+
let entry;
|
|
3676
|
+
try {
|
|
3677
|
+
entry = JSON.parse(line);
|
|
3678
|
+
} catch {
|
|
3679
|
+
continue;
|
|
3680
|
+
}
|
|
3681
|
+
const role = entry.role;
|
|
3682
|
+
if (role !== "user" && role !== "assistant")
|
|
3683
|
+
continue;
|
|
3684
|
+
const content = entry.content;
|
|
3685
|
+
if (typeof content === "string") {
|
|
3686
|
+
messages.push({ role, text: content });
|
|
3687
|
+
continue;
|
|
3688
|
+
}
|
|
3689
|
+
if (Array.isArray(content)) {
|
|
3690
|
+
const textParts = [];
|
|
3691
|
+
for (const block of content) {
|
|
3692
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
3693
|
+
textParts.push(block.text);
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
if (textParts.length > 0) {
|
|
3697
|
+
messages.push({ role, text: textParts.join(`
|
|
3698
|
+
`) });
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
return messages;
|
|
3703
|
+
}
|
|
3704
|
+
function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
|
|
3705
|
+
const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
|
|
3706
|
+
...message,
|
|
3707
|
+
text: message.text.trim()
|
|
3708
|
+
})).filter((message) => message.text.length > 0);
|
|
3709
|
+
if (messages.length === 0)
|
|
3710
|
+
return { imported: 0, total: 0 };
|
|
3711
|
+
const session = db.getSessionById(sessionId);
|
|
3712
|
+
const projectId = session?.project_id ?? null;
|
|
3713
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3714
|
+
let imported = 0;
|
|
3715
|
+
for (let index = 0;index < messages.length; index++) {
|
|
3716
|
+
const transcriptIndex = index + 1;
|
|
3717
|
+
if (db.getTranscriptChatMessage(sessionId, transcriptIndex))
|
|
3718
|
+
continue;
|
|
3719
|
+
const message = messages[index];
|
|
3720
|
+
const createdAtEpoch = Math.max(0, now - (messages.length - transcriptIndex));
|
|
3721
|
+
const row = db.insertChatMessage({
|
|
3722
|
+
session_id: sessionId,
|
|
3723
|
+
project_id: projectId,
|
|
3724
|
+
role: message.role,
|
|
3725
|
+
content: message.text,
|
|
3726
|
+
user_id: config.user_id,
|
|
3727
|
+
device_id: config.device_id,
|
|
3728
|
+
agent: "claude-code",
|
|
3729
|
+
created_at_epoch: createdAtEpoch,
|
|
3730
|
+
source_kind: "transcript",
|
|
3731
|
+
transcript_index: transcriptIndex
|
|
3732
|
+
});
|
|
3733
|
+
db.addToOutbox("chat_message", row.id);
|
|
3734
|
+
imported++;
|
|
3735
|
+
}
|
|
3736
|
+
return { imported, total: messages.length };
|
|
3737
|
+
}
|
|
3738
|
+
function truncateTranscript(messages, maxBytes = 50000) {
|
|
3739
|
+
const lines = [];
|
|
3740
|
+
for (const msg of messages) {
|
|
3741
|
+
lines.push(`[${msg.role}]: ${msg.text}`);
|
|
3742
|
+
}
|
|
3743
|
+
const full = lines.join(`
|
|
3744
|
+
`);
|
|
3745
|
+
if (Buffer.byteLength(full, "utf-8") <= maxBytes)
|
|
3746
|
+
return full;
|
|
3747
|
+
let result = "";
|
|
3748
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
3749
|
+
const candidate = lines[i] + `
|
|
3750
|
+
` + result;
|
|
3751
|
+
if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
|
|
3752
|
+
break;
|
|
3753
|
+
result = candidate;
|
|
3754
|
+
}
|
|
3755
|
+
return result.trim();
|
|
3756
|
+
}
|
|
3757
|
+
async function analyzeTranscript(config, transcript, sessionId) {
|
|
3758
|
+
if (!config.candengo_url || !config.candengo_api_key)
|
|
3759
|
+
return null;
|
|
3760
|
+
const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
|
|
3761
|
+
const controller = new AbortController;
|
|
3762
|
+
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
3763
|
+
try {
|
|
3764
|
+
const response = await fetch(url, {
|
|
3765
|
+
method: "POST",
|
|
3766
|
+
headers: {
|
|
3767
|
+
"Content-Type": "application/json",
|
|
3768
|
+
Authorization: `Bearer ${config.candengo_api_key}`
|
|
3769
|
+
},
|
|
3770
|
+
body: JSON.stringify({
|
|
3771
|
+
transcript,
|
|
3772
|
+
session_id: sessionId
|
|
3773
|
+
}),
|
|
3774
|
+
signal: controller.signal
|
|
3775
|
+
});
|
|
3776
|
+
if (!response.ok)
|
|
3777
|
+
return null;
|
|
3778
|
+
const data = await response.json();
|
|
3779
|
+
if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
|
|
3780
|
+
return null;
|
|
3781
|
+
}
|
|
3782
|
+
return data;
|
|
3783
|
+
} catch {
|
|
3784
|
+
return null;
|
|
3785
|
+
} finally {
|
|
3786
|
+
clearTimeout(timeout);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
async function saveTranscriptResults(db, config, results, sessionId, cwd) {
|
|
3790
|
+
let saved = 0;
|
|
3791
|
+
const items = [
|
|
3792
|
+
...results.plans.map((item) => ({ item, type: "decision" })),
|
|
3793
|
+
...results.decisions.map((item) => ({ item, type: "decision" })),
|
|
3794
|
+
...results.insights.map((item) => ({ item, type: "discovery" }))
|
|
3795
|
+
];
|
|
3796
|
+
for (const { item, type } of items) {
|
|
3797
|
+
if (!item.title || item.title.trim().length === 0)
|
|
3798
|
+
continue;
|
|
3799
|
+
const result = await saveObservation(db, config, {
|
|
3800
|
+
type,
|
|
3801
|
+
title: item.title.slice(0, 80),
|
|
3802
|
+
narrative: item.narrative,
|
|
3803
|
+
concepts: item.concepts,
|
|
3804
|
+
session_id: sessionId,
|
|
3805
|
+
cwd
|
|
3806
|
+
});
|
|
3807
|
+
if (result.success)
|
|
3808
|
+
saved++;
|
|
3809
|
+
}
|
|
3810
|
+
return saved;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
// src/tools/session-story.ts
|
|
3814
|
+
function getSessionStory(db, input) {
|
|
3815
|
+
const session = db.getSessionById(input.session_id);
|
|
3816
|
+
const summary = db.getSessionSummary(input.session_id);
|
|
3817
|
+
const prompts = db.getSessionUserPrompts(input.session_id, 50);
|
|
3818
|
+
const chatMessages = db.getSessionChatMessages(input.session_id, 50);
|
|
3819
|
+
const toolEvents = db.getSessionToolEvents(input.session_id, 100);
|
|
3820
|
+
const allObservations = db.getObservationsBySession(input.session_id);
|
|
3821
|
+
const handoffs = allObservations.filter((obs) => looksLikeHandoff(obs));
|
|
3822
|
+
const rollingHandoffDrafts = handoffs.filter((obs) => isDraftHandoff(obs));
|
|
3823
|
+
const savedHandoffs = handoffs.filter((obs) => !isDraftHandoff(obs));
|
|
3824
|
+
const observations = allObservations.filter((obs) => !looksLikeHandoff(obs));
|
|
3825
|
+
const metrics = db.getSessionMetrics(input.session_id);
|
|
3826
|
+
const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
|
|
3827
|
+
const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
|
|
3828
|
+
return {
|
|
3829
|
+
session,
|
|
3830
|
+
project_name: projectName,
|
|
3831
|
+
summary,
|
|
3832
|
+
prompts,
|
|
3833
|
+
chat_messages: chatMessages,
|
|
3834
|
+
tool_events: toolEvents,
|
|
3835
|
+
observations,
|
|
3836
|
+
handoffs,
|
|
3837
|
+
saved_handoffs: savedHandoffs,
|
|
3838
|
+
rolling_handoff_drafts: rollingHandoffDrafts,
|
|
3839
|
+
metrics,
|
|
3840
|
+
capture_state: classifyCaptureState({
|
|
3841
|
+
hasSummary: Boolean(summary?.request || summary?.completed),
|
|
3842
|
+
promptCount: prompts.length,
|
|
3843
|
+
toolEventCount: toolEvents.length
|
|
3844
|
+
}),
|
|
3845
|
+
capture_gaps: buildCaptureGaps({
|
|
3846
|
+
promptCount: prompts.length,
|
|
3847
|
+
toolEventCount: toolEvents.length,
|
|
3848
|
+
toolCallsCount: metrics?.tool_calls_count ?? 0,
|
|
3849
|
+
observationCount: observations.length,
|
|
3850
|
+
hasSummary: Boolean(summary?.request || summary?.completed)
|
|
3851
|
+
}),
|
|
3852
|
+
latest_request: latestRequest,
|
|
3853
|
+
recent_outcomes: collectRecentOutcomes(observations),
|
|
3854
|
+
hot_files: collectHotFiles(observations),
|
|
3855
|
+
provenance_summary: collectProvenanceSummary(observations)
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
function classifyCaptureState(input) {
|
|
3859
|
+
if (input.promptCount > 0 && input.toolEventCount > 0)
|
|
3860
|
+
return "rich";
|
|
3861
|
+
if (input.promptCount > 0 || input.toolEventCount > 0)
|
|
3862
|
+
return "partial";
|
|
3863
|
+
if (input.hasSummary)
|
|
3864
|
+
return "summary-only";
|
|
3865
|
+
return "legacy";
|
|
3866
|
+
}
|
|
3867
|
+
function buildCaptureGaps(input) {
|
|
3868
|
+
const gaps = [];
|
|
3869
|
+
if (input.promptCount === 0)
|
|
3870
|
+
gaps.push("missing prompts");
|
|
3871
|
+
if (input.toolCallsCount > 0 && input.toolEventCount === 0) {
|
|
3872
|
+
gaps.push("missing raw tool chronology");
|
|
3873
|
+
} else if (input.toolEventCount === 0) {
|
|
3874
|
+
gaps.push("no tool events");
|
|
3875
|
+
}
|
|
3876
|
+
if (input.observationCount === 0 && input.hasSummary) {
|
|
3877
|
+
gaps.push("summary without reusable observations");
|
|
3878
|
+
}
|
|
3879
|
+
return gaps;
|
|
3880
|
+
}
|
|
3881
|
+
function collectRecentOutcomes(observations) {
|
|
3882
|
+
const seen = new Set;
|
|
3883
|
+
const outcomes = [];
|
|
3884
|
+
for (const obs of observations) {
|
|
3885
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
3886
|
+
continue;
|
|
3887
|
+
const title = obs.title.trim();
|
|
3888
|
+
if (!title || looksLikeFileOperationTitle(title))
|
|
3889
|
+
continue;
|
|
3890
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3891
|
+
if (seen.has(normalized))
|
|
3892
|
+
continue;
|
|
3893
|
+
seen.add(normalized);
|
|
3894
|
+
outcomes.push(title);
|
|
3895
|
+
if (outcomes.length >= 6)
|
|
3896
|
+
break;
|
|
3897
|
+
}
|
|
3898
|
+
return outcomes;
|
|
3899
|
+
}
|
|
3900
|
+
function collectHotFiles(observations) {
|
|
3901
|
+
const counts = new Map;
|
|
3902
|
+
for (const obs of observations) {
|
|
3903
|
+
for (const path of [...parseJsonArray2(obs.files_modified), ...parseJsonArray2(obs.files_read)]) {
|
|
3904
|
+
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
return Array.from(counts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || a.path.localeCompare(b.path)).slice(0, 8);
|
|
3908
|
+
}
|
|
3909
|
+
function parseJsonArray2(value) {
|
|
3910
|
+
if (!value)
|
|
3911
|
+
return [];
|
|
3912
|
+
try {
|
|
3913
|
+
const parsed = JSON.parse(value);
|
|
3914
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
3915
|
+
} catch {
|
|
3916
|
+
return [];
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
function looksLikeFileOperationTitle(value) {
|
|
3920
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
3921
|
+
}
|
|
3922
|
+
function collectProvenanceSummary(observations) {
|
|
3923
|
+
const counts = new Map;
|
|
3924
|
+
for (const obs of observations) {
|
|
3925
|
+
if (!obs.source_tool)
|
|
3926
|
+
continue;
|
|
3927
|
+
counts.set(obs.source_tool, (counts.get(obs.source_tool) ?? 0) + 1);
|
|
3928
|
+
}
|
|
3929
|
+
return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
// src/tools/handoffs.ts
|
|
3933
|
+
async function upsertRollingHandoff(db, config, input) {
|
|
3934
|
+
const resolved = resolveTargetSession(db, input.cwd, config.user_id, input.session_id);
|
|
3935
|
+
if (!resolved.session) {
|
|
3936
|
+
return {
|
|
3937
|
+
success: false,
|
|
3938
|
+
reason: "No recent session found to draft a handoff yet"
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
const story = getSessionStory(db, { session_id: resolved.session.session_id });
|
|
3942
|
+
if (!story.session) {
|
|
3943
|
+
return {
|
|
3944
|
+
success: false,
|
|
3945
|
+
reason: `Session ${resolved.session.session_id} not found`
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
const includeChat = input.include_chat === true || input.include_chat !== false && shouldAutoIncludeChat(story);
|
|
3949
|
+
const chatLimit = Math.max(1, Math.min(input.chat_limit ?? 3, 6));
|
|
3950
|
+
const title = `Handoff Draft: ${buildHandoffTitle(story.summary, story.latest_request)}`;
|
|
3951
|
+
const narrative = buildHandoffNarrative(story.summary, story, {
|
|
3952
|
+
includeChat,
|
|
3953
|
+
chatLimit
|
|
3954
|
+
});
|
|
3955
|
+
const facts = buildHandoffFacts(story.summary, story);
|
|
3956
|
+
const concepts = buildDraftHandoffConcepts(story.project_name, story.capture_state);
|
|
3957
|
+
const existing = getSessionRollingHandoff(db, story.session.session_id);
|
|
3958
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3959
|
+
if (existing) {
|
|
3960
|
+
const nextFacts = JSON.stringify(facts);
|
|
3961
|
+
const nextConcepts = JSON.stringify(concepts);
|
|
3962
|
+
const shouldRefresh = existing.title !== title || (existing.narrative ?? null) !== narrative || (existing.facts ?? null) !== nextFacts || (existing.concepts ?? null) !== nextConcepts || now - existing.created_at_epoch >= 120;
|
|
3963
|
+
if (!shouldRefresh) {
|
|
3964
|
+
return {
|
|
3965
|
+
success: true,
|
|
3966
|
+
observation_id: existing.id,
|
|
3967
|
+
session_id: story.session.session_id,
|
|
3968
|
+
title: existing.title
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
const updated = db.updateObservationContent(existing.id, {
|
|
3972
|
+
title,
|
|
3973
|
+
narrative,
|
|
3974
|
+
facts: nextFacts,
|
|
3975
|
+
concepts: nextConcepts,
|
|
3976
|
+
created_at_epoch: now
|
|
3977
|
+
});
|
|
3978
|
+
if (!updated) {
|
|
3979
|
+
return {
|
|
3980
|
+
success: false,
|
|
3981
|
+
reason: "Failed to update rolling handoff draft"
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
db.addToOutbox("observation", updated.id);
|
|
3985
|
+
return {
|
|
3986
|
+
success: true,
|
|
3987
|
+
observation_id: updated.id,
|
|
3988
|
+
session_id: story.session.session_id,
|
|
3989
|
+
title: updated.title
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
const result = await saveObservation(db, config, {
|
|
3993
|
+
type: "message",
|
|
3994
|
+
title,
|
|
3995
|
+
narrative,
|
|
3996
|
+
facts,
|
|
3997
|
+
concepts,
|
|
3998
|
+
session_id: story.session.session_id,
|
|
3999
|
+
cwd: input.cwd,
|
|
4000
|
+
agent: "engrm-handoff",
|
|
4001
|
+
source_tool: "rolling_handoff"
|
|
4002
|
+
});
|
|
4003
|
+
return {
|
|
4004
|
+
success: result.success,
|
|
4005
|
+
observation_id: result.observation_id,
|
|
4006
|
+
session_id: story.session.session_id,
|
|
4007
|
+
title,
|
|
4008
|
+
reason: result.reason
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
function getRecentHandoffs(db, input) {
|
|
4012
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
|
|
4013
|
+
const queryLimit = input.current_device_id ? Math.max(limit, Math.min(limit * 5, 50)) : limit;
|
|
4014
|
+
const projectScoped = input.project_scoped !== false;
|
|
4015
|
+
let projectId = null;
|
|
4016
|
+
let projectName;
|
|
4017
|
+
if (projectScoped) {
|
|
4018
|
+
const cwd = input.cwd ?? process.cwd();
|
|
4019
|
+
const detected = detectProject(cwd);
|
|
4020
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
4021
|
+
if (project) {
|
|
4022
|
+
projectId = project.id;
|
|
4023
|
+
projectName = project.name;
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
const conditions = [
|
|
4027
|
+
"o.type = 'message'",
|
|
4028
|
+
"o.lifecycle IN ('active', 'aging', 'pinned')",
|
|
4029
|
+
"o.superseded_by IS NULL",
|
|
4030
|
+
`(o.title LIKE 'Handoff:%' OR o.concepts LIKE '%"handoff"%')`
|
|
4031
|
+
];
|
|
4032
|
+
const params = [];
|
|
4033
|
+
if (input.user_id) {
|
|
4034
|
+
conditions.push("(o.sensitivity != 'personal' OR o.user_id = ?)");
|
|
4035
|
+
params.push(input.user_id);
|
|
4036
|
+
}
|
|
4037
|
+
if (projectId !== null) {
|
|
4038
|
+
conditions.push("o.project_id = ?");
|
|
4039
|
+
params.push(projectId);
|
|
4040
|
+
}
|
|
4041
|
+
params.push(queryLimit);
|
|
4042
|
+
const handoffs = db.db.query(`SELECT o.*, p.name AS project_name
|
|
4043
|
+
FROM observations o
|
|
4044
|
+
LEFT JOIN projects p ON p.id = o.project_id
|
|
4045
|
+
WHERE ${conditions.join(" AND ")}
|
|
4046
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
4047
|
+
LIMIT ?`).all(...params);
|
|
4048
|
+
handoffs.sort((a, b) => compareHandoffs(a, b, input.current_device_id));
|
|
4049
|
+
return {
|
|
4050
|
+
handoffs: handoffs.slice(0, limit),
|
|
4051
|
+
project: projectName
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
function formatHandoffSource(handoff) {
|
|
4055
|
+
const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
|
|
4056
|
+
const ageLabel = ageSeconds < 3600 ? `${Math.max(1, Math.floor(ageSeconds / 60) || 1)}m ago` : ageSeconds < 86400 ? `${Math.floor(ageSeconds / 3600)}h ago` : `${Math.floor(ageSeconds / 86400)}d ago`;
|
|
4057
|
+
return `from ${handoff.device_id} · ${ageLabel}`;
|
|
4058
|
+
}
|
|
4059
|
+
function isDraftHandoff(obs) {
|
|
4060
|
+
if (obs.title.startsWith("Handoff Draft:"))
|
|
4061
|
+
return true;
|
|
4062
|
+
const concepts = parseJsonArray3(obs.concepts);
|
|
4063
|
+
return concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
|
|
4064
|
+
}
|
|
4065
|
+
function getSessionRollingHandoff(db, sessionId) {
|
|
4066
|
+
return db.db.query(`SELECT o.*, p.name AS project_name
|
|
4067
|
+
FROM observations o
|
|
4068
|
+
LEFT JOIN projects p ON p.id = o.project_id
|
|
4069
|
+
WHERE o.session_id = ?
|
|
4070
|
+
AND o.type = 'message'
|
|
4071
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
4072
|
+
AND o.superseded_by IS NULL
|
|
4073
|
+
AND (o.title LIKE 'Handoff Draft:%' OR o.concepts LIKE '%"draft-handoff"%')
|
|
4074
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
4075
|
+
LIMIT 1`).get(sessionId) ?? null;
|
|
4076
|
+
}
|
|
4077
|
+
function compareHandoffs(a, b, currentDeviceId) {
|
|
4078
|
+
const aDraft = isDraftHandoff(a) ? 1 : 0;
|
|
4079
|
+
const bDraft = isDraftHandoff(b) ? 1 : 0;
|
|
4080
|
+
if (aDraft !== bDraft)
|
|
4081
|
+
return aDraft - bDraft;
|
|
4082
|
+
if (currentDeviceId) {
|
|
4083
|
+
const aOther = a.device_id !== currentDeviceId ? 1 : 0;
|
|
4084
|
+
const bOther = b.device_id !== currentDeviceId ? 1 : 0;
|
|
4085
|
+
if (aOther !== bOther)
|
|
4086
|
+
return bOther - aOther;
|
|
4087
|
+
}
|
|
4088
|
+
if (b.created_at_epoch !== a.created_at_epoch) {
|
|
4089
|
+
return b.created_at_epoch - a.created_at_epoch;
|
|
4090
|
+
}
|
|
4091
|
+
return b.id - a.id;
|
|
4092
|
+
}
|
|
4093
|
+
function resolveTargetSession(db, cwd, userId, sessionId) {
|
|
4094
|
+
if (sessionId) {
|
|
4095
|
+
const session = db.getSessionById(sessionId);
|
|
4096
|
+
if (!session)
|
|
4097
|
+
return { session: null };
|
|
4098
|
+
const projectName = session.project_id ? db.getProjectById(session.project_id)?.name : undefined;
|
|
4099
|
+
return {
|
|
4100
|
+
session: {
|
|
4101
|
+
...session,
|
|
4102
|
+
project_name: projectName ?? null,
|
|
4103
|
+
request: db.getSessionSummary(sessionId)?.request ?? null,
|
|
4104
|
+
completed: db.getSessionSummary(sessionId)?.completed ?? null,
|
|
4105
|
+
current_thread: db.getSessionSummary(sessionId)?.current_thread ?? null,
|
|
4106
|
+
capture_state: db.getSessionSummary(sessionId)?.capture_state ?? null,
|
|
4107
|
+
recent_tool_names: db.getSessionSummary(sessionId)?.recent_tool_names ?? null,
|
|
4108
|
+
hot_files: db.getSessionSummary(sessionId)?.hot_files ?? null,
|
|
4109
|
+
recent_outcomes: db.getSessionSummary(sessionId)?.recent_outcomes ?? null,
|
|
4110
|
+
prompt_count: db.getSessionUserPrompts(sessionId, 200).length,
|
|
4111
|
+
tool_event_count: db.getSessionToolEvents(sessionId, 200).length
|
|
4112
|
+
},
|
|
4113
|
+
projectName: projectName ?? undefined
|
|
4114
|
+
};
|
|
4115
|
+
}
|
|
4116
|
+
const detected = detectProject(cwd ?? process.cwd());
|
|
4117
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
4118
|
+
const sessions = db.getRecentSessions(project?.id ?? null, 10, userId);
|
|
4119
|
+
return {
|
|
4120
|
+
session: sessions[0] ?? null,
|
|
4121
|
+
projectName: project?.name
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
function buildHandoffTitle(summary, latestRequest, explicit) {
|
|
4125
|
+
const chosen = explicit?.trim() || summary?.current_thread?.trim() || summary?.completed?.trim() || latestRequest?.trim() || "Current work";
|
|
4126
|
+
return compactLine2(chosen) ?? "Current work";
|
|
4127
|
+
}
|
|
4128
|
+
function buildHandoffNarrative(summary, story, options) {
|
|
4129
|
+
const sections = [];
|
|
4130
|
+
if (summary?.request || story.latest_request) {
|
|
4131
|
+
sections.push(`Request: ${summary?.request ?? story.latest_request}`);
|
|
4132
|
+
}
|
|
4133
|
+
if (summary?.current_thread) {
|
|
4134
|
+
sections.push(`Current thread: ${summary.current_thread}`);
|
|
4135
|
+
}
|
|
4136
|
+
if (summary?.investigated) {
|
|
4137
|
+
sections.push(`Investigated: ${summary.investigated}`);
|
|
4138
|
+
}
|
|
4139
|
+
if (summary?.learned) {
|
|
4140
|
+
sections.push(`Learned: ${summary.learned}`);
|
|
4141
|
+
}
|
|
4142
|
+
if (summary?.completed) {
|
|
4143
|
+
sections.push(`Completed: ${summary.completed}`);
|
|
4144
|
+
}
|
|
4145
|
+
if (summary?.next_steps) {
|
|
4146
|
+
sections.push(`Next Steps: ${summary.next_steps}`);
|
|
4147
|
+
}
|
|
4148
|
+
if (story.recent_outcomes.length > 0) {
|
|
4149
|
+
sections.push(`Recent outcomes:
|
|
4150
|
+
${story.recent_outcomes.slice(0, 5).map((item) => `- ${item}`).join(`
|
|
4151
|
+
`)}`);
|
|
4152
|
+
}
|
|
4153
|
+
if (story.hot_files.length > 0) {
|
|
4154
|
+
sections.push(`Hot files:
|
|
4155
|
+
${story.hot_files.slice(0, 5).map((file) => `- ${file.path}`).join(`
|
|
4156
|
+
`)}`);
|
|
4157
|
+
}
|
|
4158
|
+
if (story.provenance_summary.length > 0) {
|
|
4159
|
+
sections.push(`Tool trail:
|
|
4160
|
+
${story.provenance_summary.slice(0, 5).map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
4161
|
+
`)}`);
|
|
4162
|
+
}
|
|
4163
|
+
if (options.includeChat && story.chat_messages.length > 0) {
|
|
4164
|
+
const chatLines = story.chat_messages.slice(-options.chatLimit).map((msg) => `- [${msg.role}] ${compactLine2(msg.content) ?? msg.content.slice(0, 120)}`);
|
|
4165
|
+
sections.push(`Chat snippets:
|
|
4166
|
+
${chatLines.join(`
|
|
4167
|
+
`)}`);
|
|
4168
|
+
}
|
|
4169
|
+
return sections.filter(Boolean).join(`
|
|
4170
|
+
|
|
4171
|
+
`);
|
|
4172
|
+
}
|
|
4173
|
+
function shouldAutoIncludeChat(story) {
|
|
4174
|
+
if (story.chat_messages.length === 0)
|
|
4175
|
+
return false;
|
|
4176
|
+
const summary = story.summary;
|
|
4177
|
+
const thinSummary = !summary?.completed && !summary?.current_thread && story.recent_outcomes.length < 2;
|
|
4178
|
+
const thinChronology = story.capture_state !== "rich" || story.tool_events.length === 0;
|
|
4179
|
+
return thinSummary || thinChronology;
|
|
4180
|
+
}
|
|
4181
|
+
function buildHandoffFacts(summary, story) {
|
|
4182
|
+
const facts = [
|
|
4183
|
+
`session_id=${story.session?.session_id ?? "unknown"}`,
|
|
4184
|
+
`capture_state=${story.capture_state}`,
|
|
4185
|
+
story.project_name ? `project=${story.project_name}` : null,
|
|
4186
|
+
summary?.current_thread ? `current_thread=${summary.current_thread}` : null,
|
|
4187
|
+
story.hot_files[0] ? `hot_file=${story.hot_files[0].path}` : null,
|
|
4188
|
+
story.provenance_summary[0] ? `primary_tool=${story.provenance_summary[0].tool}` : null
|
|
4189
|
+
];
|
|
4190
|
+
return facts.filter((item) => Boolean(item));
|
|
4191
|
+
}
|
|
4192
|
+
function buildDraftHandoffConcepts(projectName, captureState) {
|
|
4193
|
+
return [
|
|
4194
|
+
"handoff",
|
|
4195
|
+
"draft-handoff",
|
|
4196
|
+
"auto-handoff",
|
|
4197
|
+
`capture:${captureState}`,
|
|
4198
|
+
...projectName ? [projectName] : []
|
|
4199
|
+
];
|
|
4200
|
+
}
|
|
4201
|
+
function looksLikeHandoff(obs) {
|
|
4202
|
+
if (obs.title.startsWith("Handoff:") || obs.title.startsWith("Handoff Draft:"))
|
|
4203
|
+
return true;
|
|
4204
|
+
const concepts = parseJsonArray3(obs.concepts);
|
|
4205
|
+
return concepts.includes("handoff") || concepts.includes("session-handoff") || concepts.includes("draft-handoff");
|
|
4206
|
+
}
|
|
4207
|
+
function parseJsonArray3(value) {
|
|
4208
|
+
if (!value)
|
|
4209
|
+
return [];
|
|
4210
|
+
try {
|
|
4211
|
+
const parsed = JSON.parse(value);
|
|
4212
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
4213
|
+
} catch {
|
|
4214
|
+
return [];
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
function compactLine2(value) {
|
|
4218
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
4219
|
+
if (!trimmed)
|
|
4220
|
+
return null;
|
|
4221
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
4222
|
+
}
|
|
4223
|
+
|
|
3565
4224
|
// hooks/post-tool-use.ts
|
|
3566
4225
|
async function main() {
|
|
3567
4226
|
const raw = await readStdin();
|
|
@@ -3589,6 +4248,7 @@ async function main() {
|
|
|
3589
4248
|
try {
|
|
3590
4249
|
if (event.session_id) {
|
|
3591
4250
|
persistRawToolChronology(event, config.user_id, config.device_id);
|
|
4251
|
+
syncTranscriptChat(db, config, event.session_id, event.cwd);
|
|
3592
4252
|
}
|
|
3593
4253
|
const textToScan = extractScanText(event);
|
|
3594
4254
|
if (textToScan) {
|
|
@@ -3666,7 +4326,7 @@ async function main() {
|
|
|
3666
4326
|
}), 1000);
|
|
3667
4327
|
if (observed) {
|
|
3668
4328
|
const result = await saveObservation(db, config, observed);
|
|
3669
|
-
updateRollingSummaryFromObservation(db, result.observation_id, event, config.user_id);
|
|
4329
|
+
await updateRollingSummaryFromObservation(db, result.observation_id, event, config.user_id, config);
|
|
3670
4330
|
incrementObserverSaveCount(event.session_id);
|
|
3671
4331
|
saved = true;
|
|
3672
4332
|
}
|
|
@@ -3685,7 +4345,7 @@ async function main() {
|
|
|
3685
4345
|
cwd: event.cwd,
|
|
3686
4346
|
source_tool: event.tool_name
|
|
3687
4347
|
});
|
|
3688
|
-
updateRollingSummaryFromObservation(db, result.observation_id, event, config.user_id);
|
|
4348
|
+
await updateRollingSummaryFromObservation(db, result.observation_id, event, config.user_id, config);
|
|
3689
4349
|
incrementObserverSaveCount(event.session_id);
|
|
3690
4350
|
}
|
|
3691
4351
|
}
|
|
@@ -3750,7 +4410,7 @@ function detectProjectForEvent(event) {
|
|
|
3750
4410
|
const touchedPaths = extractTouchedPaths(event);
|
|
3751
4411
|
return touchedPaths.length > 0 ? detectProjectFromTouchedPaths(touchedPaths, event.cwd) : detectProject(event.cwd);
|
|
3752
4412
|
}
|
|
3753
|
-
function updateRollingSummaryFromObservation(db, observationId, event, userId) {
|
|
4413
|
+
async function updateRollingSummaryFromObservation(db, observationId, event, userId, config) {
|
|
3754
4414
|
if (!observationId || !event.session_id)
|
|
3755
4415
|
return;
|
|
3756
4416
|
const observation = db.getObservationById(observationId);
|
|
@@ -3782,6 +4442,10 @@ function updateRollingSummaryFromObservation(db, observationId, event, userId) {
|
|
|
3782
4442
|
recent_outcomes: JSON.stringify(handoff.recent_outcomes)
|
|
3783
4443
|
});
|
|
3784
4444
|
db.addToOutbox("summary", summary.id);
|
|
4445
|
+
await upsertRollingHandoff(db, config, {
|
|
4446
|
+
session_id: event.session_id,
|
|
4447
|
+
cwd: event.cwd
|
|
4448
|
+
});
|
|
3785
4449
|
}
|
|
3786
4450
|
function extractTouchedPaths(event) {
|
|
3787
4451
|
const paths = [];
|
|
@@ -3853,16 +4517,16 @@ function extractScanText(event) {
|
|
|
3853
4517
|
}
|
|
3854
4518
|
function incrementRecallMetrics(sessionId, hit) {
|
|
3855
4519
|
try {
|
|
3856
|
-
const { existsSync:
|
|
3857
|
-
const { join:
|
|
3858
|
-
const { homedir:
|
|
3859
|
-
const dir =
|
|
3860
|
-
const path =
|
|
4520
|
+
const { existsSync: existsSync5, readFileSync: readFileSync5, writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = __require("node:fs");
|
|
4521
|
+
const { join: join5 } = __require("node:path");
|
|
4522
|
+
const { homedir: homedir4 } = __require("node:os");
|
|
4523
|
+
const dir = join5(homedir4(), ".engrm", "observer-sessions");
|
|
4524
|
+
const path = join5(dir, `${sessionId}.json`);
|
|
3861
4525
|
let state = {};
|
|
3862
|
-
if (
|
|
3863
|
-
state = JSON.parse(
|
|
4526
|
+
if (existsSync5(path)) {
|
|
4527
|
+
state = JSON.parse(readFileSync5(path, "utf-8"));
|
|
3864
4528
|
} else {
|
|
3865
|
-
if (!
|
|
4529
|
+
if (!existsSync5(dir))
|
|
3866
4530
|
mkdirSync3(dir, { recursive: true });
|
|
3867
4531
|
}
|
|
3868
4532
|
state.recallAttempts = (state.recallAttempts || 0) + 1;
|