engrm 0.4.19 → 0.4.22
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/dist/cli.js +64 -4
- package/dist/hooks/elicitation-result.js +58 -4
- package/dist/hooks/post-tool-use.js +195 -6
- package/dist/hooks/pre-compact.js +91 -7
- package/dist/hooks/sentinel.js +58 -4
- package/dist/hooks/session-start.js +139 -11
- package/dist/hooks/stop.js +106 -50
- package/dist/hooks/user-prompt-submit.js +111 -5
- package/dist/server.js +144 -52
- package/package.json +1 -1
package/dist/hooks/stop.js
CHANGED
|
@@ -883,6 +883,16 @@ var MIGRATIONS = [
|
|
|
883
883
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
884
884
|
`
|
|
885
885
|
},
|
|
886
|
+
{
|
|
887
|
+
version: 12,
|
|
888
|
+
description: "Add synced handoff metadata to session summaries",
|
|
889
|
+
sql: `
|
|
890
|
+
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
891
|
+
ALTER TABLE session_summaries ADD COLUMN recent_tool_names TEXT;
|
|
892
|
+
ALTER TABLE session_summaries ADD COLUMN hot_files TEXT;
|
|
893
|
+
ALTER TABLE session_summaries ADD COLUMN recent_outcomes TEXT;
|
|
894
|
+
`
|
|
895
|
+
},
|
|
886
896
|
{
|
|
887
897
|
version: 11,
|
|
888
898
|
description: "Add observation provenance from tool and prompt chronology",
|
|
@@ -949,6 +959,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
949
959
|
version = Math.max(version, 10);
|
|
950
960
|
if (columnExists(db, "observations", "source_tool"))
|
|
951
961
|
version = Math.max(version, 11);
|
|
962
|
+
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")) {
|
|
963
|
+
version = Math.max(version, 12);
|
|
964
|
+
}
|
|
952
965
|
return version;
|
|
953
966
|
}
|
|
954
967
|
function runMigrations(db) {
|
|
@@ -1027,6 +1040,27 @@ function ensureObservationTypes(db) {
|
|
|
1027
1040
|
}
|
|
1028
1041
|
}
|
|
1029
1042
|
}
|
|
1043
|
+
function ensureSessionSummaryColumns(db) {
|
|
1044
|
+
const required = [
|
|
1045
|
+
"capture_state",
|
|
1046
|
+
"recent_tool_names",
|
|
1047
|
+
"hot_files",
|
|
1048
|
+
"recent_outcomes"
|
|
1049
|
+
];
|
|
1050
|
+
for (const column of required) {
|
|
1051
|
+
if (columnExists(db, "session_summaries", column))
|
|
1052
|
+
continue;
|
|
1053
|
+
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
1054
|
+
}
|
|
1055
|
+
const current = getSchemaVersion(db);
|
|
1056
|
+
if (current < 12) {
|
|
1057
|
+
db.exec("PRAGMA user_version = 12");
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function getSchemaVersion(db) {
|
|
1061
|
+
const result = db.query("PRAGMA user_version").get();
|
|
1062
|
+
return result.user_version;
|
|
1063
|
+
}
|
|
1030
1064
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
1031
1065
|
|
|
1032
1066
|
// src/storage/sqlite.ts
|
|
@@ -1101,6 +1135,7 @@ class MemDatabase {
|
|
|
1101
1135
|
this.vecAvailable = this.loadVecExtension();
|
|
1102
1136
|
runMigrations(this.db);
|
|
1103
1137
|
ensureObservationTypes(this.db);
|
|
1138
|
+
ensureSessionSummaryColumns(this.db);
|
|
1104
1139
|
}
|
|
1105
1140
|
loadVecExtension() {
|
|
1106
1141
|
try {
|
|
@@ -1326,6 +1361,10 @@ class MemDatabase {
|
|
|
1326
1361
|
p.name AS project_name,
|
|
1327
1362
|
ss.request AS request,
|
|
1328
1363
|
ss.completed AS completed,
|
|
1364
|
+
ss.capture_state AS capture_state,
|
|
1365
|
+
ss.recent_tool_names AS recent_tool_names,
|
|
1366
|
+
ss.hot_files AS hot_files,
|
|
1367
|
+
ss.recent_outcomes AS recent_outcomes,
|
|
1329
1368
|
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
1330
1369
|
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
1331
1370
|
FROM sessions s
|
|
@@ -1340,6 +1379,10 @@ class MemDatabase {
|
|
|
1340
1379
|
p.name AS project_name,
|
|
1341
1380
|
ss.request AS request,
|
|
1342
1381
|
ss.completed AS completed,
|
|
1382
|
+
ss.capture_state AS capture_state,
|
|
1383
|
+
ss.recent_tool_names AS recent_tool_names,
|
|
1384
|
+
ss.hot_files AS hot_files,
|
|
1385
|
+
ss.recent_outcomes AS recent_outcomes,
|
|
1343
1386
|
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
1344
1387
|
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
1345
1388
|
FROM sessions s
|
|
@@ -1512,8 +1555,11 @@ class MemDatabase {
|
|
|
1512
1555
|
completed: normalizeSummarySection(summary.completed),
|
|
1513
1556
|
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1514
1557
|
};
|
|
1515
|
-
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1516
|
-
|
|
1558
|
+
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1559
|
+
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1560
|
+
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1561
|
+
)
|
|
1562
|
+
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);
|
|
1517
1563
|
const id = Number(result.lastInsertRowid);
|
|
1518
1564
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1519
1565
|
}
|
|
@@ -1528,7 +1574,11 @@ class MemDatabase {
|
|
|
1528
1574
|
investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
|
|
1529
1575
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1530
1576
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1531
|
-
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
|
|
1577
|
+
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1578
|
+
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1579
|
+
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1580
|
+
hot_files: summary.hot_files ?? existing.hot_files,
|
|
1581
|
+
recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
|
|
1532
1582
|
};
|
|
1533
1583
|
this.db.query(`UPDATE session_summaries
|
|
1534
1584
|
SET project_id = ?,
|
|
@@ -1538,8 +1588,12 @@ class MemDatabase {
|
|
|
1538
1588
|
learned = ?,
|
|
1539
1589
|
completed = ?,
|
|
1540
1590
|
next_steps = ?,
|
|
1591
|
+
capture_state = ?,
|
|
1592
|
+
recent_tool_names = ?,
|
|
1593
|
+
hot_files = ?,
|
|
1594
|
+
recent_outcomes = ?,
|
|
1541
1595
|
created_at_epoch = ?
|
|
1542
|
-
WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
|
|
1596
|
+
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);
|
|
1543
1597
|
return this.getSessionSummary(summary.session_id);
|
|
1544
1598
|
}
|
|
1545
1599
|
getSessionSummary(sessionId) {
|
|
@@ -1972,6 +2026,50 @@ function computeSessionValueSignals(observations, securityFindings = []) {
|
|
|
1972
2026
|
};
|
|
1973
2027
|
}
|
|
1974
2028
|
|
|
2029
|
+
// src/capture/session-handoff.ts
|
|
2030
|
+
function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
2031
|
+
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
2032
|
+
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
2033
|
+
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
2034
|
+
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
2035
|
+
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
2036
|
+
...parseJsonArray2(obs.files_modified),
|
|
2037
|
+
...parseJsonArray2(obs.files_read)
|
|
2038
|
+
]).filter(Boolean))].slice(0, 6);
|
|
2039
|
+
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
|
|
2040
|
+
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
2041
|
+
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
2042
|
+
if (!obs.source_tool)
|
|
2043
|
+
return acc;
|
|
2044
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
2045
|
+
return acc;
|
|
2046
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
2047
|
+
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
2048
|
+
return {
|
|
2049
|
+
prompt_count: prompts.length,
|
|
2050
|
+
tool_event_count: toolEvents.length,
|
|
2051
|
+
recent_request_prompts: recentRequestPrompts,
|
|
2052
|
+
latest_request: latestRequest,
|
|
2053
|
+
recent_tool_names: recentToolNames,
|
|
2054
|
+
recent_tool_commands: recentToolCommands,
|
|
2055
|
+
capture_state: captureState,
|
|
2056
|
+
hot_files: hotFiles,
|
|
2057
|
+
recent_outcomes: recentOutcomes,
|
|
2058
|
+
observation_source_tools: observationSourceTools,
|
|
2059
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
function parseJsonArray2(value) {
|
|
2063
|
+
if (!value)
|
|
2064
|
+
return [];
|
|
2065
|
+
try {
|
|
2066
|
+
const parsed = JSON.parse(value);
|
|
2067
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2068
|
+
} catch {
|
|
2069
|
+
return [];
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1975
2073
|
// src/sync/push.ts
|
|
1976
2074
|
function buildVectorDocument(obs, config, project) {
|
|
1977
2075
|
const parts = [obs.title];
|
|
@@ -2112,7 +2210,7 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
2112
2210
|
const doc2 = buildSummaryVectorDocument(summary, config, {
|
|
2113
2211
|
canonical_id: project2.canonical_id,
|
|
2114
2212
|
name: project2.name
|
|
2115
|
-
}, summaryObservations,
|
|
2213
|
+
}, summaryObservations, buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, summaryObservations));
|
|
2116
2214
|
batch.push({ entryId: entry.id, doc: doc2 });
|
|
2117
2215
|
continue;
|
|
2118
2216
|
}
|
|
@@ -2183,48 +2281,6 @@ function countPresentSections(summary) {
|
|
|
2183
2281
|
function extractSectionItems(section) {
|
|
2184
2282
|
return extractSummaryItems(section, 4);
|
|
2185
2283
|
}
|
|
2186
|
-
function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
2187
|
-
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
2188
|
-
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
2189
|
-
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
2190
|
-
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
2191
|
-
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
2192
|
-
...parseJsonArray2(obs.files_modified),
|
|
2193
|
-
...parseJsonArray2(obs.files_read)
|
|
2194
|
-
]).filter(Boolean))].slice(0, 6);
|
|
2195
|
-
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
|
|
2196
|
-
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
2197
|
-
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
2198
|
-
if (!obs.source_tool)
|
|
2199
|
-
return acc;
|
|
2200
|
-
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
2201
|
-
return acc;
|
|
2202
|
-
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
2203
|
-
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
2204
|
-
return {
|
|
2205
|
-
prompt_count: prompts.length,
|
|
2206
|
-
tool_event_count: toolEvents.length,
|
|
2207
|
-
recent_request_prompts: recentRequestPrompts,
|
|
2208
|
-
latest_request: latestRequest,
|
|
2209
|
-
recent_tool_names: recentToolNames,
|
|
2210
|
-
recent_tool_commands: recentToolCommands,
|
|
2211
|
-
capture_state: captureState,
|
|
2212
|
-
hot_files: hotFiles,
|
|
2213
|
-
recent_outcomes: recentOutcomes,
|
|
2214
|
-
observation_source_tools: observationSourceTools,
|
|
2215
|
-
latest_observation_prompt_number: latestObservationPromptNumber
|
|
2216
|
-
};
|
|
2217
|
-
}
|
|
2218
|
-
function parseJsonArray2(value) {
|
|
2219
|
-
if (!value)
|
|
2220
|
-
return [];
|
|
2221
|
-
try {
|
|
2222
|
-
const parsed = JSON.parse(value);
|
|
2223
|
-
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
2224
|
-
} catch {
|
|
2225
|
-
return [];
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
2284
|
|
|
2229
2285
|
// src/embeddings/embedder.ts
|
|
2230
2286
|
var _available = null;
|
|
@@ -2558,7 +2614,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
2558
2614
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
2559
2615
|
risk_score: riskScore,
|
|
2560
2616
|
stacks_detected: stacks,
|
|
2561
|
-
client_version: "0.4.
|
|
2617
|
+
client_version: "0.4.22",
|
|
2562
2618
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
2563
2619
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
2564
2620
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3594,7 +3650,7 @@ async function main() {
|
|
|
3594
3650
|
const assistantSections = extractAssistantSummarySections(event.last_assistant_message);
|
|
3595
3651
|
const summary = mergeSessionSummary(retrospective, assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? mergeSessionSummary(buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message), assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message);
|
|
3596
3652
|
if (summary) {
|
|
3597
|
-
const row = db.
|
|
3653
|
+
const row = db.upsertSessionSummary(summary);
|
|
3598
3654
|
db.addToOutbox("summary", row.id);
|
|
3599
3655
|
let securityFindings = [];
|
|
3600
3656
|
try {
|
|
@@ -3940,7 +3996,7 @@ function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
|
|
|
3940
3996
|
}
|
|
3941
3997
|
function isGenericCheckpointLine(value) {
|
|
3942
3998
|
const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3943
|
-
return normalized === "here's where things stand:" || normalized === "here's where things stand" || normalized === "where things stand:" || normalized === "where things stand" || normalized === "current status:" || normalized === "current status" || normalized === "status update:" || normalized === "status update";
|
|
3999
|
+
return normalized === "here's where things stand:" || normalized === "here's where things stand" || normalized === "where things stand:" || normalized === "where things stand" || normalized === "current status:" || normalized === "current status" || normalized === "status update:" || normalized === "status update" || normalized.startsWith("all clean. here's a summary of what was fixed") || normalized.startsWith("all clean, here's a summary of what was fixed") || normalized.startsWith("now i have enough to give a clear, accurate assessment") || normalized.startsWith("here's the real picture") || normalized === "tl;dr:" || normalized.startsWith("tl;dr:");
|
|
3944
4000
|
}
|
|
3945
4001
|
function detectUnsavedPlans(message) {
|
|
3946
4002
|
const hints = [];
|
|
@@ -714,6 +714,16 @@ var MIGRATIONS = [
|
|
|
714
714
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
715
715
|
`
|
|
716
716
|
},
|
|
717
|
+
{
|
|
718
|
+
version: 12,
|
|
719
|
+
description: "Add synced handoff metadata to session summaries",
|
|
720
|
+
sql: `
|
|
721
|
+
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
722
|
+
ALTER TABLE session_summaries ADD COLUMN recent_tool_names TEXT;
|
|
723
|
+
ALTER TABLE session_summaries ADD COLUMN hot_files TEXT;
|
|
724
|
+
ALTER TABLE session_summaries ADD COLUMN recent_outcomes TEXT;
|
|
725
|
+
`
|
|
726
|
+
},
|
|
717
727
|
{
|
|
718
728
|
version: 11,
|
|
719
729
|
description: "Add observation provenance from tool and prompt chronology",
|
|
@@ -780,6 +790,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
780
790
|
version = Math.max(version, 10);
|
|
781
791
|
if (columnExists(db, "observations", "source_tool"))
|
|
782
792
|
version = Math.max(version, 11);
|
|
793
|
+
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")) {
|
|
794
|
+
version = Math.max(version, 12);
|
|
795
|
+
}
|
|
783
796
|
return version;
|
|
784
797
|
}
|
|
785
798
|
function runMigrations(db) {
|
|
@@ -858,6 +871,27 @@ function ensureObservationTypes(db) {
|
|
|
858
871
|
}
|
|
859
872
|
}
|
|
860
873
|
}
|
|
874
|
+
function ensureSessionSummaryColumns(db) {
|
|
875
|
+
const required = [
|
|
876
|
+
"capture_state",
|
|
877
|
+
"recent_tool_names",
|
|
878
|
+
"hot_files",
|
|
879
|
+
"recent_outcomes"
|
|
880
|
+
];
|
|
881
|
+
for (const column of required) {
|
|
882
|
+
if (columnExists(db, "session_summaries", column))
|
|
883
|
+
continue;
|
|
884
|
+
db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
|
|
885
|
+
}
|
|
886
|
+
const current = getSchemaVersion(db);
|
|
887
|
+
if (current < 12) {
|
|
888
|
+
db.exec("PRAGMA user_version = 12");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function getSchemaVersion(db) {
|
|
892
|
+
const result = db.query("PRAGMA user_version").get();
|
|
893
|
+
return result.user_version;
|
|
894
|
+
}
|
|
861
895
|
var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
|
|
862
896
|
|
|
863
897
|
// src/storage/sqlite.ts
|
|
@@ -1012,6 +1046,7 @@ class MemDatabase {
|
|
|
1012
1046
|
this.vecAvailable = this.loadVecExtension();
|
|
1013
1047
|
runMigrations(this.db);
|
|
1014
1048
|
ensureObservationTypes(this.db);
|
|
1049
|
+
ensureSessionSummaryColumns(this.db);
|
|
1015
1050
|
}
|
|
1016
1051
|
loadVecExtension() {
|
|
1017
1052
|
try {
|
|
@@ -1237,6 +1272,10 @@ class MemDatabase {
|
|
|
1237
1272
|
p.name AS project_name,
|
|
1238
1273
|
ss.request AS request,
|
|
1239
1274
|
ss.completed AS completed,
|
|
1275
|
+
ss.capture_state AS capture_state,
|
|
1276
|
+
ss.recent_tool_names AS recent_tool_names,
|
|
1277
|
+
ss.hot_files AS hot_files,
|
|
1278
|
+
ss.recent_outcomes AS recent_outcomes,
|
|
1240
1279
|
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
1241
1280
|
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
1242
1281
|
FROM sessions s
|
|
@@ -1251,6 +1290,10 @@ class MemDatabase {
|
|
|
1251
1290
|
p.name AS project_name,
|
|
1252
1291
|
ss.request AS request,
|
|
1253
1292
|
ss.completed AS completed,
|
|
1293
|
+
ss.capture_state AS capture_state,
|
|
1294
|
+
ss.recent_tool_names AS recent_tool_names,
|
|
1295
|
+
ss.hot_files AS hot_files,
|
|
1296
|
+
ss.recent_outcomes AS recent_outcomes,
|
|
1254
1297
|
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
1255
1298
|
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
1256
1299
|
FROM sessions s
|
|
@@ -1423,8 +1466,11 @@ class MemDatabase {
|
|
|
1423
1466
|
completed: normalizeSummarySection(summary.completed),
|
|
1424
1467
|
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1425
1468
|
};
|
|
1426
|
-
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1427
|
-
|
|
1469
|
+
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1470
|
+
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1471
|
+
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1472
|
+
)
|
|
1473
|
+
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);
|
|
1428
1474
|
const id = Number(result.lastInsertRowid);
|
|
1429
1475
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1430
1476
|
}
|
|
@@ -1439,7 +1485,11 @@ class MemDatabase {
|
|
|
1439
1485
|
investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
|
|
1440
1486
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1441
1487
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1442
|
-
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
|
|
1488
|
+
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1489
|
+
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1490
|
+
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1491
|
+
hot_files: summary.hot_files ?? existing.hot_files,
|
|
1492
|
+
recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
|
|
1443
1493
|
};
|
|
1444
1494
|
this.db.query(`UPDATE session_summaries
|
|
1445
1495
|
SET project_id = ?,
|
|
@@ -1449,8 +1499,12 @@ class MemDatabase {
|
|
|
1449
1499
|
learned = ?,
|
|
1450
1500
|
completed = ?,
|
|
1451
1501
|
next_steps = ?,
|
|
1502
|
+
capture_state = ?,
|
|
1503
|
+
recent_tool_names = ?,
|
|
1504
|
+
hot_files = ?,
|
|
1505
|
+
recent_outcomes = ?,
|
|
1452
1506
|
created_at_epoch = ?
|
|
1453
|
-
WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
|
|
1507
|
+
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);
|
|
1454
1508
|
return this.getSessionSummary(summary.session_id);
|
|
1455
1509
|
}
|
|
1456
1510
|
getSessionSummary(sessionId) {
|
|
@@ -1597,6 +1651,50 @@ function runHook(hookName, fn) {
|
|
|
1597
1651
|
});
|
|
1598
1652
|
}
|
|
1599
1653
|
|
|
1654
|
+
// src/capture/session-handoff.ts
|
|
1655
|
+
function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
|
|
1656
|
+
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
1657
|
+
const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
|
|
1658
|
+
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
1659
|
+
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
1660
|
+
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
1661
|
+
...parseJsonArray(obs.files_modified),
|
|
1662
|
+
...parseJsonArray(obs.files_read)
|
|
1663
|
+
]).filter(Boolean))].slice(0, 6);
|
|
1664
|
+
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
|
|
1665
|
+
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
1666
|
+
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
1667
|
+
if (!obs.source_tool)
|
|
1668
|
+
return acc;
|
|
1669
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
1670
|
+
return acc;
|
|
1671
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
1672
|
+
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
1673
|
+
return {
|
|
1674
|
+
prompt_count: prompts.length,
|
|
1675
|
+
tool_event_count: toolEvents.length,
|
|
1676
|
+
recent_request_prompts: recentRequestPrompts,
|
|
1677
|
+
latest_request: latestRequest,
|
|
1678
|
+
recent_tool_names: recentToolNames,
|
|
1679
|
+
recent_tool_commands: recentToolCommands,
|
|
1680
|
+
capture_state: captureState,
|
|
1681
|
+
hot_files: hotFiles,
|
|
1682
|
+
recent_outcomes: recentOutcomes,
|
|
1683
|
+
observation_source_tools: observationSourceTools,
|
|
1684
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function parseJsonArray(value) {
|
|
1688
|
+
if (!value)
|
|
1689
|
+
return [];
|
|
1690
|
+
try {
|
|
1691
|
+
const parsed = JSON.parse(value);
|
|
1692
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1693
|
+
} catch {
|
|
1694
|
+
return [];
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1600
1698
|
// hooks/user-prompt-submit.ts
|
|
1601
1699
|
async function main() {
|
|
1602
1700
|
const event = await parseStdinJson();
|
|
@@ -1626,6 +1724,10 @@ async function main() {
|
|
|
1626
1724
|
});
|
|
1627
1725
|
const compactPrompt = event.prompt.replace(/\s+/g, " ").trim();
|
|
1628
1726
|
if (compactPrompt.length >= 8) {
|
|
1727
|
+
const sessionPrompts = db.getSessionUserPrompts(event.session_id, 20);
|
|
1728
|
+
const sessionToolEvents = db.getSessionToolEvents(event.session_id, 20);
|
|
1729
|
+
const sessionObservations = db.getObservationsBySession(event.session_id);
|
|
1730
|
+
const handoff = buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, sessionObservations);
|
|
1629
1731
|
const summary = db.upsertSessionSummary({
|
|
1630
1732
|
session_id: event.session_id,
|
|
1631
1733
|
project_id: project.id,
|
|
@@ -1634,7 +1736,11 @@ async function main() {
|
|
|
1634
1736
|
investigated: null,
|
|
1635
1737
|
learned: null,
|
|
1636
1738
|
completed: null,
|
|
1637
|
-
next_steps: null
|
|
1739
|
+
next_steps: null,
|
|
1740
|
+
capture_state: handoff.capture_state,
|
|
1741
|
+
recent_tool_names: JSON.stringify(handoff.recent_tool_names),
|
|
1742
|
+
hot_files: JSON.stringify(handoff.hot_files),
|
|
1743
|
+
recent_outcomes: JSON.stringify(handoff.recent_outcomes)
|
|
1638
1744
|
});
|
|
1639
1745
|
db.addToOutbox("summary", summary.id);
|
|
1640
1746
|
}
|