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
package/dist/server.js
CHANGED
|
@@ -14119,6 +14119,18 @@ var MIGRATIONS = [
|
|
|
14119
14119
|
CREATE INDEX IF NOT EXISTS idx_tool_events_created
|
|
14120
14120
|
ON tool_events(created_at_epoch DESC, id DESC);
|
|
14121
14121
|
`
|
|
14122
|
+
},
|
|
14123
|
+
{
|
|
14124
|
+
version: 11,
|
|
14125
|
+
description: "Add observation provenance from tool and prompt chronology",
|
|
14126
|
+
sql: `
|
|
14127
|
+
ALTER TABLE observations ADD COLUMN source_tool TEXT;
|
|
14128
|
+
ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
|
|
14129
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_tool
|
|
14130
|
+
ON observations(source_tool, created_at_epoch DESC);
|
|
14131
|
+
CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
|
|
14132
|
+
ON observations(session_id, source_prompt_number DESC);
|
|
14133
|
+
`
|
|
14122
14134
|
}
|
|
14123
14135
|
];
|
|
14124
14136
|
function isVecExtensionLoaded(db) {
|
|
@@ -14172,6 +14184,8 @@ function inferLegacySchemaVersion(db) {
|
|
|
14172
14184
|
version2 = Math.max(version2, 9);
|
|
14173
14185
|
if (tableExists(db, "tool_events"))
|
|
14174
14186
|
version2 = Math.max(version2, 10);
|
|
14187
|
+
if (columnExists(db, "observations", "source_tool"))
|
|
14188
|
+
version2 = Math.max(version2, 11);
|
|
14175
14189
|
return version2;
|
|
14176
14190
|
}
|
|
14177
14191
|
function runMigrations(db) {
|
|
@@ -14258,6 +14272,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
|
|
|
14258
14272
|
|
|
14259
14273
|
// src/storage/sqlite.ts
|
|
14260
14274
|
import { createHash as createHash2 } from "node:crypto";
|
|
14275
|
+
|
|
14276
|
+
// src/intelligence/summary-sections.ts
|
|
14277
|
+
function extractSummaryItems(section, limit) {
|
|
14278
|
+
if (!section || !section.trim())
|
|
14279
|
+
return [];
|
|
14280
|
+
const rawLines = section.split(`
|
|
14281
|
+
`).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
14282
|
+
const items = [];
|
|
14283
|
+
const seen = new Set;
|
|
14284
|
+
let heading = null;
|
|
14285
|
+
for (const rawLine of rawLines) {
|
|
14286
|
+
const line = stripSectionPrefix(rawLine);
|
|
14287
|
+
if (!line)
|
|
14288
|
+
continue;
|
|
14289
|
+
const headingOnly = parseHeading(line);
|
|
14290
|
+
if (headingOnly) {
|
|
14291
|
+
heading = headingOnly;
|
|
14292
|
+
continue;
|
|
14293
|
+
}
|
|
14294
|
+
const isBullet = /^[-*•]\s+/.test(line);
|
|
14295
|
+
const stripped = line.replace(/^[-*•]\s+/, "").trim();
|
|
14296
|
+
if (!stripped)
|
|
14297
|
+
continue;
|
|
14298
|
+
const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
|
|
14299
|
+
const normalized = normalizeItem(item);
|
|
14300
|
+
if (!normalized || seen.has(normalized))
|
|
14301
|
+
continue;
|
|
14302
|
+
seen.add(normalized);
|
|
14303
|
+
items.push(item);
|
|
14304
|
+
if (limit && items.length >= limit)
|
|
14305
|
+
break;
|
|
14306
|
+
}
|
|
14307
|
+
return items;
|
|
14308
|
+
}
|
|
14309
|
+
function formatSummaryItems(section, maxLen) {
|
|
14310
|
+
const items = extractSummaryItems(section);
|
|
14311
|
+
if (items.length === 0)
|
|
14312
|
+
return null;
|
|
14313
|
+
const cleaned = items.map((item) => `- ${item}`).join(`
|
|
14314
|
+
`);
|
|
14315
|
+
if (cleaned.length <= maxLen)
|
|
14316
|
+
return cleaned;
|
|
14317
|
+
const truncated = cleaned.slice(0, maxLen).trimEnd();
|
|
14318
|
+
const lastBreak = Math.max(truncated.lastIndexOf(`
|
|
14319
|
+
`), truncated.lastIndexOf(" "));
|
|
14320
|
+
const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
|
|
14321
|
+
return `${safe.trimEnd()}…`;
|
|
14322
|
+
}
|
|
14323
|
+
function normalizeSummarySection(section) {
|
|
14324
|
+
const items = extractSummaryItems(section);
|
|
14325
|
+
if (items.length === 0) {
|
|
14326
|
+
const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
|
|
14327
|
+
return cleaned || null;
|
|
14328
|
+
}
|
|
14329
|
+
return items.map((item) => `- ${item}`).join(`
|
|
14330
|
+
`);
|
|
14331
|
+
}
|
|
14332
|
+
function normalizeSummaryRequest(value) {
|
|
14333
|
+
const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
14334
|
+
return cleaned || null;
|
|
14335
|
+
}
|
|
14336
|
+
function stripSectionPrefix(value) {
|
|
14337
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
|
|
14338
|
+
}
|
|
14339
|
+
function parseHeading(value) {
|
|
14340
|
+
const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
|
|
14341
|
+
if (boldMatch?.[1]) {
|
|
14342
|
+
return boldMatch[1].trim().replace(/\s+/g, " ");
|
|
14343
|
+
}
|
|
14344
|
+
const plainMatch = value.match(/^(.+?):$/);
|
|
14345
|
+
if (plainMatch?.[1]) {
|
|
14346
|
+
return plainMatch[1].trim().replace(/\s+/g, " ");
|
|
14347
|
+
}
|
|
14348
|
+
return null;
|
|
14349
|
+
}
|
|
14350
|
+
function normalizeItem(value) {
|
|
14351
|
+
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
14352
|
+
}
|
|
14353
|
+
|
|
14354
|
+
// src/storage/sqlite.ts
|
|
14261
14355
|
var IS_BUN = typeof globalThis.Bun !== "undefined";
|
|
14262
14356
|
function openDatabase(dbPath) {
|
|
14263
14357
|
if (IS_BUN) {
|
|
@@ -14294,6 +14388,7 @@ function openNodeDatabase(dbPath) {
|
|
|
14294
14388
|
const BetterSqlite3 = __require("better-sqlite3");
|
|
14295
14389
|
const raw = new BetterSqlite3(dbPath);
|
|
14296
14390
|
return {
|
|
14391
|
+
__raw: raw,
|
|
14297
14392
|
query(sql) {
|
|
14298
14393
|
const stmt = raw.prepare(sql);
|
|
14299
14394
|
return {
|
|
@@ -14331,7 +14426,7 @@ class MemDatabase {
|
|
|
14331
14426
|
loadVecExtension() {
|
|
14332
14427
|
try {
|
|
14333
14428
|
const sqliteVec = __require("sqlite-vec");
|
|
14334
|
-
sqliteVec.load(this.db);
|
|
14429
|
+
sqliteVec.load(this.db.__raw ?? this.db);
|
|
14335
14430
|
return true;
|
|
14336
14431
|
} catch {
|
|
14337
14432
|
return false;
|
|
@@ -14372,8 +14467,9 @@ class MemDatabase {
|
|
|
14372
14467
|
const result = this.db.query(`INSERT INTO observations (
|
|
14373
14468
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
14374
14469
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
14375
|
-
user_id, device_id, agent,
|
|
14376
|
-
|
|
14470
|
+
user_id, device_id, agent, source_tool, source_prompt_number,
|
|
14471
|
+
created_at, created_at_epoch
|
|
14472
|
+
) 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);
|
|
14377
14473
|
const id = Number(result.lastInsertRowid);
|
|
14378
14474
|
const row = this.getObservationById(id);
|
|
14379
14475
|
this.ftsInsert(row);
|
|
@@ -14614,6 +14710,13 @@ class MemDatabase {
|
|
|
14614
14710
|
ORDER BY prompt_number ASC
|
|
14615
14711
|
LIMIT ?`).all(sessionId, limit);
|
|
14616
14712
|
}
|
|
14713
|
+
getLatestSessionPromptNumber(sessionId) {
|
|
14714
|
+
const row = this.db.query(`SELECT prompt_number FROM user_prompts
|
|
14715
|
+
WHERE session_id = ?
|
|
14716
|
+
ORDER BY prompt_number DESC
|
|
14717
|
+
LIMIT 1`).get(sessionId);
|
|
14718
|
+
return row?.prompt_number ?? null;
|
|
14719
|
+
}
|
|
14617
14720
|
insertToolEvent(input) {
|
|
14618
14721
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
14619
14722
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -14723,8 +14826,15 @@ class MemDatabase {
|
|
|
14723
14826
|
}
|
|
14724
14827
|
insertSessionSummary(summary) {
|
|
14725
14828
|
const now = Math.floor(Date.now() / 1000);
|
|
14829
|
+
const normalized = {
|
|
14830
|
+
request: normalizeSummaryRequest(summary.request),
|
|
14831
|
+
investigated: normalizeSummarySection(summary.investigated),
|
|
14832
|
+
learned: normalizeSummarySection(summary.learned),
|
|
14833
|
+
completed: normalizeSummarySection(summary.completed),
|
|
14834
|
+
next_steps: normalizeSummarySection(summary.next_steps)
|
|
14835
|
+
};
|
|
14726
14836
|
const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
|
|
14727
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id,
|
|
14837
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
|
|
14728
14838
|
const id = Number(result.lastInsertRowid);
|
|
14729
14839
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
14730
14840
|
}
|
|
@@ -15624,6 +15734,7 @@ async function saveObservation(db, config2, input) {
|
|
|
15624
15734
|
reason: `Merged into existing observation #${duplicate.id}`
|
|
15625
15735
|
};
|
|
15626
15736
|
}
|
|
15737
|
+
const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
|
|
15627
15738
|
const obs = db.insertObservation({
|
|
15628
15739
|
session_id: input.session_id ?? null,
|
|
15629
15740
|
project_id: project.id,
|
|
@@ -15639,7 +15750,9 @@ async function saveObservation(db, config2, input) {
|
|
|
15639
15750
|
sensitivity,
|
|
15640
15751
|
user_id: config2.user_id,
|
|
15641
15752
|
device_id: config2.device_id,
|
|
15642
|
-
agent: input.agent ?? "claude-code"
|
|
15753
|
+
agent: input.agent ?? "claude-code",
|
|
15754
|
+
source_tool: input.source_tool ?? null,
|
|
15755
|
+
source_prompt_number: sourcePromptNumber
|
|
15643
15756
|
});
|
|
15644
15757
|
db.addToOutbox("observation", obs.id);
|
|
15645
15758
|
if (db.vecAvailable) {
|
|
@@ -16007,8 +16120,11 @@ function getSessionStory(db, input) {
|
|
|
16007
16120
|
const toolEvents = db.getSessionToolEvents(input.session_id, 100);
|
|
16008
16121
|
const observations = db.getObservationsBySession(input.session_id);
|
|
16009
16122
|
const metrics = db.getSessionMetrics(input.session_id);
|
|
16123
|
+
const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
|
|
16124
|
+
const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
|
|
16010
16125
|
return {
|
|
16011
16126
|
session,
|
|
16127
|
+
project_name: projectName,
|
|
16012
16128
|
summary,
|
|
16013
16129
|
prompts,
|
|
16014
16130
|
tool_events: toolEvents,
|
|
@@ -16025,7 +16141,11 @@ function getSessionStory(db, input) {
|
|
|
16025
16141
|
toolCallsCount: metrics?.tool_calls_count ?? 0,
|
|
16026
16142
|
observationCount: observations.length,
|
|
16027
16143
|
hasSummary: Boolean(summary?.request || summary?.completed)
|
|
16028
|
-
})
|
|
16144
|
+
}),
|
|
16145
|
+
latest_request: latestRequest,
|
|
16146
|
+
recent_outcomes: collectRecentOutcomes(observations),
|
|
16147
|
+
hot_files: collectHotFiles(observations),
|
|
16148
|
+
provenance_summary: collectProvenanceSummary(observations)
|
|
16029
16149
|
};
|
|
16030
16150
|
}
|
|
16031
16151
|
function classifyCaptureState(input) {
|
|
@@ -16051,6 +16171,56 @@ function buildCaptureGaps(input) {
|
|
|
16051
16171
|
}
|
|
16052
16172
|
return gaps;
|
|
16053
16173
|
}
|
|
16174
|
+
function collectRecentOutcomes(observations) {
|
|
16175
|
+
const seen = new Set;
|
|
16176
|
+
const outcomes = [];
|
|
16177
|
+
for (const obs of observations) {
|
|
16178
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
16179
|
+
continue;
|
|
16180
|
+
const title = obs.title.trim();
|
|
16181
|
+
if (!title || looksLikeFileOperationTitle(title))
|
|
16182
|
+
continue;
|
|
16183
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
16184
|
+
if (seen.has(normalized))
|
|
16185
|
+
continue;
|
|
16186
|
+
seen.add(normalized);
|
|
16187
|
+
outcomes.push(title);
|
|
16188
|
+
if (outcomes.length >= 6)
|
|
16189
|
+
break;
|
|
16190
|
+
}
|
|
16191
|
+
return outcomes;
|
|
16192
|
+
}
|
|
16193
|
+
function collectHotFiles(observations) {
|
|
16194
|
+
const counts = new Map;
|
|
16195
|
+
for (const obs of observations) {
|
|
16196
|
+
for (const path of [...parseJsonArray(obs.files_modified), ...parseJsonArray(obs.files_read)]) {
|
|
16197
|
+
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
16198
|
+
}
|
|
16199
|
+
}
|
|
16200
|
+
return Array.from(counts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || a.path.localeCompare(b.path)).slice(0, 8);
|
|
16201
|
+
}
|
|
16202
|
+
function parseJsonArray(value) {
|
|
16203
|
+
if (!value)
|
|
16204
|
+
return [];
|
|
16205
|
+
try {
|
|
16206
|
+
const parsed = JSON.parse(value);
|
|
16207
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
16208
|
+
} catch {
|
|
16209
|
+
return [];
|
|
16210
|
+
}
|
|
16211
|
+
}
|
|
16212
|
+
function looksLikeFileOperationTitle(value) {
|
|
16213
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
16214
|
+
}
|
|
16215
|
+
function collectProvenanceSummary(observations) {
|
|
16216
|
+
const counts = new Map;
|
|
16217
|
+
for (const obs of observations) {
|
|
16218
|
+
if (!obs.source_tool)
|
|
16219
|
+
continue;
|
|
16220
|
+
counts.set(obs.source_tool, (counts.get(obs.source_tool) ?? 0) + 1);
|
|
16221
|
+
}
|
|
16222
|
+
return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
16223
|
+
}
|
|
16054
16224
|
|
|
16055
16225
|
// src/tools/recent-sessions.ts
|
|
16056
16226
|
function getRecentSessions(db, input) {
|
|
@@ -16088,1292 +16258,1641 @@ function classifyCaptureState2(session) {
|
|
|
16088
16258
|
return "legacy";
|
|
16089
16259
|
}
|
|
16090
16260
|
|
|
16091
|
-
// src/
|
|
16092
|
-
|
|
16093
|
-
|
|
16094
|
-
|
|
16095
|
-
|
|
16096
|
-
|
|
16097
|
-
|
|
16098
|
-
|
|
16099
|
-
|
|
16100
|
-
|
|
16101
|
-
|
|
16102
|
-
|
|
16103
|
-
|
|
16104
|
-
|
|
16105
|
-
|
|
16106
|
-
const
|
|
16107
|
-
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
|
|
16113
|
-
|
|
16261
|
+
// src/intelligence/followthrough.ts
|
|
16262
|
+
var FOLLOW_THROUGH_THRESHOLD = 0.25;
|
|
16263
|
+
var STALE_AFTER_DAYS = 3;
|
|
16264
|
+
var DECISION_WINDOW_DAYS = 30;
|
|
16265
|
+
var IMPLEMENTATION_TYPES = new Set([
|
|
16266
|
+
"feature",
|
|
16267
|
+
"bugfix",
|
|
16268
|
+
"change",
|
|
16269
|
+
"refactor"
|
|
16270
|
+
]);
|
|
16271
|
+
function findStaleDecisions(db, projectId, options) {
|
|
16272
|
+
const staleAfterDays = options?.staleAfterDays ?? STALE_AFTER_DAYS;
|
|
16273
|
+
const windowDays = options?.windowDays ?? DECISION_WINDOW_DAYS;
|
|
16274
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
16275
|
+
const windowStart = nowEpoch - windowDays * 86400;
|
|
16276
|
+
const staleThreshold = nowEpoch - staleAfterDays * 86400;
|
|
16277
|
+
const decisions = db.db.query(`SELECT * FROM observations
|
|
16278
|
+
WHERE project_id = ? AND type = 'decision'
|
|
16279
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16280
|
+
AND superseded_by IS NULL
|
|
16281
|
+
AND created_at_epoch >= ?
|
|
16282
|
+
ORDER BY created_at_epoch DESC`).all(projectId, windowStart);
|
|
16283
|
+
if (decisions.length === 0)
|
|
16284
|
+
return [];
|
|
16285
|
+
const implementations = db.db.query(`SELECT * FROM observations
|
|
16286
|
+
WHERE project_id = ? AND type IN ('feature', 'bugfix', 'change', 'refactor')
|
|
16287
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16288
|
+
AND superseded_by IS NULL
|
|
16289
|
+
AND created_at_epoch >= ?
|
|
16290
|
+
ORDER BY created_at_epoch DESC`).all(projectId, windowStart);
|
|
16291
|
+
const crossProjectImpls = db.db.query(`SELECT * FROM observations
|
|
16292
|
+
WHERE project_id != ? AND type IN ('feature', 'bugfix', 'change', 'refactor')
|
|
16293
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16294
|
+
AND superseded_by IS NULL
|
|
16295
|
+
AND created_at_epoch >= ?
|
|
16296
|
+
ORDER BY created_at_epoch DESC
|
|
16297
|
+
LIMIT 200`).all(projectId, windowStart);
|
|
16298
|
+
const allImpls = [...implementations, ...crossProjectImpls];
|
|
16299
|
+
const stale = [];
|
|
16300
|
+
for (const decision of decisions) {
|
|
16301
|
+
if (decision.created_at_epoch > staleThreshold)
|
|
16302
|
+
continue;
|
|
16303
|
+
const daysAgo = Math.floor((nowEpoch - decision.created_at_epoch) / 86400);
|
|
16304
|
+
let decisionConcepts = [];
|
|
16305
|
+
try {
|
|
16306
|
+
const parsed = decision.concepts ? JSON.parse(decision.concepts) : [];
|
|
16307
|
+
if (Array.isArray(parsed))
|
|
16308
|
+
decisionConcepts = parsed;
|
|
16309
|
+
} catch {}
|
|
16310
|
+
let bestTitle = "";
|
|
16311
|
+
let bestScore = 0;
|
|
16312
|
+
for (const impl of allImpls) {
|
|
16313
|
+
if (impl.created_at_epoch <= decision.created_at_epoch)
|
|
16314
|
+
continue;
|
|
16315
|
+
const titleScore = jaccardSimilarity(decision.title, impl.title);
|
|
16316
|
+
let conceptBoost = 0;
|
|
16317
|
+
if (decisionConcepts.length > 0) {
|
|
16318
|
+
try {
|
|
16319
|
+
const implConcepts = impl.concepts ? JSON.parse(impl.concepts) : [];
|
|
16320
|
+
if (Array.isArray(implConcepts) && implConcepts.length > 0) {
|
|
16321
|
+
const decSet = new Set(decisionConcepts.map((c) => c.toLowerCase()));
|
|
16322
|
+
const overlap = implConcepts.filter((c) => decSet.has(c.toLowerCase())).length;
|
|
16323
|
+
conceptBoost = overlap / Math.max(decisionConcepts.length, 1) * 0.15;
|
|
16324
|
+
}
|
|
16325
|
+
} catch {}
|
|
16326
|
+
}
|
|
16327
|
+
let narrativeBoost = 0;
|
|
16328
|
+
if (impl.narrative) {
|
|
16329
|
+
const decWords = new Set(decision.title.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 3));
|
|
16330
|
+
if (decWords.size > 0) {
|
|
16331
|
+
const implNarrativeLower = impl.narrative.toLowerCase();
|
|
16332
|
+
const hits = [...decWords].filter((w) => implNarrativeLower.includes(w)).length;
|
|
16333
|
+
narrativeBoost = hits / decWords.size * 0.1;
|
|
16334
|
+
}
|
|
16335
|
+
}
|
|
16336
|
+
const totalScore = titleScore + conceptBoost + narrativeBoost;
|
|
16337
|
+
if (totalScore > bestScore) {
|
|
16338
|
+
bestScore = totalScore;
|
|
16339
|
+
bestTitle = impl.title;
|
|
16340
|
+
}
|
|
16341
|
+
}
|
|
16342
|
+
if (bestScore < FOLLOW_THROUGH_THRESHOLD) {
|
|
16343
|
+
stale.push({
|
|
16344
|
+
id: decision.id,
|
|
16345
|
+
title: decision.title,
|
|
16346
|
+
narrative: decision.narrative,
|
|
16347
|
+
concepts: decisionConcepts,
|
|
16348
|
+
created_at: decision.created_at,
|
|
16349
|
+
days_ago: daysAgo,
|
|
16350
|
+
...bestScore > 0.1 ? {
|
|
16351
|
+
best_match_title: bestTitle,
|
|
16352
|
+
best_match_similarity: Math.round(bestScore * 100) / 100
|
|
16353
|
+
} : {}
|
|
16354
|
+
});
|
|
16114
16355
|
}
|
|
16115
16356
|
}
|
|
16116
|
-
|
|
16117
|
-
|
|
16118
|
-
type: obs.type,
|
|
16119
|
-
title: obs.title,
|
|
16120
|
-
id: obs.id
|
|
16121
|
-
}));
|
|
16122
|
-
const recentSessions = getRecentSessions(db, {
|
|
16123
|
-
cwd,
|
|
16124
|
-
project_scoped: true,
|
|
16125
|
-
user_id: input.user_id,
|
|
16126
|
-
limit: 6
|
|
16127
|
-
}).sessions;
|
|
16128
|
-
const recentRequestsCount = getRecentRequests(db, {
|
|
16129
|
-
cwd,
|
|
16130
|
-
project_scoped: true,
|
|
16131
|
-
user_id: input.user_id,
|
|
16132
|
-
limit: 20
|
|
16133
|
-
}).prompts.length;
|
|
16134
|
-
const recentToolsCount = getRecentTools(db, {
|
|
16135
|
-
cwd,
|
|
16136
|
-
project_scoped: true,
|
|
16137
|
-
user_id: input.user_id,
|
|
16138
|
-
limit: 20
|
|
16139
|
-
}).tool_events.length;
|
|
16140
|
-
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0 && !looksLikeFileOperationTitle(title)).slice(0, 8);
|
|
16141
|
-
return {
|
|
16142
|
-
project: project.name,
|
|
16143
|
-
canonical_id: project.canonical_id,
|
|
16144
|
-
observation_counts: counts,
|
|
16145
|
-
recent_sessions: recentSessions,
|
|
16146
|
-
recent_outcomes: recentOutcomes,
|
|
16147
|
-
recent_requests_count: recentRequestsCount,
|
|
16148
|
-
recent_tools_count: recentToolsCount,
|
|
16149
|
-
raw_capture_active: recentRequestsCount > 0 || recentToolsCount > 0,
|
|
16150
|
-
hot_files: hotFiles,
|
|
16151
|
-
top_titles: topTitles
|
|
16152
|
-
};
|
|
16357
|
+
stale.sort((a, b) => b.days_ago - a.days_ago);
|
|
16358
|
+
return stale.slice(0, 5);
|
|
16153
16359
|
}
|
|
16154
|
-
function
|
|
16155
|
-
|
|
16156
|
-
|
|
16157
|
-
|
|
16158
|
-
|
|
16159
|
-
|
|
16160
|
-
|
|
16161
|
-
|
|
16162
|
-
|
|
16360
|
+
function findStaleDecisionsGlobal(db, options) {
|
|
16361
|
+
const staleAfterDays = options?.staleAfterDays ?? STALE_AFTER_DAYS;
|
|
16362
|
+
const windowDays = options?.windowDays ?? DECISION_WINDOW_DAYS;
|
|
16363
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
16364
|
+
const windowStart = nowEpoch - windowDays * 86400;
|
|
16365
|
+
const staleThreshold = nowEpoch - staleAfterDays * 86400;
|
|
16366
|
+
const decisions = db.db.query(`SELECT * FROM observations
|
|
16367
|
+
WHERE type = 'decision'
|
|
16368
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16369
|
+
AND superseded_by IS NULL
|
|
16370
|
+
AND created_at_epoch >= ?
|
|
16371
|
+
ORDER BY created_at_epoch DESC`).all(windowStart);
|
|
16372
|
+
if (decisions.length === 0)
|
|
16163
16373
|
return [];
|
|
16164
|
-
|
|
16165
|
-
|
|
16166
|
-
|
|
16167
|
-
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
const
|
|
16173
|
-
|
|
16174
|
-
|
|
16175
|
-
|
|
16176
|
-
|
|
16177
|
-
|
|
16178
|
-
|
|
16179
|
-
|
|
16180
|
-
|
|
16181
|
-
|
|
16182
|
-
|
|
16183
|
-
|
|
16184
|
-
|
|
16185
|
-
|
|
16186
|
-
|
|
16187
|
-
|
|
16188
|
-
|
|
16189
|
-
|
|
16190
|
-
|
|
16191
|
-
|
|
16192
|
-
|
|
16193
|
-
|
|
16194
|
-
|
|
16195
|
-
|
|
16196
|
-
|
|
16197
|
-
|
|
16198
|
-
|
|
16199
|
-
|
|
16200
|
-
|
|
16201
|
-
|
|
16202
|
-
|
|
16203
|
-
|
|
16204
|
-
return {
|
|
16205
|
-
project: project?.name,
|
|
16206
|
-
capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
|
|
16207
|
-
sessions,
|
|
16208
|
-
requests,
|
|
16209
|
-
tools,
|
|
16210
|
-
observations,
|
|
16211
|
-
recent_outcomes: projectIndex?.recent_outcomes ?? [],
|
|
16212
|
-
hot_files: projectIndex?.hot_files ?? []
|
|
16213
|
-
};
|
|
16214
|
-
}
|
|
16215
|
-
|
|
16216
|
-
// src/tools/workspace-memory-index.ts
|
|
16217
|
-
function getWorkspaceMemoryIndex(db, input) {
|
|
16218
|
-
const limit = Math.max(1, Math.min(input.limit ?? 12, 50));
|
|
16219
|
-
const visibilityClause = input.user_id ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
|
|
16220
|
-
const params = input.user_id ? [input.user_id, limit] : [limit];
|
|
16221
|
-
const projects = db.db.query(`SELECT
|
|
16222
|
-
p.canonical_id,
|
|
16223
|
-
p.name,
|
|
16224
|
-
(
|
|
16225
|
-
SELECT COUNT(*) FROM observations o
|
|
16226
|
-
WHERE o.project_id = p.id
|
|
16227
|
-
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
16228
|
-
AND o.superseded_by IS NULL
|
|
16229
|
-
${visibilityClause}
|
|
16230
|
-
) AS observation_count,
|
|
16231
|
-
(SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,
|
|
16232
|
-
(SELECT COUNT(*) FROM user_prompts up WHERE up.project_id = p.id) AS prompt_count,
|
|
16233
|
-
(SELECT COUNT(*) FROM tool_events te WHERE te.project_id = p.id) AS tool_event_count,
|
|
16234
|
-
p.last_active_epoch
|
|
16235
|
-
FROM projects p
|
|
16236
|
-
ORDER BY p.last_active_epoch DESC
|
|
16237
|
-
LIMIT ?`).all(...params);
|
|
16238
|
-
const totals = projects.reduce((acc, project) => {
|
|
16239
|
-
acc.observations += project.observation_count;
|
|
16240
|
-
acc.sessions += project.session_count;
|
|
16241
|
-
acc.prompts += project.prompt_count;
|
|
16242
|
-
acc.tool_events += project.tool_event_count;
|
|
16243
|
-
return acc;
|
|
16244
|
-
}, { observations: 0, sessions: 0, prompts: 0, tool_events: 0 });
|
|
16245
|
-
return {
|
|
16246
|
-
projects,
|
|
16247
|
-
totals,
|
|
16248
|
-
projects_with_raw_capture: projects.filter((project) => project.prompt_count > 0 || project.tool_event_count > 0).length
|
|
16249
|
-
};
|
|
16250
|
-
}
|
|
16251
|
-
|
|
16252
|
-
// src/tools/project-related-work.ts
|
|
16253
|
-
function getProjectRelatedWork(db, input) {
|
|
16254
|
-
const cwd = input.cwd ?? process.cwd();
|
|
16255
|
-
const detected = detectProject(cwd);
|
|
16256
|
-
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16257
|
-
const visibilityClause = input.user_id ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
|
|
16258
|
-
const visibilityParams = input.user_id ? [input.user_id] : [];
|
|
16259
|
-
const terms = buildProjectTerms(detected);
|
|
16260
|
-
if (terms.length === 0) {
|
|
16261
|
-
return {
|
|
16262
|
-
project: detected.name,
|
|
16263
|
-
canonical_id: detected.canonical_id,
|
|
16264
|
-
related: []
|
|
16265
|
-
};
|
|
16266
|
-
}
|
|
16267
|
-
const localProject = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16268
|
-
const localProjectId = localProject?.id ?? -1;
|
|
16269
|
-
const params = [];
|
|
16270
|
-
const whereParts = [];
|
|
16271
|
-
for (const term of terms) {
|
|
16272
|
-
const like = `%${term}%`;
|
|
16273
|
-
whereParts.push(`(LOWER(o.title) LIKE ? OR LOWER(COALESCE(o.narrative, '')) LIKE ? OR LOWER(COALESCE(o.files_read, '')) LIKE ? OR LOWER(COALESCE(o.files_modified, '')) LIKE ?)`);
|
|
16274
|
-
params.push(like, like, like, like);
|
|
16275
|
-
}
|
|
16276
|
-
const rows = db.db.query(`SELECT
|
|
16277
|
-
o.id,
|
|
16278
|
-
o.type,
|
|
16279
|
-
o.title,
|
|
16280
|
-
o.project_id as source_project_id,
|
|
16281
|
-
p.name as source_project,
|
|
16282
|
-
o.narrative,
|
|
16283
|
-
o.files_read,
|
|
16284
|
-
o.files_modified
|
|
16285
|
-
FROM observations o
|
|
16286
|
-
LEFT JOIN projects p ON p.id = o.project_id
|
|
16287
|
-
WHERE o.project_id != ?
|
|
16288
|
-
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
16289
|
-
AND o.superseded_by IS NULL
|
|
16290
|
-
${visibilityClause}
|
|
16291
|
-
AND (${whereParts.join(" OR ")})
|
|
16292
|
-
ORDER BY o.created_at_epoch DESC
|
|
16293
|
-
LIMIT ?`).all(localProjectId, ...visibilityParams, ...params, limit);
|
|
16294
|
-
return {
|
|
16295
|
-
project: detected.name,
|
|
16296
|
-
canonical_id: detected.canonical_id,
|
|
16297
|
-
related: rows.map((row) => ({
|
|
16298
|
-
id: row.id,
|
|
16299
|
-
type: row.type,
|
|
16300
|
-
title: row.title,
|
|
16301
|
-
source_project: row.source_project ?? "unknown",
|
|
16302
|
-
source_project_id: row.source_project_id,
|
|
16303
|
-
matched_on: classifyMatch(row, terms)
|
|
16304
|
-
}))
|
|
16305
|
-
};
|
|
16306
|
-
}
|
|
16307
|
-
function buildProjectTerms(detected) {
|
|
16308
|
-
const explicit = new Set;
|
|
16309
|
-
explicit.add(detected.name.toLowerCase());
|
|
16310
|
-
const canonicalParts = detected.canonical_id.toLowerCase().split("/");
|
|
16311
|
-
for (const part of canonicalParts) {
|
|
16312
|
-
if (part.length >= 4)
|
|
16313
|
-
explicit.add(part);
|
|
16314
|
-
}
|
|
16315
|
-
if (detected.name.toLowerCase() === "huginn") {
|
|
16316
|
-
explicit.add("aiserver");
|
|
16317
|
-
}
|
|
16318
|
-
return [...explicit].filter(Boolean);
|
|
16319
|
-
}
|
|
16320
|
-
function classifyMatch(row, terms) {
|
|
16321
|
-
const title = row.title.toLowerCase();
|
|
16322
|
-
const narrative = (row.narrative ?? "").toLowerCase();
|
|
16323
|
-
const files = `${row.files_read ?? ""} ${row.files_modified ?? ""}`.toLowerCase();
|
|
16324
|
-
for (const term of terms) {
|
|
16325
|
-
if (files.includes(term))
|
|
16326
|
-
return `files:${term}`;
|
|
16327
|
-
if (title.includes(term))
|
|
16328
|
-
return `title:${term}`;
|
|
16329
|
-
if (narrative.includes(term))
|
|
16330
|
-
return `narrative:${term}`;
|
|
16331
|
-
}
|
|
16332
|
-
return "related";
|
|
16333
|
-
}
|
|
16334
|
-
|
|
16335
|
-
// src/tools/reclassify-project-memory.ts
|
|
16336
|
-
function reclassifyProjectMemory(db, input) {
|
|
16337
|
-
const cwd = input.cwd ?? process.cwd();
|
|
16338
|
-
const detected = detectProject(cwd);
|
|
16339
|
-
const target = db.upsertProject({
|
|
16340
|
-
canonical_id: detected.canonical_id,
|
|
16341
|
-
name: detected.name,
|
|
16342
|
-
local_path: detected.local_path,
|
|
16343
|
-
remote_url: detected.remote_url
|
|
16344
|
-
});
|
|
16345
|
-
const related = getProjectRelatedWork(db, {
|
|
16346
|
-
cwd,
|
|
16347
|
-
user_id: input.user_id,
|
|
16348
|
-
limit: input.limit ?? 50
|
|
16349
|
-
}).related;
|
|
16350
|
-
let moved = 0;
|
|
16351
|
-
const candidates = related.map((item) => {
|
|
16352
|
-
const eligible = item.matched_on.startsWith("files:") || item.matched_on.startsWith("title:") || item.matched_on.startsWith("narrative:");
|
|
16353
|
-
const shouldMove = eligible && item.source_project_id !== target.id;
|
|
16354
|
-
if (shouldMove && input.dry_run !== true) {
|
|
16355
|
-
const ok = db.reassignObservationProject(item.id, target.id);
|
|
16356
|
-
if (ok)
|
|
16357
|
-
moved += 1;
|
|
16358
|
-
return {
|
|
16359
|
-
id: item.id,
|
|
16360
|
-
title: item.title,
|
|
16361
|
-
type: item.type,
|
|
16362
|
-
from: item.source_project,
|
|
16363
|
-
matched_on: item.matched_on,
|
|
16364
|
-
moved: ok
|
|
16365
|
-
};
|
|
16374
|
+
const implementations = db.db.query(`SELECT * FROM observations
|
|
16375
|
+
WHERE type IN ('feature', 'bugfix', 'change', 'refactor')
|
|
16376
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16377
|
+
AND superseded_by IS NULL
|
|
16378
|
+
AND created_at_epoch >= ?
|
|
16379
|
+
ORDER BY created_at_epoch DESC
|
|
16380
|
+
LIMIT 500`).all(windowStart);
|
|
16381
|
+
const stale = [];
|
|
16382
|
+
for (const decision of decisions) {
|
|
16383
|
+
if (decision.created_at_epoch > staleThreshold)
|
|
16384
|
+
continue;
|
|
16385
|
+
const daysAgo = Math.floor((nowEpoch - decision.created_at_epoch) / 86400);
|
|
16386
|
+
let decisionConcepts = [];
|
|
16387
|
+
try {
|
|
16388
|
+
const parsed = decision.concepts ? JSON.parse(decision.concepts) : [];
|
|
16389
|
+
if (Array.isArray(parsed))
|
|
16390
|
+
decisionConcepts = parsed;
|
|
16391
|
+
} catch {}
|
|
16392
|
+
let bestScore = 0;
|
|
16393
|
+
let bestTitle = "";
|
|
16394
|
+
for (const impl of implementations) {
|
|
16395
|
+
if (impl.created_at_epoch <= decision.created_at_epoch)
|
|
16396
|
+
continue;
|
|
16397
|
+
const titleScore = jaccardSimilarity(decision.title, impl.title);
|
|
16398
|
+
let conceptBoost = 0;
|
|
16399
|
+
if (decisionConcepts.length > 0) {
|
|
16400
|
+
try {
|
|
16401
|
+
const implConcepts = impl.concepts ? JSON.parse(impl.concepts) : [];
|
|
16402
|
+
if (Array.isArray(implConcepts) && implConcepts.length > 0) {
|
|
16403
|
+
const decSet = new Set(decisionConcepts.map((c) => c.toLowerCase()));
|
|
16404
|
+
const overlap = implConcepts.filter((c) => decSet.has(c.toLowerCase())).length;
|
|
16405
|
+
conceptBoost = overlap / Math.max(decisionConcepts.length, 1) * 0.15;
|
|
16406
|
+
}
|
|
16407
|
+
} catch {}
|
|
16408
|
+
}
|
|
16409
|
+
const totalScore = titleScore + conceptBoost;
|
|
16410
|
+
if (totalScore > bestScore) {
|
|
16411
|
+
bestScore = totalScore;
|
|
16412
|
+
bestTitle = impl.title;
|
|
16413
|
+
}
|
|
16366
16414
|
}
|
|
16367
|
-
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
16372
|
-
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16377
|
-
|
|
16378
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16415
|
+
if (bestScore < FOLLOW_THROUGH_THRESHOLD) {
|
|
16416
|
+
stale.push({
|
|
16417
|
+
id: decision.id,
|
|
16418
|
+
title: decision.title,
|
|
16419
|
+
narrative: decision.narrative,
|
|
16420
|
+
concepts: decisionConcepts,
|
|
16421
|
+
created_at: decision.created_at,
|
|
16422
|
+
days_ago: daysAgo,
|
|
16423
|
+
...bestScore > 0.1 ? {
|
|
16424
|
+
best_match_title: bestTitle,
|
|
16425
|
+
best_match_similarity: Math.round(bestScore * 100) / 100
|
|
16426
|
+
} : {}
|
|
16427
|
+
});
|
|
16428
|
+
}
|
|
16429
|
+
}
|
|
16430
|
+
stale.sort((a, b) => b.days_ago - a.days_ago);
|
|
16431
|
+
return stale.slice(0, 5);
|
|
16383
16432
|
}
|
|
16384
16433
|
|
|
16385
|
-
// src/
|
|
16386
|
-
function
|
|
16387
|
-
return {
|
|
16388
|
-
kind: "prompt",
|
|
16389
|
-
created_at_epoch: prompt.created_at_epoch,
|
|
16390
|
-
session_id: prompt.session_id,
|
|
16391
|
-
id: prompt.id,
|
|
16392
|
-
title: `#${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`
|
|
16393
|
-
};
|
|
16394
|
-
}
|
|
16395
|
-
function toToolEvent(tool) {
|
|
16396
|
-
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? undefined;
|
|
16397
|
-
return {
|
|
16398
|
-
kind: "tool",
|
|
16399
|
-
created_at_epoch: tool.created_at_epoch,
|
|
16400
|
-
session_id: tool.session_id,
|
|
16401
|
-
id: tool.id,
|
|
16402
|
-
title: tool.tool_name,
|
|
16403
|
-
detail: detail?.replace(/\s+/g, " ").trim()
|
|
16404
|
-
};
|
|
16434
|
+
// src/context/inject.ts
|
|
16435
|
+
function tokenizeProjectHint(text) {
|
|
16436
|
+
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
16405
16437
|
}
|
|
16406
|
-
function
|
|
16407
|
-
|
|
16408
|
-
|
|
16409
|
-
|
|
16410
|
-
|
|
16411
|
-
|
|
16412
|
-
|
|
16413
|
-
|
|
16414
|
-
|
|
16438
|
+
function isObservationRelatedToProject(obs, detected) {
|
|
16439
|
+
const hints = new Set([
|
|
16440
|
+
...tokenizeProjectHint(detected.name),
|
|
16441
|
+
...tokenizeProjectHint(detected.canonical_id)
|
|
16442
|
+
]);
|
|
16443
|
+
if (hints.size === 0)
|
|
16444
|
+
return false;
|
|
16445
|
+
const haystack = [
|
|
16446
|
+
obs.title,
|
|
16447
|
+
obs.narrative ?? "",
|
|
16448
|
+
obs.facts ?? "",
|
|
16449
|
+
obs.concepts ?? "",
|
|
16450
|
+
obs.files_read ?? "",
|
|
16451
|
+
obs.files_modified ?? "",
|
|
16452
|
+
obs._source_project ?? ""
|
|
16453
|
+
].join(`
|
|
16454
|
+
`).toLowerCase();
|
|
16455
|
+
for (const hint of hints) {
|
|
16456
|
+
if (haystack.includes(hint))
|
|
16457
|
+
return true;
|
|
16458
|
+
}
|
|
16459
|
+
return false;
|
|
16415
16460
|
}
|
|
16416
|
-
function
|
|
16417
|
-
|
|
16418
|
-
|
|
16419
|
-
|
|
16420
|
-
const detail = [
|
|
16421
|
-
summary.completed && summary.completed !== title ? `Completed: ${summary.completed}` : null,
|
|
16422
|
-
summary.learned ? `Learned: ${summary.learned}` : null,
|
|
16423
|
-
summary.next_steps ? `Next: ${summary.next_steps}` : null
|
|
16424
|
-
].filter(Boolean).join(" | ");
|
|
16425
|
-
return {
|
|
16426
|
-
kind: "summary",
|
|
16427
|
-
created_at_epoch: summary.created_at_epoch ?? fallbackEpoch,
|
|
16428
|
-
session_id: summary.session_id,
|
|
16429
|
-
id: summary.id,
|
|
16430
|
-
title: title.replace(/\s+/g, " ").trim(),
|
|
16431
|
-
detail: detail || undefined
|
|
16432
|
-
};
|
|
16461
|
+
function estimateTokens(text) {
|
|
16462
|
+
if (!text)
|
|
16463
|
+
return 0;
|
|
16464
|
+
return Math.ceil(text.length / 4);
|
|
16433
16465
|
}
|
|
16434
|
-
function
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
const
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16466
|
+
function buildSessionContext(db, cwd, options = {}) {
|
|
16467
|
+
const opts = typeof options === "number" ? { maxCount: options } : options;
|
|
16468
|
+
const tokenBudget = opts.tokenBudget ?? 3000;
|
|
16469
|
+
const maxCount = opts.maxCount;
|
|
16470
|
+
const visibilityClause = opts.userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
16471
|
+
const visibilityParams = opts.userId ? [opts.userId] : [];
|
|
16472
|
+
const detected = detectProject(cwd);
|
|
16473
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16474
|
+
const projectId = project?.id ?? -1;
|
|
16475
|
+
const isNewProject = !project;
|
|
16476
|
+
const totalActive = isNewProject ? (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
16477
|
+
WHERE lifecycle IN ('active', 'aging', 'pinned')
|
|
16478
|
+
${visibilityClause}
|
|
16479
|
+
AND superseded_by IS NULL`).get(...visibilityParams) ?? { c: 0 }).c : (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
16480
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16481
|
+
${visibilityClause}
|
|
16482
|
+
AND superseded_by IS NULL`).get(projectId, ...visibilityParams) ?? { c: 0 }).c;
|
|
16483
|
+
const candidateLimit = maxCount ?? 50;
|
|
16484
|
+
let pinned = [];
|
|
16485
|
+
let recent = [];
|
|
16486
|
+
let candidates = [];
|
|
16487
|
+
if (!isNewProject) {
|
|
16488
|
+
const MAX_PINNED = 5;
|
|
16489
|
+
pinned = db.db.query(`SELECT * FROM observations
|
|
16490
|
+
WHERE project_id = ? AND lifecycle = 'pinned'
|
|
16491
|
+
AND superseded_by IS NULL
|
|
16492
|
+
${visibilityClause}
|
|
16493
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
16494
|
+
LIMIT ?`).all(projectId, ...visibilityParams, MAX_PINNED);
|
|
16495
|
+
const MAX_RECENT = 5;
|
|
16496
|
+
recent = db.db.query(`SELECT * FROM observations
|
|
16497
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
16498
|
+
AND superseded_by IS NULL
|
|
16499
|
+
${visibilityClause}
|
|
16500
|
+
ORDER BY created_at_epoch DESC
|
|
16501
|
+
LIMIT ?`).all(projectId, ...visibilityParams, MAX_RECENT);
|
|
16502
|
+
candidates = db.db.query(`SELECT * FROM observations
|
|
16503
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
16504
|
+
AND quality >= 0.3
|
|
16505
|
+
AND superseded_by IS NULL
|
|
16506
|
+
${visibilityClause}
|
|
16507
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
16508
|
+
LIMIT ?`).all(projectId, ...visibilityParams, candidateLimit);
|
|
16446
16509
|
}
|
|
16447
|
-
|
|
16448
|
-
|
|
16449
|
-
|
|
16450
|
-
|
|
16451
|
-
|
|
16452
|
-
|
|
16453
|
-
|
|
16454
|
-
|
|
16455
|
-
|
|
16456
|
-
|
|
16457
|
-
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16510
|
+
let crossProjectCandidates = [];
|
|
16511
|
+
if (opts.scope === "all" || isNewProject) {
|
|
16512
|
+
const crossLimit = isNewProject ? Math.max(30, candidateLimit) : Math.max(10, Math.floor(candidateLimit / 3));
|
|
16513
|
+
const qualityThreshold = isNewProject ? 0.3 : 0.5;
|
|
16514
|
+
const rawCross = isNewProject ? db.db.query(`SELECT * FROM observations
|
|
16515
|
+
WHERE lifecycle IN ('active', 'aging', 'pinned')
|
|
16516
|
+
AND quality >= ?
|
|
16517
|
+
AND superseded_by IS NULL
|
|
16518
|
+
${visibilityClause}
|
|
16519
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
16520
|
+
LIMIT ?`).all(qualityThreshold, ...visibilityParams, crossLimit) : db.db.query(`SELECT * FROM observations
|
|
16521
|
+
WHERE project_id != ? AND lifecycle IN ('active', 'aging')
|
|
16522
|
+
AND quality >= ?
|
|
16523
|
+
AND superseded_by IS NULL
|
|
16524
|
+
${visibilityClause}
|
|
16525
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
16526
|
+
LIMIT ?`).all(projectId, qualityThreshold, ...visibilityParams, crossLimit);
|
|
16527
|
+
const projectNameCache = new Map;
|
|
16528
|
+
crossProjectCandidates = rawCross.map((obs) => {
|
|
16529
|
+
if (!projectNameCache.has(obs.project_id)) {
|
|
16530
|
+
const proj = db.getProjectById(obs.project_id);
|
|
16531
|
+
if (proj)
|
|
16532
|
+
projectNameCache.set(obs.project_id, proj.name);
|
|
16533
|
+
}
|
|
16534
|
+
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
16535
|
+
});
|
|
16536
|
+
if (isNewProject) {
|
|
16537
|
+
crossProjectCandidates = crossProjectCandidates.filter((obs) => isObservationRelatedToProject(obs, detected));
|
|
16538
|
+
}
|
|
16461
16539
|
}
|
|
16462
|
-
const
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16540
|
+
const seenIds = new Set(pinned.map((o) => o.id));
|
|
16541
|
+
const dedupedRecent = recent.filter((o) => {
|
|
16542
|
+
if (seenIds.has(o.id))
|
|
16543
|
+
return false;
|
|
16544
|
+
seenIds.add(o.id);
|
|
16545
|
+
return true;
|
|
16546
|
+
});
|
|
16547
|
+
const deduped = candidates.filter((o) => !seenIds.has(o.id));
|
|
16548
|
+
for (const obs of crossProjectCandidates) {
|
|
16549
|
+
if (!seenIds.has(obs.id)) {
|
|
16550
|
+
seenIds.add(obs.id);
|
|
16551
|
+
deduped.push(obs);
|
|
16470
16552
|
}
|
|
16471
16553
|
}
|
|
16472
|
-
const
|
|
16473
|
-
const
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
const
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
|
|
16498
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
|
|
16502
|
-
|
|
16503
|
-
|
|
16504
|
-
|
|
16505
|
-
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
|
|
16509
|
-
|
|
16510
|
-
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
const
|
|
16514
|
-
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
|
|
16520
|
-
const
|
|
16521
|
-
const
|
|
16522
|
-
const
|
|
16523
|
-
const
|
|
16524
|
-
const
|
|
16525
|
-
|
|
16526
|
-
let
|
|
16527
|
-
|
|
16528
|
-
let claudePostToolHook = false;
|
|
16529
|
-
let claudeStopHook = false;
|
|
16530
|
-
if (claudeHooksRegistered) {
|
|
16554
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
16555
|
+
const sorted = [...deduped].sort((a, b) => {
|
|
16556
|
+
const scoreA = computeObservationPriority(a, nowEpoch);
|
|
16557
|
+
const scoreB = computeObservationPriority(b, nowEpoch);
|
|
16558
|
+
return scoreB - scoreA;
|
|
16559
|
+
});
|
|
16560
|
+
const projectName = project?.name ?? detected.name;
|
|
16561
|
+
const canonicalId = project?.canonical_id ?? detected.canonical_id;
|
|
16562
|
+
if (maxCount !== undefined) {
|
|
16563
|
+
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
16564
|
+
const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
16565
|
+
const recentPrompts2 = db.getRecentUserPrompts(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16566
|
+
const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16567
|
+
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16568
|
+
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16569
|
+
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
16570
|
+
return {
|
|
16571
|
+
project_name: projectName,
|
|
16572
|
+
canonical_id: canonicalId,
|
|
16573
|
+
observations: all.map(toContextObservation),
|
|
16574
|
+
session_count: all.length,
|
|
16575
|
+
total_active: totalActive,
|
|
16576
|
+
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
16577
|
+
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
16578
|
+
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
16579
|
+
projectTypeCounts: projectTypeCounts2,
|
|
16580
|
+
recentOutcomes: recentOutcomes2
|
|
16581
|
+
};
|
|
16582
|
+
}
|
|
16583
|
+
let remainingBudget = tokenBudget - 30;
|
|
16584
|
+
const selected = [];
|
|
16585
|
+
for (const obs of pinned) {
|
|
16586
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
16587
|
+
remainingBudget -= cost;
|
|
16588
|
+
selected.push(obs);
|
|
16589
|
+
}
|
|
16590
|
+
for (const obs of dedupedRecent) {
|
|
16591
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
16592
|
+
remainingBudget -= cost;
|
|
16593
|
+
selected.push(obs);
|
|
16594
|
+
}
|
|
16595
|
+
for (const obs of sorted) {
|
|
16596
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
16597
|
+
if (remainingBudget - cost < 0 && selected.length > 0)
|
|
16598
|
+
break;
|
|
16599
|
+
remainingBudget -= cost;
|
|
16600
|
+
selected.push(obs);
|
|
16601
|
+
}
|
|
16602
|
+
const summaries = isNewProject ? [] : db.getRecentSummaries(projectId, 5);
|
|
16603
|
+
const recentPrompts = db.getRecentUserPrompts(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16604
|
+
const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16605
|
+
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16606
|
+
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16607
|
+
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
16608
|
+
let securityFindings = [];
|
|
16609
|
+
if (!isNewProject) {
|
|
16531
16610
|
try {
|
|
16532
|
-
const
|
|
16533
|
-
|
|
16534
|
-
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
|
|
16611
|
+
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
16612
|
+
securityFindings = db.db.query(`SELECT * FROM security_findings
|
|
16613
|
+
WHERE project_id = ? AND created_at_epoch > ?
|
|
16614
|
+
ORDER BY severity DESC, created_at_epoch DESC
|
|
16615
|
+
LIMIT ?`).all(projectId, weekAgo, 10);
|
|
16616
|
+
} catch {}
|
|
16617
|
+
}
|
|
16618
|
+
let recentProjects;
|
|
16619
|
+
if (isNewProject) {
|
|
16620
|
+
try {
|
|
16621
|
+
const nowEpochSec = Math.floor(Date.now() / 1000);
|
|
16622
|
+
const projectRows = db.db.query(`SELECT p.name, p.canonical_id, p.last_active_epoch,
|
|
16623
|
+
(SELECT COUNT(*) FROM observations o
|
|
16624
|
+
WHERE o.project_id = p.id
|
|
16625
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
16626
|
+
${opts.userId ? "AND (o.sensitivity != 'personal' OR o.user_id = ?)" : ""}
|
|
16627
|
+
AND o.superseded_by IS NULL) as obs_count
|
|
16628
|
+
FROM projects p
|
|
16629
|
+
ORDER BY p.last_active_epoch DESC
|
|
16630
|
+
LIMIT 10`).all(...visibilityParams);
|
|
16631
|
+
if (projectRows.length > 0) {
|
|
16632
|
+
recentProjects = projectRows.map((r) => {
|
|
16633
|
+
const daysAgo = Math.max(0, Math.floor((nowEpochSec - r.last_active_epoch) / 86400));
|
|
16634
|
+
const lastActive = new Date(r.last_active_epoch * 1000).toISOString().split("T")[0];
|
|
16635
|
+
return {
|
|
16636
|
+
name: r.name,
|
|
16637
|
+
canonical_id: r.canonical_id,
|
|
16638
|
+
observation_count: r.obs_count,
|
|
16639
|
+
last_active: lastActive,
|
|
16640
|
+
days_ago: daysAgo
|
|
16641
|
+
};
|
|
16642
|
+
});
|
|
16547
16643
|
}
|
|
16548
16644
|
} catch {}
|
|
16549
16645
|
}
|
|
16550
|
-
let
|
|
16551
|
-
let codexStopHook = false;
|
|
16646
|
+
let staleDecisions;
|
|
16552
16647
|
try {
|
|
16553
|
-
const
|
|
16554
|
-
|
|
16555
|
-
|
|
16648
|
+
const stale = isNewProject ? findStaleDecisionsGlobal(db) : findStaleDecisions(db, projectId);
|
|
16649
|
+
if (stale.length > 0)
|
|
16650
|
+
staleDecisions = stale;
|
|
16556
16651
|
} catch {}
|
|
16557
|
-
const visibilityClause = input.user_id ? " AND user_id = ?" : "";
|
|
16558
|
-
const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
|
|
16559
|
-
const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
|
|
16560
|
-
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
16561
|
-
const recentToolEvents = db.db.query(`SELECT COUNT(*) as count FROM tool_events
|
|
16562
|
-
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
16563
|
-
const recentSessionsWithRawCapture = db.db.query(`SELECT COUNT(*) as count
|
|
16564
|
-
FROM sessions s
|
|
16565
|
-
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
16566
|
-
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
16567
|
-
AND (
|
|
16568
|
-
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
16569
|
-
OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
16570
|
-
)`).get(...params)?.count ?? 0;
|
|
16571
|
-
const recentSessionsWithPartialCapture = db.db.query(`SELECT COUNT(*) as count
|
|
16572
|
-
FROM sessions s
|
|
16573
|
-
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
16574
|
-
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
16575
|
-
AND (
|
|
16576
|
-
(s.tool_calls_count > 0 AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id))
|
|
16577
|
-
OR (
|
|
16578
|
-
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
16579
|
-
AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
16580
|
-
)
|
|
16581
|
-
)`).get(...params)?.count ?? 0;
|
|
16582
|
-
const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
|
|
16583
|
-
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
16584
|
-
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
16585
|
-
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
16586
|
-
const latestToolEventEpoch = db.db.query(`SELECT created_at_epoch FROM tool_events
|
|
16587
|
-
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
16588
|
-
ORDER BY created_at_epoch DESC, id DESC
|
|
16589
|
-
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
16590
|
-
const latestPostToolHookEpoch = parseNullableInt(db.getSyncState("hook_post_tool_last_seen_epoch"));
|
|
16591
|
-
const latestPostToolParseStatus = db.getSyncState("hook_post_tool_last_parse_status");
|
|
16592
|
-
const latestPostToolName = db.getSyncState("hook_post_tool_last_tool_name");
|
|
16593
|
-
const schemaVersion = getSchemaVersion(db.db);
|
|
16594
16652
|
return {
|
|
16595
|
-
|
|
16596
|
-
|
|
16597
|
-
|
|
16598
|
-
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
recent_user_prompts: recentUserPrompts,
|
|
16610
|
-
recent_tool_events: recentToolEvents,
|
|
16611
|
-
recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
|
|
16612
|
-
recent_sessions_with_partial_capture: recentSessionsWithPartialCapture,
|
|
16613
|
-
latest_prompt_epoch: latestPromptEpoch,
|
|
16614
|
-
latest_tool_event_epoch: latestToolEventEpoch,
|
|
16615
|
-
latest_post_tool_hook_epoch: latestPostToolHookEpoch,
|
|
16616
|
-
latest_post_tool_parse_status: latestPostToolParseStatus,
|
|
16617
|
-
latest_post_tool_name: latestPostToolName,
|
|
16618
|
-
raw_capture_active: recentUserPrompts > 0 || recentToolEvents > 0 || recentSessionsWithRawCapture > 0
|
|
16653
|
+
project_name: projectName,
|
|
16654
|
+
canonical_id: canonicalId,
|
|
16655
|
+
observations: selected.map(toContextObservation),
|
|
16656
|
+
session_count: selected.length,
|
|
16657
|
+
total_active: totalActive,
|
|
16658
|
+
summaries: summaries.length > 0 ? summaries : undefined,
|
|
16659
|
+
securityFindings: securityFindings.length > 0 ? securityFindings : undefined,
|
|
16660
|
+
recentProjects,
|
|
16661
|
+
staleDecisions,
|
|
16662
|
+
recentPrompts: recentPrompts.length > 0 ? recentPrompts : undefined,
|
|
16663
|
+
recentToolEvents: recentToolEvents.length > 0 ? recentToolEvents : undefined,
|
|
16664
|
+
recentSessions: recentSessions.length > 0 ? recentSessions : undefined,
|
|
16665
|
+
projectTypeCounts,
|
|
16666
|
+
recentOutcomes
|
|
16619
16667
|
};
|
|
16620
16668
|
}
|
|
16621
|
-
function
|
|
16622
|
-
|
|
16623
|
-
|
|
16624
|
-
|
|
16625
|
-
|
|
16626
|
-
}
|
|
16627
|
-
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
|
|
16632
|
-
|
|
16633
|
-
|
|
16634
|
-
|
|
16635
|
-
|
|
16636
|
-
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
|
|
16640
|
-
|
|
16641
|
-
|
|
16642
|
-
|
|
16643
|
-
|
|
16644
|
-
|
|
16645
|
-
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16650
|
-
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
|
|
16656
|
-
|
|
16657
|
-
|
|
16658
|
-
|
|
16659
|
-
|
|
16660
|
-
|
|
16661
|
-
AND superseded_by IS NULL
|
|
16662
|
-
AND created_at_epoch >= ?
|
|
16663
|
-
ORDER BY created_at_epoch DESC
|
|
16664
|
-
LIMIT 200`).all(projectId, windowStart);
|
|
16665
|
-
const allImpls = [...implementations, ...crossProjectImpls];
|
|
16666
|
-
const stale = [];
|
|
16667
|
-
for (const decision of decisions) {
|
|
16668
|
-
if (decision.created_at_epoch > staleThreshold)
|
|
16669
|
-
continue;
|
|
16670
|
-
const daysAgo = Math.floor((nowEpoch - decision.created_at_epoch) / 86400);
|
|
16671
|
-
let decisionConcepts = [];
|
|
16672
|
-
try {
|
|
16673
|
-
const parsed = decision.concepts ? JSON.parse(decision.concepts) : [];
|
|
16674
|
-
if (Array.isArray(parsed))
|
|
16675
|
-
decisionConcepts = parsed;
|
|
16676
|
-
} catch {}
|
|
16677
|
-
let bestTitle = "";
|
|
16678
|
-
let bestScore = 0;
|
|
16679
|
-
for (const impl of allImpls) {
|
|
16680
|
-
if (impl.created_at_epoch <= decision.created_at_epoch)
|
|
16681
|
-
continue;
|
|
16682
|
-
const titleScore = jaccardSimilarity(decision.title, impl.title);
|
|
16683
|
-
let conceptBoost = 0;
|
|
16684
|
-
if (decisionConcepts.length > 0) {
|
|
16685
|
-
try {
|
|
16686
|
-
const implConcepts = impl.concepts ? JSON.parse(impl.concepts) : [];
|
|
16687
|
-
if (Array.isArray(implConcepts) && implConcepts.length > 0) {
|
|
16688
|
-
const decSet = new Set(decisionConcepts.map((c) => c.toLowerCase()));
|
|
16689
|
-
const overlap = implConcepts.filter((c) => decSet.has(c.toLowerCase())).length;
|
|
16690
|
-
conceptBoost = overlap / Math.max(decisionConcepts.length, 1) * 0.15;
|
|
16691
|
-
}
|
|
16692
|
-
} catch {}
|
|
16669
|
+
function estimateObservationTokens(obs, index) {
|
|
16670
|
+
const DETAILED_THRESHOLD = 5;
|
|
16671
|
+
const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|
|
16672
|
+
if (index >= DETAILED_THRESHOLD) {
|
|
16673
|
+
return titleCost;
|
|
16674
|
+
}
|
|
16675
|
+
const detailText = formatObservationDetail(obs);
|
|
16676
|
+
return titleCost + estimateTokens(detailText);
|
|
16677
|
+
}
|
|
16678
|
+
function formatContextForInjection(context) {
|
|
16679
|
+
if (context.observations.length === 0 && (!context.recentPrompts || context.recentPrompts.length === 0) && (!context.recentToolEvents || context.recentToolEvents.length === 0) && (!context.recentSessions || context.recentSessions.length === 0) && (!context.projectTypeCounts || Object.keys(context.projectTypeCounts).length === 0)) {
|
|
16680
|
+
return `Project: ${context.project_name} (no prior observations)`;
|
|
16681
|
+
}
|
|
16682
|
+
const DETAILED_COUNT = 5;
|
|
16683
|
+
const isCrossProject = context.recentProjects && context.recentProjects.length > 0;
|
|
16684
|
+
const lines = [];
|
|
16685
|
+
if (isCrossProject) {
|
|
16686
|
+
lines.push(`## Engrm Memory — Workspace Overview`);
|
|
16687
|
+
lines.push(`This is a new project folder. Here is context from your recent work:`);
|
|
16688
|
+
lines.push("");
|
|
16689
|
+
lines.push("**Active projects in memory:**");
|
|
16690
|
+
for (const rp of context.recentProjects) {
|
|
16691
|
+
const activity = rp.days_ago === 0 ? "today" : rp.days_ago === 1 ? "yesterday" : `${rp.days_ago}d ago`;
|
|
16692
|
+
lines.push(`- **${rp.name}** — ${rp.observation_count} observations, last active ${activity}`);
|
|
16693
|
+
}
|
|
16694
|
+
lines.push("");
|
|
16695
|
+
lines.push(`${context.session_count} relevant observation(s) from across projects:`);
|
|
16696
|
+
lines.push("");
|
|
16697
|
+
} else {
|
|
16698
|
+
lines.push(`## Project Memory: ${context.project_name}`);
|
|
16699
|
+
lines.push(`${context.session_count} relevant observation(s) from prior sessions:`);
|
|
16700
|
+
lines.push("");
|
|
16701
|
+
}
|
|
16702
|
+
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
16703
|
+
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt(prompt.prompt)).slice(0, 5);
|
|
16704
|
+
if (promptLines.length > 0) {
|
|
16705
|
+
lines.push("## Recent Requests");
|
|
16706
|
+
for (const prompt of promptLines) {
|
|
16707
|
+
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
16708
|
+
lines.push(`- ${label}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
16693
16709
|
}
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16710
|
+
lines.push("");
|
|
16711
|
+
}
|
|
16712
|
+
}
|
|
16713
|
+
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
16714
|
+
lines.push("## Recent Tools");
|
|
16715
|
+
for (const tool of context.recentToolEvents.slice(0, 5)) {
|
|
16716
|
+
lines.push(`- ${tool.tool_name}: ${formatToolEventDetail(tool)}`);
|
|
16717
|
+
}
|
|
16718
|
+
lines.push("");
|
|
16719
|
+
}
|
|
16720
|
+
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
16721
|
+
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
16722
|
+
const summary = chooseMeaningfulSessionHeadline(session.request, session.completed);
|
|
16723
|
+
if (summary === "(no summary)")
|
|
16724
|
+
return null;
|
|
16725
|
+
return `- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
16726
|
+
}).filter((line) => Boolean(line));
|
|
16727
|
+
if (recentSessionLines.length > 0) {
|
|
16728
|
+
lines.push("## Recent Sessions");
|
|
16729
|
+
lines.push(...recentSessionLines);
|
|
16730
|
+
lines.push("");
|
|
16731
|
+
}
|
|
16732
|
+
}
|
|
16733
|
+
if (context.recentOutcomes && context.recentOutcomes.length > 0) {
|
|
16734
|
+
lines.push("## Recent Outcomes");
|
|
16735
|
+
for (const outcome of context.recentOutcomes.slice(0, 5)) {
|
|
16736
|
+
lines.push(`- ${truncateText(outcome, 160)}`);
|
|
16737
|
+
}
|
|
16738
|
+
lines.push("");
|
|
16739
|
+
}
|
|
16740
|
+
if (context.projectTypeCounts && Object.keys(context.projectTypeCounts).length > 0) {
|
|
16741
|
+
const topTypes = Object.entries(context.projectTypeCounts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5).map(([type, count]) => `${type} ${count}`).join(" · ");
|
|
16742
|
+
if (topTypes) {
|
|
16743
|
+
lines.push(`## Project Signals`);
|
|
16744
|
+
lines.push(`Top memory types: ${topTypes}`);
|
|
16745
|
+
lines.push("");
|
|
16746
|
+
}
|
|
16747
|
+
}
|
|
16748
|
+
for (let i = 0;i < context.observations.length; i++) {
|
|
16749
|
+
const obs = context.observations[i];
|
|
16750
|
+
const date5 = obs.created_at.split("T")[0];
|
|
16751
|
+
const fromLabel = obs.source_project ? ` [from: ${obs.source_project}]` : "";
|
|
16752
|
+
const fileLabel = formatObservationFiles(obs);
|
|
16753
|
+
lines.push(`- **#${obs.id} [${obs.type}]** ${obs.title} (${date5}, q=${obs.quality.toFixed(1)})${fromLabel}${fileLabel}`);
|
|
16754
|
+
if (i < DETAILED_COUNT) {
|
|
16755
|
+
const detail = formatObservationDetailFromContext(obs);
|
|
16756
|
+
if (detail) {
|
|
16757
|
+
lines.push(detail);
|
|
16702
16758
|
}
|
|
16703
|
-
|
|
16704
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
16759
|
+
}
|
|
16760
|
+
}
|
|
16761
|
+
if (context.summaries && context.summaries.length > 0) {
|
|
16762
|
+
lines.push("");
|
|
16763
|
+
lines.push("## Recent Project Briefs");
|
|
16764
|
+
for (const summary of context.summaries.slice(0, 3)) {
|
|
16765
|
+
lines.push(...formatSessionBrief(summary));
|
|
16766
|
+
lines.push("");
|
|
16767
|
+
}
|
|
16768
|
+
}
|
|
16769
|
+
if (context.securityFindings && context.securityFindings.length > 0) {
|
|
16770
|
+
lines.push("");
|
|
16771
|
+
lines.push("Security findings (recent):");
|
|
16772
|
+
for (const finding of context.securityFindings) {
|
|
16773
|
+
const date5 = new Date(finding.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
16774
|
+
const file2 = finding.file_path ? ` in ${finding.file_path}` : finding.tool_name ? ` via ${finding.tool_name}` : "";
|
|
16775
|
+
lines.push(`- [${finding.severity.toUpperCase()}] ${finding.pattern_name}${file2} (${date5})`);
|
|
16776
|
+
}
|
|
16777
|
+
}
|
|
16778
|
+
if (context.staleDecisions && context.staleDecisions.length > 0) {
|
|
16779
|
+
lines.push("");
|
|
16780
|
+
lines.push("Stale commitments (decided but no implementation observed):");
|
|
16781
|
+
for (const sd of context.staleDecisions) {
|
|
16782
|
+
const date5 = sd.created_at.split("T")[0];
|
|
16783
|
+
lines.push(`- [DECISION] ${sd.title} (${date5}, ${sd.days_ago}d ago)`);
|
|
16784
|
+
if (sd.best_match_title) {
|
|
16785
|
+
lines.push(` Closest match: "${sd.best_match_title}" (${Math.round((sd.best_match_similarity ?? 0) * 100)}% similar — not enough to count as done)`);
|
|
16707
16786
|
}
|
|
16708
16787
|
}
|
|
16709
|
-
|
|
16710
|
-
|
|
16711
|
-
|
|
16712
|
-
|
|
16713
|
-
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16788
|
+
}
|
|
16789
|
+
const remaining = context.total_active - context.session_count;
|
|
16790
|
+
if (remaining > 0) {
|
|
16791
|
+
lines.push("");
|
|
16792
|
+
lines.push(`${remaining} more observation(s) available via search tool.`);
|
|
16793
|
+
}
|
|
16794
|
+
return lines.join(`
|
|
16795
|
+
`);
|
|
16796
|
+
}
|
|
16797
|
+
function formatSessionBrief(summary) {
|
|
16798
|
+
const lines = [];
|
|
16799
|
+
const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
16800
|
+
lines.push(heading);
|
|
16801
|
+
const sections = [
|
|
16802
|
+
["Investigated", summary.investigated, 180],
|
|
16803
|
+
["Learned", summary.learned, 180],
|
|
16804
|
+
["Completed", summary.completed, 180],
|
|
16805
|
+
["Next Steps", summary.next_steps, 140]
|
|
16806
|
+
];
|
|
16807
|
+
for (const [label, value, maxLen] of sections) {
|
|
16808
|
+
const formatted = formatSummarySection(value, maxLen);
|
|
16809
|
+
if (formatted) {
|
|
16810
|
+
lines.push(`${label}:`);
|
|
16811
|
+
lines.push(formatted);
|
|
16722
16812
|
}
|
|
16723
16813
|
}
|
|
16724
|
-
|
|
16725
|
-
return stale.slice(0, 5);
|
|
16814
|
+
return lines;
|
|
16726
16815
|
}
|
|
16727
|
-
function
|
|
16728
|
-
|
|
16729
|
-
|
|
16730
|
-
const
|
|
16731
|
-
|
|
16732
|
-
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
|
|
16739
|
-
if (
|
|
16816
|
+
function chooseMeaningfulSessionHeadline(request, completed) {
|
|
16817
|
+
if (request && !looksLikeFileOperationTitle2(request))
|
|
16818
|
+
return request;
|
|
16819
|
+
const completedItems = extractMeaningfulLines(completed, 1);
|
|
16820
|
+
if (completedItems.length > 0)
|
|
16821
|
+
return completedItems[0];
|
|
16822
|
+
return request ?? completed ?? "(no summary)";
|
|
16823
|
+
}
|
|
16824
|
+
function formatSummarySection(value, maxLen) {
|
|
16825
|
+
return formatSummaryItems(value, maxLen);
|
|
16826
|
+
}
|
|
16827
|
+
function truncateText(text, maxLen) {
|
|
16828
|
+
if (text.length <= maxLen)
|
|
16829
|
+
return text;
|
|
16830
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
16831
|
+
}
|
|
16832
|
+
function isMeaningfulPrompt(value) {
|
|
16833
|
+
if (!value)
|
|
16834
|
+
return false;
|
|
16835
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
16836
|
+
if (compact.length < 8)
|
|
16837
|
+
return false;
|
|
16838
|
+
return /[a-z]{3,}/i.test(compact);
|
|
16839
|
+
}
|
|
16840
|
+
function looksLikeFileOperationTitle2(value) {
|
|
16841
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
16842
|
+
}
|
|
16843
|
+
function stripInlineSectionLabel(value) {
|
|
16844
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
16845
|
+
}
|
|
16846
|
+
function extractMeaningfulLines(value, limit) {
|
|
16847
|
+
if (!value)
|
|
16740
16848
|
return [];
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
const
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
continue;
|
|
16764
|
-
const titleScore = jaccardSimilarity(decision.title, impl.title);
|
|
16765
|
-
let conceptBoost = 0;
|
|
16766
|
-
if (decisionConcepts.length > 0) {
|
|
16767
|
-
try {
|
|
16768
|
-
const implConcepts = impl.concepts ? JSON.parse(impl.concepts) : [];
|
|
16769
|
-
if (Array.isArray(implConcepts) && implConcepts.length > 0) {
|
|
16770
|
-
const decSet = new Set(decisionConcepts.map((c) => c.toLowerCase()));
|
|
16771
|
-
const overlap = implConcepts.filter((c) => decSet.has(c.toLowerCase())).length;
|
|
16772
|
-
conceptBoost = overlap / Math.max(decisionConcepts.length, 1) * 0.15;
|
|
16773
|
-
}
|
|
16774
|
-
} catch {}
|
|
16775
|
-
}
|
|
16776
|
-
const totalScore = titleScore + conceptBoost;
|
|
16777
|
-
if (totalScore > bestScore) {
|
|
16778
|
-
bestScore = totalScore;
|
|
16779
|
-
bestTitle = impl.title;
|
|
16780
|
-
}
|
|
16849
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle2(line)).slice(0, limit);
|
|
16850
|
+
}
|
|
16851
|
+
function formatObservationDetailFromContext(obs) {
|
|
16852
|
+
if (obs.facts) {
|
|
16853
|
+
const bullets = parseFacts(obs.facts);
|
|
16854
|
+
if (bullets.length > 0) {
|
|
16855
|
+
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
16856
|
+
`);
|
|
16857
|
+
}
|
|
16858
|
+
}
|
|
16859
|
+
if (obs.narrative) {
|
|
16860
|
+
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
16861
|
+
return ` ${snippet}`;
|
|
16862
|
+
}
|
|
16863
|
+
return null;
|
|
16864
|
+
}
|
|
16865
|
+
function formatObservationDetail(obs) {
|
|
16866
|
+
if (obs.facts) {
|
|
16867
|
+
const bullets = parseFacts(obs.facts);
|
|
16868
|
+
if (bullets.length > 0) {
|
|
16869
|
+
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
16870
|
+
`);
|
|
16781
16871
|
}
|
|
16782
|
-
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16872
|
+
}
|
|
16873
|
+
if (obs.narrative) {
|
|
16874
|
+
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
16875
|
+
return ` ${snippet}`;
|
|
16876
|
+
}
|
|
16877
|
+
return "";
|
|
16878
|
+
}
|
|
16879
|
+
function parseFacts(facts) {
|
|
16880
|
+
if (!facts)
|
|
16881
|
+
return [];
|
|
16882
|
+
try {
|
|
16883
|
+
const parsed = JSON.parse(facts);
|
|
16884
|
+
if (Array.isArray(parsed)) {
|
|
16885
|
+
return parsed.filter((f) => typeof f === "string" && f.length > 0);
|
|
16886
|
+
}
|
|
16887
|
+
} catch {
|
|
16888
|
+
if (facts.trim().length > 0) {
|
|
16889
|
+
return [facts.trim()];
|
|
16795
16890
|
}
|
|
16796
16891
|
}
|
|
16797
|
-
|
|
16798
|
-
return stale.slice(0, 5);
|
|
16892
|
+
return [];
|
|
16799
16893
|
}
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16894
|
+
function toContextObservation(obs) {
|
|
16895
|
+
return {
|
|
16896
|
+
id: obs.id,
|
|
16897
|
+
type: obs.type,
|
|
16898
|
+
title: obs.title,
|
|
16899
|
+
narrative: obs.narrative,
|
|
16900
|
+
facts: obs.facts,
|
|
16901
|
+
files_read: obs.files_read,
|
|
16902
|
+
files_modified: obs.files_modified,
|
|
16903
|
+
quality: obs.quality,
|
|
16904
|
+
created_at: obs.created_at,
|
|
16905
|
+
...obs._source_project ? { source_project: obs._source_project } : {}
|
|
16906
|
+
};
|
|
16804
16907
|
}
|
|
16805
|
-
function
|
|
16806
|
-
const
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
]);
|
|
16810
|
-
if (hints.size === 0)
|
|
16811
|
-
return false;
|
|
16812
|
-
const haystack = [
|
|
16813
|
-
obs.title,
|
|
16814
|
-
obs.narrative ?? "",
|
|
16815
|
-
obs.facts ?? "",
|
|
16816
|
-
obs.concepts ?? "",
|
|
16817
|
-
obs.files_read ?? "",
|
|
16818
|
-
obs.files_modified ?? "",
|
|
16819
|
-
obs._source_project ?? ""
|
|
16820
|
-
].join(`
|
|
16821
|
-
`).toLowerCase();
|
|
16822
|
-
for (const hint of hints) {
|
|
16823
|
-
if (haystack.includes(hint))
|
|
16824
|
-
return true;
|
|
16908
|
+
function formatObservationFiles(obs) {
|
|
16909
|
+
const modified = parseJsonStringArray(obs.files_modified);
|
|
16910
|
+
if (modified.length > 0) {
|
|
16911
|
+
return ` · files: ${truncateText(modified.slice(0, 2).join(", "), 60)}`;
|
|
16825
16912
|
}
|
|
16826
|
-
|
|
16913
|
+
const read = parseJsonStringArray(obs.files_read);
|
|
16914
|
+
if (read.length > 0) {
|
|
16915
|
+
return ` · read: ${truncateText(read.slice(0, 2).join(", "), 60)}`;
|
|
16916
|
+
}
|
|
16917
|
+
return "";
|
|
16827
16918
|
}
|
|
16828
|
-
function
|
|
16829
|
-
if (!
|
|
16830
|
-
return
|
|
16831
|
-
|
|
16919
|
+
function parseJsonStringArray(value) {
|
|
16920
|
+
if (!value)
|
|
16921
|
+
return [];
|
|
16922
|
+
try {
|
|
16923
|
+
const parsed = JSON.parse(value);
|
|
16924
|
+
if (!Array.isArray(parsed))
|
|
16925
|
+
return [];
|
|
16926
|
+
return parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
16927
|
+
} catch {
|
|
16928
|
+
return [];
|
|
16929
|
+
}
|
|
16832
16930
|
}
|
|
16833
|
-
function
|
|
16834
|
-
const
|
|
16835
|
-
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
const
|
|
16839
|
-
const
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
const totalActive = isNewProject ? (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
16844
|
-
WHERE lifecycle IN ('active', 'aging', 'pinned')
|
|
16845
|
-
${visibilityClause}
|
|
16846
|
-
AND superseded_by IS NULL`).get(...visibilityParams) ?? { c: 0 }).c : (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
16847
|
-
WHERE project_id = ? AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16848
|
-
${visibilityClause}
|
|
16849
|
-
AND superseded_by IS NULL`).get(projectId, ...visibilityParams) ?? { c: 0 }).c;
|
|
16850
|
-
const candidateLimit = maxCount ?? 50;
|
|
16851
|
-
let pinned = [];
|
|
16852
|
-
let recent = [];
|
|
16853
|
-
let candidates = [];
|
|
16854
|
-
if (!isNewProject) {
|
|
16855
|
-
const MAX_PINNED = 5;
|
|
16856
|
-
pinned = db.db.query(`SELECT * FROM observations
|
|
16857
|
-
WHERE project_id = ? AND lifecycle = 'pinned'
|
|
16858
|
-
AND superseded_by IS NULL
|
|
16859
|
-
${visibilityClause}
|
|
16860
|
-
ORDER BY quality DESC, created_at_epoch DESC
|
|
16861
|
-
LIMIT ?`).all(projectId, ...visibilityParams, MAX_PINNED);
|
|
16862
|
-
const MAX_RECENT = 5;
|
|
16863
|
-
recent = db.db.query(`SELECT * FROM observations
|
|
16864
|
-
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
16865
|
-
AND superseded_by IS NULL
|
|
16866
|
-
${visibilityClause}
|
|
16867
|
-
ORDER BY created_at_epoch DESC
|
|
16868
|
-
LIMIT ?`).all(projectId, ...visibilityParams, MAX_RECENT);
|
|
16869
|
-
candidates = db.db.query(`SELECT * FROM observations
|
|
16870
|
-
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
16871
|
-
AND quality >= 0.3
|
|
16931
|
+
function formatToolEventDetail(tool) {
|
|
16932
|
+
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
16933
|
+
return truncateText(detail || "recent tool execution", 160);
|
|
16934
|
+
}
|
|
16935
|
+
function getProjectTypeCounts(db, projectId, userId) {
|
|
16936
|
+
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
16937
|
+
const rows = db.db.query(`SELECT type, COUNT(*) as count
|
|
16938
|
+
FROM observations
|
|
16939
|
+
WHERE project_id = ?
|
|
16940
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16872
16941
|
AND superseded_by IS NULL
|
|
16873
16942
|
${visibilityClause}
|
|
16874
|
-
|
|
16875
|
-
|
|
16943
|
+
GROUP BY type`).all(projectId, ...userId ? [userId] : []);
|
|
16944
|
+
const counts = {};
|
|
16945
|
+
for (const row of rows) {
|
|
16946
|
+
counts[row.type] = row.count;
|
|
16876
16947
|
}
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
16885
|
-
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
|
|
16894
|
-
|
|
16895
|
-
|
|
16896
|
-
|
|
16897
|
-
|
|
16898
|
-
|
|
16899
|
-
|
|
16900
|
-
}
|
|
16901
|
-
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
16902
|
-
});
|
|
16903
|
-
if (isNewProject) {
|
|
16904
|
-
crossProjectCandidates = crossProjectCandidates.filter((obs) => isObservationRelatedToProject(obs, detected));
|
|
16948
|
+
return counts;
|
|
16949
|
+
}
|
|
16950
|
+
function getRecentOutcomes(db, projectId, userId) {
|
|
16951
|
+
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
16952
|
+
const visibilityParams = userId ? [userId] : [];
|
|
16953
|
+
const summaries = db.db.query(`SELECT * FROM session_summaries
|
|
16954
|
+
WHERE project_id = ?
|
|
16955
|
+
ORDER BY created_at_epoch DESC
|
|
16956
|
+
LIMIT 6`).all(projectId);
|
|
16957
|
+
const picked = [];
|
|
16958
|
+
const seen = new Set;
|
|
16959
|
+
for (const summary of summaries) {
|
|
16960
|
+
for (const line of [
|
|
16961
|
+
...extractMeaningfulLines(summary.completed, 2),
|
|
16962
|
+
...extractMeaningfulLines(summary.learned, 1)
|
|
16963
|
+
]) {
|
|
16964
|
+
const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
16965
|
+
if (!normalized || seen.has(normalized))
|
|
16966
|
+
continue;
|
|
16967
|
+
seen.add(normalized);
|
|
16968
|
+
picked.push(line);
|
|
16969
|
+
if (picked.length >= 5)
|
|
16970
|
+
return picked;
|
|
16905
16971
|
}
|
|
16906
16972
|
}
|
|
16907
|
-
const
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
const
|
|
16915
|
-
|
|
16916
|
-
|
|
16917
|
-
|
|
16918
|
-
|
|
16919
|
-
|
|
16973
|
+
const rows = db.db.query(`SELECT * FROM observations
|
|
16974
|
+
WHERE project_id = ?
|
|
16975
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16976
|
+
AND superseded_by IS NULL
|
|
16977
|
+
${visibilityClause}
|
|
16978
|
+
ORDER BY created_at_epoch DESC
|
|
16979
|
+
LIMIT 20`).all(projectId, ...visibilityParams);
|
|
16980
|
+
for (const obs of rows) {
|
|
16981
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
16982
|
+
continue;
|
|
16983
|
+
const title = stripInlineSectionLabel(obs.title);
|
|
16984
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
16985
|
+
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle2(title))
|
|
16986
|
+
continue;
|
|
16987
|
+
seen.add(normalized);
|
|
16988
|
+
picked.push(title);
|
|
16989
|
+
if (picked.length >= 5)
|
|
16990
|
+
break;
|
|
16991
|
+
}
|
|
16992
|
+
return picked;
|
|
16993
|
+
}
|
|
16994
|
+
|
|
16995
|
+
// src/tools/project-memory-index.ts
|
|
16996
|
+
function getProjectMemoryIndex(db, input) {
|
|
16997
|
+
const cwd = input.cwd ?? process.cwd();
|
|
16998
|
+
const detected = detectProject(cwd);
|
|
16999
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
17000
|
+
if (!project)
|
|
17001
|
+
return null;
|
|
17002
|
+
const visibilityClause = input.user_id ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
17003
|
+
const visibilityParams = input.user_id ? [input.user_id] : [];
|
|
17004
|
+
const observations = db.db.query(`SELECT * FROM observations
|
|
17005
|
+
WHERE project_id = ?
|
|
17006
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
17007
|
+
AND superseded_by IS NULL
|
|
17008
|
+
${visibilityClause}
|
|
17009
|
+
ORDER BY created_at_epoch DESC`).all(project.id, ...visibilityParams);
|
|
17010
|
+
const counts = {};
|
|
17011
|
+
for (const obs of observations) {
|
|
17012
|
+
counts[obs.type] = (counts[obs.type] ?? 0) + 1;
|
|
16920
17013
|
}
|
|
16921
|
-
const
|
|
16922
|
-
const
|
|
16923
|
-
const
|
|
16924
|
-
|
|
16925
|
-
|
|
16926
|
-
});
|
|
16927
|
-
const projectName = project?.name ?? detected.name;
|
|
16928
|
-
const canonicalId = project?.canonical_id ?? detected.canonical_id;
|
|
16929
|
-
if (maxCount !== undefined) {
|
|
16930
|
-
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
16931
|
-
const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
16932
|
-
const recentPrompts2 = db.getRecentUserPrompts(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16933
|
-
const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16934
|
-
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16935
|
-
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16936
|
-
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
16937
|
-
return {
|
|
16938
|
-
project_name: projectName,
|
|
16939
|
-
canonical_id: canonicalId,
|
|
16940
|
-
observations: all.map(toContextObservation),
|
|
16941
|
-
session_count: all.length,
|
|
16942
|
-
total_active: totalActive,
|
|
16943
|
-
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
16944
|
-
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
16945
|
-
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
16946
|
-
projectTypeCounts: projectTypeCounts2,
|
|
16947
|
-
recentOutcomes: recentOutcomes2
|
|
16948
|
-
};
|
|
17014
|
+
const fileCounts = new Map;
|
|
17015
|
+
for (const obs of observations) {
|
|
17016
|
+
for (const path of extractPaths(obs.files_modified)) {
|
|
17017
|
+
fileCounts.set(path, (fileCounts.get(path) ?? 0) + 1);
|
|
17018
|
+
}
|
|
16949
17019
|
}
|
|
16950
|
-
|
|
16951
|
-
const
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
17020
|
+
const hotFiles = Array.from(fileCounts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || a.path.localeCompare(b.path)).slice(0, 8);
|
|
17021
|
+
const provenanceSummary = Array.from(observations.reduce((acc, obs) => {
|
|
17022
|
+
if (!obs.source_tool)
|
|
17023
|
+
return acc;
|
|
17024
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
17025
|
+
return acc;
|
|
17026
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
17027
|
+
const assistantCheckpointCount = observations.filter((obs) => obs.source_tool === "assistant-stop").length;
|
|
17028
|
+
const assistantCheckpointTypes = Array.from(observations.filter((obs) => obs.source_tool === "assistant-stop").reduce((acc, obs) => {
|
|
17029
|
+
acc.set(obs.type, (acc.get(obs.type) ?? 0) + 1);
|
|
17030
|
+
return acc;
|
|
17031
|
+
}, new Map).entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
|
|
17032
|
+
const topTitles = observations.slice(0, 12).map((obs) => ({
|
|
17033
|
+
type: obs.type,
|
|
17034
|
+
title: obs.title,
|
|
17035
|
+
id: obs.id
|
|
17036
|
+
}));
|
|
17037
|
+
const recentSessions = getRecentSessions(db, {
|
|
17038
|
+
cwd,
|
|
17039
|
+
project_scoped: true,
|
|
17040
|
+
user_id: input.user_id,
|
|
17041
|
+
limit: 6
|
|
17042
|
+
}).sessions;
|
|
17043
|
+
const recentRequestsCount = getRecentRequests(db, {
|
|
17044
|
+
cwd,
|
|
17045
|
+
project_scoped: true,
|
|
17046
|
+
user_id: input.user_id,
|
|
17047
|
+
limit: 20
|
|
17048
|
+
}).prompts.length;
|
|
17049
|
+
const recentToolsCount = getRecentTools(db, {
|
|
17050
|
+
cwd,
|
|
17051
|
+
project_scoped: true,
|
|
17052
|
+
user_id: input.user_id,
|
|
17053
|
+
limit: 20
|
|
17054
|
+
}).tool_events.length;
|
|
17055
|
+
const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0 && !looksLikeFileOperationTitle3(title)).slice(0, 8);
|
|
17056
|
+
const captureSummary = summarizeCaptureState(recentSessions);
|
|
17057
|
+
const topTypes = Object.entries(counts).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
|
|
17058
|
+
const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length);
|
|
17059
|
+
const estimatedReadTokens = estimateTokens([
|
|
17060
|
+
recentOutcomes.join(`
|
|
17061
|
+
`),
|
|
17062
|
+
topTitles.map((item) => `${item.type}: ${item.title}`).join(`
|
|
17063
|
+
`),
|
|
17064
|
+
hotFiles.map((item) => `${item.path} (${item.count})`).join(`
|
|
17065
|
+
`)
|
|
17066
|
+
].filter(Boolean).join(`
|
|
17067
|
+
`));
|
|
17068
|
+
return {
|
|
17069
|
+
project: project.name,
|
|
17070
|
+
canonical_id: project.canonical_id,
|
|
17071
|
+
observation_counts: counts,
|
|
17072
|
+
recent_sessions: recentSessions,
|
|
17073
|
+
recent_outcomes: recentOutcomes,
|
|
17074
|
+
recent_requests_count: recentRequestsCount,
|
|
17075
|
+
recent_tools_count: recentToolsCount,
|
|
17076
|
+
raw_capture_active: recentRequestsCount > 0 || recentToolsCount > 0,
|
|
17077
|
+
capture_summary: captureSummary,
|
|
17078
|
+
hot_files: hotFiles,
|
|
17079
|
+
provenance_summary: provenanceSummary,
|
|
17080
|
+
assistant_checkpoint_count: assistantCheckpointCount,
|
|
17081
|
+
assistant_checkpoint_types: assistantCheckpointTypes,
|
|
17082
|
+
top_titles: topTitles,
|
|
17083
|
+
top_types: topTypes,
|
|
17084
|
+
estimated_read_tokens: estimatedReadTokens,
|
|
17085
|
+
suggested_tools: suggestedTools
|
|
17086
|
+
};
|
|
17087
|
+
}
|
|
17088
|
+
function extractPaths(value) {
|
|
17089
|
+
if (!value)
|
|
17090
|
+
return [];
|
|
17091
|
+
try {
|
|
17092
|
+
const parsed = JSON.parse(value);
|
|
17093
|
+
if (!Array.isArray(parsed))
|
|
17094
|
+
return [];
|
|
17095
|
+
return parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
17096
|
+
} catch {
|
|
17097
|
+
return [];
|
|
16956
17098
|
}
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
|
|
16960
|
-
|
|
17099
|
+
}
|
|
17100
|
+
function looksLikeFileOperationTitle3(value) {
|
|
17101
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
17102
|
+
}
|
|
17103
|
+
function summarizeCaptureState(sessions) {
|
|
17104
|
+
const summary = {
|
|
17105
|
+
rich_sessions: 0,
|
|
17106
|
+
partial_sessions: 0,
|
|
17107
|
+
summary_only_sessions: 0,
|
|
17108
|
+
legacy_sessions: 0
|
|
17109
|
+
};
|
|
17110
|
+
for (const session of sessions) {
|
|
17111
|
+
switch (session.capture_state) {
|
|
17112
|
+
case "rich":
|
|
17113
|
+
summary.rich_sessions += 1;
|
|
17114
|
+
break;
|
|
17115
|
+
case "partial":
|
|
17116
|
+
summary.partial_sessions += 1;
|
|
17117
|
+
break;
|
|
17118
|
+
case "summary-only":
|
|
17119
|
+
summary.summary_only_sessions += 1;
|
|
17120
|
+
break;
|
|
17121
|
+
default:
|
|
17122
|
+
summary.legacy_sessions += 1;
|
|
17123
|
+
break;
|
|
17124
|
+
}
|
|
16961
17125
|
}
|
|
16962
|
-
|
|
16963
|
-
|
|
16964
|
-
|
|
16965
|
-
|
|
16966
|
-
|
|
16967
|
-
|
|
17126
|
+
return summary;
|
|
17127
|
+
}
|
|
17128
|
+
function buildSuggestedTools(sessions, requestCount, toolCount, observationCount) {
|
|
17129
|
+
const suggested = [];
|
|
17130
|
+
if (sessions.length > 0) {
|
|
17131
|
+
suggested.push("recent_sessions");
|
|
16968
17132
|
}
|
|
16969
|
-
|
|
16970
|
-
|
|
16971
|
-
const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16972
|
-
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16973
|
-
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16974
|
-
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
16975
|
-
let securityFindings = [];
|
|
16976
|
-
if (!isNewProject) {
|
|
16977
|
-
try {
|
|
16978
|
-
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
16979
|
-
securityFindings = db.db.query(`SELECT * FROM security_findings
|
|
16980
|
-
WHERE project_id = ? AND created_at_epoch > ?
|
|
16981
|
-
ORDER BY severity DESC, created_at_epoch DESC
|
|
16982
|
-
LIMIT ?`).all(projectId, weekAgo, 10);
|
|
16983
|
-
} catch {}
|
|
17133
|
+
if (requestCount > 0 || toolCount > 0) {
|
|
17134
|
+
suggested.push("activity_feed");
|
|
16984
17135
|
}
|
|
16985
|
-
|
|
16986
|
-
|
|
16987
|
-
try {
|
|
16988
|
-
const nowEpochSec = Math.floor(Date.now() / 1000);
|
|
16989
|
-
const projectRows = db.db.query(`SELECT p.name, p.canonical_id, p.last_active_epoch,
|
|
16990
|
-
(SELECT COUNT(*) FROM observations o
|
|
16991
|
-
WHERE o.project_id = p.id
|
|
16992
|
-
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
16993
|
-
${opts.userId ? "AND (o.sensitivity != 'personal' OR o.user_id = ?)" : ""}
|
|
16994
|
-
AND o.superseded_by IS NULL) as obs_count
|
|
16995
|
-
FROM projects p
|
|
16996
|
-
ORDER BY p.last_active_epoch DESC
|
|
16997
|
-
LIMIT 10`).all(...visibilityParams);
|
|
16998
|
-
if (projectRows.length > 0) {
|
|
16999
|
-
recentProjects = projectRows.map((r) => {
|
|
17000
|
-
const daysAgo = Math.max(0, Math.floor((nowEpochSec - r.last_active_epoch) / 86400));
|
|
17001
|
-
const lastActive = new Date(r.last_active_epoch * 1000).toISOString().split("T")[0];
|
|
17002
|
-
return {
|
|
17003
|
-
name: r.name,
|
|
17004
|
-
canonical_id: r.canonical_id,
|
|
17005
|
-
observation_count: r.obs_count,
|
|
17006
|
-
last_active: lastActive,
|
|
17007
|
-
days_ago: daysAgo
|
|
17008
|
-
};
|
|
17009
|
-
});
|
|
17010
|
-
}
|
|
17011
|
-
} catch {}
|
|
17136
|
+
if (observationCount > 0) {
|
|
17137
|
+
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
17012
17138
|
}
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17139
|
+
return Array.from(new Set(suggested)).slice(0, 4);
|
|
17140
|
+
}
|
|
17141
|
+
|
|
17142
|
+
// src/tools/memory-console.ts
|
|
17143
|
+
function getMemoryConsole(db, input) {
|
|
17144
|
+
const cwd = input.cwd ?? process.cwd();
|
|
17145
|
+
const projectScoped = input.project_scoped !== false;
|
|
17146
|
+
const detected = projectScoped ? detectProject(cwd) : null;
|
|
17147
|
+
const project = detected ? db.getProjectByCanonicalId(detected.canonical_id) : null;
|
|
17148
|
+
const sessions = getRecentSessions(db, {
|
|
17149
|
+
cwd,
|
|
17150
|
+
project_scoped: projectScoped,
|
|
17151
|
+
user_id: input.user_id,
|
|
17152
|
+
limit: 6
|
|
17153
|
+
}).sessions;
|
|
17154
|
+
const requests = getRecentRequests(db, {
|
|
17155
|
+
cwd,
|
|
17156
|
+
project_scoped: projectScoped,
|
|
17157
|
+
user_id: input.user_id,
|
|
17158
|
+
limit: 6
|
|
17159
|
+
}).prompts;
|
|
17160
|
+
const tools = getRecentTools(db, {
|
|
17161
|
+
cwd,
|
|
17162
|
+
project_scoped: projectScoped,
|
|
17163
|
+
user_id: input.user_id,
|
|
17164
|
+
limit: 8
|
|
17165
|
+
}).tool_events;
|
|
17166
|
+
const observations = getRecentActivity(db, {
|
|
17167
|
+
cwd,
|
|
17168
|
+
project_scoped: projectScoped,
|
|
17169
|
+
user_id: input.user_id,
|
|
17170
|
+
limit: 8
|
|
17171
|
+
}).observations;
|
|
17172
|
+
const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
|
|
17173
|
+
cwd,
|
|
17174
|
+
user_id: input.user_id
|
|
17175
|
+
}) : null;
|
|
17019
17176
|
return {
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17177
|
+
project: project?.name,
|
|
17178
|
+
capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
|
|
17179
|
+
sessions,
|
|
17180
|
+
requests,
|
|
17181
|
+
tools,
|
|
17182
|
+
observations,
|
|
17183
|
+
capture_summary: projectIndex?.capture_summary,
|
|
17184
|
+
recent_outcomes: projectIndex?.recent_outcomes ?? [],
|
|
17185
|
+
hot_files: projectIndex?.hot_files ?? [],
|
|
17186
|
+
provenance_summary: projectIndex?.provenance_summary ?? [],
|
|
17187
|
+
assistant_checkpoint_count: projectIndex?.assistant_checkpoint_count,
|
|
17188
|
+
assistant_checkpoint_types: projectIndex?.assistant_checkpoint_types ?? [],
|
|
17189
|
+
top_types: projectIndex?.top_types ?? [],
|
|
17190
|
+
estimated_read_tokens: projectIndex?.estimated_read_tokens,
|
|
17191
|
+
suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length)
|
|
17192
|
+
};
|
|
17193
|
+
}
|
|
17194
|
+
function buildFallbackSuggestedTools(sessionCount, requestCount, toolCount, observationCount) {
|
|
17195
|
+
const suggested = [];
|
|
17196
|
+
if (sessionCount > 0)
|
|
17197
|
+
suggested.push("recent_sessions");
|
|
17198
|
+
if (requestCount > 0 || toolCount > 0)
|
|
17199
|
+
suggested.push("activity_feed");
|
|
17200
|
+
if (observationCount > 0)
|
|
17201
|
+
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
17202
|
+
return Array.from(new Set(suggested)).slice(0, 4);
|
|
17203
|
+
}
|
|
17204
|
+
|
|
17205
|
+
// src/tools/workspace-memory-index.ts
|
|
17206
|
+
function getWorkspaceMemoryIndex(db, input) {
|
|
17207
|
+
const limit = Math.max(1, Math.min(input.limit ?? 12, 50));
|
|
17208
|
+
const visibilityClause = input.user_id ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
|
|
17209
|
+
const projects = db.db.query(`SELECT
|
|
17210
|
+
p.canonical_id,
|
|
17211
|
+
p.name,
|
|
17212
|
+
(
|
|
17213
|
+
SELECT COUNT(*) FROM observations o
|
|
17214
|
+
WHERE o.project_id = p.id
|
|
17215
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
17216
|
+
AND o.superseded_by IS NULL
|
|
17217
|
+
${visibilityClause}
|
|
17218
|
+
) AS observation_count,
|
|
17219
|
+
(SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,
|
|
17220
|
+
(SELECT COUNT(*) FROM user_prompts up WHERE up.project_id = p.id) AS prompt_count,
|
|
17221
|
+
(SELECT COUNT(*) FROM tool_events te WHERE te.project_id = p.id) AS tool_event_count,
|
|
17222
|
+
(
|
|
17223
|
+
SELECT COUNT(*) FROM observations o
|
|
17224
|
+
WHERE o.project_id = p.id
|
|
17225
|
+
AND o.source_tool = 'assistant-stop'
|
|
17226
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
17227
|
+
AND o.superseded_by IS NULL
|
|
17228
|
+
${visibilityClause}
|
|
17229
|
+
) AS assistant_checkpoint_count,
|
|
17230
|
+
p.last_active_epoch
|
|
17231
|
+
FROM projects p
|
|
17232
|
+
ORDER BY p.last_active_epoch DESC
|
|
17233
|
+
LIMIT ?`).all(...input.user_id ? [input.user_id, input.user_id, limit] : [limit]);
|
|
17234
|
+
const totals = projects.reduce((acc, project) => {
|
|
17235
|
+
acc.observations += project.observation_count;
|
|
17236
|
+
acc.sessions += project.session_count;
|
|
17237
|
+
acc.prompts += project.prompt_count;
|
|
17238
|
+
acc.tool_events += project.tool_event_count;
|
|
17239
|
+
acc.assistant_checkpoints += project.assistant_checkpoint_count;
|
|
17240
|
+
return acc;
|
|
17241
|
+
}, { observations: 0, sessions: 0, prompts: 0, tool_events: 0, assistant_checkpoints: 0 });
|
|
17242
|
+
const visibilityClauseForProvenance = input.user_id ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
17243
|
+
const provenanceRows = db.db.query(`SELECT source_tool, COUNT(*) as count
|
|
17244
|
+
FROM observations
|
|
17245
|
+
WHERE source_tool IS NOT NULL
|
|
17246
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
17247
|
+
AND superseded_by IS NULL
|
|
17248
|
+
${visibilityClauseForProvenance}
|
|
17249
|
+
GROUP BY source_tool
|
|
17250
|
+
ORDER BY count DESC, source_tool ASC
|
|
17251
|
+
LIMIT 8`).all(...input.user_id ? [input.user_id] : []);
|
|
17252
|
+
return {
|
|
17253
|
+
projects,
|
|
17254
|
+
totals,
|
|
17255
|
+
projects_with_raw_capture: projects.filter((project) => project.prompt_count > 0 || project.tool_event_count > 0).length,
|
|
17256
|
+
provenance_summary: provenanceRows.map((row) => ({
|
|
17257
|
+
tool: row.source_tool,
|
|
17258
|
+
count: row.count
|
|
17259
|
+
}))
|
|
17034
17260
|
};
|
|
17035
17261
|
}
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
const
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
const lines = [];
|
|
17052
|
-
if (isCrossProject) {
|
|
17053
|
-
lines.push(`## Engrm Memory — Workspace Overview`);
|
|
17054
|
-
lines.push(`This is a new project folder. Here is context from your recent work:`);
|
|
17055
|
-
lines.push("");
|
|
17056
|
-
lines.push("**Active projects in memory:**");
|
|
17057
|
-
for (const rp of context.recentProjects) {
|
|
17058
|
-
const activity = rp.days_ago === 0 ? "today" : rp.days_ago === 1 ? "yesterday" : `${rp.days_ago}d ago`;
|
|
17059
|
-
lines.push(`- **${rp.name}** — ${rp.observation_count} observations, last active ${activity}`);
|
|
17060
|
-
}
|
|
17061
|
-
lines.push("");
|
|
17062
|
-
lines.push(`${context.session_count} relevant observation(s) from across projects:`);
|
|
17063
|
-
lines.push("");
|
|
17064
|
-
} else {
|
|
17065
|
-
lines.push(`## Project Memory: ${context.project_name}`);
|
|
17066
|
-
lines.push(`${context.session_count} relevant observation(s) from prior sessions:`);
|
|
17067
|
-
lines.push("");
|
|
17068
|
-
}
|
|
17069
|
-
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
17070
|
-
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt(prompt.prompt)).slice(0, 5);
|
|
17071
|
-
if (promptLines.length > 0) {
|
|
17072
|
-
lines.push("## Recent Requests");
|
|
17073
|
-
for (const prompt of promptLines) {
|
|
17074
|
-
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
17075
|
-
lines.push(`- ${label}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
17076
|
-
}
|
|
17077
|
-
lines.push("");
|
|
17078
|
-
}
|
|
17079
|
-
}
|
|
17080
|
-
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
17081
|
-
lines.push("## Recent Tools");
|
|
17082
|
-
for (const tool of context.recentToolEvents.slice(0, 5)) {
|
|
17083
|
-
lines.push(`- ${tool.tool_name}: ${formatToolEventDetail(tool)}`);
|
|
17084
|
-
}
|
|
17085
|
-
lines.push("");
|
|
17086
|
-
}
|
|
17087
|
-
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
17088
|
-
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
17089
|
-
const summary = chooseMeaningfulSessionHeadline(session.request, session.completed);
|
|
17090
|
-
if (summary === "(no summary)")
|
|
17091
|
-
return null;
|
|
17092
|
-
return `- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
17093
|
-
}).filter((line) => Boolean(line));
|
|
17094
|
-
if (recentSessionLines.length > 0) {
|
|
17095
|
-
lines.push("## Recent Sessions");
|
|
17096
|
-
lines.push(...recentSessionLines);
|
|
17097
|
-
lines.push("");
|
|
17098
|
-
}
|
|
17262
|
+
|
|
17263
|
+
// src/tools/project-related-work.ts
|
|
17264
|
+
function getProjectRelatedWork(db, input) {
|
|
17265
|
+
const cwd = input.cwd ?? process.cwd();
|
|
17266
|
+
const detected = detectProject(cwd);
|
|
17267
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
17268
|
+
const visibilityClause = input.user_id ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
|
|
17269
|
+
const visibilityParams = input.user_id ? [input.user_id] : [];
|
|
17270
|
+
const terms = buildProjectTerms(detected);
|
|
17271
|
+
if (terms.length === 0) {
|
|
17272
|
+
return {
|
|
17273
|
+
project: detected.name,
|
|
17274
|
+
canonical_id: detected.canonical_id,
|
|
17275
|
+
related: []
|
|
17276
|
+
};
|
|
17099
17277
|
}
|
|
17100
|
-
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
|
|
17278
|
+
const localProject = db.getProjectByCanonicalId(detected.canonical_id);
|
|
17279
|
+
const localProjectId = localProject?.id ?? -1;
|
|
17280
|
+
const params = [];
|
|
17281
|
+
const whereParts = [];
|
|
17282
|
+
for (const term of terms) {
|
|
17283
|
+
const like = `%${term}%`;
|
|
17284
|
+
whereParts.push(`(LOWER(o.title) LIKE ? OR LOWER(COALESCE(o.narrative, '')) LIKE ? OR LOWER(COALESCE(o.files_read, '')) LIKE ? OR LOWER(COALESCE(o.files_modified, '')) LIKE ?)`);
|
|
17285
|
+
params.push(like, like, like, like);
|
|
17106
17286
|
}
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17287
|
+
const rows = db.db.query(`SELECT
|
|
17288
|
+
o.id,
|
|
17289
|
+
o.type,
|
|
17290
|
+
o.title,
|
|
17291
|
+
o.project_id as source_project_id,
|
|
17292
|
+
p.name as source_project,
|
|
17293
|
+
o.narrative,
|
|
17294
|
+
o.files_read,
|
|
17295
|
+
o.files_modified
|
|
17296
|
+
FROM observations o
|
|
17297
|
+
LEFT JOIN projects p ON p.id = o.project_id
|
|
17298
|
+
WHERE o.project_id != ?
|
|
17299
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
17300
|
+
AND o.superseded_by IS NULL
|
|
17301
|
+
${visibilityClause}
|
|
17302
|
+
AND (${whereParts.join(" OR ")})
|
|
17303
|
+
ORDER BY o.created_at_epoch DESC
|
|
17304
|
+
LIMIT ?`).all(localProjectId, ...visibilityParams, ...params, limit);
|
|
17305
|
+
return {
|
|
17306
|
+
project: detected.name,
|
|
17307
|
+
canonical_id: detected.canonical_id,
|
|
17308
|
+
related: rows.map((row) => ({
|
|
17309
|
+
id: row.id,
|
|
17310
|
+
type: row.type,
|
|
17311
|
+
title: row.title,
|
|
17312
|
+
source_project: row.source_project ?? "unknown",
|
|
17313
|
+
source_project_id: row.source_project_id,
|
|
17314
|
+
matched_on: classifyMatch(row, terms)
|
|
17315
|
+
}))
|
|
17316
|
+
};
|
|
17317
|
+
}
|
|
17318
|
+
function buildProjectTerms(detected) {
|
|
17319
|
+
const explicit = new Set;
|
|
17320
|
+
explicit.add(detected.name.toLowerCase());
|
|
17321
|
+
const canonicalParts = detected.canonical_id.toLowerCase().split("/");
|
|
17322
|
+
for (const part of canonicalParts) {
|
|
17323
|
+
if (part.length >= 4)
|
|
17324
|
+
explicit.add(part);
|
|
17114
17325
|
}
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
const date5 = obs.created_at.split("T")[0];
|
|
17118
|
-
const fromLabel = obs.source_project ? ` [from: ${obs.source_project}]` : "";
|
|
17119
|
-
const fileLabel = formatObservationFiles(obs);
|
|
17120
|
-
lines.push(`- **#${obs.id} [${obs.type}]** ${obs.title} (${date5}, q=${obs.quality.toFixed(1)})${fromLabel}${fileLabel}`);
|
|
17121
|
-
if (i < DETAILED_COUNT) {
|
|
17122
|
-
const detail = formatObservationDetailFromContext(obs);
|
|
17123
|
-
if (detail) {
|
|
17124
|
-
lines.push(detail);
|
|
17125
|
-
}
|
|
17126
|
-
}
|
|
17326
|
+
if (detected.name.toLowerCase() === "huginn") {
|
|
17327
|
+
explicit.add("aiserver");
|
|
17127
17328
|
}
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17133
|
-
|
|
17134
|
-
|
|
17329
|
+
return [...explicit].filter(Boolean);
|
|
17330
|
+
}
|
|
17331
|
+
function classifyMatch(row, terms) {
|
|
17332
|
+
const title = row.title.toLowerCase();
|
|
17333
|
+
const narrative = (row.narrative ?? "").toLowerCase();
|
|
17334
|
+
const files = `${row.files_read ?? ""} ${row.files_modified ?? ""}`.toLowerCase();
|
|
17335
|
+
for (const term of terms) {
|
|
17336
|
+
if (files.includes(term))
|
|
17337
|
+
return `files:${term}`;
|
|
17338
|
+
if (title.includes(term))
|
|
17339
|
+
return `title:${term}`;
|
|
17340
|
+
if (narrative.includes(term))
|
|
17341
|
+
return `narrative:${term}`;
|
|
17135
17342
|
}
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17343
|
+
return "related";
|
|
17344
|
+
}
|
|
17345
|
+
|
|
17346
|
+
// src/tools/reclassify-project-memory.ts
|
|
17347
|
+
function reclassifyProjectMemory(db, input) {
|
|
17348
|
+
const cwd = input.cwd ?? process.cwd();
|
|
17349
|
+
const detected = detectProject(cwd);
|
|
17350
|
+
const target = db.upsertProject({
|
|
17351
|
+
canonical_id: detected.canonical_id,
|
|
17352
|
+
name: detected.name,
|
|
17353
|
+
local_path: detected.local_path,
|
|
17354
|
+
remote_url: detected.remote_url
|
|
17355
|
+
});
|
|
17356
|
+
const related = getProjectRelatedWork(db, {
|
|
17357
|
+
cwd,
|
|
17358
|
+
user_id: input.user_id,
|
|
17359
|
+
limit: input.limit ?? 50
|
|
17360
|
+
}).related;
|
|
17361
|
+
let moved = 0;
|
|
17362
|
+
const candidates = related.map((item) => {
|
|
17363
|
+
const eligible = item.matched_on.startsWith("files:") || item.matched_on.startsWith("title:") || item.matched_on.startsWith("narrative:");
|
|
17364
|
+
const shouldMove = eligible && item.source_project_id !== target.id;
|
|
17365
|
+
if (shouldMove && input.dry_run !== true) {
|
|
17366
|
+
const ok = db.reassignObservationProject(item.id, target.id);
|
|
17367
|
+
if (ok)
|
|
17368
|
+
moved += 1;
|
|
17369
|
+
return {
|
|
17370
|
+
id: item.id,
|
|
17371
|
+
title: item.title,
|
|
17372
|
+
type: item.type,
|
|
17373
|
+
from: item.source_project,
|
|
17374
|
+
matched_on: item.matched_on,
|
|
17375
|
+
moved: ok
|
|
17376
|
+
};
|
|
17143
17377
|
}
|
|
17378
|
+
return {
|
|
17379
|
+
id: item.id,
|
|
17380
|
+
title: item.title,
|
|
17381
|
+
type: item.type,
|
|
17382
|
+
from: item.source_project,
|
|
17383
|
+
matched_on: item.matched_on,
|
|
17384
|
+
moved: false
|
|
17385
|
+
};
|
|
17386
|
+
});
|
|
17387
|
+
return {
|
|
17388
|
+
project: target.name,
|
|
17389
|
+
canonical_id: target.canonical_id,
|
|
17390
|
+
target_project_id: target.id,
|
|
17391
|
+
moved,
|
|
17392
|
+
candidates
|
|
17393
|
+
};
|
|
17394
|
+
}
|
|
17395
|
+
|
|
17396
|
+
// src/tools/activity-feed.ts
|
|
17397
|
+
function toPromptEvent(prompt) {
|
|
17398
|
+
return {
|
|
17399
|
+
kind: "prompt",
|
|
17400
|
+
created_at_epoch: prompt.created_at_epoch,
|
|
17401
|
+
session_id: prompt.session_id,
|
|
17402
|
+
id: prompt.id,
|
|
17403
|
+
title: `#${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`
|
|
17404
|
+
};
|
|
17405
|
+
}
|
|
17406
|
+
function toToolEvent(tool) {
|
|
17407
|
+
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? undefined;
|
|
17408
|
+
return {
|
|
17409
|
+
kind: "tool",
|
|
17410
|
+
created_at_epoch: tool.created_at_epoch,
|
|
17411
|
+
session_id: tool.session_id,
|
|
17412
|
+
id: tool.id,
|
|
17413
|
+
title: tool.tool_name,
|
|
17414
|
+
detail: detail?.replace(/\s+/g, " ").trim()
|
|
17415
|
+
};
|
|
17416
|
+
}
|
|
17417
|
+
function toObservationEvent(obs) {
|
|
17418
|
+
const detailBits = [];
|
|
17419
|
+
if (obs.source_tool)
|
|
17420
|
+
detailBits.push(`via ${obs.source_tool}`);
|
|
17421
|
+
if (typeof obs.source_prompt_number === "number") {
|
|
17422
|
+
detailBits.push(`#${obs.source_prompt_number}`);
|
|
17144
17423
|
}
|
|
17145
|
-
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17149
|
-
|
|
17150
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17424
|
+
return {
|
|
17425
|
+
kind: "observation",
|
|
17426
|
+
created_at_epoch: obs.created_at_epoch,
|
|
17427
|
+
session_id: obs.session_id,
|
|
17428
|
+
id: obs.id,
|
|
17429
|
+
title: obs.title,
|
|
17430
|
+
detail: detailBits.length > 0 ? detailBits.join(" · ") : undefined,
|
|
17431
|
+
observation_type: obs.type
|
|
17432
|
+
};
|
|
17433
|
+
}
|
|
17434
|
+
function toSummaryEvent(summary, fallbackEpoch = 0, extras) {
|
|
17435
|
+
const title = summary.request ?? summary.completed ?? summary.learned ?? summary.investigated;
|
|
17436
|
+
if (!title)
|
|
17437
|
+
return null;
|
|
17438
|
+
const detail = [
|
|
17439
|
+
extras?.capture_state ? `Capture: ${extras.capture_state}` : null,
|
|
17440
|
+
typeof extras?.prompt_count === "number" && typeof extras?.tool_event_count === "number" ? `Prompts/tools: ${extras.prompt_count}/${extras.tool_event_count}` : null,
|
|
17441
|
+
extras?.latest_request && extras.latest_request !== title ? `Latest request: ${extras.latest_request}` : null,
|
|
17442
|
+
summary.completed && summary.completed !== title ? `Completed: ${summary.completed}` : null,
|
|
17443
|
+
summary.learned ? `Learned: ${summary.learned}` : null,
|
|
17444
|
+
summary.next_steps ? `Next: ${summary.next_steps}` : null
|
|
17445
|
+
].filter(Boolean).join(" | ");
|
|
17446
|
+
return {
|
|
17447
|
+
kind: "summary",
|
|
17448
|
+
created_at_epoch: summary.created_at_epoch ?? fallbackEpoch,
|
|
17449
|
+
session_id: summary.session_id,
|
|
17450
|
+
id: summary.id,
|
|
17451
|
+
title: title.replace(/\s+/g, " ").trim(),
|
|
17452
|
+
detail: detail || undefined
|
|
17453
|
+
};
|
|
17454
|
+
}
|
|
17455
|
+
function compareEvents(a, b) {
|
|
17456
|
+
if (b.created_at_epoch !== a.created_at_epoch) {
|
|
17457
|
+
return b.created_at_epoch - a.created_at_epoch;
|
|
17155
17458
|
}
|
|
17156
|
-
const
|
|
17157
|
-
|
|
17158
|
-
|
|
17159
|
-
|
|
17459
|
+
const kindOrder = {
|
|
17460
|
+
summary: 0,
|
|
17461
|
+
observation: 1,
|
|
17462
|
+
tool: 2,
|
|
17463
|
+
prompt: 3
|
|
17464
|
+
};
|
|
17465
|
+
if (kindOrder[a.kind] !== kindOrder[b.kind]) {
|
|
17466
|
+
return kindOrder[a.kind] - kindOrder[b.kind];
|
|
17160
17467
|
}
|
|
17161
|
-
return
|
|
17162
|
-
`);
|
|
17468
|
+
return (b.id ?? 0) - (a.id ?? 0);
|
|
17163
17469
|
}
|
|
17164
|
-
function
|
|
17165
|
-
const
|
|
17166
|
-
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
|
|
17170
|
-
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17174
|
-
|
|
17175
|
-
|
|
17176
|
-
|
|
17177
|
-
|
|
17178
|
-
|
|
17470
|
+
function getActivityFeed(db, input) {
|
|
17471
|
+
const limit = Math.max(1, Math.min(input.limit ?? 30, 100));
|
|
17472
|
+
if (input.session_id) {
|
|
17473
|
+
const story = getSessionStory(db, { session_id: input.session_id });
|
|
17474
|
+
const project = story.session?.project_id !== null && story.session?.project_id !== undefined ? db.getProjectById(story.session.project_id)?.name : undefined;
|
|
17475
|
+
const events2 = [
|
|
17476
|
+
...story.summary ? [toSummaryEvent(story.summary, story.session?.completed_at_epoch ?? story.session?.started_at_epoch ?? 0, {
|
|
17477
|
+
capture_state: story.capture_state,
|
|
17478
|
+
prompt_count: story.prompts.length,
|
|
17479
|
+
tool_event_count: story.tool_events.length,
|
|
17480
|
+
latest_request: story.latest_request
|
|
17481
|
+
})].filter((event) => event !== null) : [],
|
|
17482
|
+
...story.prompts.map(toPromptEvent),
|
|
17483
|
+
...story.tool_events.map(toToolEvent),
|
|
17484
|
+
...story.observations.map(toObservationEvent)
|
|
17485
|
+
].sort(compareEvents).slice(0, limit);
|
|
17486
|
+
return { events: events2, project };
|
|
17487
|
+
}
|
|
17488
|
+
const projectScoped = input.project_scoped !== false;
|
|
17489
|
+
let projectName;
|
|
17490
|
+
if (projectScoped) {
|
|
17491
|
+
const cwd = input.cwd ?? process.cwd();
|
|
17492
|
+
const detected = detectProject(cwd);
|
|
17493
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
17494
|
+
if (project) {
|
|
17495
|
+
projectName = project.name;
|
|
17179
17496
|
}
|
|
17180
17497
|
}
|
|
17181
|
-
|
|
17182
|
-
}
|
|
17183
|
-
|
|
17184
|
-
|
|
17185
|
-
|
|
17186
|
-
|
|
17187
|
-
|
|
17188
|
-
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
|
|
17192
|
-
|
|
17193
|
-
|
|
17194
|
-
|
|
17195
|
-
|
|
17196
|
-
|
|
17197
|
-
|
|
17198
|
-
|
|
17199
|
-
|
|
17200
|
-
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17498
|
+
const prompts = getRecentRequests(db, { ...input, limit }).prompts;
|
|
17499
|
+
const tools = getRecentTools(db, { ...input, limit }).tool_events;
|
|
17500
|
+
const observations = getRecentActivity(db, {
|
|
17501
|
+
limit,
|
|
17502
|
+
project_scoped: input.project_scoped,
|
|
17503
|
+
cwd: input.cwd,
|
|
17504
|
+
user_id: input.user_id
|
|
17505
|
+
}).observations;
|
|
17506
|
+
const sessions = getRecentSessions(db, {
|
|
17507
|
+
limit,
|
|
17508
|
+
project_scoped: input.project_scoped,
|
|
17509
|
+
cwd: input.cwd,
|
|
17510
|
+
user_id: input.user_id
|
|
17511
|
+
}).sessions;
|
|
17512
|
+
const summaryEvents = sessions.map((session) => {
|
|
17513
|
+
const summary = db.getSessionSummary(session.session_id);
|
|
17514
|
+
if (!summary)
|
|
17515
|
+
return null;
|
|
17516
|
+
return toSummaryEvent(summary, session.completed_at_epoch ?? session.started_at_epoch ?? 0, {
|
|
17517
|
+
capture_state: session.capture_state,
|
|
17518
|
+
prompt_count: session.prompt_count,
|
|
17519
|
+
tool_event_count: session.tool_event_count,
|
|
17520
|
+
latest_request: summary.request
|
|
17521
|
+
});
|
|
17522
|
+
}).filter((event) => event !== null);
|
|
17523
|
+
const events = [
|
|
17524
|
+
...summaryEvents,
|
|
17525
|
+
...prompts.map(toPromptEvent),
|
|
17526
|
+
...tools.map(toToolEvent),
|
|
17527
|
+
...observations.map(toObservationEvent)
|
|
17528
|
+
].sort(compareEvents).slice(0, limit);
|
|
17529
|
+
return {
|
|
17530
|
+
events,
|
|
17531
|
+
project: projectName
|
|
17532
|
+
};
|
|
17209
17533
|
}
|
|
17210
|
-
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
|
|
17534
|
+
|
|
17535
|
+
// src/tools/capture-status.ts
|
|
17536
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
17537
|
+
import { homedir as homedir2 } from "node:os";
|
|
17538
|
+
import { join as join3 } from "node:path";
|
|
17539
|
+
var LEGACY_CODEX_SERVER_NAME = `candengo-${"mem"}`;
|
|
17540
|
+
function getCaptureStatus(db, input = {}) {
|
|
17541
|
+
const hours = Math.max(1, Math.min(input.lookback_hours ?? 24, 24 * 30));
|
|
17542
|
+
const sinceEpoch = Math.floor(Date.now() / 1000) - hours * 3600;
|
|
17543
|
+
const home = input.home_dir ?? homedir2();
|
|
17544
|
+
const claudeJson = join3(home, ".claude.json");
|
|
17545
|
+
const claudeSettings = join3(home, ".claude", "settings.json");
|
|
17546
|
+
const codexConfig = join3(home, ".codex", "config.toml");
|
|
17547
|
+
const codexHooks = join3(home, ".codex", "hooks.json");
|
|
17548
|
+
const claudeJsonContent = existsSync3(claudeJson) ? readFileSync3(claudeJson, "utf-8") : "";
|
|
17549
|
+
const claudeSettingsContent = existsSync3(claudeSettings) ? readFileSync3(claudeSettings, "utf-8") : "";
|
|
17550
|
+
const codexConfigContent = existsSync3(codexConfig) ? readFileSync3(codexConfig, "utf-8") : "";
|
|
17551
|
+
const codexHooksContent = existsSync3(codexHooks) ? readFileSync3(codexHooks, "utf-8") : "";
|
|
17552
|
+
const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
|
|
17553
|
+
const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
|
|
17554
|
+
const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME}]`);
|
|
17555
|
+
const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
|
|
17556
|
+
let claudeHookCount = 0;
|
|
17557
|
+
let claudeSessionStartHook = false;
|
|
17558
|
+
let claudeUserPromptHook = false;
|
|
17559
|
+
let claudePostToolHook = false;
|
|
17560
|
+
let claudeStopHook = false;
|
|
17561
|
+
if (claudeHooksRegistered) {
|
|
17562
|
+
try {
|
|
17563
|
+
const settings = JSON.parse(claudeSettingsContent);
|
|
17564
|
+
const hooks = settings?.hooks ?? {};
|
|
17565
|
+
claudeSessionStartHook = Array.isArray(hooks["SessionStart"]);
|
|
17566
|
+
claudeUserPromptHook = Array.isArray(hooks["UserPromptSubmit"]);
|
|
17567
|
+
claudePostToolHook = Array.isArray(hooks["PostToolUse"]);
|
|
17568
|
+
claudeStopHook = Array.isArray(hooks["Stop"]);
|
|
17569
|
+
for (const entries of Object.values(hooks)) {
|
|
17570
|
+
if (!Array.isArray(entries))
|
|
17571
|
+
continue;
|
|
17572
|
+
for (const entry of entries) {
|
|
17573
|
+
const e = entry;
|
|
17574
|
+
if (e.hooks?.some((h) => h.command?.includes("engrm") || h.command?.includes("session-start") || h.command?.includes("user-prompt-submit") || h.command?.includes("sentinel") || h.command?.includes("post-tool-use") || h.command?.includes("pre-compact") || h.command?.includes("stop") || h.command?.includes("elicitation"))) {
|
|
17575
|
+
claudeHookCount++;
|
|
17576
|
+
}
|
|
17577
|
+
}
|
|
17578
|
+
}
|
|
17579
|
+
} catch {}
|
|
17580
|
+
}
|
|
17581
|
+
let codexSessionStartHook = false;
|
|
17582
|
+
let codexStopHook = false;
|
|
17583
|
+
try {
|
|
17584
|
+
const hooks = codexHooksContent ? JSON.parse(codexHooksContent)?.hooks ?? {} : {};
|
|
17585
|
+
codexSessionStartHook = Array.isArray(hooks["SessionStart"]);
|
|
17586
|
+
codexStopHook = Array.isArray(hooks["Stop"]);
|
|
17587
|
+
} catch {}
|
|
17588
|
+
const visibilityClause = input.user_id ? " AND user_id = ?" : "";
|
|
17589
|
+
const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
|
|
17590
|
+
const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
|
|
17591
|
+
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
17592
|
+
const recentToolEvents = db.db.query(`SELECT COUNT(*) as count FROM tool_events
|
|
17593
|
+
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
17594
|
+
const recentSessionsWithRawCapture = db.db.query(`SELECT COUNT(*) as count
|
|
17595
|
+
FROM sessions s
|
|
17596
|
+
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
17597
|
+
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
17598
|
+
AND (
|
|
17599
|
+
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
17600
|
+
OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
17601
|
+
)`).get(...params)?.count ?? 0;
|
|
17602
|
+
const recentSessionsWithPartialCapture = db.db.query(`SELECT COUNT(*) as count
|
|
17603
|
+
FROM sessions s
|
|
17604
|
+
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
17605
|
+
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
17606
|
+
AND (
|
|
17607
|
+
(s.tool_calls_count > 0 AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id))
|
|
17608
|
+
OR (
|
|
17609
|
+
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
17610
|
+
AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
17611
|
+
)
|
|
17612
|
+
)`).get(...params)?.count ?? 0;
|
|
17613
|
+
const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
|
|
17614
|
+
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
17615
|
+
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
17616
|
+
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
17617
|
+
const latestToolEventEpoch = db.db.query(`SELECT created_at_epoch FROM tool_events
|
|
17618
|
+
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
17619
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
17620
|
+
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
17621
|
+
const latestPostToolHookEpoch = parseNullableInt(db.getSyncState("hook_post_tool_last_seen_epoch"));
|
|
17622
|
+
const latestPostToolParseStatus = db.getSyncState("hook_post_tool_last_parse_status");
|
|
17623
|
+
const latestPostToolName = db.getSyncState("hook_post_tool_last_tool_name");
|
|
17624
|
+
const schemaVersion = getSchemaVersion(db.db);
|
|
17625
|
+
return {
|
|
17626
|
+
schema_version: schemaVersion,
|
|
17627
|
+
schema_current: schemaVersion >= LATEST_SCHEMA_VERSION,
|
|
17628
|
+
claude_mcp_registered: claudeMcpRegistered,
|
|
17629
|
+
claude_hooks_registered: claudeHooksRegistered,
|
|
17630
|
+
claude_hook_count: claudeHookCount,
|
|
17631
|
+
claude_session_start_hook: claudeSessionStartHook,
|
|
17632
|
+
claude_user_prompt_hook: claudeUserPromptHook,
|
|
17633
|
+
claude_post_tool_hook: claudePostToolHook,
|
|
17634
|
+
claude_stop_hook: claudeStopHook,
|
|
17635
|
+
codex_mcp_registered: codexMcpRegistered,
|
|
17636
|
+
codex_hooks_registered: codexHooksRegistered,
|
|
17637
|
+
codex_session_start_hook: codexSessionStartHook,
|
|
17638
|
+
codex_stop_hook: codexStopHook,
|
|
17639
|
+
codex_raw_chronology_supported: false,
|
|
17640
|
+
recent_user_prompts: recentUserPrompts,
|
|
17641
|
+
recent_tool_events: recentToolEvents,
|
|
17642
|
+
recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
|
|
17643
|
+
recent_sessions_with_partial_capture: recentSessionsWithPartialCapture,
|
|
17644
|
+
latest_prompt_epoch: latestPromptEpoch,
|
|
17645
|
+
latest_tool_event_epoch: latestToolEventEpoch,
|
|
17646
|
+
latest_post_tool_hook_epoch: latestPostToolHookEpoch,
|
|
17647
|
+
latest_post_tool_parse_status: latestPostToolParseStatus,
|
|
17648
|
+
latest_post_tool_name: latestPostToolName,
|
|
17649
|
+
raw_capture_active: recentUserPrompts > 0 || recentToolEvents > 0 || recentSessionsWithRawCapture > 0
|
|
17650
|
+
};
|
|
17214
17651
|
}
|
|
17215
|
-
function
|
|
17652
|
+
function parseNullableInt(value) {
|
|
17216
17653
|
if (!value)
|
|
17217
|
-
return
|
|
17218
|
-
const
|
|
17219
|
-
|
|
17220
|
-
return false;
|
|
17221
|
-
return /[a-z]{3,}/i.test(compact);
|
|
17222
|
-
}
|
|
17223
|
-
function looksLikeFileOperationTitle2(value) {
|
|
17224
|
-
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
17654
|
+
return null;
|
|
17655
|
+
const parsed = Number.parseInt(value, 10);
|
|
17656
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
17225
17657
|
}
|
|
17226
|
-
|
|
17227
|
-
|
|
17658
|
+
|
|
17659
|
+
// src/tools/capture-quality.ts
|
|
17660
|
+
function getCaptureQuality(db, input = {}) {
|
|
17661
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
|
|
17662
|
+
const workspace = getWorkspaceMemoryIndex(db, {
|
|
17663
|
+
limit,
|
|
17664
|
+
user_id: input.user_id
|
|
17665
|
+
});
|
|
17666
|
+
const allSessions = getRecentSessions(db, {
|
|
17667
|
+
limit: 200,
|
|
17668
|
+
project_scoped: false,
|
|
17669
|
+
user_id: input.user_id
|
|
17670
|
+
}).sessions;
|
|
17671
|
+
const sessionStates = {
|
|
17672
|
+
rich: allSessions.filter((s) => s.capture_state === "rich").length,
|
|
17673
|
+
partial: allSessions.filter((s) => s.capture_state === "partial").length,
|
|
17674
|
+
summary_only: allSessions.filter((s) => s.capture_state === "summary-only").length,
|
|
17675
|
+
legacy: allSessions.filter((s) => s.capture_state === "legacy").length
|
|
17676
|
+
};
|
|
17677
|
+
const topProjects = workspace.projects.slice(0, limit).map((project) => ({
|
|
17678
|
+
name: project.name,
|
|
17679
|
+
canonical_id: project.canonical_id,
|
|
17680
|
+
observation_count: project.observation_count,
|
|
17681
|
+
session_count: project.session_count,
|
|
17682
|
+
prompt_count: project.prompt_count,
|
|
17683
|
+
tool_event_count: project.tool_event_count,
|
|
17684
|
+
assistant_checkpoint_count: project.assistant_checkpoint_count,
|
|
17685
|
+
raw_capture_state: project.prompt_count > 0 && project.tool_event_count > 0 ? "rich" : project.prompt_count > 0 || project.tool_event_count > 0 ? "partial" : "summary-only"
|
|
17686
|
+
}));
|
|
17687
|
+
const checkpointTypeRows = db.db.query(`SELECT type, COUNT(*) as count
|
|
17688
|
+
FROM observations
|
|
17689
|
+
WHERE source_tool = 'assistant-stop'
|
|
17690
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
17691
|
+
AND superseded_by IS NULL
|
|
17692
|
+
${input.user_id ? " AND (sensitivity != 'personal' OR user_id = ?)" : ""}
|
|
17693
|
+
GROUP BY type
|
|
17694
|
+
ORDER BY count DESC, type ASC
|
|
17695
|
+
LIMIT 8`).all(...input.user_id ? [input.user_id] : []);
|
|
17696
|
+
const provenanceTypeRows = db.db.query(`SELECT source_tool, type, COUNT(*) as count
|
|
17697
|
+
FROM observations
|
|
17698
|
+
WHERE source_tool IS NOT NULL
|
|
17699
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
17700
|
+
AND superseded_by IS NULL
|
|
17701
|
+
${input.user_id ? " AND (sensitivity != 'personal' OR user_id = ?)" : ""}
|
|
17702
|
+
GROUP BY source_tool, type
|
|
17703
|
+
ORDER BY source_tool ASC, count DESC, type ASC`).all(...input.user_id ? [input.user_id] : []);
|
|
17704
|
+
const provenanceTypeMix = Array.from(provenanceTypeRows.reduce((acc, row) => {
|
|
17705
|
+
const group = acc.get(row.source_tool) ?? [];
|
|
17706
|
+
group.push({ type: row.type, count: row.count });
|
|
17707
|
+
acc.set(row.source_tool, group);
|
|
17708
|
+
return acc;
|
|
17709
|
+
}, new Map).entries()).map(([tool, topTypes]) => ({
|
|
17710
|
+
tool,
|
|
17711
|
+
count: topTypes.reduce((sum, item) => sum + item.count, 0),
|
|
17712
|
+
top_types: topTypes.slice(0, 4)
|
|
17713
|
+
})).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 8);
|
|
17714
|
+
return {
|
|
17715
|
+
totals: {
|
|
17716
|
+
projects: workspace.projects.length,
|
|
17717
|
+
observations: workspace.totals.observations,
|
|
17718
|
+
sessions: workspace.totals.sessions,
|
|
17719
|
+
prompts: workspace.totals.prompts,
|
|
17720
|
+
tool_events: workspace.totals.tool_events,
|
|
17721
|
+
assistant_checkpoints: workspace.totals.assistant_checkpoints
|
|
17722
|
+
},
|
|
17723
|
+
session_states: sessionStates,
|
|
17724
|
+
projects_with_raw_capture: workspace.projects_with_raw_capture,
|
|
17725
|
+
provenance_summary: workspace.provenance_summary,
|
|
17726
|
+
provenance_type_mix: provenanceTypeMix,
|
|
17727
|
+
assistant_checkpoint_types: checkpointTypeRows.map((row) => ({
|
|
17728
|
+
type: row.type,
|
|
17729
|
+
count: row.count
|
|
17730
|
+
})),
|
|
17731
|
+
top_projects: topProjects
|
|
17732
|
+
};
|
|
17228
17733
|
}
|
|
17229
|
-
|
|
17734
|
+
|
|
17735
|
+
// src/tools/tool-memory-index.ts
|
|
17736
|
+
function parseConcepts(value) {
|
|
17230
17737
|
if (!value)
|
|
17231
17738
|
return [];
|
|
17232
|
-
return value.split(`
|
|
17233
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle2(line)).slice(0, limit);
|
|
17234
|
-
}
|
|
17235
|
-
function formatObservationDetailFromContext(obs) {
|
|
17236
|
-
if (obs.facts) {
|
|
17237
|
-
const bullets = parseFacts(obs.facts);
|
|
17238
|
-
if (bullets.length > 0) {
|
|
17239
|
-
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
17240
|
-
`);
|
|
17241
|
-
}
|
|
17242
|
-
}
|
|
17243
|
-
if (obs.narrative) {
|
|
17244
|
-
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
17245
|
-
return ` ${snippet}`;
|
|
17246
|
-
}
|
|
17247
|
-
return null;
|
|
17248
|
-
}
|
|
17249
|
-
function formatObservationDetail(obs) {
|
|
17250
|
-
if (obs.facts) {
|
|
17251
|
-
const bullets = parseFacts(obs.facts);
|
|
17252
|
-
if (bullets.length > 0) {
|
|
17253
|
-
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
17254
|
-
`);
|
|
17255
|
-
}
|
|
17256
|
-
}
|
|
17257
|
-
if (obs.narrative) {
|
|
17258
|
-
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
17259
|
-
return ` ${snippet}`;
|
|
17260
|
-
}
|
|
17261
|
-
return "";
|
|
17262
|
-
}
|
|
17263
|
-
function parseFacts(facts) {
|
|
17264
|
-
if (!facts)
|
|
17265
|
-
return [];
|
|
17266
17739
|
try {
|
|
17267
|
-
const parsed = JSON.parse(
|
|
17268
|
-
|
|
17269
|
-
return parsed.filter((f) => typeof f === "string" && f.length > 0);
|
|
17270
|
-
}
|
|
17740
|
+
const parsed = JSON.parse(value);
|
|
17741
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
17271
17742
|
} catch {
|
|
17272
|
-
|
|
17273
|
-
return [facts.trim()];
|
|
17274
|
-
}
|
|
17743
|
+
return [];
|
|
17275
17744
|
}
|
|
17276
|
-
return [];
|
|
17277
17745
|
}
|
|
17278
|
-
function
|
|
17746
|
+
function getToolMemoryIndex(db, input = {}) {
|
|
17747
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
|
|
17748
|
+
const projectScoped = input.project_scoped !== false;
|
|
17749
|
+
const cwd = input.cwd ?? process.cwd();
|
|
17750
|
+
const detected = projectScoped ? detectProject(cwd) : null;
|
|
17751
|
+
const project = detected ? db.getProjectByCanonicalId(detected.canonical_id) : null;
|
|
17752
|
+
const projectClause = project ? " AND o.project_id = ?" : "";
|
|
17753
|
+
const visibilityClause = input.user_id ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
|
|
17754
|
+
const baseParams = [];
|
|
17755
|
+
if (project)
|
|
17756
|
+
baseParams.push(project.id);
|
|
17757
|
+
if (input.user_id)
|
|
17758
|
+
baseParams.push(input.user_id);
|
|
17759
|
+
const toolRows = db.db.query(`SELECT
|
|
17760
|
+
o.source_tool,
|
|
17761
|
+
COUNT(*) as observation_count,
|
|
17762
|
+
MAX(o.created_at_epoch) as latest_epoch,
|
|
17763
|
+
COUNT(DISTINCT o.session_id) as session_count,
|
|
17764
|
+
MAX(o.source_prompt_number) as latest_prompt_number
|
|
17765
|
+
FROM observations o
|
|
17766
|
+
WHERE o.source_tool IS NOT NULL
|
|
17767
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
17768
|
+
AND o.superseded_by IS NULL
|
|
17769
|
+
${projectClause}
|
|
17770
|
+
${visibilityClause}
|
|
17771
|
+
GROUP BY o.source_tool
|
|
17772
|
+
ORDER BY observation_count DESC, latest_epoch DESC, o.source_tool ASC
|
|
17773
|
+
LIMIT ?`).all(...baseParams, limit);
|
|
17774
|
+
const tools = toolRows.map((row) => {
|
|
17775
|
+
const rowParams = [row.source_tool];
|
|
17776
|
+
if (project)
|
|
17777
|
+
rowParams.push(project.id);
|
|
17778
|
+
if (input.user_id)
|
|
17779
|
+
rowParams.push(input.user_id);
|
|
17780
|
+
const topTypes = db.db.query(`SELECT
|
|
17781
|
+
o.type,
|
|
17782
|
+
COUNT(*) as count
|
|
17783
|
+
FROM observations o
|
|
17784
|
+
WHERE o.source_tool = ?
|
|
17785
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
17786
|
+
AND o.superseded_by IS NULL
|
|
17787
|
+
${projectClause}
|
|
17788
|
+
${visibilityClause}
|
|
17789
|
+
GROUP BY o.type
|
|
17790
|
+
ORDER BY count DESC, o.type ASC
|
|
17791
|
+
LIMIT 5`).all(...rowParams).map((typeRow) => ({
|
|
17792
|
+
type: typeRow.type,
|
|
17793
|
+
count: typeRow.count
|
|
17794
|
+
}));
|
|
17795
|
+
const observationRows = db.db.query(`SELECT o.title, o.concepts
|
|
17796
|
+
FROM observations o
|
|
17797
|
+
WHERE o.source_tool = ?
|
|
17798
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
17799
|
+
AND o.superseded_by IS NULL
|
|
17800
|
+
${projectClause}
|
|
17801
|
+
${visibilityClause}
|
|
17802
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
17803
|
+
LIMIT 50`).all(...rowParams);
|
|
17804
|
+
const topPlugins = Array.from(observationRows.reduce((acc, obs) => {
|
|
17805
|
+
for (const concept of parseConcepts(obs.concepts)) {
|
|
17806
|
+
if (!concept.startsWith("plugin:"))
|
|
17807
|
+
continue;
|
|
17808
|
+
const plugin = concept.slice("plugin:".length);
|
|
17809
|
+
if (!plugin)
|
|
17810
|
+
continue;
|
|
17811
|
+
acc.set(plugin, (acc.get(plugin) ?? 0) + 1);
|
|
17812
|
+
}
|
|
17813
|
+
return acc;
|
|
17814
|
+
}, new Map).entries()).map(([plugin, count]) => ({ plugin, count })).sort((a, b) => b.count - a.count || a.plugin.localeCompare(b.plugin)).slice(0, 4);
|
|
17815
|
+
const sampleTitles = observationRows.map((sample) => sample.title).slice(0, 4);
|
|
17816
|
+
return {
|
|
17817
|
+
tool: row.source_tool,
|
|
17818
|
+
observation_count: row.observation_count,
|
|
17819
|
+
latest_epoch: row.latest_epoch,
|
|
17820
|
+
top_types: topTypes,
|
|
17821
|
+
top_plugins: topPlugins,
|
|
17822
|
+
sample_titles: sampleTitles,
|
|
17823
|
+
session_count: row.session_count,
|
|
17824
|
+
latest_prompt_number: row.latest_prompt_number
|
|
17825
|
+
};
|
|
17826
|
+
});
|
|
17279
17827
|
return {
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
title: obs.title,
|
|
17283
|
-
narrative: obs.narrative,
|
|
17284
|
-
facts: obs.facts,
|
|
17285
|
-
files_read: obs.files_read,
|
|
17286
|
-
files_modified: obs.files_modified,
|
|
17287
|
-
quality: obs.quality,
|
|
17288
|
-
created_at: obs.created_at,
|
|
17289
|
-
...obs._source_project ? { source_project: obs._source_project } : {}
|
|
17828
|
+
project: project?.name,
|
|
17829
|
+
tools
|
|
17290
17830
|
};
|
|
17291
17831
|
}
|
|
17292
|
-
|
|
17293
|
-
|
|
17294
|
-
|
|
17295
|
-
return ` · files: ${truncateText(modified.slice(0, 2).join(", "), 60)}`;
|
|
17296
|
-
}
|
|
17297
|
-
const read = parseJsonStringArray(obs.files_read);
|
|
17298
|
-
if (read.length > 0) {
|
|
17299
|
-
return ` · read: ${truncateText(read.slice(0, 2).join(", "), 60)}`;
|
|
17300
|
-
}
|
|
17301
|
-
return "";
|
|
17302
|
-
}
|
|
17303
|
-
function parseJsonStringArray(value) {
|
|
17832
|
+
|
|
17833
|
+
// src/tools/session-tool-memory.ts
|
|
17834
|
+
function parseConcepts2(value) {
|
|
17304
17835
|
if (!value)
|
|
17305
17836
|
return [];
|
|
17306
17837
|
try {
|
|
17307
17838
|
const parsed = JSON.parse(value);
|
|
17308
|
-
|
|
17309
|
-
return [];
|
|
17310
|
-
return parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
17839
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
17311
17840
|
} catch {
|
|
17312
17841
|
return [];
|
|
17313
17842
|
}
|
|
17314
17843
|
}
|
|
17315
|
-
function
|
|
17316
|
-
const
|
|
17317
|
-
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17330
|
-
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
|
|
17334
|
-
|
|
17335
|
-
|
|
17336
|
-
|
|
17337
|
-
|
|
17338
|
-
|
|
17339
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
17359
|
-
|
|
17360
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17365
|
-
|
|
17366
|
-
|
|
17367
|
-
const title = stripInlineSectionLabel(obs.title);
|
|
17368
|
-
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
17369
|
-
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle2(title))
|
|
17370
|
-
continue;
|
|
17371
|
-
seen.add(normalized);
|
|
17372
|
-
picked.push(title);
|
|
17373
|
-
if (picked.length >= 5)
|
|
17374
|
-
break;
|
|
17375
|
-
}
|
|
17376
|
-
return picked;
|
|
17844
|
+
function getSessionToolMemory(db, input) {
|
|
17845
|
+
const toolEvents = db.getSessionToolEvents(input.session_id, 500);
|
|
17846
|
+
const observations = db.getObservationsBySession(input.session_id);
|
|
17847
|
+
const toolEventCounts = toolEvents.reduce((acc, event) => {
|
|
17848
|
+
acc.set(event.tool_name, (acc.get(event.tool_name) ?? 0) + 1);
|
|
17849
|
+
return acc;
|
|
17850
|
+
}, new Map);
|
|
17851
|
+
const observationGroups = observations.reduce((acc, obs) => {
|
|
17852
|
+
if (!obs.source_tool)
|
|
17853
|
+
return acc;
|
|
17854
|
+
const group = acc.get(obs.source_tool) ?? [];
|
|
17855
|
+
group.push(obs);
|
|
17856
|
+
acc.set(obs.source_tool, group);
|
|
17857
|
+
return acc;
|
|
17858
|
+
}, new Map);
|
|
17859
|
+
const tools = Array.from(observationGroups.entries()).map(([tool, groupedObservations]) => {
|
|
17860
|
+
const topTypes = Array.from(groupedObservations.reduce((acc, obs) => {
|
|
17861
|
+
acc.set(obs.type, (acc.get(obs.type) ?? 0) + 1);
|
|
17862
|
+
return acc;
|
|
17863
|
+
}, new Map).entries()).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
|
|
17864
|
+
const sampleTitles = groupedObservations.map((obs) => obs.title).filter((title, index, all) => all.indexOf(title) === index).slice(0, 4);
|
|
17865
|
+
const topPlugins = Array.from(groupedObservations.reduce((acc, obs) => {
|
|
17866
|
+
for (const concept of parseConcepts2(obs.concepts)) {
|
|
17867
|
+
if (!concept.startsWith("plugin:"))
|
|
17868
|
+
continue;
|
|
17869
|
+
const plugin = concept.slice("plugin:".length);
|
|
17870
|
+
if (!plugin)
|
|
17871
|
+
continue;
|
|
17872
|
+
acc.set(plugin, (acc.get(plugin) ?? 0) + 1);
|
|
17873
|
+
}
|
|
17874
|
+
return acc;
|
|
17875
|
+
}, new Map).entries()).map(([plugin, count]) => ({ plugin, count })).sort((a, b) => b.count - a.count || a.plugin.localeCompare(b.plugin)).slice(0, 4);
|
|
17876
|
+
const latestPromptNumber = groupedObservations.reduce((latest, obs) => typeof obs.source_prompt_number === "number" ? latest === null || obs.source_prompt_number > latest ? obs.source_prompt_number : latest : latest, null);
|
|
17877
|
+
return {
|
|
17878
|
+
tool,
|
|
17879
|
+
tool_event_count: toolEventCounts.get(tool) ?? 0,
|
|
17880
|
+
observation_count: groupedObservations.length,
|
|
17881
|
+
top_types: topTypes,
|
|
17882
|
+
top_plugins: topPlugins,
|
|
17883
|
+
sample_titles: sampleTitles,
|
|
17884
|
+
latest_prompt_number: latestPromptNumber
|
|
17885
|
+
};
|
|
17886
|
+
}).sort((a, b) => b.observation_count - a.observation_count || a.tool.localeCompare(b.tool));
|
|
17887
|
+
const toolsWithoutMemory = Array.from(toolEventCounts.entries()).filter(([tool]) => !observationGroups.has(tool)).map(([tool, toolEventCount]) => ({
|
|
17888
|
+
tool,
|
|
17889
|
+
tool_event_count: toolEventCount
|
|
17890
|
+
})).sort((a, b) => b.tool_event_count - a.tool_event_count || a.tool.localeCompare(b.tool));
|
|
17891
|
+
return {
|
|
17892
|
+
session_id: input.session_id,
|
|
17893
|
+
tools,
|
|
17894
|
+
tools_without_memory: toolsWithoutMemory
|
|
17895
|
+
};
|
|
17377
17896
|
}
|
|
17378
17897
|
|
|
17379
17898
|
// src/tools/session-context.ts
|
|
@@ -17386,6 +17905,7 @@ function getSessionContext(db, input) {
|
|
|
17386
17905
|
});
|
|
17387
17906
|
if (!context)
|
|
17388
17907
|
return null;
|
|
17908
|
+
const preview = formatContextForInjection(context);
|
|
17389
17909
|
const recentRequests = context.recentPrompts?.length ?? 0;
|
|
17390
17910
|
const recentTools = context.recentToolEvents?.length ?? 0;
|
|
17391
17911
|
const captureState = recentRequests > 0 && recentTools > 0 ? "rich" : recentRequests > 0 || recentTools > 0 ? "partial" : "summary-only";
|
|
@@ -17402,19 +17922,21 @@ function getSessionContext(db, input) {
|
|
|
17402
17922
|
hot_files: hotFiles,
|
|
17403
17923
|
capture_state: captureState,
|
|
17404
17924
|
raw_capture_active: recentRequests > 0 || recentTools > 0,
|
|
17405
|
-
|
|
17925
|
+
estimated_read_tokens: estimateTokens(preview),
|
|
17926
|
+
suggested_tools: buildSuggestedTools2(context),
|
|
17927
|
+
preview
|
|
17406
17928
|
};
|
|
17407
17929
|
}
|
|
17408
17930
|
function buildHotFiles(context) {
|
|
17409
17931
|
const counts = new Map;
|
|
17410
17932
|
for (const obs of context.observations) {
|
|
17411
|
-
for (const path of [...
|
|
17933
|
+
for (const path of [...parseJsonArray2(obs.files_read), ...parseJsonArray2(obs.files_modified)]) {
|
|
17412
17934
|
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
17413
17935
|
}
|
|
17414
17936
|
}
|
|
17415
17937
|
return Array.from(counts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || a.path.localeCompare(b.path)).slice(0, 6);
|
|
17416
17938
|
}
|
|
17417
|
-
function
|
|
17939
|
+
function parseJsonArray2(value) {
|
|
17418
17940
|
if (!value)
|
|
17419
17941
|
return [];
|
|
17420
17942
|
try {
|
|
@@ -17424,6 +17946,161 @@ function parseJsonArray(value) {
|
|
|
17424
17946
|
return [];
|
|
17425
17947
|
}
|
|
17426
17948
|
}
|
|
17949
|
+
function buildSuggestedTools2(context) {
|
|
17950
|
+
const tools = [];
|
|
17951
|
+
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
17952
|
+
tools.push("recent_sessions");
|
|
17953
|
+
}
|
|
17954
|
+
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
|
|
17955
|
+
tools.push("activity_feed");
|
|
17956
|
+
}
|
|
17957
|
+
if (context.observations.length > 0) {
|
|
17958
|
+
tools.push("tool_memory_index", "capture_git_worktree");
|
|
17959
|
+
}
|
|
17960
|
+
return Array.from(new Set(tools)).slice(0, 4);
|
|
17961
|
+
}
|
|
17962
|
+
|
|
17963
|
+
// src/tools/capture-git-worktree.ts
|
|
17964
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
17965
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
17966
|
+
import { resolve as resolve2 } from "node:path";
|
|
17967
|
+
function runGitCommand(cwd, args) {
|
|
17968
|
+
return execSync2(`git ${args.join(" ")}`, {
|
|
17969
|
+
cwd,
|
|
17970
|
+
encoding: "utf-8",
|
|
17971
|
+
timeout: 8000,
|
|
17972
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
17973
|
+
}).trim();
|
|
17974
|
+
}
|
|
17975
|
+
function captureGitWorktree(input = {}) {
|
|
17976
|
+
const cwd = resolve2(input.cwd ?? process.cwd());
|
|
17977
|
+
if (!existsSync4(cwd)) {
|
|
17978
|
+
throw new Error(`Path does not exist: ${cwd}`);
|
|
17979
|
+
}
|
|
17980
|
+
try {
|
|
17981
|
+
runGitCommand(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
17982
|
+
} catch {
|
|
17983
|
+
throw new Error(`Not a git repository: ${cwd}`);
|
|
17984
|
+
}
|
|
17985
|
+
const diffArgs = input.staged ? ["diff", "--cached", "--no-ext-diff", "--minimal"] : ["diff", "--no-ext-diff", "--minimal"];
|
|
17986
|
+
const fileArgs = input.staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only"];
|
|
17987
|
+
const diff = runGitCommand(cwd, diffArgs);
|
|
17988
|
+
const files = runGitCommand(cwd, fileArgs).split(`
|
|
17989
|
+
`).map((line) => line.trim()).filter(Boolean);
|
|
17990
|
+
return {
|
|
17991
|
+
cwd,
|
|
17992
|
+
diff,
|
|
17993
|
+
files
|
|
17994
|
+
};
|
|
17995
|
+
}
|
|
17996
|
+
|
|
17997
|
+
// src/tools/capture-repo-scan.ts
|
|
17998
|
+
import { execSync as execSync3 } from "node:child_process";
|
|
17999
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
18000
|
+
import { resolve as resolve3 } from "node:path";
|
|
18001
|
+
function runRg(cwd, args) {
|
|
18002
|
+
return execSync3(`rg ${args.join(" ")}`, {
|
|
18003
|
+
cwd,
|
|
18004
|
+
encoding: "utf-8",
|
|
18005
|
+
timeout: 8000,
|
|
18006
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
18007
|
+
}).trim();
|
|
18008
|
+
}
|
|
18009
|
+
function safeRunRg(cwd, args) {
|
|
18010
|
+
try {
|
|
18011
|
+
return runRg(cwd, args);
|
|
18012
|
+
} catch {
|
|
18013
|
+
return "";
|
|
18014
|
+
}
|
|
18015
|
+
}
|
|
18016
|
+
function quotePattern(pattern) {
|
|
18017
|
+
return JSON.stringify(pattern);
|
|
18018
|
+
}
|
|
18019
|
+
function parseUniqueFiles(output) {
|
|
18020
|
+
return Array.from(new Set(output.split(`
|
|
18021
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => line.split(":")[0] ?? line).filter(Boolean)));
|
|
18022
|
+
}
|
|
18023
|
+
function buildDefaultFindings(cwd) {
|
|
18024
|
+
const findings = [];
|
|
18025
|
+
const todoOutput = safeRunRg(cwd, ["-n", "-S", "-m", "12", quotePattern("TODO|FIXME|HACK|XXX"), "."]);
|
|
18026
|
+
const todoFiles = parseUniqueFiles(todoOutput);
|
|
18027
|
+
if (todoFiles.length > 0) {
|
|
18028
|
+
findings.push({
|
|
18029
|
+
kind: "risk",
|
|
18030
|
+
title: `Outstanding TODO/FIXME markers found in ${todoFiles.length} files`,
|
|
18031
|
+
severity: todoFiles.length >= 5 ? "high" : "medium",
|
|
18032
|
+
file: todoFiles[0],
|
|
18033
|
+
detail: todoOutput.split(`
|
|
18034
|
+
`).slice(0, 3).join(`
|
|
18035
|
+
`)
|
|
18036
|
+
});
|
|
18037
|
+
}
|
|
18038
|
+
const authOutput = safeRunRg(cwd, ["-l", "-S", quotePattern("auth|oauth|token|session|middleware"), "."]);
|
|
18039
|
+
const authFiles = parseUniqueFiles(authOutput).slice(0, 8);
|
|
18040
|
+
if (authFiles.length > 0) {
|
|
18041
|
+
findings.push({
|
|
18042
|
+
kind: "discovery",
|
|
18043
|
+
title: `Auth/session logic concentrated in ${authFiles.length} files`,
|
|
18044
|
+
file: authFiles[0],
|
|
18045
|
+
detail: authFiles.join(", ")
|
|
18046
|
+
});
|
|
18047
|
+
}
|
|
18048
|
+
const routeOutput = safeRunRg(cwd, ["-l", "-S", quotePattern("router|route|endpoint|express\\.Router|FastAPI|APIRouter"), "."]);
|
|
18049
|
+
const routeFiles = parseUniqueFiles(routeOutput).slice(0, 8);
|
|
18050
|
+
if (routeFiles.length > 0) {
|
|
18051
|
+
findings.push({
|
|
18052
|
+
kind: "pattern",
|
|
18053
|
+
title: `Routing/API structure appears in ${routeFiles.length} files`,
|
|
18054
|
+
file: routeFiles[0],
|
|
18055
|
+
detail: routeFiles.join(", ")
|
|
18056
|
+
});
|
|
18057
|
+
}
|
|
18058
|
+
const testOutput = safeRunRg(cwd, ["-l", "-S", quotePattern("\\.test\\.|\\.spec\\.|describe\\(|test\\("), "."]);
|
|
18059
|
+
const testFiles = parseUniqueFiles(testOutput).slice(0, 8);
|
|
18060
|
+
if (testFiles.length > 0) {
|
|
18061
|
+
findings.push({
|
|
18062
|
+
kind: "change",
|
|
18063
|
+
title: `Test-related files present across ${testFiles.length} files`,
|
|
18064
|
+
file: testFiles[0],
|
|
18065
|
+
detail: testFiles.join(", ")
|
|
18066
|
+
});
|
|
18067
|
+
}
|
|
18068
|
+
return findings;
|
|
18069
|
+
}
|
|
18070
|
+
function buildFocusFindings(cwd, focus) {
|
|
18071
|
+
const findings = [];
|
|
18072
|
+
for (const topic of focus) {
|
|
18073
|
+
const cleaned = topic.trim();
|
|
18074
|
+
if (!cleaned)
|
|
18075
|
+
continue;
|
|
18076
|
+
const output = safeRunRg(cwd, ["-n", "-S", "-m", "12", quotePattern(cleaned), "."]);
|
|
18077
|
+
const files = parseUniqueFiles(output).slice(0, 8);
|
|
18078
|
+
if (files.length === 0)
|
|
18079
|
+
continue;
|
|
18080
|
+
findings.push({
|
|
18081
|
+
kind: "discovery",
|
|
18082
|
+
title: `Found ${cleaned} references in ${files.length} files`,
|
|
18083
|
+
file: files[0],
|
|
18084
|
+
detail: files.join(", ")
|
|
18085
|
+
});
|
|
18086
|
+
}
|
|
18087
|
+
return findings;
|
|
18088
|
+
}
|
|
18089
|
+
function captureRepoScan(input = {}) {
|
|
18090
|
+
const cwd = resolve3(input.cwd ?? process.cwd());
|
|
18091
|
+
if (!existsSync5(cwd)) {
|
|
18092
|
+
throw new Error(`Path does not exist: ${cwd}`);
|
|
18093
|
+
}
|
|
18094
|
+
const focus = (input.focus ?? []).map((item) => item.trim()).filter(Boolean);
|
|
18095
|
+
const findings = [
|
|
18096
|
+
...buildDefaultFindings(cwd),
|
|
18097
|
+
...buildFocusFindings(cwd, focus)
|
|
18098
|
+
].slice(0, Math.max(1, Math.min(input.max_findings ?? 8, 20)));
|
|
18099
|
+
return {
|
|
18100
|
+
cwd,
|
|
18101
|
+
findings
|
|
18102
|
+
};
|
|
18103
|
+
}
|
|
17427
18104
|
|
|
17428
18105
|
// src/tools/send-message.ts
|
|
17429
18106
|
async function sendMessage(db, config2, input) {
|
|
@@ -17549,10 +18226,7 @@ function countPresentSections(summary) {
|
|
|
17549
18226
|
].filter(hasContent).length;
|
|
17550
18227
|
}
|
|
17551
18228
|
function extractSectionItems(section) {
|
|
17552
|
-
|
|
17553
|
-
return [];
|
|
17554
|
-
return section.split(`
|
|
17555
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
|
|
18229
|
+
return extractSummaryItems(section);
|
|
17556
18230
|
}
|
|
17557
18231
|
function extractObservationTitles(observations, types) {
|
|
17558
18232
|
const typeSet = new Set(types);
|
|
@@ -17994,6 +18668,8 @@ function buildVectorDocument(obs, config2, project) {
|
|
|
17994
18668
|
concepts: obs.concepts ? JSON.parse(obs.concepts) : [],
|
|
17995
18669
|
files_read: obs.files_read ? JSON.parse(obs.files_read) : [],
|
|
17996
18670
|
files_modified: obs.files_modified ? JSON.parse(obs.files_modified) : [],
|
|
18671
|
+
source_tool: obs.source_tool,
|
|
18672
|
+
source_prompt_number: obs.source_prompt_number,
|
|
17997
18673
|
session_id: obs.session_id,
|
|
17998
18674
|
created_at_epoch: obs.created_at_epoch,
|
|
17999
18675
|
created_at: obs.created_at,
|
|
@@ -18047,6 +18723,8 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
|
|
|
18047
18723
|
recent_tool_commands: captureContext?.recent_tool_commands ?? [],
|
|
18048
18724
|
hot_files: captureContext?.hot_files ?? [],
|
|
18049
18725
|
recent_outcomes: captureContext?.recent_outcomes ?? [],
|
|
18726
|
+
observation_source_tools: captureContext?.observation_source_tools ?? [],
|
|
18727
|
+
latest_observation_prompt_number: captureContext?.latest_observation_prompt_number ?? null,
|
|
18050
18728
|
decisions_count: valueSignals.decisions_count,
|
|
18051
18729
|
lessons_count: valueSignals.lessons_count,
|
|
18052
18730
|
discoveries_count: valueSignals.discoveries_count,
|
|
@@ -18159,10 +18837,7 @@ function countPresentSections2(summary) {
|
|
|
18159
18837
|
].filter((value) => Boolean(value && value.trim())).length;
|
|
18160
18838
|
}
|
|
18161
18839
|
function extractSectionItems2(section) {
|
|
18162
|
-
|
|
18163
|
-
return [];
|
|
18164
|
-
return section.split(`
|
|
18165
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
|
|
18840
|
+
return extractSummaryItems(section, 4);
|
|
18166
18841
|
}
|
|
18167
18842
|
function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
18168
18843
|
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
@@ -18170,11 +18845,18 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
|
18170
18845
|
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
18171
18846
|
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
18172
18847
|
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
18173
|
-
...
|
|
18174
|
-
...
|
|
18848
|
+
...parseJsonArray3(obs.files_modified),
|
|
18849
|
+
...parseJsonArray3(obs.files_read)
|
|
18175
18850
|
]).filter(Boolean))].slice(0, 6);
|
|
18176
18851
|
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);
|
|
18177
18852
|
const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
|
|
18853
|
+
const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
|
|
18854
|
+
if (!obs.source_tool)
|
|
18855
|
+
return acc;
|
|
18856
|
+
acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
|
|
18857
|
+
return acc;
|
|
18858
|
+
}, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
|
|
18859
|
+
const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
|
|
18178
18860
|
return {
|
|
18179
18861
|
prompt_count: prompts.length,
|
|
18180
18862
|
tool_event_count: toolEvents.length,
|
|
@@ -18184,10 +18866,12 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
|
18184
18866
|
recent_tool_commands: recentToolCommands,
|
|
18185
18867
|
capture_state: captureState,
|
|
18186
18868
|
hot_files: hotFiles,
|
|
18187
|
-
recent_outcomes: recentOutcomes
|
|
18869
|
+
recent_outcomes: recentOutcomes,
|
|
18870
|
+
observation_source_tools: observationSourceTools,
|
|
18871
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
18188
18872
|
};
|
|
18189
18873
|
}
|
|
18190
|
-
function
|
|
18874
|
+
function parseJsonArray3(value) {
|
|
18191
18875
|
if (!value)
|
|
18192
18876
|
return [];
|
|
18193
18877
|
try {
|
|
@@ -18449,7 +19133,7 @@ async function backfillEmbeddings(db, batchSize = 50) {
|
|
|
18449
19133
|
}
|
|
18450
19134
|
|
|
18451
19135
|
// src/packs/recommender.ts
|
|
18452
|
-
import { existsSync as
|
|
19136
|
+
import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
18453
19137
|
import { join as join4, basename as basename2, dirname as dirname2 } from "node:path";
|
|
18454
19138
|
import { fileURLToPath } from "node:url";
|
|
18455
19139
|
function getPacksDir() {
|
|
@@ -18458,7 +19142,7 @@ function getPacksDir() {
|
|
|
18458
19142
|
}
|
|
18459
19143
|
function loadPack(name) {
|
|
18460
19144
|
const packPath = join4(getPacksDir(), `${name}.json`);
|
|
18461
|
-
if (!
|
|
19145
|
+
if (!existsSync6(packPath))
|
|
18462
19146
|
return null;
|
|
18463
19147
|
try {
|
|
18464
19148
|
const raw = readFileSync4(packPath, "utf-8");
|
|
@@ -18469,7 +19153,7 @@ function loadPack(name) {
|
|
|
18469
19153
|
}
|
|
18470
19154
|
|
|
18471
19155
|
// src/server.ts
|
|
18472
|
-
import { existsSync as
|
|
19156
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
18473
19157
|
import { join as join5 } from "node:path";
|
|
18474
19158
|
import { homedir as homedir3 } from "node:os";
|
|
18475
19159
|
|
|
@@ -18798,6 +19482,218 @@ function reduceGitDiffToMemory(input) {
|
|
|
18798
19482
|
};
|
|
18799
19483
|
}
|
|
18800
19484
|
|
|
19485
|
+
// src/plugins/repo-scan.ts
|
|
19486
|
+
function uniq2(items) {
|
|
19487
|
+
const seen = new Set;
|
|
19488
|
+
const out = [];
|
|
19489
|
+
for (const item of items) {
|
|
19490
|
+
const cleaned = item.trim();
|
|
19491
|
+
if (!cleaned)
|
|
19492
|
+
continue;
|
|
19493
|
+
const key = cleaned.toLowerCase();
|
|
19494
|
+
if (seen.has(key))
|
|
19495
|
+
continue;
|
|
19496
|
+
seen.add(key);
|
|
19497
|
+
out.push(cleaned);
|
|
19498
|
+
}
|
|
19499
|
+
return out;
|
|
19500
|
+
}
|
|
19501
|
+
function countByKind(findings) {
|
|
19502
|
+
return findings.reduce((acc, finding) => {
|
|
19503
|
+
acc[finding.kind] += 1;
|
|
19504
|
+
return acc;
|
|
19505
|
+
}, { risk: 0, discovery: 0, pattern: 0, change: 0 });
|
|
19506
|
+
}
|
|
19507
|
+
function inferType(findings) {
|
|
19508
|
+
if (findings.some((finding) => finding.kind === "risk"))
|
|
19509
|
+
return "pattern";
|
|
19510
|
+
if (findings.some((finding) => finding.kind === "discovery"))
|
|
19511
|
+
return "discovery";
|
|
19512
|
+
return "change";
|
|
19513
|
+
}
|
|
19514
|
+
function inferTitle2(summary, findings) {
|
|
19515
|
+
const cleanedSummary = summary?.trim();
|
|
19516
|
+
if (cleanedSummary && cleanedSummary.length >= 8) {
|
|
19517
|
+
return cleanedSummary.replace(/\.$/, "");
|
|
19518
|
+
}
|
|
19519
|
+
const firstRisk = findings.find((finding) => finding.kind === "risk");
|
|
19520
|
+
if (firstRisk) {
|
|
19521
|
+
return `Repo scan: ${firstRisk.title}`;
|
|
19522
|
+
}
|
|
19523
|
+
const firstDiscovery = findings.find((finding) => finding.kind === "discovery");
|
|
19524
|
+
if (firstDiscovery) {
|
|
19525
|
+
return `Repo scan: ${firstDiscovery.title}`;
|
|
19526
|
+
}
|
|
19527
|
+
return "Repo scan findings";
|
|
19528
|
+
}
|
|
19529
|
+
function buildFacts2(findings) {
|
|
19530
|
+
const facts = [];
|
|
19531
|
+
const counts = countByKind(findings);
|
|
19532
|
+
facts.push(`Repo scan findings: ${counts.risk} risks, ${counts.discovery} discoveries, ${counts.pattern} patterns, ${counts.change} changes`);
|
|
19533
|
+
for (const finding of findings.slice(0, 3)) {
|
|
19534
|
+
const severity = finding.severity ? `${finding.severity} ` : "";
|
|
19535
|
+
const file2 = finding.file ? ` (${finding.file})` : "";
|
|
19536
|
+
facts.push(`${severity}${finding.title}${file2}`.trim());
|
|
19537
|
+
}
|
|
19538
|
+
return uniq2(facts).slice(0, 4);
|
|
19539
|
+
}
|
|
19540
|
+
function buildTags(findings) {
|
|
19541
|
+
const tags = ["repo-scan"];
|
|
19542
|
+
if (findings.some((finding) => finding.kind === "risk"))
|
|
19543
|
+
tags.push("risk-finding");
|
|
19544
|
+
if (findings.some((finding) => finding.kind === "discovery"))
|
|
19545
|
+
tags.push("discovery");
|
|
19546
|
+
if (findings.some((finding) => finding.kind === "pattern"))
|
|
19547
|
+
tags.push("pattern");
|
|
19548
|
+
for (const severity of ["critical", "high", "medium", "low"]) {
|
|
19549
|
+
if (findings.some((finding) => finding.severity === severity)) {
|
|
19550
|
+
tags.push(`severity:${severity}`);
|
|
19551
|
+
}
|
|
19552
|
+
}
|
|
19553
|
+
return uniq2(tags);
|
|
19554
|
+
}
|
|
19555
|
+
function buildSummary2(summary, findings) {
|
|
19556
|
+
if (summary && summary.trim().length >= 16) {
|
|
19557
|
+
return summary.trim();
|
|
19558
|
+
}
|
|
19559
|
+
const counts = countByKind(findings);
|
|
19560
|
+
return `Reduced a repository scan into reusable memory with ` + `${counts.risk} risks, ${counts.discovery} discoveries, ` + `${counts.pattern} patterns, and ${counts.change} changes.`;
|
|
19561
|
+
}
|
|
19562
|
+
function reduceRepoScanToMemory(input) {
|
|
19563
|
+
const findings = input.findings.filter((finding) => finding.title.trim().length > 0);
|
|
19564
|
+
const files = uniq2(findings.map((finding) => finding.file ?? "").filter(Boolean)).slice(0, 6);
|
|
19565
|
+
return {
|
|
19566
|
+
plugin_id: "engrm.repo-scan",
|
|
19567
|
+
type: inferType(findings),
|
|
19568
|
+
title: inferTitle2(input.summary, findings),
|
|
19569
|
+
summary: buildSummary2(input.summary, findings),
|
|
19570
|
+
facts: buildFacts2(findings),
|
|
19571
|
+
tags: buildTags(findings),
|
|
19572
|
+
source: "repo-scan",
|
|
19573
|
+
source_refs: files.map((file2) => ({ kind: "file", value: file2 })),
|
|
19574
|
+
surfaces: ["startup", "briefs", "sentinel", "insights"],
|
|
19575
|
+
files_read: files,
|
|
19576
|
+
session_id: input.session_id,
|
|
19577
|
+
cwd: input.cwd,
|
|
19578
|
+
agent: input.agent
|
|
19579
|
+
};
|
|
19580
|
+
}
|
|
19581
|
+
|
|
19582
|
+
// src/plugins/openclaw-content.ts
|
|
19583
|
+
function dedupe2(items) {
|
|
19584
|
+
const seen = new Set;
|
|
19585
|
+
const output = [];
|
|
19586
|
+
for (const item of items) {
|
|
19587
|
+
const cleaned = item.trim();
|
|
19588
|
+
if (!cleaned)
|
|
19589
|
+
continue;
|
|
19590
|
+
const key = cleaned.toLowerCase();
|
|
19591
|
+
if (seen.has(key))
|
|
19592
|
+
continue;
|
|
19593
|
+
seen.add(key);
|
|
19594
|
+
output.push(cleaned);
|
|
19595
|
+
}
|
|
19596
|
+
return output;
|
|
19597
|
+
}
|
|
19598
|
+
function inferType2(input) {
|
|
19599
|
+
if ((input.next_actions ?? []).length > 0)
|
|
19600
|
+
return "decision";
|
|
19601
|
+
if ((input.researched ?? []).length > 0)
|
|
19602
|
+
return "discovery";
|
|
19603
|
+
if ((input.posted ?? []).length > 0 || (input.outcomes ?? []).length > 0)
|
|
19604
|
+
return "change";
|
|
19605
|
+
return "message";
|
|
19606
|
+
}
|
|
19607
|
+
function inferTitle3(input) {
|
|
19608
|
+
const explicit = input.title?.trim();
|
|
19609
|
+
if (explicit && explicit.length >= 8)
|
|
19610
|
+
return explicit.replace(/\.$/, "");
|
|
19611
|
+
const firstPosted = input.posted?.find((item) => item.trim().length > 0);
|
|
19612
|
+
if (firstPosted)
|
|
19613
|
+
return firstPosted;
|
|
19614
|
+
const firstOutcome = input.outcomes?.find((item) => item.trim().length > 0);
|
|
19615
|
+
if (firstOutcome)
|
|
19616
|
+
return firstOutcome;
|
|
19617
|
+
const firstResearch = input.researched?.find((item) => item.trim().length > 0);
|
|
19618
|
+
if (firstResearch)
|
|
19619
|
+
return firstResearch;
|
|
19620
|
+
return "OpenClaw content work";
|
|
19621
|
+
}
|
|
19622
|
+
function buildFacts3(input) {
|
|
19623
|
+
const posted = dedupe2(input.posted ?? []);
|
|
19624
|
+
const researched = dedupe2(input.researched ?? []);
|
|
19625
|
+
const outcomes = dedupe2(input.outcomes ?? []);
|
|
19626
|
+
const nextActions = dedupe2(input.next_actions ?? []);
|
|
19627
|
+
const facts = [
|
|
19628
|
+
posted.length > 0 ? `Posted: ${posted.length}` : null,
|
|
19629
|
+
researched.length > 0 ? `Researched: ${researched.length}` : null,
|
|
19630
|
+
outcomes.length > 0 ? `Outcomes: ${outcomes.length}` : null,
|
|
19631
|
+
nextActions.length > 0 ? `Next actions: ${nextActions.length}` : null,
|
|
19632
|
+
...posted.slice(0, 2),
|
|
19633
|
+
...researched.slice(0, 2),
|
|
19634
|
+
...outcomes.slice(0, 2),
|
|
19635
|
+
...nextActions.slice(0, 2)
|
|
19636
|
+
].filter((item) => Boolean(item));
|
|
19637
|
+
return dedupe2(facts).slice(0, 8);
|
|
19638
|
+
}
|
|
19639
|
+
function buildSummary3(input) {
|
|
19640
|
+
const sections = [];
|
|
19641
|
+
const posted = dedupe2(input.posted ?? []);
|
|
19642
|
+
const researched = dedupe2(input.researched ?? []);
|
|
19643
|
+
const outcomes = dedupe2(input.outcomes ?? []);
|
|
19644
|
+
const nextActions = dedupe2(input.next_actions ?? []);
|
|
19645
|
+
if (posted.length > 0) {
|
|
19646
|
+
sections.push(`Posted:
|
|
19647
|
+
${posted.map((item) => `- ${item}`).join(`
|
|
19648
|
+
`)}`);
|
|
19649
|
+
}
|
|
19650
|
+
if (researched.length > 0) {
|
|
19651
|
+
sections.push(`Researched:
|
|
19652
|
+
${researched.map((item) => `- ${item}`).join(`
|
|
19653
|
+
`)}`);
|
|
19654
|
+
}
|
|
19655
|
+
if (outcomes.length > 0) {
|
|
19656
|
+
sections.push(`Outcomes:
|
|
19657
|
+
${outcomes.map((item) => `- ${item}`).join(`
|
|
19658
|
+
`)}`);
|
|
19659
|
+
}
|
|
19660
|
+
if (nextActions.length > 0) {
|
|
19661
|
+
sections.push(`Next Actions:
|
|
19662
|
+
${nextActions.map((item) => `- ${item}`).join(`
|
|
19663
|
+
`)}`);
|
|
19664
|
+
}
|
|
19665
|
+
return sections.join(`
|
|
19666
|
+
|
|
19667
|
+
`) || "Reduced OpenClaw content activity into reusable memory.";
|
|
19668
|
+
}
|
|
19669
|
+
function reduceOpenClawContentToMemory(input) {
|
|
19670
|
+
const links = dedupe2(input.links ?? []);
|
|
19671
|
+
const posted = dedupe2(input.posted ?? []);
|
|
19672
|
+
const researched = dedupe2(input.researched ?? []);
|
|
19673
|
+
const outcomes = dedupe2(input.outcomes ?? []);
|
|
19674
|
+
const nextActions = dedupe2(input.next_actions ?? []);
|
|
19675
|
+
return {
|
|
19676
|
+
plugin_id: "engrm.openclaw-content",
|
|
19677
|
+
type: inferType2(input),
|
|
19678
|
+
title: inferTitle3(input),
|
|
19679
|
+
summary: buildSummary3(input),
|
|
19680
|
+
facts: buildFacts3(input),
|
|
19681
|
+
tags: dedupe2([
|
|
19682
|
+
"openclaw-content",
|
|
19683
|
+
posted.length > 0 ? "posted" : "",
|
|
19684
|
+
researched.length > 0 ? "researched" : "",
|
|
19685
|
+
outcomes.length > 0 ? "outcomes" : "",
|
|
19686
|
+
nextActions.length > 0 ? "next-actions" : ""
|
|
19687
|
+
]),
|
|
19688
|
+
source: "openclaw",
|
|
19689
|
+
source_refs: links.map((value) => ({ kind: "thread", value })),
|
|
19690
|
+
surfaces: ["briefs", "startup", "insights"],
|
|
19691
|
+
session_id: input.session_id,
|
|
19692
|
+
cwd: input.cwd,
|
|
19693
|
+
agent: input.agent
|
|
19694
|
+
};
|
|
19695
|
+
}
|
|
19696
|
+
|
|
18801
19697
|
// src/server.ts
|
|
18802
19698
|
if (!configExists()) {
|
|
18803
19699
|
console.error("Engrm is not configured. Run: engrm init --manual");
|
|
@@ -18819,7 +19715,7 @@ var MCP_METRICS_PATH = join5(homedir3(), ".engrm", "mcp-session-metrics.json");
|
|
|
18819
19715
|
function persistSessionMetrics() {
|
|
18820
19716
|
try {
|
|
18821
19717
|
const dir = join5(homedir3(), ".engrm");
|
|
18822
|
-
if (!
|
|
19718
|
+
if (!existsSync7(dir))
|
|
18823
19719
|
mkdirSync2(dir, { recursive: true });
|
|
18824
19720
|
writeFileSync2(MCP_METRICS_PATH, JSON.stringify(sessionMetrics), "utf-8");
|
|
18825
19721
|
} catch {}
|
|
@@ -18868,7 +19764,7 @@ process.on("SIGTERM", () => {
|
|
|
18868
19764
|
});
|
|
18869
19765
|
var server = new McpServer({
|
|
18870
19766
|
name: "engrm",
|
|
18871
|
-
version: "0.4.
|
|
19767
|
+
version: "0.4.13"
|
|
18872
19768
|
});
|
|
18873
19769
|
server.tool("save_observation", "Save an observation to memory", {
|
|
18874
19770
|
type: exports_external.enum([
|
|
@@ -19044,6 +19940,164 @@ Facts: ${reduced.facts.join("; ")}` : "";
|
|
|
19044
19940
|
]
|
|
19045
19941
|
};
|
|
19046
19942
|
});
|
|
19943
|
+
server.tool("capture_git_worktree", "Read the current git worktree diff, reduce it into durable memory, and save it with plugin provenance", {
|
|
19944
|
+
cwd: exports_external.string().optional().describe("Git repo path; defaults to the current working directory"),
|
|
19945
|
+
staged: exports_external.boolean().optional().describe("Capture staged changes instead of unstaged worktree changes"),
|
|
19946
|
+
summary: exports_external.string().optional().describe("Optional human summary or commit-style title"),
|
|
19947
|
+
session_id: exports_external.string().optional()
|
|
19948
|
+
}, async (params) => {
|
|
19949
|
+
let worktree;
|
|
19950
|
+
try {
|
|
19951
|
+
worktree = captureGitWorktree({
|
|
19952
|
+
cwd: params.cwd ?? process.cwd(),
|
|
19953
|
+
staged: params.staged
|
|
19954
|
+
});
|
|
19955
|
+
} catch (error48) {
|
|
19956
|
+
return {
|
|
19957
|
+
content: [
|
|
19958
|
+
{
|
|
19959
|
+
type: "text",
|
|
19960
|
+
text: `Not captured: ${error48 instanceof Error ? error48.message : "unable to read git worktree"}`
|
|
19961
|
+
}
|
|
19962
|
+
]
|
|
19963
|
+
};
|
|
19964
|
+
}
|
|
19965
|
+
if (!worktree.diff.trim()) {
|
|
19966
|
+
return {
|
|
19967
|
+
content: [
|
|
19968
|
+
{
|
|
19969
|
+
type: "text",
|
|
19970
|
+
text: `No ${params.staged ? "staged" : "unstaged"} git diff found in ${worktree.cwd}`
|
|
19971
|
+
}
|
|
19972
|
+
]
|
|
19973
|
+
};
|
|
19974
|
+
}
|
|
19975
|
+
const reduced = reduceGitDiffToMemory({
|
|
19976
|
+
diff: worktree.diff,
|
|
19977
|
+
summary: params.summary,
|
|
19978
|
+
files: worktree.files,
|
|
19979
|
+
session_id: params.session_id,
|
|
19980
|
+
cwd: worktree.cwd,
|
|
19981
|
+
agent: getDetectedAgent()
|
|
19982
|
+
});
|
|
19983
|
+
const result = await savePluginMemory(db, config2, reduced);
|
|
19984
|
+
if (!result.success) {
|
|
19985
|
+
return {
|
|
19986
|
+
content: [
|
|
19987
|
+
{
|
|
19988
|
+
type: "text",
|
|
19989
|
+
text: `Not saved: ${result.reason}`
|
|
19990
|
+
}
|
|
19991
|
+
]
|
|
19992
|
+
};
|
|
19993
|
+
}
|
|
19994
|
+
return {
|
|
19995
|
+
content: [
|
|
19996
|
+
{
|
|
19997
|
+
type: "text",
|
|
19998
|
+
text: `Saved ${params.staged ? "staged" : "worktree"} git diff as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})`
|
|
19999
|
+
}
|
|
20000
|
+
]
|
|
20001
|
+
};
|
|
20002
|
+
});
|
|
20003
|
+
server.tool("capture_repo_scan", "Run a lightweight repository scan, reduce the findings into durable memory, and save it with plugin provenance", {
|
|
20004
|
+
cwd: exports_external.string().optional().describe("Repo path to scan; defaults to the current working directory"),
|
|
20005
|
+
focus: exports_external.array(exports_external.string()).optional().describe("Optional extra topics to search for, like 'billing' or 'validation'"),
|
|
20006
|
+
max_findings: exports_external.number().optional().describe("Maximum findings to keep"),
|
|
20007
|
+
summary: exports_external.string().optional().describe("Optional human summary for the scan"),
|
|
20008
|
+
session_id: exports_external.string().optional()
|
|
20009
|
+
}, async (params) => {
|
|
20010
|
+
let scan;
|
|
20011
|
+
try {
|
|
20012
|
+
scan = captureRepoScan({
|
|
20013
|
+
cwd: params.cwd ?? process.cwd(),
|
|
20014
|
+
focus: params.focus,
|
|
20015
|
+
max_findings: params.max_findings
|
|
20016
|
+
});
|
|
20017
|
+
} catch (error48) {
|
|
20018
|
+
return {
|
|
20019
|
+
content: [
|
|
20020
|
+
{
|
|
20021
|
+
type: "text",
|
|
20022
|
+
text: `Not captured: ${error48 instanceof Error ? error48.message : "unable to scan repository"}`
|
|
20023
|
+
}
|
|
20024
|
+
]
|
|
20025
|
+
};
|
|
20026
|
+
}
|
|
20027
|
+
if (scan.findings.length === 0) {
|
|
20028
|
+
return {
|
|
20029
|
+
content: [
|
|
20030
|
+
{
|
|
20031
|
+
type: "text",
|
|
20032
|
+
text: `No lightweight repo-scan findings found in ${scan.cwd}`
|
|
20033
|
+
}
|
|
20034
|
+
]
|
|
20035
|
+
};
|
|
20036
|
+
}
|
|
20037
|
+
const reduced = reduceRepoScanToMemory({
|
|
20038
|
+
summary: params.summary,
|
|
20039
|
+
findings: scan.findings,
|
|
20040
|
+
session_id: params.session_id,
|
|
20041
|
+
cwd: scan.cwd,
|
|
20042
|
+
agent: getDetectedAgent()
|
|
20043
|
+
});
|
|
20044
|
+
const result = await savePluginMemory(db, config2, reduced);
|
|
20045
|
+
if (!result.success) {
|
|
20046
|
+
return {
|
|
20047
|
+
content: [
|
|
20048
|
+
{
|
|
20049
|
+
type: "text",
|
|
20050
|
+
text: `Not saved: ${result.reason}`
|
|
20051
|
+
}
|
|
20052
|
+
]
|
|
20053
|
+
};
|
|
20054
|
+
}
|
|
20055
|
+
const findingSummary = scan.findings.slice(0, 3).map((finding) => finding.title).join("; ");
|
|
20056
|
+
return {
|
|
20057
|
+
content: [
|
|
20058
|
+
{
|
|
20059
|
+
type: "text",
|
|
20060
|
+
text: `Saved repo scan as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})` + `${findingSummary ? `
|
|
20061
|
+
Findings: ${findingSummary}` : ""}`
|
|
20062
|
+
}
|
|
20063
|
+
]
|
|
20064
|
+
};
|
|
20065
|
+
});
|
|
20066
|
+
server.tool("capture_openclaw_content", "Reduce OpenClaw content/research activity into durable memory and save it with plugin provenance", {
|
|
20067
|
+
title: exports_external.string().optional().describe("Short content or campaign title"),
|
|
20068
|
+
posted: exports_external.array(exports_external.string()).optional().describe("Concrete posted items or shipped content outcomes"),
|
|
20069
|
+
researched: exports_external.array(exports_external.string()).optional().describe("Research or discovery items"),
|
|
20070
|
+
outcomes: exports_external.array(exports_external.string()).optional().describe("Meaningful outcomes from the run"),
|
|
20071
|
+
next_actions: exports_external.array(exports_external.string()).optional().describe("Real follow-up actions"),
|
|
20072
|
+
links: exports_external.array(exports_external.string()).optional().describe("Thread or source URLs"),
|
|
20073
|
+
session_id: exports_external.string().optional(),
|
|
20074
|
+
cwd: exports_external.string().optional()
|
|
20075
|
+
}, async (params) => {
|
|
20076
|
+
const reduced = reduceOpenClawContentToMemory({
|
|
20077
|
+
...params,
|
|
20078
|
+
cwd: params.cwd ?? process.cwd(),
|
|
20079
|
+
agent: getDetectedAgent()
|
|
20080
|
+
});
|
|
20081
|
+
const result = await savePluginMemory(db, config2, reduced);
|
|
20082
|
+
if (!result.success) {
|
|
20083
|
+
return {
|
|
20084
|
+
content: [
|
|
20085
|
+
{
|
|
20086
|
+
type: "text",
|
|
20087
|
+
text: `Not saved: ${result.reason}`
|
|
20088
|
+
}
|
|
20089
|
+
]
|
|
20090
|
+
};
|
|
20091
|
+
}
|
|
20092
|
+
return {
|
|
20093
|
+
content: [
|
|
20094
|
+
{
|
|
20095
|
+
type: "text",
|
|
20096
|
+
text: `Saved OpenClaw content memory as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})`
|
|
20097
|
+
}
|
|
20098
|
+
]
|
|
20099
|
+
};
|
|
20100
|
+
});
|
|
19047
20101
|
server.tool("search", "Search memory for observations", {
|
|
19048
20102
|
query: exports_external.string().describe("Search query"),
|
|
19049
20103
|
project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
|
|
@@ -19389,7 +20443,22 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
19389
20443
|
return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
|
|
19390
20444
|
}).join(`
|
|
19391
20445
|
`) : "- (none)";
|
|
19392
|
-
const observationLines = result.observations.length > 0 ? result.observations.map((obs) =>
|
|
20446
|
+
const observationLines = result.observations.length > 0 ? result.observations.map((obs) => {
|
|
20447
|
+
const provenance = [];
|
|
20448
|
+
if (obs.source_tool)
|
|
20449
|
+
provenance.push(`via ${obs.source_tool}`);
|
|
20450
|
+
if (typeof obs.source_prompt_number === "number")
|
|
20451
|
+
provenance.push(`#${obs.source_prompt_number}`);
|
|
20452
|
+
return `- #${obs.id} [${obs.type}] ${obs.title}${provenance.length ? ` (${provenance.join(" · ")})` : ""}`;
|
|
20453
|
+
}).join(`
|
|
20454
|
+
`) : "- (none)";
|
|
20455
|
+
const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
20456
|
+
`) : "- (none)";
|
|
20457
|
+
const provenanceMixLines2 = result.provenance_type_mix.length > 0 ? result.provenance_type_mix.map((item) => `- ${item.tool}: ${item.top_types.map((entry) => `${entry.type} ${entry.count}`).join(", ")}`).join(`
|
|
20458
|
+
`) : "- (none)";
|
|
20459
|
+
const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
|
|
20460
|
+
`) : "- (none)";
|
|
20461
|
+
const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
|
|
19393
20462
|
`) : "- (none)";
|
|
19394
20463
|
const projectLine = result.project ? `Project: ${result.project}
|
|
19395
20464
|
|
|
@@ -19403,7 +20472,20 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
19403
20472
|
content: [
|
|
19404
20473
|
{
|
|
19405
20474
|
type: "text",
|
|
19406
|
-
text: `${projectLine}` + `${captureLine}` + `
|
|
20475
|
+
text: `${projectLine}` + `${captureLine}` + `${typeof result.assistant_checkpoint_count === "number" ? `Assistant checkpoints: ${result.assistant_checkpoint_count}
|
|
20476
|
+
` : ""}` + `${typeof result.estimated_read_tokens === "number" ? `Estimated read cost: ~${result.estimated_read_tokens}t
|
|
20477
|
+
` : ""}` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
|
|
20478
|
+
|
|
20479
|
+
` + `Top types:
|
|
20480
|
+
${topTypes}
|
|
20481
|
+
|
|
20482
|
+
` + `Assistant checkpoint types:
|
|
20483
|
+
${checkpointTypeLines}
|
|
20484
|
+
|
|
20485
|
+
` + `Observation provenance:
|
|
20486
|
+
${provenanceLines}
|
|
20487
|
+
|
|
20488
|
+
` + `Recent sessions:
|
|
19407
20489
|
${sessionLines}
|
|
19408
20490
|
|
|
19409
20491
|
` + `Recent requests:
|
|
@@ -19454,6 +20536,106 @@ server.tool("capture_status", "Show whether Engrm hook registration and recent p
|
|
|
19454
20536
|
]
|
|
19455
20537
|
};
|
|
19456
20538
|
});
|
|
20539
|
+
server.tool("capture_quality", "Show workspace-wide capture richness, checkpoints, and provenance across projects", {
|
|
20540
|
+
limit: exports_external.number().optional(),
|
|
20541
|
+
user_id: exports_external.string().optional()
|
|
20542
|
+
}, async (params) => {
|
|
20543
|
+
const result = getCaptureQuality(db, {
|
|
20544
|
+
limit: params.limit,
|
|
20545
|
+
user_id: params.user_id ?? config2.user_id
|
|
20546
|
+
});
|
|
20547
|
+
const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
20548
|
+
`) : "- (none)";
|
|
20549
|
+
const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
|
|
20550
|
+
`) : "- (none)";
|
|
20551
|
+
const projectLines = result.top_projects.length > 0 ? result.top_projects.map((project) => `- ${project.name} [${project.raw_capture_state}] obs=${project.observation_count} sessions=${project.session_count} prompts=${project.prompt_count} tools=${project.tool_event_count} checkpoints=${project.assistant_checkpoint_count}`).join(`
|
|
20552
|
+
`) : "- (none)";
|
|
20553
|
+
return {
|
|
20554
|
+
content: [
|
|
20555
|
+
{
|
|
20556
|
+
type: "text",
|
|
20557
|
+
text: `Workspace totals: projects=${result.totals.projects}, observations=${result.totals.observations}, sessions=${result.totals.sessions}, prompts=${result.totals.prompts}, tools=${result.totals.tool_events}, checkpoints=${result.totals.assistant_checkpoints}
|
|
20558
|
+
|
|
20559
|
+
` + `Session capture states: rich=${result.session_states.rich}, partial=${result.session_states.partial}, summary-only=${result.session_states.summary_only}, legacy=${result.session_states.legacy}
|
|
20560
|
+
|
|
20561
|
+
` + `Projects with raw capture: ${result.projects_with_raw_capture}
|
|
20562
|
+
|
|
20563
|
+
` + `Assistant checkpoints by type:
|
|
20564
|
+
${checkpointTypeLines}
|
|
20565
|
+
|
|
20566
|
+
` + `Observation provenance:
|
|
20567
|
+
${provenanceLines}
|
|
20568
|
+
|
|
20569
|
+
` + `Provenance type mix:
|
|
20570
|
+
${provenanceMixLines}
|
|
20571
|
+
|
|
20572
|
+
` + `Top projects:
|
|
20573
|
+
${projectLines}`
|
|
20574
|
+
}
|
|
20575
|
+
]
|
|
20576
|
+
};
|
|
20577
|
+
});
|
|
20578
|
+
server.tool("tool_memory_index", "Show which source tools are producing durable memory and what kinds of memory they create", {
|
|
20579
|
+
cwd: exports_external.string().optional(),
|
|
20580
|
+
project_scoped: exports_external.boolean().optional(),
|
|
20581
|
+
limit: exports_external.number().optional(),
|
|
20582
|
+
user_id: exports_external.string().optional()
|
|
20583
|
+
}, async (params) => {
|
|
20584
|
+
const result = getToolMemoryIndex(db, {
|
|
20585
|
+
cwd: params.cwd ?? process.cwd(),
|
|
20586
|
+
project_scoped: params.project_scoped,
|
|
20587
|
+
limit: params.limit,
|
|
20588
|
+
user_id: params.user_id ?? config2.user_id
|
|
20589
|
+
});
|
|
20590
|
+
const toolLines = result.tools.length > 0 ? result.tools.map((tool) => {
|
|
20591
|
+
const typeMix = tool.top_types.map((item) => `${item.type} ${item.count}`).join(", ");
|
|
20592
|
+
const pluginMix = tool.top_plugins.length > 0 ? ` plugins=[${tool.top_plugins.map((item) => `${item.plugin} ${item.count}`).join(", ")}]` : "";
|
|
20593
|
+
const sample = tool.sample_titles[0] ? ` sample="${tool.sample_titles[0]}"` : "";
|
|
20594
|
+
const promptInfo = typeof tool.latest_prompt_number === "number" ? ` latest_prompt=#${tool.latest_prompt_number}` : "";
|
|
20595
|
+
return `- ${tool.tool}: obs=${tool.observation_count} sessions=${tool.session_count}${promptInfo} types=[${typeMix}]${pluginMix}${sample}`;
|
|
20596
|
+
}).join(`
|
|
20597
|
+
`) : "- (none)";
|
|
20598
|
+
return {
|
|
20599
|
+
content: [
|
|
20600
|
+
{
|
|
20601
|
+
type: "text",
|
|
20602
|
+
text: `${result.project ? `Project: ${result.project}
|
|
20603
|
+
|
|
20604
|
+
` : ""}` + `Tools producing durable memory:
|
|
20605
|
+
${toolLines}`
|
|
20606
|
+
}
|
|
20607
|
+
]
|
|
20608
|
+
};
|
|
20609
|
+
});
|
|
20610
|
+
server.tool("session_tool_memory", "Show which tools in one session produced durable memory and which tools produced none", {
|
|
20611
|
+
session_id: exports_external.string().describe("Session ID to inspect")
|
|
20612
|
+
}, async (params) => {
|
|
20613
|
+
const result = getSessionToolMemory(db, params);
|
|
20614
|
+
const toolLines = result.tools.length > 0 ? result.tools.map((tool) => {
|
|
20615
|
+
const typeMix = tool.top_types.map((item) => `${item.type} ${item.count}`).join(", ");
|
|
20616
|
+
const pluginMix = tool.top_plugins.length > 0 ? ` plugins=[${tool.top_plugins.map((item) => `${item.plugin} ${item.count}`).join(", ")}]` : "";
|
|
20617
|
+
const sample = tool.sample_titles[0] ? ` sample="${tool.sample_titles[0]}"` : "";
|
|
20618
|
+
const promptInfo = typeof tool.latest_prompt_number === "number" ? ` latest_prompt=#${tool.latest_prompt_number}` : "";
|
|
20619
|
+
return `- ${tool.tool}: events=${tool.tool_event_count} observations=${tool.observation_count}${promptInfo} types=[${typeMix}]${pluginMix}${sample}`;
|
|
20620
|
+
}).join(`
|
|
20621
|
+
`) : "- (none)";
|
|
20622
|
+
const unmappedLines = result.tools_without_memory.length > 0 ? result.tools_without_memory.map((tool) => `- ${tool.tool}: events=${tool.tool_event_count}`).join(`
|
|
20623
|
+
`) : "- (none)";
|
|
20624
|
+
return {
|
|
20625
|
+
content: [
|
|
20626
|
+
{
|
|
20627
|
+
type: "text",
|
|
20628
|
+
text: `Session: ${result.session_id}
|
|
20629
|
+
|
|
20630
|
+
` + `Tools producing durable memory:
|
|
20631
|
+
${toolLines}
|
|
20632
|
+
|
|
20633
|
+
` + `Tools without durable memory:
|
|
20634
|
+
${unmappedLines}`
|
|
20635
|
+
}
|
|
20636
|
+
]
|
|
20637
|
+
};
|
|
20638
|
+
});
|
|
19457
20639
|
server.tool("session_context", "Preview the exact project memory context Engrm would inject at session start", {
|
|
19458
20640
|
cwd: exports_external.string().optional(),
|
|
19459
20641
|
token_budget: exports_external.number().optional(),
|
|
@@ -19540,6 +20722,10 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
19540
20722
|
}).join(`
|
|
19541
20723
|
`) : "- (none)";
|
|
19542
20724
|
const hotFiles = result.hot_files.length > 0 ? result.hot_files.map((file2) => `- ${file2.path} (${file2.count})`).join(`
|
|
20725
|
+
`) : "- (none)";
|
|
20726
|
+
const provenance = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
20727
|
+
`) : "- (none)";
|
|
20728
|
+
const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
|
|
19543
20729
|
`) : "- (none)";
|
|
19544
20730
|
const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
|
|
19545
20731
|
`) : "- (none)";
|
|
@@ -19554,15 +20740,25 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
19554
20740
|
|
|
19555
20741
|
` + `Raw chronology: ${result.raw_capture_active ? "active" : "observations-only so far"}
|
|
19556
20742
|
|
|
20743
|
+
` + `Assistant checkpoints: ${result.assistant_checkpoint_count}
|
|
20744
|
+
` + `Estimated read cost: ~${result.estimated_read_tokens}t
|
|
20745
|
+
` + `Suggested tools: ${result.suggested_tools.join(", ") || "(none)"}
|
|
20746
|
+
|
|
19557
20747
|
` + `Observation counts:
|
|
19558
20748
|
${counts}
|
|
19559
20749
|
|
|
20750
|
+
` + `Top types:
|
|
20751
|
+
${topTypes}
|
|
20752
|
+
|
|
19560
20753
|
` + `Recent sessions:
|
|
19561
20754
|
${sessions}
|
|
19562
20755
|
|
|
19563
20756
|
` + `Hot files:
|
|
19564
20757
|
${hotFiles}
|
|
19565
20758
|
|
|
20759
|
+
` + `Observation provenance:
|
|
20760
|
+
${provenance}
|
|
20761
|
+
|
|
19566
20762
|
` + `Recent memory objects:
|
|
19567
20763
|
${topTitles}`
|
|
19568
20764
|
}
|
|
@@ -19633,17 +20829,22 @@ server.tool("workspace_memory_index", "Show a cross-project local memory index f
|
|
|
19633
20829
|
});
|
|
19634
20830
|
const projectLines = result.projects.length > 0 ? result.projects.map((project) => {
|
|
19635
20831
|
const when = new Date(project.last_active_epoch * 1000).toISOString().split("T")[0];
|
|
19636
|
-
return `- ${project.name} (${when}) obs=${project.observation_count} sessions=${project.session_count} prompts=${project.prompt_count} tools=${project.tool_event_count}`;
|
|
20832
|
+
return `- ${project.name} (${when}) obs=${project.observation_count} sessions=${project.session_count} prompts=${project.prompt_count} tools=${project.tool_event_count} checkpoints=${project.assistant_checkpoint_count}`;
|
|
19637
20833
|
}).join(`
|
|
20834
|
+
`) : "- (none)";
|
|
20835
|
+
const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
19638
20836
|
`) : "- (none)";
|
|
19639
20837
|
return {
|
|
19640
20838
|
content: [
|
|
19641
20839
|
{
|
|
19642
20840
|
type: "text",
|
|
19643
|
-
text: `Workspace totals: observations=${result.totals.observations}, sessions=${result.totals.sessions}, prompts=${result.totals.prompts}, tools=${result.totals.tool_events}
|
|
20841
|
+
text: `Workspace totals: observations=${result.totals.observations}, sessions=${result.totals.sessions}, prompts=${result.totals.prompts}, tools=${result.totals.tool_events}, checkpoints=${result.totals.assistant_checkpoints}
|
|
19644
20842
|
|
|
19645
20843
|
` + `Projects with raw chronology: ${result.projects_with_raw_capture}
|
|
19646
20844
|
|
|
20845
|
+
` + `Observation provenance:
|
|
20846
|
+
${provenanceLines}
|
|
20847
|
+
|
|
19647
20848
|
` + `Projects:
|
|
19648
20849
|
${projectLines}`
|
|
19649
20850
|
}
|
|
@@ -19751,10 +20952,19 @@ server.tool("session_story", "Show the full local memory story for one session",
|
|
|
19751
20952
|
return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
|
|
19752
20953
|
}).join(`
|
|
19753
20954
|
`) : "- (none)";
|
|
19754
|
-
const observationLines = result.observations.length > 0 ? result.observations.slice(-15).map((obs) =>
|
|
20955
|
+
const observationLines = result.observations.length > 0 ? result.observations.slice(-15).map((obs) => {
|
|
20956
|
+
const provenance = [];
|
|
20957
|
+
if (obs.source_tool)
|
|
20958
|
+
provenance.push(`via ${obs.source_tool}`);
|
|
20959
|
+
if (typeof obs.source_prompt_number === "number")
|
|
20960
|
+
provenance.push(`#${obs.source_prompt_number}`);
|
|
20961
|
+
return `- #${obs.id} [${obs.type}] ${obs.title}${provenance.length ? ` (${provenance.join(" · ")})` : ""}`;
|
|
20962
|
+
}).join(`
|
|
19755
20963
|
`) : "- (none)";
|
|
19756
20964
|
const metrics = result.metrics ? `files=${result.metrics.files_touched_count}, searches=${result.metrics.searches_performed}, tools=${result.metrics.tool_calls_count}, observations=${result.metrics.observation_count}` : "metrics unavailable";
|
|
19757
20965
|
const captureGaps = result.capture_gaps.length > 0 ? result.capture_gaps.map((gap) => `- ${gap}`).join(`
|
|
20966
|
+
`) : "- none";
|
|
20967
|
+
const provenanceSummary = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
19758
20968
|
`) : "- none";
|
|
19759
20969
|
return {
|
|
19760
20970
|
content: [
|
|
@@ -19774,6 +20984,9 @@ ${promptLines}
|
|
|
19774
20984
|
` + `Tools:
|
|
19775
20985
|
${toolLines}
|
|
19776
20986
|
|
|
20987
|
+
` + `Provenance:
|
|
20988
|
+
${provenanceSummary}
|
|
20989
|
+
|
|
19777
20990
|
` + `Capture gaps:
|
|
19778
20991
|
${captureGaps}
|
|
19779
20992
|
|