engrm 0.4.12 → 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 +136 -6
- package/dist/hooks/elicitation-result.js +116 -4
- package/dist/hooks/post-tool-use.js +118 -5
- package/dist/hooks/pre-compact.js +114 -22
- package/dist/hooks/sentinel.js +112 -3
- package/dist/hooks/session-start.js +160 -24
- package/dist/hooks/stop.js +209 -14
- package/dist/hooks/user-prompt-submit.js +112 -3
- package/dist/server.js +2440 -1228
- package/package.json +1 -1
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) {
|
|
@@ -1054,8 +1148,9 @@ class MemDatabase {
|
|
|
1054
1148
|
const result = this.db.query(`INSERT INTO observations (
|
|
1055
1149
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
1056
1150
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
1057
|
-
user_id, device_id, agent,
|
|
1058
|
-
|
|
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);
|
|
1059
1154
|
const id = Number(result.lastInsertRowid);
|
|
1060
1155
|
const row = this.getObservationById(id);
|
|
1061
1156
|
this.ftsInsert(row);
|
|
@@ -1296,6 +1391,13 @@ class MemDatabase {
|
|
|
1296
1391
|
ORDER BY prompt_number ASC
|
|
1297
1392
|
LIMIT ?`).all(sessionId, limit);
|
|
1298
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
|
+
}
|
|
1299
1401
|
insertToolEvent(input) {
|
|
1300
1402
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1301
1403
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1405,8 +1507,15 @@ class MemDatabase {
|
|
|
1405
1507
|
}
|
|
1406
1508
|
insertSessionSummary(summary) {
|
|
1407
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
|
+
};
|
|
1408
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)
|
|
1409
|
-
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);
|
|
1410
1519
|
const id = Number(result.lastInsertRowid);
|
|
1411
1520
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1412
1521
|
}
|
|
@@ -1880,6 +1989,8 @@ function buildVectorDocument(obs, config, project) {
|
|
|
1880
1989
|
concepts: obs.concepts ? JSON.parse(obs.concepts) : [],
|
|
1881
1990
|
files_read: obs.files_read ? JSON.parse(obs.files_read) : [],
|
|
1882
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,
|
|
1883
1994
|
session_id: obs.session_id,
|
|
1884
1995
|
created_at_epoch: obs.created_at_epoch,
|
|
1885
1996
|
created_at: obs.created_at,
|
|
@@ -1933,6 +2044,8 @@ function buildSummaryVectorDocument(summary, config, project, observations = [],
|
|
|
1933
2044
|
recent_tool_commands: captureContext?.recent_tool_commands ?? [],
|
|
1934
2045
|
hot_files: captureContext?.hot_files ?? [],
|
|
1935
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,
|
|
1936
2049
|
decisions_count: valueSignals.decisions_count,
|
|
1937
2050
|
lessons_count: valueSignals.lessons_count,
|
|
1938
2051
|
discoveries_count: valueSignals.discoveries_count,
|
|
@@ -2045,10 +2158,7 @@ function countPresentSections(summary) {
|
|
|
2045
2158
|
].filter((value) => Boolean(value && value.trim())).length;
|
|
2046
2159
|
}
|
|
2047
2160
|
function extractSectionItems(section) {
|
|
2048
|
-
|
|
2049
|
-
return [];
|
|
2050
|
-
return section.split(`
|
|
2051
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
|
|
2161
|
+
return extractSummaryItems(section, 4);
|
|
2052
2162
|
}
|
|
2053
2163
|
function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
2054
2164
|
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
@@ -2061,6 +2171,13 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
|
2061
2171
|
]).filter(Boolean))].slice(0, 6);
|
|
2062
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);
|
|
2063
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;
|
|
2064
2181
|
return {
|
|
2065
2182
|
prompt_count: prompts.length,
|
|
2066
2183
|
tool_event_count: toolEvents.length,
|
|
@@ -2070,7 +2187,9 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
|
2070
2187
|
recent_tool_commands: recentToolCommands,
|
|
2071
2188
|
capture_state: captureState,
|
|
2072
2189
|
hot_files: hotFiles,
|
|
2073
|
-
recent_outcomes: recentOutcomes
|
|
2190
|
+
recent_outcomes: recentOutcomes,
|
|
2191
|
+
observation_source_tools: observationSourceTools,
|
|
2192
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
2074
2193
|
};
|
|
2075
2194
|
}
|
|
2076
2195
|
function parseJsonArray2(value) {
|
|
@@ -2298,10 +2417,7 @@ function countPresentSections2(summary) {
|
|
|
2298
2417
|
].filter(hasContent).length;
|
|
2299
2418
|
}
|
|
2300
2419
|
function extractSectionItems2(section) {
|
|
2301
|
-
|
|
2302
|
-
return [];
|
|
2303
|
-
return section.split(`
|
|
2304
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
|
|
2420
|
+
return extractSummaryItems(section);
|
|
2305
2421
|
}
|
|
2306
2422
|
function extractObservationTitles(observations, types) {
|
|
2307
2423
|
const typeSet = new Set(types);
|
|
@@ -2419,7 +2535,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
2419
2535
|
sentinel_used: valueSignals.security_findings_count > 0,
|
|
2420
2536
|
risk_score: riskScore,
|
|
2421
2537
|
stacks_detected: stacks,
|
|
2422
|
-
client_version: "0.4.
|
|
2538
|
+
client_version: "0.4.13",
|
|
2423
2539
|
context_observations_injected: metrics?.contextObsInjected ?? 0,
|
|
2424
2540
|
context_total_available: metrics?.contextTotalAvailable ?? 0,
|
|
2425
2541
|
recall_attempts: metrics?.recallAttempts ?? 0,
|
|
@@ -3203,6 +3319,7 @@ async function saveObservation(db, config, input) {
|
|
|
3203
3319
|
reason: `Merged into existing observation #${duplicate.id}`
|
|
3204
3320
|
};
|
|
3205
3321
|
}
|
|
3322
|
+
const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
|
|
3206
3323
|
const obs = db.insertObservation({
|
|
3207
3324
|
session_id: input.session_id ?? null,
|
|
3208
3325
|
project_id: project.id,
|
|
@@ -3218,7 +3335,9 @@ async function saveObservation(db, config, input) {
|
|
|
3218
3335
|
sensitivity,
|
|
3219
3336
|
user_id: config.user_id,
|
|
3220
3337
|
device_id: config.device_id,
|
|
3221
|
-
agent: input.agent ?? "claude-code"
|
|
3338
|
+
agent: input.agent ?? "claude-code",
|
|
3339
|
+
source_tool: input.source_tool ?? null,
|
|
3340
|
+
source_prompt_number: sourcePromptNumber
|
|
3222
3341
|
});
|
|
3223
3342
|
db.addToOutbox("observation", obs.id);
|
|
3224
3343
|
if (db.vecAvailable) {
|
|
@@ -3470,6 +3589,11 @@ async function main() {
|
|
|
3470
3589
|
}
|
|
3471
3590
|
}
|
|
3472
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
|
+
}
|
|
3473
3597
|
const unsaved = detectUnsavedPlans(event.last_assistant_message);
|
|
3474
3598
|
if (unsaved.length > 0) {
|
|
3475
3599
|
console.error("");
|
|
@@ -3609,6 +3733,71 @@ ${sections.join(`
|
|
|
3609
3733
|
});
|
|
3610
3734
|
db.addToOutbox("observation", digestObs.id);
|
|
3611
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
|
+
}
|
|
3612
3801
|
function detectUnsavedPlans(message) {
|
|
3613
3802
|
const hints = [];
|
|
3614
3803
|
const lower = message.toLowerCase();
|
|
@@ -3678,4 +3867,10 @@ function readSessionMetrics(sessionId) {
|
|
|
3678
3867
|
} catch {}
|
|
3679
3868
|
return result;
|
|
3680
3869
|
}
|
|
3870
|
+
var __testables = {
|
|
3871
|
+
extractAssistantCheckpoint
|
|
3872
|
+
};
|
|
3681
3873
|
runHook("stop", main);
|
|
3874
|
+
export {
|
|
3875
|
+
__testables
|
|
3876
|
+
};
|
|
@@ -713,6 +713,18 @@ var MIGRATIONS = [
|
|
|
713
713
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
714
714
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
715
715
|
`
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
version: 11,
|
|
719
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
720
|
+
sql: `
|
|
721
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
722
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
723
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
724
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
725
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
726
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
727
|
+
`
|
|
716
728
|
}
|
|
717
729
|
];
|
|
718
730
|
function isVecExtensionLoaded(db) {
|
|
@@ -766,6 +778,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
766
778
|
version = Math.max(version, 9);
|
|
767
779
|
if (tableExists(db, "tool_events"))
|
|
768
780
|
version = Math.max(version, 10);
|
|
781
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
782
|
+
version = Math.max(version, 11);
|
|
769
783
|
return version;
|
|
770
784
|
}
|
|
771
785
|
function runMigrations(db) {
|
|
@@ -848,6 +862,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
848
862
|
|
|
849
863
|
// src/storage/sqlite.ts
|
|
850
864
|
import { createHash as createHash2 } from "node:crypto";
|
|
865
|
+
|
|
866
|
+
// src/intelligence/summary-sections.ts
|
|
867
|
+
function extractSummaryItems(section, limit) {
|
|
868
|
+
if (!section || !section.trim())
|
|
869
|
+
return [];
|
|
870
|
+
const rawLines = section.split(`
|
|
871
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
872
|
+
const items = [];
|
|
873
|
+
const seen = new Set;
|
|
874
|
+
let heading = null;
|
|
875
|
+
for (const rawLine of rawLines) {
|
|
876
|
+
const line = stripSectionPrefix(rawLine);
|
|
877
|
+
if (!line)
|
|
878
|
+
continue;
|
|
879
|
+
const headingOnly = parseHeading(line);
|
|
880
|
+
if (headingOnly) {
|
|
881
|
+
heading = headingOnly;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
885
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
886
|
+
if (!stripped)
|
|
887
|
+
continue;
|
|
888
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
889
|
+
const normalized = normalizeItem(item);
|
|
890
|
+
if (!normalized || seen.has(normalized))
|
|
891
|
+
continue;
|
|
892
|
+
seen.add(normalized);
|
|
893
|
+
items.push(item);
|
|
894
|
+
if (limit && items.length >= limit)
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
return items;
|
|
898
|
+
}
|
|
899
|
+
function formatSummaryItems(section, maxLen) {
|
|
900
|
+
const items = extractSummaryItems(section);
|
|
901
|
+
if (items.length === 0)
|
|
902
|
+
return null;
|
|
903
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
904
|
+
`);
|
|
905
|
+
if (cleaned.length <= maxLen)
|
|
906
|
+
return cleaned;
|
|
907
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
908
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
909
|
+
`), truncated.lastIndexOf(" "));
|
|
910
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
911
|
+
return `${safe.trimEnd()}…`;
|
|
912
|
+
}
|
|
913
|
+
function normalizeSummarySection(section) {
|
|
914
|
+
const items = extractSummaryItems(section);
|
|
915
|
+
if (items.length === 0) {
|
|
916
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
917
|
+
return cleaned || null;
|
|
918
|
+
}
|
|
919
|
+
return items.map((item) => `- ${item}`).join(`
|
|
920
|
+
`);
|
|
921
|
+
}
|
|
922
|
+
function normalizeSummaryRequest(value) {
|
|
923
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
924
|
+
return cleaned || null;
|
|
925
|
+
}
|
|
926
|
+
function stripSectionPrefix(value) {
|
|
927
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
928
|
+
}
|
|
929
|
+
function parseHeading(value) {
|
|
930
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
931
|
+
if (boldMatch?.[1]) {
|
|
932
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
933
|
+
}
|
|
934
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
935
|
+
if (plainMatch?.[1]) {
|
|
936
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
function normalizeItem(value) {
|
|
941
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/storage/sqlite.ts
|
|
851
945
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
852
946
|
function openDatabase(dbPath) {
|
|
853
947
|
if (IS_BUN) {
|
|
@@ -963,8 +1057,9 @@ class MemDatabase {
|
|
|
963
1057
|
const result = this.db.query(`INSERT INTO observations (
|
|
964
1058
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
965
1059
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
966
|
-
user_id, device_id, agent,
|
|
967
|
-
|
|
1060
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
1061
|
+
created_at, created_at_epoch
|
|
1062
|
+
) 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);
|
|
968
1063
|
const id = Number(result.lastInsertRowid);
|
|
969
1064
|
const row = this.getObservationById(id);
|
|
970
1065
|
this.ftsInsert(row);
|
|
@@ -1205,6 +1300,13 @@ class MemDatabase {
|
|
|
1205
1300
|
ORDER BY prompt_number ASC
|
|
1206
1301
|
LIMIT ?`).all(sessionId, limit);
|
|
1207
1302
|
}
|
|
1303
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
1304
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
1305
|
+
WHERE session_id = ?
|
|
1306
|
+
ORDER BY prompt_number DESC
|
|
1307
|
+
LIMIT 1`).get(sessionId);
|
|
1308
|
+
return row?.prompt_number ?? null;
|
|
1309
|
+
}
|
|
1208
1310
|
insertToolEvent(input) {
|
|
1209
1311
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
1210
1312
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -1314,8 +1416,15 @@ class MemDatabase {
|
|
|
1314
1416
|
}
|
|
1315
1417
|
insertSessionSummary(summary) {
|
|
1316
1418
|
const now = Math.floor(Date.now() / 1000);
|
|
1419
|
+
const normalized = {
|
|
1420
|
+
request: normalizeSummaryRequest(summary.request),
|
|
1421
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
1422
|
+
learned: normalizeSummarySection(summary.learned),
|
|
1423
|
+
completed: normalizeSummarySection(summary.completed),
|
|
1424
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
1425
|
+
};
|
|
1317
1426
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
1318
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
1427
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
1319
1428
|
const id = Number(result.lastInsertRowid);
|
|
1320
1429
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
1321
1430
|
}
|