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
|
@@ -775,6 +775,18 @@ var MIGRATIONS = [
|
|
|
775
775
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
776
776
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
777
777
|
`
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
version: 11,
|
|
781
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
782
|
+
sql: `
|
|
783
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
784
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
785
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
786
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
787
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
788
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
789
|
+
`
|
|
778
790
|
}
|
|
779
791
|
];
|
|
780
792
|
function isVecExtensionLoaded(db) {
|
|
@@ -828,6 +840,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
828
840
|
version = Math.max(version, 9);
|
|
829
841
|
if (tableExists(db, "tool_events"))
|
|
830
842
|
version = Math.max(version, 10);
|
|
843
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
844
|
+
version = Math.max(version, 11);
|
|
831
845
|
return version;
|
|
832
846
|
}
|
|
833
847
|
function runMigrations(db) {
|
|
@@ -910,6 +924,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
910
924
|
|
|
911
925
|
// src/storage/sqlite.ts
|
|
912
926
|
import { createHash as createHash2 } from "node:crypto";
|
|
927
|
+
|
|
928
|
+
// src/intelligence/summary-sections.ts
|
|
929
|
+
function extractSummaryItems(section, limit) {
|
|
930
|
+
if (!section || !section.trim())
|
|
931
|
+
return [];
|
|
932
|
+
const rawLines = section.split(`
|
|
933
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
934
|
+
const items = [];
|
|
935
|
+
const seen = new Set;
|
|
936
|
+
let heading = null;
|
|
937
|
+
for (const rawLine of rawLines) {
|
|
938
|
+
const line = stripSectionPrefix(rawLine);
|
|
939
|
+
if (!line)
|
|
940
|
+
continue;
|
|
941
|
+
const headingOnly = parseHeading(line);
|
|
942
|
+
if (headingOnly) {
|
|
943
|
+
heading = headingOnly;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
947
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
948
|
+
if (!stripped)
|
|
949
|
+
continue;
|
|
950
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
951
|
+
const normalized = normalizeItem(item);
|
|
952
|
+
if (!normalized || seen.has(normalized))
|
|
953
|
+
continue;
|
|
954
|
+
seen.add(normalized);
|
|
955
|
+
items.push(item);
|
|
956
|
+
if (limit && items.length >= limit)
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
return items;
|
|
960
|
+
}
|
|
961
|
+
function formatSummaryItems(section, maxLen) {
|
|
962
|
+
const items = extractSummaryItems(section);
|
|
963
|
+
if (items.length === 0)
|
|
964
|
+
return null;
|
|
965
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
966
|
+
`);
|
|
967
|
+
if (cleaned.length <= maxLen)
|
|
968
|
+
return cleaned;
|
|
969
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
970
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
971
|
+
`), truncated.lastIndexOf(" "));
|
|
972
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
973
|
+
return `${safe.trimEnd()}…`;
|
|
974
|
+
}
|
|
975
|
+
function normalizeSummarySection(section) {
|
|
976
|
+
const items = extractSummaryItems(section);
|
|
977
|
+
if (items.length === 0) {
|
|
978
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
979
|
+
return cleaned || null;
|
|
980
|
+
}
|
|
981
|
+
return items.map((item) => `- ${item}`).join(`
|
|
982
|
+
`);
|
|
983
|
+
}
|
|
984
|
+
function normalizeSummaryRequest(value) {
|
|
985
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
986
|
+
return cleaned || null;
|
|
987
|
+
}
|
|
988
|
+
function stripSectionPrefix(value) {
|
|
989
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
990
|
+
}
|
|
991
|
+
function parseHeading(value) {
|
|
992
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
993
|
+
if (boldMatch?.[1]) {
|
|
994
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
995
|
+
}
|
|
996
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
997
|
+
if (plainMatch?.[1]) {
|
|
998
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
function normalizeItem(value) {
|
|
1003
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/storage/sqlite.ts
|
|
913
1007
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
914
1008
|
function openDatabase(dbPath) {
|
|
915
1009
|
if (IS_BUN) {
|
|
@@ -946,6 +1040,7 @@ function openNodeDatabase(dbPath) {
|
|
|
946
1040
|
const BetterSqlite3 = __require("better-sqlite3");
|
|
947
1041
|
const raw = new BetterSqlite3(dbPath);
|
|
948
1042
|
return {
|
|
1043
|
+
__raw: raw,
|
|
949
1044
|
query(sql) {
|
|
950
1045
|
const stmt = raw.prepare(sql);
|
|
951
1046
|
return {
|
|
@@ -983,7 +1078,7 @@ class MemDatabase {
|
|
|
983
1078
|
loadVecExtension() {
|
|
984
1079
|
try {
|
|
985
1080
|
const sqliteVec = __require("sqlite-vec");
|
|
986
|
-
sqliteVec.load(this.db);
|
|
1081
|
+
sqliteVec.load(this.db.__raw ?? this.db);
|
|
987
1082
|
return true;
|
|
988
1083
|
} catch {
|
|
989
1084
|
return false;
|
|
@@ -1024,8 +1119,9 @@ class MemDatabase {
|
|
|
1024
1119
|
const result = this.db.query(`INSERT INTO observations (
|
|
1025
1120
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
1026
1121
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
1027
|
-
user_id, device_id, agent,
|
|
1028
|
-
|
|
1122
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
1123
|
+
created_at, created_at_epoch
|
|
1124
|
+
) 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);
|
|
1029
1125
|
const id = Number(result.lastInsertRowid);
|
|
1030
1126
|
const row = this.getObservationById(id);
|
|
1031
1127
|
this.ftsInsert(row);
|
|
@@ -1266,6 +1362,13 @@ class MemDatabase {
|
|
|
1266
1362
|
ORDER BY prompt_number ASC
|
|
1267
1363
|
LIMIT ?`).all(sessionId, limit);
|
|
1268
1364
|
}
|
|
1365
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1366
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1367
|
+
WHERE session_id = ?
|
|
1368
|
+
ORDER BY prompt_number DESC
|
|
1369
|
+
LIMIT 1`).get(sessionId);
|
|
1370
|
+
return row?.prompt_number ?? null;
|
|
1371
|
+
}
|
|
1269
1372
|
insertToolEvent(input) {
|
|
1270
1373
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1271
1374
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1375,8 +1478,15 @@ class MemDatabase {
|
|
|
1375
1478
|
}
|
|
1376
1479
|
insertSessionSummary(summary) {
|
|
1377
1480
|
const now = Math.floor(Date.now() / 1000);
|
|
1481
|
+
const normalized = {
|
|
1482
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1483
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1484
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1485
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1486
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1487
|
+
};
|
|
1378
1488
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1379
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1489
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1380
1490
|
const id = Number(result.lastInsertRowid);
|
|
1381
1491
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1382
1492
|
}
|
|
@@ -2319,6 +2429,7 @@ async function saveObservation(db, config, input) {
|
|
|
2319
2429
|
reason: `Merged into existing observation #${duplicate.id}`
|
|
2320
2430
|
};
|
|
2321
2431
|
}
|
|
2432
|
+
const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
|
|
2322
2433
|
const obs = db.insertObservation({
|
|
2323
2434
|
session_id: input.session_id ?? null,
|
|
2324
2435
|
project_id: project.id,
|
|
@@ -2334,7 +2445,9 @@ async function saveObservation(db, config, input) {
|
|
|
2334
2445
|
sensitivity,
|
|
2335
2446
|
user_id: config.user_id,
|
|
2336
2447
|
device_id: config.device_id,
|
|
2337
|
-
agent: input.agent ?? "claude-code"
|
|
2448
|
+
agent: input.agent ?? "claude-code",
|
|
2449
|
+
source_tool: input.source_tool ?? null,
|
|
2450
|
+
source_prompt_number: sourcePromptNumber
|
|
2338
2451
|
});
|
|
2339
2452
|
db.addToOutbox("observation", obs.id);
|
|
2340
2453
|
if (db.vecAvailable) {
|
|
@@ -3147,7 +3260,8 @@ async function main() {
|
|
|
3147
3260
|
files_read: extracted.files_read,
|
|
3148
3261
|
files_modified: extracted.files_modified,
|
|
3149
3262
|
session_id: event.session_id,
|
|
3150
|
-
cwd: event.cwd
|
|
3263
|
+
cwd: event.cwd,
|
|
3264
|
+
source_tool: event.tool_name
|
|
3151
3265
|
});
|
|
3152
3266
|
incrementObserverSaveCount(event.session_id);
|
|
3153
3267
|
}
|
|
@@ -569,6 +569,18 @@ var MIGRATIONS = [
|
|
|
569
569
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
570
570
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
571
571
|
`
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
version: 11,
|
|
575
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
576
|
+
sql: `
|
|
577
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
578
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
579
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
580
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
581
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
582
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
583
|
+
`
|
|
572
584
|
}
|
|
573
585
|
];
|
|
574
586
|
function isVecExtensionLoaded(db) {
|
|
@@ -622,6 +634,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
622
634
|
version = Math.max(version, 9);
|
|
623
635
|
if (tableExists(db, "tool_events"))
|
|
624
636
|
version = Math.max(version, 10);
|
|
637
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
638
|
+
version = Math.max(version, 11);
|
|
625
639
|
return version;
|
|
626
640
|
}
|
|
627
641
|
function runMigrations(db) {
|
|
@@ -704,6 +718,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
704
718
|
|
|
705
719
|
// src/storage/sqlite.ts
|
|
706
720
|
import { createHash as createHash2 } from "node:crypto";
|
|
721
|
+
|
|
722
|
+
// src/intelligence/summary-sections.ts
|
|
723
|
+
function extractSummaryItems(section, limit) {
|
|
724
|
+
if (!section || !section.trim())
|
|
725
|
+
return [];
|
|
726
|
+
const rawLines = section.split(`
|
|
727
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
728
|
+
const items = [];
|
|
729
|
+
const seen = new Set;
|
|
730
|
+
let heading = null;
|
|
731
|
+
for (const rawLine of rawLines) {
|
|
732
|
+
const line = stripSectionPrefix(rawLine);
|
|
733
|
+
if (!line)
|
|
734
|
+
continue;
|
|
735
|
+
const headingOnly = parseHeading(line);
|
|
736
|
+
if (headingOnly) {
|
|
737
|
+
heading = headingOnly;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
741
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
742
|
+
if (!stripped)
|
|
743
|
+
continue;
|
|
744
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
745
|
+
const normalized = normalizeItem(item);
|
|
746
|
+
if (!normalized || seen.has(normalized))
|
|
747
|
+
continue;
|
|
748
|
+
seen.add(normalized);
|
|
749
|
+
items.push(item);
|
|
750
|
+
if (limit && items.length >= limit)
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
return items;
|
|
754
|
+
}
|
|
755
|
+
function formatSummaryItems(section, maxLen) {
|
|
756
|
+
const items = extractSummaryItems(section);
|
|
757
|
+
if (items.length === 0)
|
|
758
|
+
return null;
|
|
759
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
760
|
+
`);
|
|
761
|
+
if (cleaned.length <= maxLen)
|
|
762
|
+
return cleaned;
|
|
763
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
764
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
765
|
+
`), truncated.lastIndexOf(" "));
|
|
766
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
767
|
+
return `${safe.trimEnd()}…`;
|
|
768
|
+
}
|
|
769
|
+
function normalizeSummarySection(section) {
|
|
770
|
+
const items = extractSummaryItems(section);
|
|
771
|
+
if (items.length === 0) {
|
|
772
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
773
|
+
return cleaned || null;
|
|
774
|
+
}
|
|
775
|
+
return items.map((item) => `- ${item}`).join(`
|
|
776
|
+
`);
|
|
777
|
+
}
|
|
778
|
+
function normalizeSummaryRequest(value) {
|
|
779
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
780
|
+
return cleaned || null;
|
|
781
|
+
}
|
|
782
|
+
function stripSectionPrefix(value) {
|
|
783
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
784
|
+
}
|
|
785
|
+
function parseHeading(value) {
|
|
786
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
787
|
+
if (boldMatch?.[1]) {
|
|
788
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
789
|
+
}
|
|
790
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
791
|
+
if (plainMatch?.[1]) {
|
|
792
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
function normalizeItem(value) {
|
|
797
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/storage/sqlite.ts
|
|
707
801
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
708
802
|
function openDatabase(dbPath) {
|
|
709
803
|
if (IS_BUN) {
|
|
@@ -740,6 +834,7 @@ function openNodeDatabase(dbPath) {
|
|
|
740
834
|
const BetterSqlite3 = __require("better-sqlite3");
|
|
741
835
|
const raw = new BetterSqlite3(dbPath);
|
|
742
836
|
return {
|
|
837
|
+
__raw: raw,
|
|
743
838
|
query(sql) {
|
|
744
839
|
const stmt = raw.prepare(sql);
|
|
745
840
|
return {
|
|
@@ -777,7 +872,7 @@ class MemDatabase {
|
|
|
777
872
|
loadVecExtension() {
|
|
778
873
|
try {
|
|
779
874
|
const sqliteVec = __require("sqlite-vec");
|
|
780
|
-
sqliteVec.load(this.db);
|
|
875
|
+
sqliteVec.load(this.db.__raw ?? this.db);
|
|
781
876
|
return true;
|
|
782
877
|
} catch {
|
|
783
878
|
return false;
|
|
@@ -818,8 +913,9 @@ class MemDatabase {
|
|
|
818
913
|
const result = this.db.query(`INSERT INTO observations (
|
|
819
914
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
820
915
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
821
|
-
user_id, device_id, agent,
|
|
822
|
-
|
|
916
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
917
|
+
created_at, created_at_epoch
|
|
918
|
+
) 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);
|
|
823
919
|
const id = Number(result.lastInsertRowid);
|
|
824
920
|
const row = this.getObservationById(id);
|
|
825
921
|
this.ftsInsert(row);
|
|
@@ -1060,6 +1156,13 @@ class MemDatabase {
|
|
|
1060
1156
|
ORDER BY prompt_number ASC
|
|
1061
1157
|
LIMIT ?`).all(sessionId, limit);
|
|
1062
1158
|
}
|
|
1159
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1160
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1161
|
+
WHERE session_id = ?
|
|
1162
|
+
ORDER BY prompt_number DESC
|
|
1163
|
+
LIMIT 1`).get(sessionId);
|
|
1164
|
+
return row?.prompt_number ?? null;
|
|
1165
|
+
}
|
|
1063
1166
|
insertToolEvent(input) {
|
|
1064
1167
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1065
1168
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1169,8 +1272,15 @@ class MemDatabase {
|
|
|
1169
1272
|
}
|
|
1170
1273
|
insertSessionSummary(summary) {
|
|
1171
1274
|
const now = Math.floor(Date.now() / 1000);
|
|
1275
|
+
const normalized = {
|
|
1276
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1277
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1278
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1279
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1280
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1281
|
+
};
|
|
1172
1282
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1173
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1283
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1174
1284
|
const id = Number(result.lastInsertRowid);
|
|
1175
1285
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1176
1286
|
}
|
|
@@ -2042,23 +2152,7 @@ function chooseMeaningfulSessionHeadline(request, completed) {
|
|
|
2042
2152
|
return request ?? completed ?? "(no summary)";
|
|
2043
2153
|
}
|
|
2044
2154
|
function formatSummarySection(value, maxLen) {
|
|
2045
|
-
|
|
2046
|
-
return null;
|
|
2047
|
-
const cleaned = value.split(`
|
|
2048
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.startsWith("-") ? line : `- ${line}`).join(`
|
|
2049
|
-
`);
|
|
2050
|
-
if (!cleaned)
|
|
2051
|
-
return null;
|
|
2052
|
-
return truncateMultilineText(cleaned, maxLen);
|
|
2053
|
-
}
|
|
2054
|
-
function truncateMultilineText(text, maxLen) {
|
|
2055
|
-
if (text.length <= maxLen)
|
|
2056
|
-
return text;
|
|
2057
|
-
const truncated = text.slice(0, maxLen).trimEnd();
|
|
2058
|
-
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
2059
|
-
`), truncated.lastIndexOf(" "));
|
|
2060
|
-
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
2061
|
-
return `${safe.trimEnd()}…`;
|
|
2155
|
+
return formatSummaryItems(value, maxLen);
|
|
2062
2156
|
}
|
|
2063
2157
|
function truncateText(text, maxLen) {
|
|
2064
2158
|
if (text.length <= maxLen)
|
|
@@ -2082,8 +2176,7 @@ function stripInlineSectionLabel(value) {
|
|
|
2082
2176
|
function extractMeaningfulLines(value, limit) {
|
|
2083
2177
|
if (!value)
|
|
2084
2178
|
return [];
|
|
2085
|
-
return value.
|
|
2086
|
-
`).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);
|
|
2179
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
|
|
2087
2180
|
}
|
|
2088
2181
|
function formatObservationDetailFromContext(obs) {
|
|
2089
2182
|
if (obs.facts) {
|
package/dist/hooks/sentinel.js
CHANGED
|
@@ -645,6 +645,18 @@ var MIGRATIONS = [
|
|
|
645
645
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
646
646
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
647
647
|
`
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
version: 11,
|
|
651
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
652
|
+
sql: `
|
|
653
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
654
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
655
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
656
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
657
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
658
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
659
|
+
`
|
|
648
660
|
}
|
|
649
661
|
];
|
|
650
662
|
function isVecExtensionLoaded(db) {
|
|
@@ -698,6 +710,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
698
710
|
version = Math.max(version, 9);
|
|
699
711
|
if (tableExists(db, "tool_events"))
|
|
700
712
|
version = Math.max(version, 10);
|
|
713
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
714
|
+
version = Math.max(version, 11);
|
|
701
715
|
return version;
|
|
702
716
|
}
|
|
703
717
|
function runMigrations(db) {
|
|
@@ -780,6 +794,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
780
794
|
|
|
781
795
|
// src/storage/sqlite.ts
|
|
782
796
|
import { createHash as createHash2 } from "node:crypto";
|
|
797
|
+
|
|
798
|
+
// src/intelligence/summary-sections.ts
|
|
799
|
+
function extractSummaryItems(section, limit) {
|
|
800
|
+
if (!section || !section.trim())
|
|
801
|
+
return [];
|
|
802
|
+
const rawLines = section.split(`
|
|
803
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
804
|
+
const items = [];
|
|
805
|
+
const seen = new Set;
|
|
806
|
+
let heading = null;
|
|
807
|
+
for (const rawLine of rawLines) {
|
|
808
|
+
const line = stripSectionPrefix(rawLine);
|
|
809
|
+
if (!line)
|
|
810
|
+
continue;
|
|
811
|
+
const headingOnly = parseHeading(line);
|
|
812
|
+
if (headingOnly) {
|
|
813
|
+
heading = headingOnly;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
817
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
818
|
+
if (!stripped)
|
|
819
|
+
continue;
|
|
820
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
821
|
+
const normalized = normalizeItem(item);
|
|
822
|
+
if (!normalized || seen.has(normalized))
|
|
823
|
+
continue;
|
|
824
|
+
seen.add(normalized);
|
|
825
|
+
items.push(item);
|
|
826
|
+
if (limit && items.length >= limit)
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
return items;
|
|
830
|
+
}
|
|
831
|
+
function formatSummaryItems(section, maxLen) {
|
|
832
|
+
const items = extractSummaryItems(section);
|
|
833
|
+
if (items.length === 0)
|
|
834
|
+
return null;
|
|
835
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
836
|
+
`);
|
|
837
|
+
if (cleaned.length <= maxLen)
|
|
838
|
+
return cleaned;
|
|
839
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
840
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
841
|
+
`), truncated.lastIndexOf(" "));
|
|
842
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
843
|
+
return `${safe.trimEnd()}…`;
|
|
844
|
+
}
|
|
845
|
+
function normalizeSummarySection(section) {
|
|
846
|
+
const items = extractSummaryItems(section);
|
|
847
|
+
if (items.length === 0) {
|
|
848
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
849
|
+
return cleaned || null;
|
|
850
|
+
}
|
|
851
|
+
return items.map((item) => `- ${item}`).join(`
|
|
852
|
+
`);
|
|
853
|
+
}
|
|
854
|
+
function normalizeSummaryRequest(value) {
|
|
855
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
856
|
+
return cleaned || null;
|
|
857
|
+
}
|
|
858
|
+
function stripSectionPrefix(value) {
|
|
859
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
860
|
+
}
|
|
861
|
+
function parseHeading(value) {
|
|
862
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
863
|
+
if (boldMatch?.[1]) {
|
|
864
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
865
|
+
}
|
|
866
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
867
|
+
if (plainMatch?.[1]) {
|
|
868
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
function normalizeItem(value) {
|
|
873
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/storage/sqlite.ts
|
|
783
877
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
784
878
|
function openDatabase(dbPath) {
|
|
785
879
|
if (IS_BUN) {
|
|
@@ -816,6 +910,7 @@ function openNodeDatabase(dbPath) {
|
|
|
816
910
|
const BetterSqlite3 = __require("better-sqlite3");
|
|
817
911
|
const raw = new BetterSqlite3(dbPath);
|
|
818
912
|
return {
|
|
913
|
+
__raw: raw,
|
|
819
914
|
query(sql) {
|
|
820
915
|
const stmt = raw.prepare(sql);
|
|
821
916
|
return {
|
|
@@ -853,7 +948,7 @@ class MemDatabase {
|
|
|
853
948
|
loadVecExtension() {
|
|
854
949
|
try {
|
|
855
950
|
const sqliteVec = __require("sqlite-vec");
|
|
856
|
-
sqliteVec.load(this.db);
|
|
951
|
+
sqliteVec.load(this.db.__raw ?? this.db);
|
|
857
952
|
return true;
|
|
858
953
|
} catch {
|
|
859
954
|
return false;
|
|
@@ -894,8 +989,9 @@ class MemDatabase {
|
|
|
894
989
|
const result = this.db.query(`INSERT INTO observations (
|
|
895
990
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
896
991
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
897
|
-
user_id, device_id, agent,
|
|
898
|
-
|
|
992
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
993
|
+
created_at, created_at_epoch
|
|
994
|
+
) 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);
|
|
899
995
|
const id = Number(result.lastInsertRowid);
|
|
900
996
|
const row = this.getObservationById(id);
|
|
901
997
|
this.ftsInsert(row);
|
|
@@ -1136,6 +1232,13 @@ class MemDatabase {
|
|
|
1136
1232
|
ORDER BY prompt_number ASC
|
|
1137
1233
|
LIMIT ?`).all(sessionId, limit);
|
|
1138
1234
|
}
|
|
1235
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1236
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1237
|
+
WHERE session_id = ?
|
|
1238
|
+
ORDER BY prompt_number DESC
|
|
1239
|
+
LIMIT 1`).get(sessionId);
|
|
1240
|
+
return row?.prompt_number ?? null;
|
|
1241
|
+
}
|
|
1139
1242
|
insertToolEvent(input) {
|
|
1140
1243
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1141
1244
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1245,8 +1348,15 @@ class MemDatabase {
|
|
|
1245
1348
|
}
|
|
1246
1349
|
insertSessionSummary(summary) {
|
|
1247
1350
|
const now = Math.floor(Date.now() / 1000);
|
|
1351
|
+
const normalized = {
|
|
1352
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1353
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1354
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1355
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1356
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1357
|
+
};
|
|
1248
1358
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1249
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1359
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1250
1360
|
const id = Number(result.lastInsertRowid);
|
|
1251
1361
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1252
1362
|
}
|