engrm 0.4.18 → 0.4.21
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 +43 -4
- package/dist/hooks/elicitation-result.js +33 -4
- package/dist/hooks/post-tool-use.js +118 -6
- package/dist/hooks/pre-compact.js +66 -7
- package/dist/hooks/sentinel.js +33 -4
- package/dist/hooks/session-start.js +120 -21
- package/dist/hooks/stop.js +199 -87
- package/dist/hooks/user-prompt-submit.js +33 -4
- package/dist/server.js +78 -9
- package/package.json +1 -1
|
@@ -477,6 +477,16 @@ function normalizeItem(value) {
|
|
|
477
477
|
function tokenizeProjectHint(text) {
|
|
478
478
|
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
479
479
|
}
|
|
480
|
+
function parseSummaryJsonList(value) {
|
|
481
|
+
if (!value)
|
|
482
|
+
return [];
|
|
483
|
+
try {
|
|
484
|
+
const parsed = JSON.parse(value);
|
|
485
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
486
|
+
} catch {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
480
490
|
function isObservationRelatedToProject(obs, detected) {
|
|
481
491
|
const hints = new Set([
|
|
482
492
|
...tokenizeProjectHint(detected.name),
|
|
@@ -608,7 +618,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
608
618
|
const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
609
619
|
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
610
620
|
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
611
|
-
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
621
|
+
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions2);
|
|
612
622
|
return {
|
|
613
623
|
project_name: projectName,
|
|
614
624
|
canonical_id: canonicalId,
|
|
@@ -646,7 +656,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
646
656
|
const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
647
657
|
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
648
658
|
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
649
|
-
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
659
|
+
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions);
|
|
650
660
|
let securityFindings = [];
|
|
651
661
|
if (!isNewProject) {
|
|
652
662
|
try {
|
|
@@ -989,7 +999,7 @@ function getProjectTypeCounts(db, projectId, userId) {
|
|
|
989
999
|
}
|
|
990
1000
|
return counts;
|
|
991
1001
|
}
|
|
992
|
-
function getRecentOutcomes(db, projectId, userId) {
|
|
1002
|
+
function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
993
1003
|
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
994
1004
|
const visibilityParams = userId ? [userId] : [];
|
|
995
1005
|
const summaries = db.db.query(`SELECT * FROM session_summaries
|
|
@@ -999,6 +1009,15 @@ function getRecentOutcomes(db, projectId, userId) {
|
|
|
999
1009
|
const picked = [];
|
|
1000
1010
|
const seen = new Set;
|
|
1001
1011
|
for (const summary of summaries) {
|
|
1012
|
+
for (const item of parseSummaryJsonList(summary.recent_outcomes)) {
|
|
1013
|
+
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1014
|
+
if (!normalized || seen.has(normalized))
|
|
1015
|
+
continue;
|
|
1016
|
+
seen.add(normalized);
|
|
1017
|
+
picked.push(item);
|
|
1018
|
+
if (picked.length >= 5)
|
|
1019
|
+
return picked;
|
|
1020
|
+
}
|
|
1002
1021
|
for (const line of [
|
|
1003
1022
|
...extractMeaningfulLines(summary.completed, 2),
|
|
1004
1023
|
...extractMeaningfulLines(summary.learned, 1)
|
|
@@ -1012,6 +1031,17 @@ function getRecentOutcomes(db, projectId, userId) {
|
|
|
1012
1031
|
return picked;
|
|
1013
1032
|
}
|
|
1014
1033
|
}
|
|
1034
|
+
for (const session of recentSessions ?? []) {
|
|
1035
|
+
for (const item of parseSummaryJsonList(session.recent_outcomes)) {
|
|
1036
|
+
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1037
|
+
if (!normalized || seen.has(normalized))
|
|
1038
|
+
continue;
|
|
1039
|
+
seen.add(normalized);
|
|
1040
|
+
picked.push(item);
|
|
1041
|
+
if (picked.length >= 5)
|
|
1042
|
+
return picked;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1015
1045
|
const rows = db.db.query(`SELECT * FROM observations
|
|
1016
1046
|
WHERE project_id = ?
|
|
1017
1047
|
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
@@ -1154,7 +1184,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
1154
1184
|
import { join as join3 } from "node:path";
|
|
1155
1185
|
import { homedir } from "node:os";
|
|
1156
1186
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
1157
|
-
var CLIENT_VERSION = "0.4.
|
|
1187
|
+
var CLIENT_VERSION = "0.4.21";
|
|
1158
1188
|
function hashFile(filePath) {
|
|
1159
1189
|
try {
|
|
1160
1190
|
if (!existsSync3(filePath))
|
|
@@ -1696,10 +1726,20 @@ function mergeRemoteSummary(db, config, change, projectId) {
|
|
|
1696
1726
|
investigated: typeof change.metadata?.investigated === "string" ? change.metadata.investigated : null,
|
|
1697
1727
|
learned: typeof change.metadata?.learned === "string" ? change.metadata.learned : null,
|
|
1698
1728
|
completed: typeof change.metadata?.completed === "string" ? change.metadata.completed : null,
|
|
1699
|
-
next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null
|
|
1729
|
+
next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null,
|
|
1730
|
+
capture_state: typeof change.metadata?.capture_state === "string" ? change.metadata.capture_state : null,
|
|
1731
|
+
recent_tool_names: encodeStringArray(change.metadata?.recent_tool_names),
|
|
1732
|
+
hot_files: encodeStringArray(change.metadata?.hot_files),
|
|
1733
|
+
recent_outcomes: encodeStringArray(change.metadata?.recent_outcomes)
|
|
1700
1734
|
});
|
|
1701
1735
|
return Boolean(summary);
|
|
1702
1736
|
}
|
|
1737
|
+
function encodeStringArray(value) {
|
|
1738
|
+
if (!Array.isArray(value))
|
|
1739
|
+
return null;
|
|
1740
|
+
const normalized = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
1741
|
+
return normalized.length > 0 ? JSON.stringify(normalized) : null;
|
|
1742
|
+
}
|
|
1703
1743
|
function normalizeRemoteObservationType(rawType, sourceId) {
|
|
1704
1744
|
const type = typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
|
|
1705
1745
|
if (type === "bugfix" || type === "discovery" || type === "decision" || type === "pattern" || type === "change" || type === "feature" || type === "refactor" || type === "digest" || type === "standard" || type === "message") {
|
|
@@ -2246,6 +2286,16 @@ var MIGRATIONS = [
|
|
|
2246
2286
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
2247
2287
|
`
|
|
2248
2288
|
},
|
|
2289
|
+
{
|
|
2290
|
+
version: 11,
|
|
2291
|
+
description: "Add synced handoff metadata to session summaries",
|
|
2292
|
+
sql: `
|
|
2293
|
+
ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
|
|
2294
|
+
ALTER TABLE session_summaries ADD COLUMN recent_tool_names TEXT;
|
|
2295
|
+
ALTER TABLE session_summaries ADD COLUMN hot_files TEXT;
|
|
2296
|
+
ALTER TABLE session_summaries ADD COLUMN recent_outcomes TEXT;
|
|
2297
|
+
`
|
|
2298
|
+
},
|
|
2249
2299
|
{
|
|
2250
2300
|
version: 11,
|
|
2251
2301
|
description: "Add observation provenance from tool and prompt chronology",
|
|
@@ -2689,6 +2739,10 @@ class MemDatabase {
|
|
|
2689
2739
|
p.name AS project_name,
|
|
2690
2740
|
ss.request AS request,
|
|
2691
2741
|
ss.completed AS completed,
|
|
2742
|
+
ss.capture_state AS capture_state,
|
|
2743
|
+
ss.recent_tool_names AS recent_tool_names,
|
|
2744
|
+
ss.hot_files AS hot_files,
|
|
2745
|
+
ss.recent_outcomes AS recent_outcomes,
|
|
2692
2746
|
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
2693
2747
|
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
2694
2748
|
FROM sessions s
|
|
@@ -2703,6 +2757,10 @@ class MemDatabase {
|
|
|
2703
2757
|
p.name AS project_name,
|
|
2704
2758
|
ss.request AS request,
|
|
2705
2759
|
ss.completed AS completed,
|
|
2760
|
+
ss.capture_state AS capture_state,
|
|
2761
|
+
ss.recent_tool_names AS recent_tool_names,
|
|
2762
|
+
ss.hot_files AS hot_files,
|
|
2763
|
+
ss.recent_outcomes AS recent_outcomes,
|
|
2706
2764
|
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
2707
2765
|
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
2708
2766
|
FROM sessions s
|
|
@@ -2875,8 +2933,11 @@ class MemDatabase {
|
|
|
2875
2933
|
completed: normalizeSummarySection(summary.completed),
|
|
2876
2934
|
next_steps: normalizeSummarySection(summary.next_steps)
|
|
2877
2935
|
};
|
|
2878
|
-
const result = this.db.query(`INSERT INTO session_summaries (
|
|
2879
|
-
|
|
2936
|
+
const result = this.db.query(`INSERT INTO session_summaries (
|
|
2937
|
+
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
2938
|
+
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
2939
|
+
)
|
|
2940
|
+
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);
|
|
2880
2941
|
const id = Number(result.lastInsertRowid);
|
|
2881
2942
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
2882
2943
|
}
|
|
@@ -2891,7 +2952,11 @@ class MemDatabase {
|
|
|
2891
2952
|
investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
|
|
2892
2953
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
2893
2954
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
2894
|
-
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
|
|
2955
|
+
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
2956
|
+
capture_state: summary.capture_state ?? existing.capture_state,
|
|
2957
|
+
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
2958
|
+
hot_files: summary.hot_files ?? existing.hot_files,
|
|
2959
|
+
recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
|
|
2895
2960
|
};
|
|
2896
2961
|
this.db.query(`UPDATE session_summaries
|
|
2897
2962
|
SET project_id = ?,
|
|
@@ -2901,8 +2966,12 @@ class MemDatabase {
|
|
|
2901
2966
|
learned = ?,
|
|
2902
2967
|
completed = ?,
|
|
2903
2968
|
next_steps = ?,
|
|
2969
|
+
capture_state = ?,
|
|
2970
|
+
recent_tool_names = ?,
|
|
2971
|
+
hot_files = ?,
|
|
2972
|
+
recent_outcomes = ?,
|
|
2904
2973
|
created_at_epoch = ?
|
|
2905
|
-
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);
|
|
2974
|
+
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);
|
|
2906
2975
|
return this.getSessionSummary(summary.session_id);
|
|
2907
2976
|
}
|
|
2908
2977
|
getSessionSummary(sessionId) {
|
|
@@ -3210,13 +3279,13 @@ function formatSplashScreen(data) {
|
|
|
3210
3279
|
}
|
|
3211
3280
|
}
|
|
3212
3281
|
const contextIndex = formatContextIndex(data.context, handoffShownItems);
|
|
3213
|
-
if (contextIndex.length > 0) {
|
|
3282
|
+
if (contextIndex.lines.length > 0) {
|
|
3214
3283
|
lines.push("");
|
|
3215
|
-
for (const line of contextIndex) {
|
|
3284
|
+
for (const line of contextIndex.lines) {
|
|
3216
3285
|
lines.push(` ${line}`);
|
|
3217
3286
|
}
|
|
3218
3287
|
}
|
|
3219
|
-
const inspectHints = formatInspectHints(data.context);
|
|
3288
|
+
const inspectHints = formatInspectHints(data.context, contextIndex.observationIds);
|
|
3220
3289
|
if (inspectHints.length > 0) {
|
|
3221
3290
|
lines.push("");
|
|
3222
3291
|
for (const line of inspectHints) {
|
|
@@ -3353,19 +3422,23 @@ function formatLegend() {
|
|
|
3353
3422
|
];
|
|
3354
3423
|
}
|
|
3355
3424
|
function formatContextIndex(context, shownItems) {
|
|
3356
|
-
const
|
|
3425
|
+
const selected = pickContextIndexObservations(context, shownItems);
|
|
3426
|
+
const rows = selected.map((obs) => {
|
|
3357
3427
|
const icon = observationIcon(obs.type);
|
|
3358
3428
|
const fileHint = extractPrimaryFileHint(obs);
|
|
3359
3429
|
return `${icon} #${obs.id} ${truncateInline(obs.title, 110)}${fileHint ? ` ${c2.dim}(${fileHint})${c2.reset}` : ""}`;
|
|
3360
3430
|
});
|
|
3361
3431
|
if (rows.length === 0)
|
|
3362
|
-
return [];
|
|
3363
|
-
return
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3432
|
+
return { lines: [], observationIds: [] };
|
|
3433
|
+
return {
|
|
3434
|
+
lines: [
|
|
3435
|
+
`${c2.dim}Handoff index:${c2.reset} use IDs when you want the deeper thread`,
|
|
3436
|
+
...rows
|
|
3437
|
+
],
|
|
3438
|
+
observationIds: selected.map((obs) => obs.id)
|
|
3439
|
+
};
|
|
3367
3440
|
}
|
|
3368
|
-
function formatInspectHints(context) {
|
|
3441
|
+
function formatInspectHints(context, visibleObservationIds = []) {
|
|
3369
3442
|
const hints = [];
|
|
3370
3443
|
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
3371
3444
|
hints.push("recent_sessions");
|
|
@@ -3380,7 +3453,7 @@ function formatInspectHints(context) {
|
|
|
3380
3453
|
const unique = Array.from(new Set(hints)).slice(0, 4);
|
|
3381
3454
|
if (unique.length === 0)
|
|
3382
3455
|
return [];
|
|
3383
|
-
const ids =
|
|
3456
|
+
const ids = visibleObservationIds.slice(0, 5);
|
|
3384
3457
|
const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
|
|
3385
3458
|
return [
|
|
3386
3459
|
`${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
|
|
@@ -3446,10 +3519,13 @@ function duplicatesPromptLine(request, promptLine) {
|
|
|
3446
3519
|
return normalizeStartupItem(request) === normalizeStartupItem(promptBody);
|
|
3447
3520
|
}
|
|
3448
3521
|
function buildToolFallbacks(context) {
|
|
3449
|
-
|
|
3522
|
+
const fromEvents = (context.recentToolEvents ?? []).slice(0, 3).map((tool) => {
|
|
3450
3523
|
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
3451
3524
|
return `${tool.tool_name}${detail ? `: ${detail}` : ""}`.trim();
|
|
3452
3525
|
}).filter((item) => item.length > 0);
|
|
3526
|
+
if (fromEvents.length > 0)
|
|
3527
|
+
return fromEvents;
|
|
3528
|
+
return (context.recentSessions ?? []).flatMap((session) => parseSessionJsonList(session.recent_tool_names)).slice(0, 3).filter((item) => item.length > 0);
|
|
3453
3529
|
}
|
|
3454
3530
|
function sessionFallbacksFromContext(context) {
|
|
3455
3531
|
return (context.recentSessions ?? []).slice(0, 2).map((session) => {
|
|
@@ -3475,6 +3551,19 @@ function buildRecentOutcomeLines(context, summary) {
|
|
|
3475
3551
|
};
|
|
3476
3552
|
push(summary?.completed);
|
|
3477
3553
|
push(summary?.learned);
|
|
3554
|
+
if (picked.length < 2) {
|
|
3555
|
+
for (const session of context.recentSessions ?? []) {
|
|
3556
|
+
for (const item of parseSessionJsonList(session.recent_outcomes)) {
|
|
3557
|
+
const normalized = normalizeStartupItem(item);
|
|
3558
|
+
if (!normalized || seen.has(normalized))
|
|
3559
|
+
continue;
|
|
3560
|
+
seen.add(normalized);
|
|
3561
|
+
picked.push(item);
|
|
3562
|
+
if (picked.length >= 2)
|
|
3563
|
+
return picked;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3478
3567
|
if (picked.length < 2) {
|
|
3479
3568
|
for (const obs of context.observations) {
|
|
3480
3569
|
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
@@ -3504,6 +3593,16 @@ function chooseMeaningfulSessionSummary(request, completed) {
|
|
|
3504
3593
|
}
|
|
3505
3594
|
return request ?? completed ?? null;
|
|
3506
3595
|
}
|
|
3596
|
+
function parseSessionJsonList(value) {
|
|
3597
|
+
if (!value)
|
|
3598
|
+
return [];
|
|
3599
|
+
try {
|
|
3600
|
+
const parsed = JSON.parse(value);
|
|
3601
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
3602
|
+
} catch {
|
|
3603
|
+
return [];
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3507
3606
|
function buildProjectSignalLine(context) {
|
|
3508
3607
|
if (!context.projectTypeCounts)
|
|
3509
3608
|
return null;
|
package/dist/hooks/stop.js
CHANGED
|
@@ -237,6 +237,84 @@ function normalizeObservationKey(value) {
|
|
|
237
237
|
return value.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\b(modified|updated|edited|touched|changed)\b/g, "").replace(/\s+/g, " ").trim();
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// src/intelligence/summary-sections.ts
|
|
241
|
+
function extractSummaryItems(section, limit) {
|
|
242
|
+
if (!section || !section.trim())
|
|
243
|
+
return [];
|
|
244
|
+
const rawLines = section.split(`
|
|
245
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
246
|
+
const items = [];
|
|
247
|
+
const seen = new Set;
|
|
248
|
+
let heading = null;
|
|
249
|
+
for (const rawLine of rawLines) {
|
|
250
|
+
const line = stripSectionPrefix(rawLine);
|
|
251
|
+
if (!line)
|
|
252
|
+
continue;
|
|
253
|
+
const headingOnly = parseHeading(line);
|
|
254
|
+
if (headingOnly) {
|
|
255
|
+
heading = headingOnly;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
259
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
260
|
+
if (!stripped)
|
|
261
|
+
continue;
|
|
262
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
263
|
+
const normalized = normalizeItem(item);
|
|
264
|
+
if (!normalized || seen.has(normalized))
|
|
265
|
+
continue;
|
|
266
|
+
seen.add(normalized);
|
|
267
|
+
items.push(item);
|
|
268
|
+
if (limit && items.length >= limit)
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
return items;
|
|
272
|
+
}
|
|
273
|
+
function formatSummaryItems(section, maxLen) {
|
|
274
|
+
const items = extractSummaryItems(section);
|
|
275
|
+
if (items.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
278
|
+
`);
|
|
279
|
+
if (cleaned.length <= maxLen)
|
|
280
|
+
return cleaned;
|
|
281
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
282
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
283
|
+
`), truncated.lastIndexOf(" "));
|
|
284
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
285
|
+
return `${safe.trimEnd()}…`;
|
|
286
|
+
}
|
|
287
|
+
function normalizeSummarySection(section) {
|
|
288
|
+
const items = extractSummaryItems(section);
|
|
289
|
+
if (items.length === 0) {
|
|
290
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
291
|
+
return cleaned || null;
|
|
292
|
+
}
|
|
293
|
+
return items.map((item) => `- ${item}`).join(`
|
|
294
|
+
`);
|
|
295
|
+
}
|
|
296
|
+
function normalizeSummaryRequest(value) {
|
|
297
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
298
|
+
return cleaned || null;
|
|
299
|
+
}
|
|
300
|
+
function stripSectionPrefix(value) {
|
|
301
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
302
|
+
}
|
|
303
|
+
function parseHeading(value) {
|
|
304
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
305
|
+
if (boldMatch?.[1]) {
|
|
306
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
307
|
+
}
|
|
308
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
309
|
+
if (plainMatch?.[1]) {
|
|
310
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function normalizeItem(value) {
|
|
315
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
316
|
+
}
|
|
317
|
+
|
|
240
318
|
// src/config.ts
|
|
241
319
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
242
320
|
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
@@ -805,6 +883,16 @@ var MIGRATIONS = [
|
|
|
805
883
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
806
884
|
`
|
|
807
885
|
},
|
|
886
|
+
{
|
|
887
|
+
version: 11,
|
|
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
|
+
},
|
|
808
896
|
{
|
|
809
897
|
version: 11,
|
|
810
898
|
description: "Add observation provenance from tool and prompt chronology",
|
|
@@ -953,86 +1041,6 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
953
1041
|
|
|
954
1042
|
// src/storage/sqlite.ts
|
|
955
1043
|
import { createHash as createHash2 } from "node:crypto";
|
|
956
|
-
|
|
957
|
-
// src/intelligence/summary-sections.ts
|
|
958
|
-
function extractSummaryItems(section, limit) {
|
|
959
|
-
if (!section || !section.trim())
|
|
960
|
-
return [];
|
|
961
|
-
const rawLines = section.split(`
|
|
962
|
-
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
963
|
-
const items = [];
|
|
964
|
-
const seen = new Set;
|
|
965
|
-
let heading = null;
|
|
966
|
-
for (const rawLine of rawLines) {
|
|
967
|
-
const line = stripSectionPrefix(rawLine);
|
|
968
|
-
if (!line)
|
|
969
|
-
continue;
|
|
970
|
-
const headingOnly = parseHeading(line);
|
|
971
|
-
if (headingOnly) {
|
|
972
|
-
heading = headingOnly;
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
const isBullet = /^[-*•]\s+/.test(line);
|
|
976
|
-
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
977
|
-
if (!stripped)
|
|
978
|
-
continue;
|
|
979
|
-
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
980
|
-
const normalized = normalizeItem(item);
|
|
981
|
-
if (!normalized || seen.has(normalized))
|
|
982
|
-
continue;
|
|
983
|
-
seen.add(normalized);
|
|
984
|
-
items.push(item);
|
|
985
|
-
if (limit && items.length >= limit)
|
|
986
|
-
break;
|
|
987
|
-
}
|
|
988
|
-
return items;
|
|
989
|
-
}
|
|
990
|
-
function formatSummaryItems(section, maxLen) {
|
|
991
|
-
const items = extractSummaryItems(section);
|
|
992
|
-
if (items.length === 0)
|
|
993
|
-
return null;
|
|
994
|
-
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
995
|
-
`);
|
|
996
|
-
if (cleaned.length <= maxLen)
|
|
997
|
-
return cleaned;
|
|
998
|
-
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
999
|
-
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
1000
|
-
`), truncated.lastIndexOf(" "));
|
|
1001
|
-
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
1002
|
-
return `${safe.trimEnd()}…`;
|
|
1003
|
-
}
|
|
1004
|
-
function normalizeSummarySection(section) {
|
|
1005
|
-
const items = extractSummaryItems(section);
|
|
1006
|
-
if (items.length === 0) {
|
|
1007
|
-
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
1008
|
-
return cleaned || null;
|
|
1009
|
-
}
|
|
1010
|
-
return items.map((item) => `- ${item}`).join(`
|
|
1011
|
-
`);
|
|
1012
|
-
}
|
|
1013
|
-
function normalizeSummaryRequest(value) {
|
|
1014
|
-
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
1015
|
-
return cleaned || null;
|
|
1016
|
-
}
|
|
1017
|
-
function stripSectionPrefix(value) {
|
|
1018
|
-
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
1019
|
-
}
|
|
1020
|
-
function parseHeading(value) {
|
|
1021
|
-
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
1022
|
-
if (boldMatch?.[1]) {
|
|
1023
|
-
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
1024
|
-
}
|
|
1025
|
-
const plainMatch = value.match(/^(.+?):$/);
|
|
1026
|
-
if (plainMatch?.[1]) {
|
|
1027
|
-
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
1028
|
-
}
|
|
1029
|
-
return null;
|
|
1030
|
-
}
|
|
1031
|
-
function normalizeItem(value) {
|
|
1032
|
-
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// src/storage/sqlite.ts
|
|
1036
1044
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
1037
1045
|
function openDatabase(dbPath) {
|
|
1038
1046
|
if (IS_BUN) {
|
|
@@ -1328,6 +1336,10 @@ class MemDatabase {
|
|
|
1328
1336
|
p.name AS project_name,
|
|
1329
1337
|
ss.request AS request,
|
|
1330
1338
|
ss.completed AS completed,
|
|
1339
|
+
ss.capture_state AS capture_state,
|
|
1340
|
+
ss.recent_tool_names AS recent_tool_names,
|
|
1341
|
+
ss.hot_files AS hot_files,
|
|
1342
|
+
ss.recent_outcomes AS recent_outcomes,
|
|
1331
1343
|
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
1332
1344
|
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
1333
1345
|
FROM sessions s
|
|
@@ -1342,6 +1354,10 @@ class MemDatabase {
|
|
|
1342
1354
|
p.name AS project_name,
|
|
1343
1355
|
ss.request AS request,
|
|
1344
1356
|
ss.completed AS completed,
|
|
1357
|
+
ss.capture_state AS capture_state,
|
|
1358
|
+
ss.recent_tool_names AS recent_tool_names,
|
|
1359
|
+
ss.hot_files AS hot_files,
|
|
1360
|
+
ss.recent_outcomes AS recent_outcomes,
|
|
1345
1361
|
(SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
|
|
1346
1362
|
(SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
|
|
1347
1363
|
FROM sessions s
|
|
@@ -1514,8 +1530,11 @@ class MemDatabase {
|
|
|
1514
1530
|
completed: normalizeSummarySection(summary.completed),
|
|
1515
1531
|
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1516
1532
|
};
|
|
1517
|
-
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1518
|
-
|
|
1533
|
+
const result = this.db.query(`INSERT INTO session_summaries (
|
|
1534
|
+
session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
|
|
1535
|
+
capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
|
|
1536
|
+
)
|
|
1537
|
+
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);
|
|
1519
1538
|
const id = Number(result.lastInsertRowid);
|
|
1520
1539
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1521
1540
|
}
|
|
@@ -1530,7 +1549,11 @@ class MemDatabase {
|
|
|
1530
1549
|
investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
|
|
1531
1550
|
learned: normalizeSummarySection(summary.learned ?? existing.learned),
|
|
1532
1551
|
completed: normalizeSummarySection(summary.completed ?? existing.completed),
|
|
1533
|
-
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
|
|
1552
|
+
next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
|
|
1553
|
+
capture_state: summary.capture_state ?? existing.capture_state,
|
|
1554
|
+
recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
|
|
1555
|
+
hot_files: summary.hot_files ?? existing.hot_files,
|
|
1556
|
+
recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
|
|
1534
1557
|
};
|
|
1535
1558
|
this.db.query(`UPDATE session_summaries
|
|
1536
1559
|
SET project_id = ?,
|
|
@@ -1540,8 +1563,12 @@ class MemDatabase {
|
|
|
1540
1563
|
learned = ?,
|
|
1541
1564
|
completed = ?,
|
|
1542
1565
|
next_steps = ?,
|
|
1566
|
+
capture_state = ?,
|
|
1567
|
+
recent_tool_names = ?,
|
|
1568
|
+
hot_files = ?,
|
|
1569
|
+
recent_outcomes = ?,
|
|
1543
1570
|
created_at_epoch = ?
|
|
1544
|
-
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);
|
|
1571
|
+
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);
|
|
1545
1572
|
return this.getSessionSummary(summary.session_id);
|
|
1546
1573
|
}
|
|
1547
1574
|
getSessionSummary(sessionId) {
|
|
@@ -2560,7 +2587,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
2560
2587
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
2561
2588
|
risk_score: riskScore,
|
|
2562
2589
|
stacks_detected: stacks,
|
|
2563
|
-
client_version: "0.4.
|
|
2590
|
+
client_version: "0.4.21",
|
|
2564
2591
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
2565
2592
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
2566
2593
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3592,7 +3619,9 @@ async function main() {
|
|
|
3592
3619
|
if (!existing) {
|
|
3593
3620
|
const observations = db.getObservationsBySession(event.session_id);
|
|
3594
3621
|
const session = db.getSessionMetrics(event.session_id);
|
|
3595
|
-
const
|
|
3622
|
+
const retrospective = extractRetrospective(observations, event.session_id, session?.project_id ?? null, config.user_id);
|
|
3623
|
+
const assistantSections = extractAssistantSummarySections(event.last_assistant_message);
|
|
3624
|
+
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
3625
|
if (summary) {
|
|
3597
3626
|
const row = db.insertSessionSummary(summary);
|
|
3598
3627
|
db.addToOutbox("summary", row.id);
|
|
@@ -3701,6 +3730,85 @@ function buildCheckpointCompleted(checkpoint) {
|
|
|
3701
3730
|
return lines.join(`
|
|
3702
3731
|
`);
|
|
3703
3732
|
}
|
|
3733
|
+
function mergeSessionSummary(base, extra, sessionId, projectId, userId) {
|
|
3734
|
+
if (!base && !extra)
|
|
3735
|
+
return null;
|
|
3736
|
+
return {
|
|
3737
|
+
session_id: sessionId,
|
|
3738
|
+
project_id: projectId,
|
|
3739
|
+
user_id: userId,
|
|
3740
|
+
request: chooseRicherSummaryValue(base?.request ?? null, extra?.request ?? null, true),
|
|
3741
|
+
investigated: chooseRicherSummaryValue(base?.investigated ?? null, extra?.investigated ?? null, false),
|
|
3742
|
+
learned: chooseRicherSummaryValue(base?.learned ?? null, extra?.learned ?? null, false),
|
|
3743
|
+
completed: chooseRicherSummaryValue(base?.completed ?? null, extra?.completed ?? null, false),
|
|
3744
|
+
next_steps: chooseRicherSummaryValue(base?.next_steps ?? null, extra?.next_steps ?? null, false)
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3747
|
+
function chooseRicherSummaryValue(base, extra, isRequest) {
|
|
3748
|
+
const normalizedBase = isRequest ? normalizeSummaryRequest(base) : normalizeSummarySection(base);
|
|
3749
|
+
const normalizedExtra = isRequest ? normalizeSummaryRequest(extra) : normalizeSummarySection(extra);
|
|
3750
|
+
if (!normalizedBase)
|
|
3751
|
+
return normalizedExtra;
|
|
3752
|
+
if (!normalizedExtra)
|
|
3753
|
+
return normalizedBase;
|
|
3754
|
+
if (normalizedExtra.length > normalizedBase.length + 24)
|
|
3755
|
+
return normalizedExtra;
|
|
3756
|
+
if (isRequest && isGenericCheckpointLine(normalizedBase) && !isGenericCheckpointLine(normalizedExtra)) {
|
|
3757
|
+
return normalizedExtra;
|
|
3758
|
+
}
|
|
3759
|
+
return normalizedBase;
|
|
3760
|
+
}
|
|
3761
|
+
function extractAssistantSummarySections(message) {
|
|
3762
|
+
const compact = message?.replace(/\r/g, "").trim();
|
|
3763
|
+
if (!compact || compact.length < 80)
|
|
3764
|
+
return null;
|
|
3765
|
+
const sections = new Map;
|
|
3766
|
+
let current = null;
|
|
3767
|
+
for (const rawLine of compact.split(`
|
|
3768
|
+
`)) {
|
|
3769
|
+
const line = rawLine.trim();
|
|
3770
|
+
if (!line)
|
|
3771
|
+
continue;
|
|
3772
|
+
const heading = parseAssistantSectionHeading(line);
|
|
3773
|
+
if (heading) {
|
|
3774
|
+
current = heading;
|
|
3775
|
+
if (!sections.has(current))
|
|
3776
|
+
sections.set(current, []);
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3779
|
+
if (!current)
|
|
3780
|
+
continue;
|
|
3781
|
+
if (current === "request" && isGenericCheckpointLine(line))
|
|
3782
|
+
continue;
|
|
3783
|
+
sections.get(current)?.push(line);
|
|
3784
|
+
}
|
|
3785
|
+
const request = normalizeSummaryRequest(sections.get("request")?.join(" ") ?? null);
|
|
3786
|
+
const investigated = normalizeSummarySection(sections.get("investigated")?.join(`
|
|
3787
|
+
`) ?? null);
|
|
3788
|
+
const learned = normalizeSummarySection(sections.get("learned")?.join(`
|
|
3789
|
+
`) ?? null);
|
|
3790
|
+
const completed = normalizeSummarySection(sections.get("completed")?.join(`
|
|
3791
|
+
`) ?? null);
|
|
3792
|
+
const next_steps = normalizeSummarySection(sections.get("next_steps")?.join(`
|
|
3793
|
+
`) ?? null);
|
|
3794
|
+
if (!request && !investigated && !learned && !completed && !next_steps)
|
|
3795
|
+
return null;
|
|
3796
|
+
return { request, investigated, learned, completed, next_steps };
|
|
3797
|
+
}
|
|
3798
|
+
function parseAssistantSectionHeading(value) {
|
|
3799
|
+
const normalized = value.toLowerCase().replace(/\*+/g, "").trim();
|
|
3800
|
+
if (/^request:/.test(normalized))
|
|
3801
|
+
return "request";
|
|
3802
|
+
if (/^investigated:/.test(normalized))
|
|
3803
|
+
return "investigated";
|
|
3804
|
+
if (/^learned:/.test(normalized))
|
|
3805
|
+
return "learned";
|
|
3806
|
+
if (/^completed:/.test(normalized))
|
|
3807
|
+
return "completed";
|
|
3808
|
+
if (/^next steps?:/.test(normalized))
|
|
3809
|
+
return "next_steps";
|
|
3810
|
+
return null;
|
|
3811
|
+
}
|
|
3704
3812
|
function createSessionDigest(db, sessionId, cwd) {
|
|
3705
3813
|
const observations = db.getObservationsBySession(sessionId);
|
|
3706
3814
|
if (observations.length < 2)
|
|
@@ -3856,9 +3964,13 @@ function extractAssistantCheckpoint(message) {
|
|
|
3856
3964
|
};
|
|
3857
3965
|
}
|
|
3858
3966
|
function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
|
|
3859
|
-
const candidates = [...bulletLines, ...substantiveLines].map((line) => line.replace(/^Completed:\s*/i, "").trim()).filter((line) => line.length > 20).filter((line) => !/^Next Steps?:/i.test(line)).filter((line) => !/^Investigated:/i.test(line)).filter((line) => !/^Learned:/i.test(line));
|
|
3967
|
+
const candidates = [...bulletLines, ...substantiveLines].map((line) => line.replace(/^Completed:\s*/i, "").trim()).filter((line) => line.length > 20).filter((line) => !isGenericCheckpointLine(line)).filter((line) => !/^Next Steps?:/i.test(line)).filter((line) => !/^Investigated:/i.test(line)).filter((line) => !/^Learned:/i.test(line));
|
|
3860
3968
|
return candidates[0] ?? null;
|
|
3861
3969
|
}
|
|
3970
|
+
function isGenericCheckpointLine(value) {
|
|
3971
|
+
const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3972
|
+
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";
|
|
3973
|
+
}
|
|
3862
3974
|
function detectUnsavedPlans(message) {
|
|
3863
3975
|
const hints = [];
|
|
3864
3976
|
const lower = message.toLowerCase();
|