engrm 0.4.11 → 0.4.13
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 -2
- package/dist/cli.js +138 -7
- package/dist/hooks/elicitation-result.js +118 -5
- package/dist/hooks/post-tool-use.js +120 -6
- package/dist/hooks/pre-compact.js +116 -23
- package/dist/hooks/sentinel.js +114 -4
- package/dist/hooks/session-start.js +162 -25
- package/dist/hooks/stop.js +211 -15
- package/dist/hooks/user-prompt-submit.js +114 -4
- package/dist/server.js +2442 -1229
- package/package.json +1 -1
|
@@ -395,6 +395,84 @@ function computeObservationPriority(obs, nowEpoch) {
|
|
|
395
395
|
return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
// src/intelligence/summary-sections.ts
|
|
399
|
+
function extractSummaryItems(section, limit) {
|
|
400
|
+
if (!section || !section.trim())
|
|
401
|
+
return [];
|
|
402
|
+
const rawLines = section.split(`
|
|
403
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
404
|
+
const items = [];
|
|
405
|
+
const seen = new Set;
|
|
406
|
+
let heading = null;
|
|
407
|
+
for (const rawLine of rawLines) {
|
|
408
|
+
const line = stripSectionPrefix(rawLine);
|
|
409
|
+
if (!line)
|
|
410
|
+
continue;
|
|
411
|
+
const headingOnly = parseHeading(line);
|
|
412
|
+
if (headingOnly) {
|
|
413
|
+
heading = headingOnly;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
417
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
418
|
+
if (!stripped)
|
|
419
|
+
continue;
|
|
420
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
421
|
+
const normalized = normalizeItem(item);
|
|
422
|
+
if (!normalized || seen.has(normalized))
|
|
423
|
+
continue;
|
|
424
|
+
seen.add(normalized);
|
|
425
|
+
items.push(item);
|
|
426
|
+
if (limit && items.length >= limit)
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
return items;
|
|
430
|
+
}
|
|
431
|
+
function formatSummaryItems(section, maxLen) {
|
|
432
|
+
const items = extractSummaryItems(section);
|
|
433
|
+
if (items.length === 0)
|
|
434
|
+
return null;
|
|
435
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
436
|
+
`);
|
|
437
|
+
if (cleaned.length <= maxLen)
|
|
438
|
+
return cleaned;
|
|
439
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
440
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
441
|
+
`), truncated.lastIndexOf(" "));
|
|
442
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
443
|
+
return `${safe.trimEnd()}…`;
|
|
444
|
+
}
|
|
445
|
+
function normalizeSummarySection(section) {
|
|
446
|
+
const items = extractSummaryItems(section);
|
|
447
|
+
if (items.length === 0) {
|
|
448
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
449
|
+
return cleaned || null;
|
|
450
|
+
}
|
|
451
|
+
return items.map((item) => `- ${item}`).join(`
|
|
452
|
+
`);
|
|
453
|
+
}
|
|
454
|
+
function normalizeSummaryRequest(value) {
|
|
455
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
456
|
+
return cleaned || null;
|
|
457
|
+
}
|
|
458
|
+
function stripSectionPrefix(value) {
|
|
459
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
460
|
+
}
|
|
461
|
+
function parseHeading(value) {
|
|
462
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
463
|
+
if (boldMatch?.[1]) {
|
|
464
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
465
|
+
}
|
|
466
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
467
|
+
if (plainMatch?.[1]) {
|
|
468
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
function normalizeItem(value) {
|
|
473
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
474
|
+
}
|
|
475
|
+
|
|
398
476
|
// src/context/inject.ts
|
|
399
477
|
function tokenizeProjectHint(text) {
|
|
400
478
|
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
@@ -786,23 +864,7 @@ function chooseMeaningfulSessionHeadline(request, completed) {
|
|
|
786
864
|
return request ?? completed ?? "(no summary)";
|
|
787
865
|
}
|
|
788
866
|
function formatSummarySection(value, maxLen) {
|
|
789
|
-
|
|
790
|
-
return null;
|
|
791
|
-
const cleaned = value.split(`
|
|
792
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.startsWith("-") ? line : `- ${line}`).join(`
|
|
793
|
-
`);
|
|
794
|
-
if (!cleaned)
|
|
795
|
-
return null;
|
|
796
|
-
return truncateMultilineText(cleaned, maxLen);
|
|
797
|
-
}
|
|
798
|
-
function truncateMultilineText(text, maxLen) {
|
|
799
|
-
if (text.length <= maxLen)
|
|
800
|
-
return text;
|
|
801
|
-
const truncated = text.slice(0, maxLen).trimEnd();
|
|
802
|
-
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
803
|
-
`), truncated.lastIndexOf(" "));
|
|
804
|
-
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
805
|
-
return `${safe.trimEnd()}…`;
|
|
867
|
+
return formatSummaryItems(value, maxLen);
|
|
806
868
|
}
|
|
807
869
|
function truncateText(text, maxLen) {
|
|
808
870
|
if (text.length <= maxLen)
|
|
@@ -826,8 +888,7 @@ function stripInlineSectionLabel(value) {
|
|
|
826
888
|
function extractMeaningfulLines(value, limit) {
|
|
827
889
|
if (!value)
|
|
828
890
|
return [];
|
|
829
|
-
return value.
|
|
830
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
|
|
891
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
|
|
831
892
|
}
|
|
832
893
|
function formatObservationDetailFromContext(obs) {
|
|
833
894
|
if (obs.facts) {
|
|
@@ -1093,7 +1154,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
1093
1154
|
import { join as join3 } from "node:path";
|
|
1094
1155
|
import { homedir } from "node:os";
|
|
1095
1156
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
1096
|
-
var CLIENT_VERSION = "0.4.
|
|
1157
|
+
var CLIENT_VERSION = "0.4.13";
|
|
1097
1158
|
function hashFile(filePath) {
|
|
1098
1159
|
try {
|
|
1099
1160
|
if (!existsSync3(filePath))
|
|
@@ -2156,6 +2217,18 @@ var MIGRATIONS = [
|
|
|
2156
2217
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
2157
2218
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
2158
2219
|
`
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
version: 11,
|
|
2223
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
2224
|
+
sql: `
|
|
2225
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
2226
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
2227
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
2228
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
2229
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
2230
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
2231
|
+
`
|
|
2159
2232
|
}
|
|
2160
2233
|
];
|
|
2161
2234
|
function isVecExtensionLoaded(db) {
|
|
@@ -2209,6 +2282,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
2209
2282
|
version = Math.max(version, 9);
|
|
2210
2283
|
if (tableExists(db, "tool_events"))
|
|
2211
2284
|
version = Math.max(version, 10);
|
|
2285
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
2286
|
+
version = Math.max(version, 11);
|
|
2212
2287
|
return version;
|
|
2213
2288
|
}
|
|
2214
2289
|
function runMigrations(db) {
|
|
@@ -2327,6 +2402,7 @@ function openNodeDatabase(dbPath) {
|
|
|
2327
2402
|
const BetterSqlite3 = __require("better-sqlite3");
|
|
2328
2403
|
const raw = new BetterSqlite3(dbPath);
|
|
2329
2404
|
return {
|
|
2405
|
+
__raw: raw,
|
|
2330
2406
|
query(sql) {
|
|
2331
2407
|
const stmt = raw.prepare(sql);
|
|
2332
2408
|
return {
|
|
@@ -2364,7 +2440,7 @@ class MemDatabase {
|
|
|
2364
2440
|
loadVecExtension() {
|
|
2365
2441
|
try {
|
|
2366
2442
|
const sqliteVec = __require("sqlite-vec");
|
|
2367
|
-
sqliteVec.load(this.db);
|
|
2443
|
+
sqliteVec.load(this.db.__raw ?? this.db);
|
|
2368
2444
|
return true;
|
|
2369
2445
|
} catch {
|
|
2370
2446
|
return false;
|
|
@@ -2405,8 +2481,9 @@ class MemDatabase {
|
|
|
2405
2481
|
const result = this.db.query(`INSERT INTO observations (
|
|
2406
2482
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
2407
2483
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
2408
|
-
user_id, device_id, agent,
|
|
2409
|
-
|
|
2484
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
2485
|
+
created_at, created_at_epoch
|
|
2486
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", obs.source_tool ?? null, obs.source_prompt_number ?? null, createdAt, now);
|
|
2410
2487
|
const id = Number(result.lastInsertRowid);
|
|
2411
2488
|
const row = this.getObservationById(id);
|
|
2412
2489
|
this.ftsInsert(row);
|
|
@@ -2647,6 +2724,13 @@ class MemDatabase {
|
|
|
2647
2724
|
ORDER BY prompt_number ASC
|
|
2648
2725
|
LIMIT ?`).all(sessionId, limit);
|
|
2649
2726
|
}
|
|
2727
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
2728
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
2729
|
+
WHERE session_id = ?
|
|
2730
|
+
ORDER BY prompt_number DESC
|
|
2731
|
+
LIMIT 1`).get(sessionId);
|
|
2732
|
+
return row?.prompt_number ?? null;
|
|
2733
|
+
}
|
|
2650
2734
|
insertToolEvent(input) {
|
|
2651
2735
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
2652
2736
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -2756,8 +2840,15 @@ class MemDatabase {
|
|
|
2756
2840
|
}
|
|
2757
2841
|
insertSessionSummary(summary) {
|
|
2758
2842
|
const now = Math.floor(Date.now() / 1000);
|
|
2843
|
+
const normalized = {
|
|
2844
|
+
request: normalizeSummaryRequest(summary.request),
|
|
2845
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
2846
|
+
learned: normalizeSummarySection(summary.learned),
|
|
2847
|
+
completed: normalizeSummarySection(summary.completed),
|
|
2848
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
2849
|
+
};
|
|
2759
2850
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
2760
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
2851
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
2761
2852
|
const id = Number(result.lastInsertRowid);
|
|
2762
2853
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
2763
2854
|
}
|
|
@@ -2963,7 +3054,8 @@ async function main() {
|
|
|
2963
3054
|
securityFindings: context.securityFindings?.length ?? 0,
|
|
2964
3055
|
unreadMessages: msgCount,
|
|
2965
3056
|
synced: syncedCount,
|
|
2966
|
-
context
|
|
3057
|
+
context,
|
|
3058
|
+
estimatedReadTokens: estimateTokens(formatContextForInjection(context))
|
|
2967
3059
|
});
|
|
2968
3060
|
let packLine = "";
|
|
2969
3061
|
try {
|
|
@@ -3048,6 +3140,20 @@ function formatSplashScreen(data) {
|
|
|
3048
3140
|
lines.push(` ${line}`);
|
|
3049
3141
|
}
|
|
3050
3142
|
}
|
|
3143
|
+
const economics = formatContextEconomics(data);
|
|
3144
|
+
if (economics.length > 0) {
|
|
3145
|
+
lines.push("");
|
|
3146
|
+
for (const line of economics) {
|
|
3147
|
+
lines.push(` ${line}`);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
const inspectHints = formatInspectHints(data.context);
|
|
3151
|
+
if (inspectHints.length > 0) {
|
|
3152
|
+
lines.push("");
|
|
3153
|
+
for (const line of inspectHints) {
|
|
3154
|
+
lines.push(` ${line}`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3051
3157
|
lines.push("");
|
|
3052
3158
|
return lines.join(`
|
|
3053
3159
|
`);
|
|
@@ -3156,6 +3262,36 @@ function formatVisibleStartupBrief(context) {
|
|
|
3156
3262
|
}
|
|
3157
3263
|
return lines.slice(0, 14);
|
|
3158
3264
|
}
|
|
3265
|
+
function formatContextEconomics(data) {
|
|
3266
|
+
const totalMemories = Math.max(0, data.loaded + data.available);
|
|
3267
|
+
const parts = [];
|
|
3268
|
+
if (totalMemories > 0) {
|
|
3269
|
+
parts.push(`${totalMemories.toLocaleString()} total memories`);
|
|
3270
|
+
}
|
|
3271
|
+
if (data.estimatedReadTokens > 0) {
|
|
3272
|
+
parts.push(`read now ~${data.estimatedReadTokens.toLocaleString()}t`);
|
|
3273
|
+
}
|
|
3274
|
+
if (parts.length === 0)
|
|
3275
|
+
return [];
|
|
3276
|
+
return [`${c2.dim}Context economics:${c2.reset} ${parts.join(" \xB7 ")}`];
|
|
3277
|
+
}
|
|
3278
|
+
function formatInspectHints(context) {
|
|
3279
|
+
const hints = [];
|
|
3280
|
+
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
3281
|
+
hints.push("recent_sessions");
|
|
3282
|
+
hints.push("session_story");
|
|
3283
|
+
}
|
|
3284
|
+
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
|
|
3285
|
+
hints.push("activity_feed");
|
|
3286
|
+
}
|
|
3287
|
+
if (context.observations.length > 0) {
|
|
3288
|
+
hints.push("memory_console");
|
|
3289
|
+
}
|
|
3290
|
+
const unique = Array.from(new Set(hints)).slice(0, 4);
|
|
3291
|
+
if (unique.length === 0)
|
|
3292
|
+
return [];
|
|
3293
|
+
return [`${c2.dim}Inspect:${c2.reset} ${unique.join(" \xB7 ")}`];
|
|
3294
|
+
}
|
|
3159
3295
|
function rememberShownItem(shown, value) {
|
|
3160
3296
|
if (!value)
|
|
3161
3297
|
return;
|
|
@@ -3501,6 +3637,7 @@ function capitalize(value) {
|
|
|
3501
3637
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
3502
3638
|
}
|
|
3503
3639
|
var __testables = {
|
|
3640
|
+
formatSplashScreen,
|
|
3504
3641
|
formatVisibleStartupBrief
|
|
3505
3642
|
};
|
|
3506
3643
|
runHook("session-start", main);
|
package/dist/hooks/stop.js
CHANGED
|
@@ -804,6 +804,18 @@ var MIGRATIONS = [
|
|
|
804
804
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
805
805
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
806
806
|
`
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
version: 11,
|
|
810
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
811
|
+
sql: `
|
|
812
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
813
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
814
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
815
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
816
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
817
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
818
|
+
`
|
|
807
819
|
}
|
|
808
820
|
];
|
|
809
821
|
function isVecExtensionLoaded(db) {
|
|
@@ -857,6 +869,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
857
869
|
version = Math.max(version, 9);
|
|
858
870
|
if (tableExists(db, "tool_events"))
|
|
859
871
|
version = Math.max(version, 10);
|
|
872
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
873
|
+
version = Math.max(version, 11);
|
|
860
874
|
return version;
|
|
861
875
|
}
|
|
862
876
|
function runMigrations(db) {
|
|
@@ -939,6 +953,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
939
953
|
|
|
940
954
|
// src/storage/sqlite.ts
|
|
941
955
|
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
|
|
942
1036
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
943
1037
|
function openDatabase(dbPath) {
|
|
944
1038
|
if (IS_BUN) {
|
|
@@ -975,6 +1069,7 @@ function openNodeDatabase(dbPath) {
|
|
|
975
1069
|
const BetterSqlite3 = __require("better-sqlite3");
|
|
976
1070
|
const raw = new BetterSqlite3(dbPath);
|
|
977
1071
|
return {
|
|
1072
|
+
__raw: raw,
|
|
978
1073
|
query(sql) {
|
|
979
1074
|
const stmt = raw.prepare(sql);
|
|
980
1075
|
return {
|
|
@@ -1012,7 +1107,7 @@ class MemDatabase {
|
|
|
1012
1107
|
loadVecExtension() {
|
|
1013
1108
|
try {
|
|
1014
1109
|
const sqliteVec = __require("sqlite-vec");
|
|
1015
|
-
sqliteVec.load(this.db);
|
|
1110
|
+
sqliteVec.load(this.db.__raw ?? this.db);
|
|
1016
1111
|
return true;
|
|
1017
1112
|
} catch {
|
|
1018
1113
|
return false;
|
|
@@ -1053,8 +1148,9 @@ class MemDatabase {
|
|
|
1053
1148
|
const result = this.db.query(`INSERT INTO observations (
|
|
1054
1149
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
1055
1150
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
1056
|
-
user_id, device_id, agent,
|
|
1057
|
-
|
|
1151
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
1152
|
+
created_at, created_at_epoch
|
|
1153
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", obs.source_tool ?? null, obs.source_prompt_number ?? null, createdAt, now);
|
|
1058
1154
|
const id = Number(result.lastInsertRowid);
|
|
1059
1155
|
const row = this.getObservationById(id);
|
|
1060
1156
|
this.ftsInsert(row);
|
|
@@ -1295,6 +1391,13 @@ class MemDatabase {
|
|
|
1295
1391
|
ORDER BY prompt_number ASC
|
|
1296
1392
|
LIMIT ?`).all(sessionId, limit);
|
|
1297
1393
|
}
|
|
1394
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1395
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1396
|
+
WHERE session_id = ?
|
|
1397
|
+
ORDER BY prompt_number DESC
|
|
1398
|
+
LIMIT 1`).get(sessionId);
|
|
1399
|
+
return row?.prompt_number ?? null;
|
|
1400
|
+
}
|
|
1298
1401
|
insertToolEvent(input) {
|
|
1299
1402
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1300
1403
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1404,8 +1507,15 @@ class MemDatabase {
|
|
|
1404
1507
|
}
|
|
1405
1508
|
insertSessionSummary(summary) {
|
|
1406
1509
|
const now = Math.floor(Date.now() / 1000);
|
|
1510
|
+
const normalized = {
|
|
1511
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1512
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1513
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1514
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1515
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1516
|
+
};
|
|
1407
1517
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1408
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1518
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1409
1519
|
const id = Number(result.lastInsertRowid);
|
|
1410
1520
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1411
1521
|
}
|
|
@@ -1879,6 +1989,8 @@ function buildVectorDocument(obs, config, project) {
|
|
|
1879
1989
|
concepts: obs.concepts ? JSON.parse(obs.concepts) : [],
|
|
1880
1990
|
files_read: obs.files_read ? JSON.parse(obs.files_read) : [],
|
|
1881
1991
|
files_modified: obs.files_modified ? JSON.parse(obs.files_modified) : [],
|
|
1992
|
+
source_tool: obs.source_tool,
|
|
1993
|
+
source_prompt_number: obs.source_prompt_number,
|
|
1882
1994
|
session_id: obs.session_id,
|
|
1883
1995
|
created_at_epoch: obs.created_at_epoch,
|
|
1884
1996
|
created_at: obs.created_at,
|
|
@@ -1932,6 +2044,8 @@ function buildSummaryVectorDocument(summary, config, project, observations = [],
|
|
|
1932
2044
|
recent_tool_commands: captureContext?.recent_tool_commands ?? [],
|
|
1933
2045
|
hot_files: captureContext?.hot_files ?? [],
|
|
1934
2046
|
recent_outcomes: captureContext?.recent_outcomes ?? [],
|
|
2047
|
+
observation_source_tools: captureContext?.observation_source_tools ?? [],
|
|
2048
|
+
latest_observation_prompt_number: captureContext?.latest_observation_prompt_number ?? null,
|
|
1935
2049
|
decisions_count: valueSignals.decisions_count,
|
|
1936
2050
|
lessons_count: valueSignals.lessons_count,
|
|
1937
2051
|
discoveries_count: valueSignals.discoveries_count,
|
|
@@ -2044,10 +2158,7 @@ function countPresentSections(summary) {
|
|
|
2044
2158
|
].filter((value) => Boolean(value && value.trim())).length;
|
|
2045
2159
|
}
|
|
2046
2160
|
function extractSectionItems(section) {
|
|
2047
|
-
|
|
2048
|
-
return [];
|
|
2049
|
-
return section.split(`
|
|
2050
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
|
|
2161
|
+
return extractSummaryItems(section, 4);
|
|
2051
2162
|
}
|
|
2052
2163
|
function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
2053
2164
|
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
@@ -2060,6 +2171,13 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
|
2060
2171
|
]).filter(Boolean))].slice(0, 6);
|
|
2061
2172
|
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);
|
|
2062
2173
|
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
2174
|
+
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
2175
|
+
if (!obs.source_tool)
|
|
2176
|
+
return acc;
|
|
2177
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
2178
|
+
return acc;
|
|
2179
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
2180
|
+
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
2063
2181
|
return {
|
|
2064
2182
|
prompt_count: prompts.length,
|
|
2065
2183
|
tool_event_count: toolEvents.length,
|
|
@@ -2069,7 +2187,9 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
|
2069
2187
|
recent_tool_commands: recentToolCommands,
|
|
2070
2188
|
capture_state: captureState,
|
|
2071
2189
|
hot_files: hotFiles,
|
|
2072
|
-
recent_outcomes: recentOutcomes
|
|
2190
|
+
recent_outcomes: recentOutcomes,
|
|
2191
|
+
observation_source_tools: observationSourceTools,
|
|
2192
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
2073
2193
|
};
|
|
2074
2194
|
}
|
|
2075
2195
|
function parseJsonArray2(value) {
|
|
@@ -2297,10 +2417,7 @@ function countPresentSections2(summary) {
|
|
|
2297
2417
|
].filter(hasContent).length;
|
|
2298
2418
|
}
|
|
2299
2419
|
function extractSectionItems2(section) {
|
|
2300
|
-
|
|
2301
|
-
return [];
|
|
2302
|
-
return section.split(`
|
|
2303
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
|
|
2420
|
+
return extractSummaryItems(section);
|
|
2304
2421
|
}
|
|
2305
2422
|
function extractObservationTitles(observations, types) {
|
|
2306
2423
|
const typeSet = new Set(types);
|
|
@@ -2418,7 +2535,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
2418
2535
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
2419
2536
|
risk_score: riskScore,
|
|
2420
2537
|
stacks_detected: stacks,
|
|
2421
|
-
client_version: "0.4.
|
|
2538
|
+
client_version: "0.4.13",
|
|
2422
2539
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
2423
2540
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
2424
2541
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3202,6 +3319,7 @@ async function saveObservation(db, config, input) {
|
|
|
3202
3319
|
reason: `Merged into existing observation #${duplicate.id}`
|
|
3203
3320
|
};
|
|
3204
3321
|
}
|
|
3322
|
+
const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
|
|
3205
3323
|
const obs = db.insertObservation({
|
|
3206
3324
|
session_id: input.session_id ?? null,
|
|
3207
3325
|
project_id: project.id,
|
|
@@ -3217,7 +3335,9 @@ async function saveObservation(db, config, input) {
|
|
|
3217
3335
|
sensitivity,
|
|
3218
3336
|
user_id: config.user_id,
|
|
3219
3337
|
device_id: config.device_id,
|
|
3220
|
-
agent: input.agent ?? "claude-code"
|
|
3338
|
+
agent: input.agent ?? "claude-code",
|
|
3339
|
+
source_tool: input.source_tool ?? null,
|
|
3340
|
+
source_prompt_number: sourcePromptNumber
|
|
3221
3341
|
});
|
|
3222
3342
|
db.addToOutbox("observation", obs.id);
|
|
3223
3343
|
if (db.vecAvailable) {
|
|
@@ -3469,6 +3589,11 @@ async function main() {
|
|
|
3469
3589
|
}
|
|
3470
3590
|
}
|
|
3471
3591
|
if (event.last_assistant_message) {
|
|
3592
|
+
if (event.session_id) {
|
|
3593
|
+
try {
|
|
3594
|
+
createAssistantCheckpoint(db, event.session_id, event.cwd, event.last_assistant_message);
|
|
3595
|
+
} catch {}
|
|
3596
|
+
}
|
|
3472
3597
|
const unsaved = detectUnsavedPlans(event.last_assistant_message);
|
|
3473
3598
|
if (unsaved.length > 0) {
|
|
3474
3599
|
console.error("");
|
|
@@ -3608,6 +3733,71 @@ ${sections.join(`
|
|
|
3608
3733
|
});
|
|
3609
3734
|
db.addToOutbox("observation", digestObs.id);
|
|
3610
3735
|
}
|
|
3736
|
+
function createAssistantCheckpoint(db, sessionId, cwd, message) {
|
|
3737
|
+
const checkpoint = extractAssistantCheckpoint(message);
|
|
3738
|
+
if (!checkpoint)
|
|
3739
|
+
return;
|
|
3740
|
+
const existing = db.getObservationsBySession(sessionId).find((obs) => obs.source_tool === "assistant-stop" && obs.title === checkpoint.title);
|
|
3741
|
+
if (existing)
|
|
3742
|
+
return;
|
|
3743
|
+
const detected = detectProject(cwd);
|
|
3744
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
3745
|
+
if (!project)
|
|
3746
|
+
return;
|
|
3747
|
+
const promptNumber = db.getLatestSessionPromptNumber(sessionId);
|
|
3748
|
+
const row = db.insertObservation({
|
|
3749
|
+
session_id: sessionId,
|
|
3750
|
+
project_id: project.id,
|
|
3751
|
+
type: checkpoint.type,
|
|
3752
|
+
title: checkpoint.title,
|
|
3753
|
+
narrative: checkpoint.narrative,
|
|
3754
|
+
facts: checkpoint.facts.length > 0 ? JSON.stringify(checkpoint.facts.slice(0, 8)) : null,
|
|
3755
|
+
quality: checkpoint.quality,
|
|
3756
|
+
lifecycle: "active",
|
|
3757
|
+
sensitivity: "shared",
|
|
3758
|
+
user_id: db.getSessionById(sessionId)?.user_id ?? "unknown",
|
|
3759
|
+
device_id: db.getSessionById(sessionId)?.device_id ?? "unknown",
|
|
3760
|
+
agent: db.getSessionById(sessionId)?.agent ?? "claude-code",
|
|
3761
|
+
source_tool: "assistant-stop",
|
|
3762
|
+
source_prompt_number: promptNumber
|
|
3763
|
+
});
|
|
3764
|
+
db.addToOutbox("observation", row.id);
|
|
3765
|
+
}
|
|
3766
|
+
function extractAssistantCheckpoint(message) {
|
|
3767
|
+
const compact = message.replace(/\r/g, "").trim();
|
|
3768
|
+
if (compact.length < 180)
|
|
3769
|
+
return null;
|
|
3770
|
+
const normalizedLines = compact.split(`
|
|
3771
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
3772
|
+
const bulletLines = compact.split(`
|
|
3773
|
+
`).map((line) => line.trim()).filter(Boolean).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter((line) => line.length > 20).slice(0, 8);
|
|
3774
|
+
const substantiveLines = compact.split(`
|
|
3775
|
+
`).map((line) => line.trim()).filter(Boolean).filter((line) => !/^#+\s*/.test(line)).filter((line) => !/^[-*]\s*$/.test(line));
|
|
3776
|
+
const title = pickAssistantCheckpointTitle(substantiveLines, bulletLines);
|
|
3777
|
+
if (!title)
|
|
3778
|
+
return null;
|
|
3779
|
+
const lowered = compact.toLowerCase();
|
|
3780
|
+
const headingText = normalizedLines.filter((line) => /^[A-Za-z][A-Za-z /_-]{2,}:$/.test(line)).join(" ").toLowerCase();
|
|
3781
|
+
const hasNextSteps = normalizedLines.some((line) => /^Next Steps?:/i.test(line));
|
|
3782
|
+
const deploymentSignals = /\bdeploy|deployment|ansible|rolled out|released to staging|pushed commit|shipped to staging|launched\b/.test(lowered) || /\bdeployment\b/.test(headingText);
|
|
3783
|
+
const decisionSignals = /\bdecid|recommend|strategy|pricing|trade.?off|agreed|approach|direction\b/.test(lowered) || /\bdecision\b/.test(headingText);
|
|
3784
|
+
const featureSignals = /\bimplemented|introduced|exposed|added|built|created|enabled|wired\b/.test(lowered) || /\bfeature\b/.test(headingText);
|
|
3785
|
+
const type = decisionSignals && !deploymentSignals ? "decision" : deploymentSignals || featureSignals ? "feature" : hasNextSteps ? "decision" : "change";
|
|
3786
|
+
const facts = bulletLines.filter((line) => line !== title);
|
|
3787
|
+
const narrative = substantiveLines.slice(0, 6).join(`
|
|
3788
|
+
`);
|
|
3789
|
+
return {
|
|
3790
|
+
type,
|
|
3791
|
+
title,
|
|
3792
|
+
narrative,
|
|
3793
|
+
facts,
|
|
3794
|
+
quality: 0.72
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
|
|
3798
|
+
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));
|
|
3799
|
+
return candidates[0] ?? null;
|
|
3800
|
+
}
|
|
3611
3801
|
function detectUnsavedPlans(message) {
|
|
3612
3802
|
const hints = [];
|
|
3613
3803
|
const lower = message.toLowerCase();
|
|
@@ -3677,4 +3867,10 @@ function readSessionMetrics(sessionId) {
|
|
|
3677
3867
|
} catch {}
|
|
3678
3868
|
return result;
|
|
3679
3869
|
}
|
|
3870
|
+
var __testables = {
|
|
3871
|
+
extractAssistantCheckpoint
|
|
3872
|
+
};
|
|
3680
3873
|
runHook("stop", main);
|
|
3874
|
+
export {
|
|
3875
|
+
__testables
|
|
3876
|
+
};
|