engrm 0.4.12 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -2
- package/dist/cli.js +136 -6
- package/dist/hooks/elicitation-result.js +116 -4
- package/dist/hooks/post-tool-use.js +118 -5
- package/dist/hooks/pre-compact.js +114 -22
- package/dist/hooks/sentinel.js +112 -3
- package/dist/hooks/session-start.js +160 -24
- package/dist/hooks/stop.js +209 -14
- package/dist/hooks/user-prompt-submit.js +112 -3
- package/dist/server.js +2440 -1228
- package/package.json +1 -1
package/dist/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) {
|
|
@@ -14373,8 +14467,9 @@ class MemDatabase {
|
|
|
14373
14467
|
const result = this.db.query(`INSERT INTO observations (
|
|
14374
14468
|
session_id, project_id, type, title, narrative, facts, concepts,
|
|
14375
14469
|
files_read, files_modified, quality, lifecycle, sensitivity,
|
|
14376
|
-
user_id, device_id, agent,
|
|
14377
|
-
|
|
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);
|
|
14378
14473
|
const id = Number(result.lastInsertRowid);
|
|
14379
14474
|
const row = this.getObservationById(id);
|
|
14380
14475
|
this.ftsInsert(row);
|
|
@@ -14615,6 +14710,13 @@ class MemDatabase {
|
|
|
14615
14710
|
ORDER BY prompt_number ASC
|
|
14616
14711
|
LIMIT ?`).all(sessionId, limit);
|
|
14617
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
|
+
}
|
|
14618
14720
|
insertToolEvent(input) {
|
|
14619
14721
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
14620
14722
|
const result = this.db.query(`INSERT INTO tool_events (
|
|
@@ -14724,8 +14826,15 @@ class MemDatabase {
|
|
|
14724
14826
|
}
|
|
14725
14827
|
insertSessionSummary(summary) {
|
|
14726
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
|
+
};
|
|
14727
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)
|
|
14728
|
-
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);
|
|
14729
14838
|
const id = Number(result.lastInsertRowid);
|
|
14730
14839
|
return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
|
|
14731
14840
|
}
|
|
@@ -15625,6 +15734,7 @@ async function saveObservation(db, config2, input) {
|
|
|
15625
15734
|
reason: `Merged into existing observation #${duplicate.id}`
|
|
15626
15735
|
};
|
|
15627
15736
|
}
|
|
15737
|
+
const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
|
|
15628
15738
|
const obs = db.insertObservation({
|
|
15629
15739
|
session_id: input.session_id ?? null,
|
|
15630
15740
|
project_id: project.id,
|
|
@@ -15640,7 +15750,9 @@ async function saveObservation(db, config2, input) {
|
|
|
15640
15750
|
sensitivity,
|
|
15641
15751
|
user_id: config2.user_id,
|
|
15642
15752
|
device_id: config2.device_id,
|
|
15643
|
-
agent: input.agent ?? "claude-code"
|
|
15753
|
+
agent: input.agent ?? "claude-code",
|
|
15754
|
+
source_tool: input.source_tool ?? null,
|
|
15755
|
+
source_prompt_number: sourcePromptNumber
|
|
15644
15756
|
});
|
|
15645
15757
|
db.addToOutbox("observation", obs.id);
|
|
15646
15758
|
if (db.vecAvailable) {
|
|
@@ -16008,8 +16120,11 @@ function getSessionStory(db, input) {
|
|
|
16008
16120
|
const toolEvents = db.getSessionToolEvents(input.session_id, 100);
|
|
16009
16121
|
const observations = db.getObservationsBySession(input.session_id);
|
|
16010
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;
|
|
16011
16125
|
return {
|
|
16012
16126
|
session,
|
|
16127
|
+
project_name: projectName,
|
|
16013
16128
|
summary,
|
|
16014
16129
|
prompts,
|
|
16015
16130
|
tool_events: toolEvents,
|
|
@@ -16026,7 +16141,11 @@ function getSessionStory(db, input) {
|
|
|
16026
16141
|
toolCallsCount: metrics?.tool_calls_count ?? 0,
|
|
16027
16142
|
observationCount: observations.length,
|
|
16028
16143
|
hasSummary: Boolean(summary?.request || summary?.completed)
|
|
16029
|
-
})
|
|
16144
|
+
}),
|
|
16145
|
+
latest_request: latestRequest,
|
|
16146
|
+
recent_outcomes: collectRecentOutcomes(observations),
|
|
16147
|
+
hot_files: collectHotFiles(observations),
|
|
16148
|
+
provenance_summary: collectProvenanceSummary(observations)
|
|
16030
16149
|
};
|
|
16031
16150
|
}
|
|
16032
16151
|
function classifyCaptureState(input) {
|
|
@@ -16052,6 +16171,56 @@ function buildCaptureGaps(input) {
|
|
|
16052
16171
|
}
|
|
16053
16172
|
return gaps;
|
|
16054
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
|
+
}
|
|
16055
16224
|
|
|
16056
16225
|
// src/tools/recent-sessions.ts
|
|
16057
16226
|
function getRecentSessions(db, input) {
|
|
@@ -16089,1292 +16258,1641 @@ function classifyCaptureState2(session) {
|
|
|
16089
16258
|
return "legacy";
|
|
16090
16259
|
}
|
|
16091
16260
|
|
|
16092
|
-
// src/
|
|
16093
|
-
|
|
16094
|
-
|
|
16095
|
-
|
|
16096
|
-
|
|
16097
|
-
|
|
16098
|
-
|
|
16099
|
-
|
|
16100
|
-
|
|
16101
|
-
|
|
16102
|
-
|
|
16103
|
-
|
|
16104
|
-
|
|
16105
|
-
|
|
16106
|
-
|
|
16107
|
-
const
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
|
|
16113
|
-
|
|
16114
|
-
|
|
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
|
+
});
|
|
16115
16355
|
}
|
|
16116
16356
|
}
|
|
16117
|
-
|
|
16118
|
-
|
|
16119
|
-
type: obs.type,
|
|
16120
|
-
title: obs.title,
|
|
16121
|
-
id: obs.id
|
|
16122
|
-
}));
|
|
16123
|
-
const recentSessions = getRecentSessions(db, {
|
|
16124
|
-
cwd,
|
|
16125
|
-
project_scoped: true,
|
|
16126
|
-
user_id: input.user_id,
|
|
16127
|
-
limit: 6
|
|
16128
|
-
}).sessions;
|
|
16129
|
-
const recentRequestsCount = getRecentRequests(db, {
|
|
16130
|
-
cwd,
|
|
16131
|
-
project_scoped: true,
|
|
16132
|
-
user_id: input.user_id,
|
|
16133
|
-
limit: 20
|
|
16134
|
-
}).prompts.length;
|
|
16135
|
-
const recentToolsCount = getRecentTools(db, {
|
|
16136
|
-
cwd,
|
|
16137
|
-
project_scoped: true,
|
|
16138
|
-
user_id: input.user_id,
|
|
16139
|
-
limit: 20
|
|
16140
|
-
}).tool_events.length;
|
|
16141
|
-
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);
|
|
16142
|
-
return {
|
|
16143
|
-
project: project.name,
|
|
16144
|
-
canonical_id: project.canonical_id,
|
|
16145
|
-
observation_counts: counts,
|
|
16146
|
-
recent_sessions: recentSessions,
|
|
16147
|
-
recent_outcomes: recentOutcomes,
|
|
16148
|
-
recent_requests_count: recentRequestsCount,
|
|
16149
|
-
recent_tools_count: recentToolsCount,
|
|
16150
|
-
raw_capture_active: recentRequestsCount > 0 || recentToolsCount > 0,
|
|
16151
|
-
hot_files: hotFiles,
|
|
16152
|
-
top_titles: topTitles
|
|
16153
|
-
};
|
|
16357
|
+
stale.sort((a, b) => b.days_ago - a.days_ago);
|
|
16358
|
+
return stale.slice(0, 5);
|
|
16154
16359
|
}
|
|
16155
|
-
function
|
|
16156
|
-
|
|
16157
|
-
|
|
16158
|
-
|
|
16159
|
-
|
|
16160
|
-
|
|
16161
|
-
|
|
16162
|
-
|
|
16163
|
-
|
|
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)
|
|
16164
16373
|
return [];
|
|
16165
|
-
|
|
16166
|
-
|
|
16167
|
-
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
|
|
16173
|
-
const
|
|
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
|
-
|
|
16205
|
-
return {
|
|
16206
|
-
project: project?.name,
|
|
16207
|
-
capture_mode: requests.length > 0 || tools.length > 0 ? "rich" : "observations-only",
|
|
16208
|
-
sessions,
|
|
16209
|
-
requests,
|
|
16210
|
-
tools,
|
|
16211
|
-
observations,
|
|
16212
|
-
recent_outcomes: projectIndex?.recent_outcomes ?? [],
|
|
16213
|
-
hot_files: projectIndex?.hot_files ?? []
|
|
16214
|
-
};
|
|
16215
|
-
}
|
|
16216
|
-
|
|
16217
|
-
// src/tools/workspace-memory-index.ts
|
|
16218
|
-
function getWorkspaceMemoryIndex(db, input) {
|
|
16219
|
-
const limit = Math.max(1, Math.min(input.limit ?? 12, 50));
|
|
16220
|
-
const visibilityClause = input.user_id ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
|
|
16221
|
-
const params = input.user_id ? [input.user_id, limit] : [limit];
|
|
16222
|
-
const projects = db.db.query(`SELECT
|
|
16223
|
-
p.canonical_id,
|
|
16224
|
-
p.name,
|
|
16225
|
-
(
|
|
16226
|
-
SELECT COUNT(*) FROM observations o
|
|
16227
|
-
WHERE o.project_id = p.id
|
|
16228
|
-
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
16229
|
-
AND o.superseded_by IS NULL
|
|
16230
|
-
${visibilityClause}
|
|
16231
|
-
) AS observation_count,
|
|
16232
|
-
(SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count,
|
|
16233
|
-
(SELECT COUNT(*) FROM user_prompts up WHERE up.project_id = p.id) AS prompt_count,
|
|
16234
|
-
(SELECT COUNT(*) FROM tool_events te WHERE te.project_id = p.id) AS tool_event_count,
|
|
16235
|
-
p.last_active_epoch
|
|
16236
|
-
FROM projects p
|
|
16237
|
-
ORDER BY p.last_active_epoch DESC
|
|
16238
|
-
LIMIT ?`).all(...params);
|
|
16239
|
-
const totals = projects.reduce((acc, project) => {
|
|
16240
|
-
acc.observations += project.observation_count;
|
|
16241
|
-
acc.sessions += project.session_count;
|
|
16242
|
-
acc.prompts += project.prompt_count;
|
|
16243
|
-
acc.tool_events += project.tool_event_count;
|
|
16244
|
-
return acc;
|
|
16245
|
-
}, { observations: 0, sessions: 0, prompts: 0, tool_events: 0 });
|
|
16246
|
-
return {
|
|
16247
|
-
projects,
|
|
16248
|
-
totals,
|
|
16249
|
-
projects_with_raw_capture: projects.filter((project) => project.prompt_count > 0 || project.tool_event_count > 0).length
|
|
16250
|
-
};
|
|
16251
|
-
}
|
|
16252
|
-
|
|
16253
|
-
// src/tools/project-related-work.ts
|
|
16254
|
-
function getProjectRelatedWork(db, input) {
|
|
16255
|
-
const cwd = input.cwd ?? process.cwd();
|
|
16256
|
-
const detected = detectProject(cwd);
|
|
16257
|
-
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
16258
|
-
const visibilityClause = input.user_id ? " AND (o.sensitivity != 'personal' OR o.user_id = ?)" : "";
|
|
16259
|
-
const visibilityParams = input.user_id ? [input.user_id] : [];
|
|
16260
|
-
const terms = buildProjectTerms(detected);
|
|
16261
|
-
if (terms.length === 0) {
|
|
16262
|
-
return {
|
|
16263
|
-
project: detected.name,
|
|
16264
|
-
canonical_id: detected.canonical_id,
|
|
16265
|
-
related: []
|
|
16266
|
-
};
|
|
16267
|
-
}
|
|
16268
|
-
const localProject = db.getProjectByCanonicalId(detected.canonical_id);
|
|
16269
|
-
const localProjectId = localProject?.id ?? -1;
|
|
16270
|
-
const params = [];
|
|
16271
|
-
const whereParts = [];
|
|
16272
|
-
for (const term of terms) {
|
|
16273
|
-
const like = `%${term}%`;
|
|
16274
|
-
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 ?)`);
|
|
16275
|
-
params.push(like, like, like, like);
|
|
16276
|
-
}
|
|
16277
|
-
const rows = db.db.query(`SELECT
|
|
16278
|
-
o.id,
|
|
16279
|
-
o.type,
|
|
16280
|
-
o.title,
|
|
16281
|
-
o.project_id as source_project_id,
|
|
16282
|
-
p.name as source_project,
|
|
16283
|
-
o.narrative,
|
|
16284
|
-
o.files_read,
|
|
16285
|
-
o.files_modified
|
|
16286
|
-
FROM observations o
|
|
16287
|
-
LEFT JOIN projects p ON p.id = o.project_id
|
|
16288
|
-
WHERE o.project_id != ?
|
|
16289
|
-
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
16290
|
-
AND o.superseded_by IS NULL
|
|
16291
|
-
${visibilityClause}
|
|
16292
|
-
AND (${whereParts.join(" OR ")})
|
|
16293
|
-
ORDER BY o.created_at_epoch DESC
|
|
16294
|
-
LIMIT ?`).all(localProjectId, ...visibilityParams, ...params, limit);
|
|
16295
|
-
return {
|
|
16296
|
-
project: detected.name,
|
|
16297
|
-
canonical_id: detected.canonical_id,
|
|
16298
|
-
related: rows.map((row) => ({
|
|
16299
|
-
id: row.id,
|
|
16300
|
-
type: row.type,
|
|
16301
|
-
title: row.title,
|
|
16302
|
-
source_project: row.source_project ?? "unknown",
|
|
16303
|
-
source_project_id: row.source_project_id,
|
|
16304
|
-
matched_on: classifyMatch(row, terms)
|
|
16305
|
-
}))
|
|
16306
|
-
};
|
|
16307
|
-
}
|
|
16308
|
-
function buildProjectTerms(detected) {
|
|
16309
|
-
const explicit = new Set;
|
|
16310
|
-
explicit.add(detected.name.toLowerCase());
|
|
16311
|
-
const canonicalParts = detected.canonical_id.toLowerCase().split("/");
|
|
16312
|
-
for (const part of canonicalParts) {
|
|
16313
|
-
if (part.length >= 4)
|
|
16314
|
-
explicit.add(part);
|
|
16315
|
-
}
|
|
16316
|
-
if (detected.name.toLowerCase() === "huginn") {
|
|
16317
|
-
explicit.add("aiserver");
|
|
16318
|
-
}
|
|
16319
|
-
return [...explicit].filter(Boolean);
|
|
16320
|
-
}
|
|
16321
|
-
function classifyMatch(row, terms) {
|
|
16322
|
-
const title = row.title.toLowerCase();
|
|
16323
|
-
const narrative = (row.narrative ?? "").toLowerCase();
|
|
16324
|
-
const files = `${row.files_read ?? ""} ${row.files_modified ?? ""}`.toLowerCase();
|
|
16325
|
-
for (const term of terms) {
|
|
16326
|
-
if (files.includes(term))
|
|
16327
|
-
return `files:${term}`;
|
|
16328
|
-
if (title.includes(term))
|
|
16329
|
-
return `title:${term}`;
|
|
16330
|
-
if (narrative.includes(term))
|
|
16331
|
-
return `narrative:${term}`;
|
|
16332
|
-
}
|
|
16333
|
-
return "related";
|
|
16334
|
-
}
|
|
16335
|
-
|
|
16336
|
-
// src/tools/reclassify-project-memory.ts
|
|
16337
|
-
function reclassifyProjectMemory(db, input) {
|
|
16338
|
-
const cwd = input.cwd ?? process.cwd();
|
|
16339
|
-
const detected = detectProject(cwd);
|
|
16340
|
-
const target = db.upsertProject({
|
|
16341
|
-
canonical_id: detected.canonical_id,
|
|
16342
|
-
name: detected.name,
|
|
16343
|
-
local_path: detected.local_path,
|
|
16344
|
-
remote_url: detected.remote_url
|
|
16345
|
-
});
|
|
16346
|
-
const related = getProjectRelatedWork(db, {
|
|
16347
|
-
cwd,
|
|
16348
|
-
user_id: input.user_id,
|
|
16349
|
-
limit: input.limit ?? 50
|
|
16350
|
-
}).related;
|
|
16351
|
-
let moved = 0;
|
|
16352
|
-
const candidates = related.map((item) => {
|
|
16353
|
-
const eligible = item.matched_on.startsWith("files:") || item.matched_on.startsWith("title:") || item.matched_on.startsWith("narrative:");
|
|
16354
|
-
const shouldMove = eligible && item.source_project_id !== target.id;
|
|
16355
|
-
if (shouldMove && input.dry_run !== true) {
|
|
16356
|
-
const ok = db.reassignObservationProject(item.id, target.id);
|
|
16357
|
-
if (ok)
|
|
16358
|
-
moved += 1;
|
|
16359
|
-
return {
|
|
16360
|
-
id: item.id,
|
|
16361
|
-
title: item.title,
|
|
16362
|
-
type: item.type,
|
|
16363
|
-
from: item.source_project,
|
|
16364
|
-
matched_on: item.matched_on,
|
|
16365
|
-
moved: ok
|
|
16366
|
-
};
|
|
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
|
+
}
|
|
16367
16414
|
}
|
|
16368
|
-
|
|
16369
|
-
|
|
16370
|
-
|
|
16371
|
-
|
|
16372
|
-
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16377
|
-
|
|
16378
|
-
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16383
|
-
|
|
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);
|
|
16384
16432
|
}
|
|
16385
16433
|
|
|
16386
|
-
// src/
|
|
16387
|
-
function
|
|
16388
|
-
return {
|
|
16389
|
-
kind: "prompt",
|
|
16390
|
-
created_at_epoch: prompt.created_at_epoch,
|
|
16391
|
-
session_id: prompt.session_id,
|
|
16392
|
-
id: prompt.id,
|
|
16393
|
-
title: `#${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`
|
|
16394
|
-
};
|
|
16395
|
-
}
|
|
16396
|
-
function toToolEvent(tool) {
|
|
16397
|
-
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? undefined;
|
|
16398
|
-
return {
|
|
16399
|
-
kind: "tool",
|
|
16400
|
-
created_at_epoch: tool.created_at_epoch,
|
|
16401
|
-
session_id: tool.session_id,
|
|
16402
|
-
id: tool.id,
|
|
16403
|
-
title: tool.tool_name,
|
|
16404
|
-
detail: detail?.replace(/\s+/g, " ").trim()
|
|
16405
|
-
};
|
|
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)));
|
|
16406
16437
|
}
|
|
16407
|
-
function
|
|
16408
|
-
|
|
16409
|
-
|
|
16410
|
-
|
|
16411
|
-
|
|
16412
|
-
|
|
16413
|
-
|
|
16414
|
-
|
|
16415
|
-
|
|
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;
|
|
16416
16460
|
}
|
|
16417
|
-
function
|
|
16418
|
-
|
|
16419
|
-
|
|
16420
|
-
|
|
16421
|
-
const detail = [
|
|
16422
|
-
summary.completed && summary.completed !== title ? `Completed: ${summary.completed}` : null,
|
|
16423
|
-
summary.learned ? `Learned: ${summary.learned}` : null,
|
|
16424
|
-
summary.next_steps ? `Next: ${summary.next_steps}` : null
|
|
16425
|
-
].filter(Boolean).join(" | ");
|
|
16426
|
-
return {
|
|
16427
|
-
kind: "summary",
|
|
16428
|
-
created_at_epoch: summary.created_at_epoch ?? fallbackEpoch,
|
|
16429
|
-
session_id: summary.session_id,
|
|
16430
|
-
id: summary.id,
|
|
16431
|
-
title: title.replace(/\s+/g, " ").trim(),
|
|
16432
|
-
detail: detail || undefined
|
|
16433
|
-
};
|
|
16461
|
+
function estimateTokens(text) {
|
|
16462
|
+
if (!text)
|
|
16463
|
+
return 0;
|
|
16464
|
+
return Math.ceil(text.length / 4);
|
|
16434
16465
|
}
|
|
16435
|
-
function
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
const
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16446
|
-
|
|
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);
|
|
16447
16509
|
}
|
|
16448
|
-
|
|
16449
|
-
|
|
16450
|
-
|
|
16451
|
-
|
|
16452
|
-
|
|
16453
|
-
|
|
16454
|
-
|
|
16455
|
-
|
|
16456
|
-
|
|
16457
|
-
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
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
|
+
}
|
|
16462
16539
|
}
|
|
16463
|
-
const
|
|
16464
|
-
|
|
16465
|
-
|
|
16466
|
-
|
|
16467
|
-
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
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);
|
|
16471
16552
|
}
|
|
16472
16553
|
}
|
|
16473
|
-
const
|
|
16474
|
-
const
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
16488
|
-
const
|
|
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
|
-
|
|
16514
|
-
const
|
|
16515
|
-
|
|
16516
|
-
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
-
const
|
|
16522
|
-
const
|
|
16523
|
-
const
|
|
16524
|
-
const
|
|
16525
|
-
const
|
|
16526
|
-
|
|
16527
|
-
let
|
|
16528
|
-
|
|
16529
|
-
let claudePostToolHook = false;
|
|
16530
|
-
let claudeStopHook = false;
|
|
16531
|
-
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) {
|
|
16532
16610
|
try {
|
|
16533
|
-
const
|
|
16534
|
-
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
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
|
+
});
|
|
16548
16643
|
}
|
|
16549
16644
|
} catch {}
|
|
16550
16645
|
}
|
|
16551
|
-
let
|
|
16552
|
-
let codexStopHook = false;
|
|
16646
|
+
let staleDecisions;
|
|
16553
16647
|
try {
|
|
16554
|
-
const
|
|
16555
|
-
|
|
16556
|
-
|
|
16648
|
+
const stale = isNewProject ? findStaleDecisionsGlobal(db) : findStaleDecisions(db, projectId);
|
|
16649
|
+
if (stale.length > 0)
|
|
16650
|
+
staleDecisions = stale;
|
|
16557
16651
|
} catch {}
|
|
16558
|
-
const visibilityClause = input.user_id ? " AND user_id = ?" : "";
|
|
16559
|
-
const params = input.user_id ? [sinceEpoch, input.user_id] : [sinceEpoch];
|
|
16560
|
-
const recentUserPrompts = db.db.query(`SELECT COUNT(*) as count FROM user_prompts
|
|
16561
|
-
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
16562
|
-
const recentToolEvents = db.db.query(`SELECT COUNT(*) as count FROM tool_events
|
|
16563
|
-
WHERE created_at_epoch >= ?${visibilityClause}`).get(...params)?.count ?? 0;
|
|
16564
|
-
const recentSessionsWithRawCapture = db.db.query(`SELECT COUNT(*) as count
|
|
16565
|
-
FROM sessions s
|
|
16566
|
-
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
16567
|
-
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
16568
|
-
AND (
|
|
16569
|
-
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
16570
|
-
OR EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
16571
|
-
)`).get(...params)?.count ?? 0;
|
|
16572
|
-
const recentSessionsWithPartialCapture = db.db.query(`SELECT COUNT(*) as count
|
|
16573
|
-
FROM sessions s
|
|
16574
|
-
WHERE COALESCE(s.completed_at_epoch, s.started_at_epoch, 0) >= ?
|
|
16575
|
-
${input.user_id ? "AND s.user_id = ?" : ""}
|
|
16576
|
-
AND (
|
|
16577
|
-
(s.tool_calls_count > 0 AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id))
|
|
16578
|
-
OR (
|
|
16579
|
-
EXISTS (SELECT 1 FROM user_prompts up WHERE up.session_id = s.session_id)
|
|
16580
|
-
AND NOT EXISTS (SELECT 1 FROM tool_events te WHERE te.session_id = s.session_id)
|
|
16581
|
-
)
|
|
16582
|
-
)`).get(...params)?.count ?? 0;
|
|
16583
|
-
const latestPromptEpoch = db.db.query(`SELECT created_at_epoch FROM user_prompts
|
|
16584
|
-
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
16585
|
-
ORDER BY created_at_epoch DESC, prompt_number DESC
|
|
16586
|
-
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
16587
|
-
const latestToolEventEpoch = db.db.query(`SELECT created_at_epoch FROM tool_events
|
|
16588
|
-
WHERE 1 = 1${input.user_id ? " AND user_id = ?" : ""}
|
|
16589
|
-
ORDER BY created_at_epoch DESC, id DESC
|
|
16590
|
-
LIMIT 1`).get(...input.user_id ? [input.user_id] : [])?.created_at_epoch ?? null;
|
|
16591
|
-
const latestPostToolHookEpoch = parseNullableInt(db.getSyncState("hook_post_tool_last_seen_epoch"));
|
|
16592
|
-
const latestPostToolParseStatus = db.getSyncState("hook_post_tool_last_parse_status");
|
|
16593
|
-
const latestPostToolName = db.getSyncState("hook_post_tool_last_tool_name");
|
|
16594
|
-
const schemaVersion = getSchemaVersion(db.db);
|
|
16595
16652
|
return {
|
|
16596
|
-
|
|
16597
|
-
|
|
16598
|
-
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
|
|
16610
|
-
recent_user_prompts: recentUserPrompts,
|
|
16611
|
-
recent_tool_events: recentToolEvents,
|
|
16612
|
-
recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
|
|
16613
|
-
recent_sessions_with_partial_capture: recentSessionsWithPartialCapture,
|
|
16614
|
-
latest_prompt_epoch: latestPromptEpoch,
|
|
16615
|
-
latest_tool_event_epoch: latestToolEventEpoch,
|
|
16616
|
-
latest_post_tool_hook_epoch: latestPostToolHookEpoch,
|
|
16617
|
-
latest_post_tool_parse_status: latestPostToolParseStatus,
|
|
16618
|
-
latest_post_tool_name: latestPostToolName,
|
|
16619
|
-
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
|
|
16620
16667
|
};
|
|
16621
16668
|
}
|
|
16622
|
-
function
|
|
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
|
-
|
|
16662
|
-
AND superseded_by IS NULL
|
|
16663
|
-
AND created_at_epoch >= ?
|
|
16664
|
-
ORDER BY created_at_epoch DESC
|
|
16665
|
-
LIMIT 200`).all(projectId, windowStart);
|
|
16666
|
-
const allImpls = [...implementations, ...crossProjectImpls];
|
|
16667
|
-
const stale = [];
|
|
16668
|
-
for (const decision of decisions) {
|
|
16669
|
-
if (decision.created_at_epoch > staleThreshold)
|
|
16670
|
-
continue;
|
|
16671
|
-
const daysAgo = Math.floor((nowEpoch - decision.created_at_epoch) / 86400);
|
|
16672
|
-
let decisionConcepts = [];
|
|
16673
|
-
try {
|
|
16674
|
-
const parsed = decision.concepts ? JSON.parse(decision.concepts) : [];
|
|
16675
|
-
if (Array.isArray(parsed))
|
|
16676
|
-
decisionConcepts = parsed;
|
|
16677
|
-
} catch {}
|
|
16678
|
-
let bestTitle = "";
|
|
16679
|
-
let bestScore = 0;
|
|
16680
|
-
for (const impl of allImpls) {
|
|
16681
|
-
if (impl.created_at_epoch <= decision.created_at_epoch)
|
|
16682
|
-
continue;
|
|
16683
|
-
const titleScore = jaccardSimilarity(decision.title, impl.title);
|
|
16684
|
-
let conceptBoost = 0;
|
|
16685
|
-
if (decisionConcepts.length > 0) {
|
|
16686
|
-
try {
|
|
16687
|
-
const implConcepts = impl.concepts ? JSON.parse(impl.concepts) : [];
|
|
16688
|
-
if (Array.isArray(implConcepts) && implConcepts.length > 0) {
|
|
16689
|
-
const decSet = new Set(decisionConcepts.map((c) => c.toLowerCase()));
|
|
16690
|
-
const overlap = implConcepts.filter((c) => decSet.has(c.toLowerCase())).length;
|
|
16691
|
-
conceptBoost = overlap / Math.max(decisionConcepts.length, 1) * 0.15;
|
|
16692
|
-
}
|
|
16693
|
-
} 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)}`);
|
|
16694
16709
|
}
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
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);
|
|
16703
16758
|
}
|
|
16704
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
16707
|
-
|
|
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)`);
|
|
16708
16786
|
}
|
|
16709
16787
|
}
|
|
16710
|
-
|
|
16711
|
-
|
|
16712
|
-
|
|
16713
|
-
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
|
|
16717
|
-
|
|
16718
|
-
|
|
16719
|
-
|
|
16720
|
-
|
|
16721
|
-
|
|
16722
|
-
|
|
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);
|
|
16723
16812
|
}
|
|
16724
16813
|
}
|
|
16725
|
-
|
|
16726
|
-
return stale.slice(0, 5);
|
|
16814
|
+
return lines;
|
|
16727
16815
|
}
|
|
16728
|
-
function
|
|
16729
|
-
|
|
16730
|
-
|
|
16731
|
-
const
|
|
16732
|
-
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
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)
|
|
16741
16848
|
return [];
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
const
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
|
|
16759
|
-
|
|
16760
|
-
|
|
16761
|
-
|
|
16762
|
-
|
|
16763
|
-
|
|
16764
|
-
continue;
|
|
16765
|
-
const titleScore = jaccardSimilarity(decision.title, impl.title);
|
|
16766
|
-
let conceptBoost = 0;
|
|
16767
|
-
if (decisionConcepts.length > 0) {
|
|
16768
|
-
try {
|
|
16769
|
-
const implConcepts = impl.concepts ? JSON.parse(impl.concepts) : [];
|
|
16770
|
-
if (Array.isArray(implConcepts) && implConcepts.length > 0) {
|
|
16771
|
-
const decSet = new Set(decisionConcepts.map((c) => c.toLowerCase()));
|
|
16772
|
-
const overlap = implConcepts.filter((c) => decSet.has(c.toLowerCase())).length;
|
|
16773
|
-
conceptBoost = overlap / Math.max(decisionConcepts.length, 1) * 0.15;
|
|
16774
|
-
}
|
|
16775
|
-
} catch {}
|
|
16776
|
-
}
|
|
16777
|
-
const totalScore = titleScore + conceptBoost;
|
|
16778
|
-
if (totalScore > bestScore) {
|
|
16779
|
-
bestScore = totalScore;
|
|
16780
|
-
bestTitle = impl.title;
|
|
16781
|
-
}
|
|
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
|
+
`);
|
|
16782
16871
|
}
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
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()];
|
|
16796
16890
|
}
|
|
16797
16891
|
}
|
|
16798
|
-
|
|
16799
|
-
return stale.slice(0, 5);
|
|
16892
|
+
return [];
|
|
16800
16893
|
}
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
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
|
+
};
|
|
16805
16907
|
}
|
|
16806
|
-
function
|
|
16807
|
-
const
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
]);
|
|
16811
|
-
if (hints.size === 0)
|
|
16812
|
-
return false;
|
|
16813
|
-
const haystack = [
|
|
16814
|
-
obs.title,
|
|
16815
|
-
obs.narrative ?? "",
|
|
16816
|
-
obs.facts ?? "",
|
|
16817
|
-
obs.concepts ?? "",
|
|
16818
|
-
obs.files_read ?? "",
|
|
16819
|
-
obs.files_modified ?? "",
|
|
16820
|
-
obs._source_project ?? ""
|
|
16821
|
-
].join(`
|
|
16822
|
-
`).toLowerCase();
|
|
16823
|
-
for (const hint of hints) {
|
|
16824
|
-
if (haystack.includes(hint))
|
|
16825
|
-
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)}`;
|
|
16826
16912
|
}
|
|
16827
|
-
|
|
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 "";
|
|
16828
16918
|
}
|
|
16829
|
-
function
|
|
16830
|
-
if (!
|
|
16831
|
-
return
|
|
16832
|
-
|
|
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
|
+
}
|
|
16833
16930
|
}
|
|
16834
|
-
function
|
|
16835
|
-
const
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
const
|
|
16840
|
-
const
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
|
|
16844
|
-
const totalActive = isNewProject ? (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
16845
|
-
WHERE lifecycle IN ('active', 'aging', 'pinned')
|
|
16846
|
-
${visibilityClause}
|
|
16847
|
-
AND superseded_by IS NULL`).get(...visibilityParams) ?? { c: 0 }).c : (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
16848
|
-
WHERE project_id = ? AND lifecycle IN ('active', 'aging', 'pinned')
|
|
16849
|
-
${visibilityClause}
|
|
16850
|
-
AND superseded_by IS NULL`).get(projectId, ...visibilityParams) ?? { c: 0 }).c;
|
|
16851
|
-
const candidateLimit = maxCount ?? 50;
|
|
16852
|
-
let pinned = [];
|
|
16853
|
-
let recent = [];
|
|
16854
|
-
let candidates = [];
|
|
16855
|
-
if (!isNewProject) {
|
|
16856
|
-
const MAX_PINNED = 5;
|
|
16857
|
-
pinned = db.db.query(`SELECT * FROM observations
|
|
16858
|
-
WHERE project_id = ? AND lifecycle = 'pinned'
|
|
16859
|
-
AND superseded_by IS NULL
|
|
16860
|
-
${visibilityClause}
|
|
16861
|
-
ORDER BY quality DESC, created_at_epoch DESC
|
|
16862
|
-
LIMIT ?`).all(projectId, ...visibilityParams, MAX_PINNED);
|
|
16863
|
-
const MAX_RECENT = 5;
|
|
16864
|
-
recent = db.db.query(`SELECT * FROM observations
|
|
16865
|
-
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
16866
|
-
AND superseded_by IS NULL
|
|
16867
|
-
${visibilityClause}
|
|
16868
|
-
ORDER BY created_at_epoch DESC
|
|
16869
|
-
LIMIT ?`).all(projectId, ...visibilityParams, MAX_RECENT);
|
|
16870
|
-
candidates = db.db.query(`SELECT * FROM observations
|
|
16871
|
-
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
16872
|
-
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')
|
|
16873
16941
|
AND superseded_by IS NULL
|
|
16874
16942
|
${visibilityClause}
|
|
16875
|
-
|
|
16876
|
-
|
|
16943
|
+
GROUP BY type`).all(projectId, ...userId ? [userId] : []);
|
|
16944
|
+
const counts = {};
|
|
16945
|
+
for (const row of rows) {
|
|
16946
|
+
counts[row.type] = row.count;
|
|
16877
16947
|
}
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
16885
|
-
|
|
16886
|
-
|
|
16887
|
-
|
|
16888
|
-
|
|
16889
|
-
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16893
|
-
|
|
16894
|
-
|
|
16895
|
-
|
|
16896
|
-
|
|
16897
|
-
|
|
16898
|
-
|
|
16899
|
-
|
|
16900
|
-
|
|
16901
|
-
}
|
|
16902
|
-
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
16903
|
-
});
|
|
16904
|
-
if (isNewProject) {
|
|
16905
|
-
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;
|
|
16906
16971
|
}
|
|
16907
16972
|
}
|
|
16908
|
-
const
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
const
|
|
16916
|
-
|
|
16917
|
-
|
|
16918
|
-
|
|
16919
|
-
|
|
16920
|
-
|
|
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;
|
|
16921
17013
|
}
|
|
16922
|
-
const
|
|
16923
|
-
const
|
|
16924
|
-
const
|
|
16925
|
-
|
|
16926
|
-
|
|
16927
|
-
});
|
|
16928
|
-
const projectName = project?.name ?? detected.name;
|
|
16929
|
-
const canonicalId = project?.canonical_id ?? detected.canonical_id;
|
|
16930
|
-
if (maxCount !== undefined) {
|
|
16931
|
-
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
16932
|
-
const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
16933
|
-
const recentPrompts2 = db.getRecentUserPrompts(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16934
|
-
const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16935
|
-
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16936
|
-
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16937
|
-
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
16938
|
-
return {
|
|
16939
|
-
project_name: projectName,
|
|
16940
|
-
canonical_id: canonicalId,
|
|
16941
|
-
observations: all.map(toContextObservation),
|
|
16942
|
-
session_count: all.length,
|
|
16943
|
-
total_active: totalActive,
|
|
16944
|
-
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
16945
|
-
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
16946
|
-
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
16947
|
-
projectTypeCounts: projectTypeCounts2,
|
|
16948
|
-
recentOutcomes: recentOutcomes2
|
|
16949
|
-
};
|
|
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
|
+
}
|
|
16950
17019
|
}
|
|
16951
|
-
|
|
16952
|
-
const
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
16956
|
-
|
|
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 [];
|
|
16957
17098
|
}
|
|
16958
|
-
|
|
16959
|
-
|
|
16960
|
-
|
|
16961
|
-
|
|
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
|
+
}
|
|
16962
17125
|
}
|
|
16963
|
-
|
|
16964
|
-
|
|
16965
|
-
|
|
16966
|
-
|
|
16967
|
-
|
|
16968
|
-
|
|
17126
|
+
return summary;
|
|
17127
|
+
}
|
|
17128
|
+
function buildSuggestedTools(sessions, requestCount, toolCount, observationCount) {
|
|
17129
|
+
const suggested = [];
|
|
17130
|
+
if (sessions.length > 0) {
|
|
17131
|
+
suggested.push("recent_sessions");
|
|
16969
17132
|
}
|
|
16970
|
-
|
|
16971
|
-
|
|
16972
|
-
const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
16973
|
-
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
16974
|
-
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
16975
|
-
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
16976
|
-
let securityFindings = [];
|
|
16977
|
-
if (!isNewProject) {
|
|
16978
|
-
try {
|
|
16979
|
-
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
16980
|
-
securityFindings = db.db.query(`SELECT * FROM security_findings
|
|
16981
|
-
WHERE project_id = ? AND created_at_epoch > ?
|
|
16982
|
-
ORDER BY severity DESC, created_at_epoch DESC
|
|
16983
|
-
LIMIT ?`).all(projectId, weekAgo, 10);
|
|
16984
|
-
} catch {}
|
|
17133
|
+
if (requestCount > 0 || toolCount > 0) {
|
|
17134
|
+
suggested.push("activity_feed");
|
|
16985
17135
|
}
|
|
16986
|
-
|
|
16987
|
-
|
|
16988
|
-
try {
|
|
16989
|
-
const nowEpochSec = Math.floor(Date.now() / 1000);
|
|
16990
|
-
const projectRows = db.db.query(`SELECT p.name, p.canonical_id, p.last_active_epoch,
|
|
16991
|
-
(SELECT COUNT(*) FROM observations o
|
|
16992
|
-
WHERE o.project_id = p.id
|
|
16993
|
-
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
16994
|
-
${opts.userId ? "AND (o.sensitivity != 'personal' OR o.user_id = ?)" : ""}
|
|
16995
|
-
AND o.superseded_by IS NULL) as obs_count
|
|
16996
|
-
FROM projects p
|
|
16997
|
-
ORDER BY p.last_active_epoch DESC
|
|
16998
|
-
LIMIT 10`).all(...visibilityParams);
|
|
16999
|
-
if (projectRows.length > 0) {
|
|
17000
|
-
recentProjects = projectRows.map((r) => {
|
|
17001
|
-
const daysAgo = Math.max(0, Math.floor((nowEpochSec - r.last_active_epoch) / 86400));
|
|
17002
|
-
const lastActive = new Date(r.last_active_epoch * 1000).toISOString().split("T")[0];
|
|
17003
|
-
return {
|
|
17004
|
-
name: r.name,
|
|
17005
|
-
canonical_id: r.canonical_id,
|
|
17006
|
-
observation_count: r.obs_count,
|
|
17007
|
-
last_active: lastActive,
|
|
17008
|
-
days_ago: daysAgo
|
|
17009
|
-
};
|
|
17010
|
-
});
|
|
17011
|
-
}
|
|
17012
|
-
} catch {}
|
|
17136
|
+
if (observationCount > 0) {
|
|
17137
|
+
suggested.push("tool_memory_index", "capture_git_worktree");
|
|
17013
17138
|
}
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
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;
|
|
17020
17176
|
return {
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17034
|
-
|
|
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
|
+
}))
|
|
17035
17260
|
};
|
|
17036
17261
|
}
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
const
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
const lines = [];
|
|
17053
|
-
if (isCrossProject) {
|
|
17054
|
-
lines.push(`## Engrm Memory — Workspace Overview`);
|
|
17055
|
-
lines.push(`This is a new project folder. Here is context from your recent work:`);
|
|
17056
|
-
lines.push("");
|
|
17057
|
-
lines.push("**Active projects in memory:**");
|
|
17058
|
-
for (const rp of context.recentProjects) {
|
|
17059
|
-
const activity = rp.days_ago === 0 ? "today" : rp.days_ago === 1 ? "yesterday" : `${rp.days_ago}d ago`;
|
|
17060
|
-
lines.push(`- **${rp.name}** — ${rp.observation_count} observations, last active ${activity}`);
|
|
17061
|
-
}
|
|
17062
|
-
lines.push("");
|
|
17063
|
-
lines.push(`${context.session_count} relevant observation(s) from across projects:`);
|
|
17064
|
-
lines.push("");
|
|
17065
|
-
} else {
|
|
17066
|
-
lines.push(`## Project Memory: ${context.project_name}`);
|
|
17067
|
-
lines.push(`${context.session_count} relevant observation(s) from prior sessions:`);
|
|
17068
|
-
lines.push("");
|
|
17069
|
-
}
|
|
17070
|
-
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
17071
|
-
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt(prompt.prompt)).slice(0, 5);
|
|
17072
|
-
if (promptLines.length > 0) {
|
|
17073
|
-
lines.push("## Recent Requests");
|
|
17074
|
-
for (const prompt of promptLines) {
|
|
17075
|
-
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
17076
|
-
lines.push(`- ${label}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
17077
|
-
}
|
|
17078
|
-
lines.push("");
|
|
17079
|
-
}
|
|
17080
|
-
}
|
|
17081
|
-
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
17082
|
-
lines.push("## Recent Tools");
|
|
17083
|
-
for (const tool of context.recentToolEvents.slice(0, 5)) {
|
|
17084
|
-
lines.push(`- ${tool.tool_name}: ${formatToolEventDetail(tool)}`);
|
|
17085
|
-
}
|
|
17086
|
-
lines.push("");
|
|
17087
|
-
}
|
|
17088
|
-
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
17089
|
-
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
17090
|
-
const summary = chooseMeaningfulSessionHeadline(session.request, session.completed);
|
|
17091
|
-
if (summary === "(no summary)")
|
|
17092
|
-
return null;
|
|
17093
|
-
return `- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
17094
|
-
}).filter((line) => Boolean(line));
|
|
17095
|
-
if (recentSessionLines.length > 0) {
|
|
17096
|
-
lines.push("## Recent Sessions");
|
|
17097
|
-
lines.push(...recentSessionLines);
|
|
17098
|
-
lines.push("");
|
|
17099
|
-
}
|
|
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
|
+
};
|
|
17100
17277
|
}
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
|
|
17106
|
-
|
|
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);
|
|
17107
17286
|
}
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
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);
|
|
17115
17325
|
}
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
const date5 = obs.created_at.split("T")[0];
|
|
17119
|
-
const fromLabel = obs.source_project ? ` [from: ${obs.source_project}]` : "";
|
|
17120
|
-
const fileLabel = formatObservationFiles(obs);
|
|
17121
|
-
lines.push(`- **#${obs.id} [${obs.type}]** ${obs.title} (${date5}, q=${obs.quality.toFixed(1)})${fromLabel}${fileLabel}`);
|
|
17122
|
-
if (i < DETAILED_COUNT) {
|
|
17123
|
-
const detail = formatObservationDetailFromContext(obs);
|
|
17124
|
-
if (detail) {
|
|
17125
|
-
lines.push(detail);
|
|
17126
|
-
}
|
|
17127
|
-
}
|
|
17326
|
+
if (detected.name.toLowerCase() === "huginn") {
|
|
17327
|
+
explicit.add("aiserver");
|
|
17128
17328
|
}
|
|
17129
|
-
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
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}`;
|
|
17136
17342
|
}
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
|
|
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
|
+
};
|
|
17144
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}`);
|
|
17145
17423
|
}
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17149
|
-
|
|
17150
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17155
|
-
|
|
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;
|
|
17156
17458
|
}
|
|
17157
|
-
const
|
|
17158
|
-
|
|
17159
|
-
|
|
17160
|
-
|
|
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];
|
|
17161
17467
|
}
|
|
17162
|
-
return
|
|
17163
|
-
`);
|
|
17468
|
+
return (b.id ?? 0) - (a.id ?? 0);
|
|
17164
17469
|
}
|
|
17165
|
-
function
|
|
17166
|
-
const
|
|
17167
|
-
|
|
17168
|
-
|
|
17169
|
-
|
|
17170
|
-
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17174
|
-
|
|
17175
|
-
|
|
17176
|
-
|
|
17177
|
-
|
|
17178
|
-
|
|
17179
|
-
|
|
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;
|
|
17180
17496
|
}
|
|
17181
17497
|
}
|
|
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
|
-
|
|
17209
|
-
|
|
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
|
+
};
|
|
17210
17533
|
}
|
|
17211
|
-
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
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
|
+
};
|
|
17215
17651
|
}
|
|
17216
|
-
function
|
|
17652
|
+
function parseNullableInt(value) {
|
|
17217
17653
|
if (!value)
|
|
17218
|
-
return
|
|
17219
|
-
const
|
|
17220
|
-
|
|
17221
|
-
return false;
|
|
17222
|
-
return /[a-z]{3,}/i.test(compact);
|
|
17223
|
-
}
|
|
17224
|
-
function looksLikeFileOperationTitle2(value) {
|
|
17225
|
-
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;
|
|
17226
17657
|
}
|
|
17227
|
-
|
|
17228
|
-
|
|
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
|
+
};
|
|
17229
17733
|
}
|
|
17230
|
-
|
|
17734
|
+
|
|
17735
|
+
// src/tools/tool-memory-index.ts
|
|
17736
|
+
function parseConcepts(value) {
|
|
17231
17737
|
if (!value)
|
|
17232
17738
|
return [];
|
|
17233
|
-
return value.split(`
|
|
17234
|
-
`).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);
|
|
17235
|
-
}
|
|
17236
|
-
function formatObservationDetailFromContext(obs) {
|
|
17237
|
-
if (obs.facts) {
|
|
17238
|
-
const bullets = parseFacts(obs.facts);
|
|
17239
|
-
if (bullets.length > 0) {
|
|
17240
|
-
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
17241
|
-
`);
|
|
17242
|
-
}
|
|
17243
|
-
}
|
|
17244
|
-
if (obs.narrative) {
|
|
17245
|
-
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
17246
|
-
return ` ${snippet}`;
|
|
17247
|
-
}
|
|
17248
|
-
return null;
|
|
17249
|
-
}
|
|
17250
|
-
function formatObservationDetail(obs) {
|
|
17251
|
-
if (obs.facts) {
|
|
17252
|
-
const bullets = parseFacts(obs.facts);
|
|
17253
|
-
if (bullets.length > 0) {
|
|
17254
|
-
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
17255
|
-
`);
|
|
17256
|
-
}
|
|
17257
|
-
}
|
|
17258
|
-
if (obs.narrative) {
|
|
17259
|
-
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
17260
|
-
return ` ${snippet}`;
|
|
17261
|
-
}
|
|
17262
|
-
return "";
|
|
17263
|
-
}
|
|
17264
|
-
function parseFacts(facts) {
|
|
17265
|
-
if (!facts)
|
|
17266
|
-
return [];
|
|
17267
17739
|
try {
|
|
17268
|
-
const parsed = JSON.parse(
|
|
17269
|
-
|
|
17270
|
-
return parsed.filter((f) => typeof f === "string" && f.length > 0);
|
|
17271
|
-
}
|
|
17740
|
+
const parsed = JSON.parse(value);
|
|
17741
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
17272
17742
|
} catch {
|
|
17273
|
-
|
|
17274
|
-
return [facts.trim()];
|
|
17275
|
-
}
|
|
17743
|
+
return [];
|
|
17276
17744
|
}
|
|
17277
|
-
return [];
|
|
17278
17745
|
}
|
|
17279
|
-
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
|
+
});
|
|
17280
17827
|
return {
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
title: obs.title,
|
|
17284
|
-
narrative: obs.narrative,
|
|
17285
|
-
facts: obs.facts,
|
|
17286
|
-
files_read: obs.files_read,
|
|
17287
|
-
files_modified: obs.files_modified,
|
|
17288
|
-
quality: obs.quality,
|
|
17289
|
-
created_at: obs.created_at,
|
|
17290
|
-
...obs._source_project ? { source_project: obs._source_project } : {}
|
|
17828
|
+
project: project?.name,
|
|
17829
|
+
tools
|
|
17291
17830
|
};
|
|
17292
17831
|
}
|
|
17293
|
-
|
|
17294
|
-
|
|
17295
|
-
|
|
17296
|
-
return ` · files: ${truncateText(modified.slice(0, 2).join(", "), 60)}`;
|
|
17297
|
-
}
|
|
17298
|
-
const read = parseJsonStringArray(obs.files_read);
|
|
17299
|
-
if (read.length > 0) {
|
|
17300
|
-
return ` · read: ${truncateText(read.slice(0, 2).join(", "), 60)}`;
|
|
17301
|
-
}
|
|
17302
|
-
return "";
|
|
17303
|
-
}
|
|
17304
|
-
function parseJsonStringArray(value) {
|
|
17832
|
+
|
|
17833
|
+
// src/tools/session-tool-memory.ts
|
|
17834
|
+
function parseConcepts2(value) {
|
|
17305
17835
|
if (!value)
|
|
17306
17836
|
return [];
|
|
17307
17837
|
try {
|
|
17308
17838
|
const parsed = JSON.parse(value);
|
|
17309
|
-
|
|
17310
|
-
return [];
|
|
17311
|
-
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) : [];
|
|
17312
17840
|
} catch {
|
|
17313
17841
|
return [];
|
|
17314
17842
|
}
|
|
17315
17843
|
}
|
|
17316
|
-
function
|
|
17317
|
-
const
|
|
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
|
-
|
|
17368
|
-
const title = stripInlineSectionLabel(obs.title);
|
|
17369
|
-
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
17370
|
-
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle2(title))
|
|
17371
|
-
continue;
|
|
17372
|
-
seen.add(normalized);
|
|
17373
|
-
picked.push(title);
|
|
17374
|
-
if (picked.length >= 5)
|
|
17375
|
-
break;
|
|
17376
|
-
}
|
|
17377
|
-
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
|
+
};
|
|
17378
17896
|
}
|
|
17379
17897
|
|
|
17380
17898
|
// src/tools/session-context.ts
|
|
@@ -17387,6 +17905,7 @@ function getSessionContext(db, input) {
|
|
|
17387
17905
|
});
|
|
17388
17906
|
if (!context)
|
|
17389
17907
|
return null;
|
|
17908
|
+
const preview = formatContextForInjection(context);
|
|
17390
17909
|
const recentRequests = context.recentPrompts?.length ?? 0;
|
|
17391
17910
|
const recentTools = context.recentToolEvents?.length ?? 0;
|
|
17392
17911
|
const captureState = recentRequests > 0 && recentTools > 0 ? "rich" : recentRequests > 0 || recentTools > 0 ? "partial" : "summary-only";
|
|
@@ -17403,19 +17922,21 @@ function getSessionContext(db, input) {
|
|
|
17403
17922
|
hot_files: hotFiles,
|
|
17404
17923
|
capture_state: captureState,
|
|
17405
17924
|
raw_capture_active: recentRequests > 0 || recentTools > 0,
|
|
17406
|
-
|
|
17925
|
+
estimated_read_tokens: estimateTokens(preview),
|
|
17926
|
+
suggested_tools: buildSuggestedTools2(context),
|
|
17927
|
+
preview
|
|
17407
17928
|
};
|
|
17408
17929
|
}
|
|
17409
17930
|
function buildHotFiles(context) {
|
|
17410
17931
|
const counts = new Map;
|
|
17411
17932
|
for (const obs of context.observations) {
|
|
17412
|
-
for (const path of [...
|
|
17933
|
+
for (const path of [...parseJsonArray2(obs.files_read), ...parseJsonArray2(obs.files_modified)]) {
|
|
17413
17934
|
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
17414
17935
|
}
|
|
17415
17936
|
}
|
|
17416
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);
|
|
17417
17938
|
}
|
|
17418
|
-
function
|
|
17939
|
+
function parseJsonArray2(value) {
|
|
17419
17940
|
if (!value)
|
|
17420
17941
|
return [];
|
|
17421
17942
|
try {
|
|
@@ -17425,6 +17946,161 @@ function parseJsonArray(value) {
|
|
|
17425
17946
|
return [];
|
|
17426
17947
|
}
|
|
17427
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
|
+
}
|
|
17428
18104
|
|
|
17429
18105
|
// src/tools/send-message.ts
|
|
17430
18106
|
async function sendMessage(db, config2, input) {
|
|
@@ -17550,10 +18226,7 @@ function countPresentSections(summary) {
|
|
|
17550
18226
|
].filter(hasContent).length;
|
|
17551
18227
|
}
|
|
17552
18228
|
function extractSectionItems(section) {
|
|
17553
|
-
|
|
17554
|
-
return [];
|
|
17555
|
-
return section.split(`
|
|
17556
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
|
|
18229
|
+
return extractSummaryItems(section);
|
|
17557
18230
|
}
|
|
17558
18231
|
function extractObservationTitles(observations, types) {
|
|
17559
18232
|
const typeSet = new Set(types);
|
|
@@ -17995,6 +18668,8 @@ function buildVectorDocument(obs, config2, project) {
|
|
|
17995
18668
|
concepts: obs.concepts ? JSON.parse(obs.concepts) : [],
|
|
17996
18669
|
files_read: obs.files_read ? JSON.parse(obs.files_read) : [],
|
|
17997
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,
|
|
17998
18673
|
session_id: obs.session_id,
|
|
17999
18674
|
created_at_epoch: obs.created_at_epoch,
|
|
18000
18675
|
created_at: obs.created_at,
|
|
@@ -18048,6 +18723,8 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
|
|
|
18048
18723
|
recent_tool_commands: captureContext?.recent_tool_commands ?? [],
|
|
18049
18724
|
hot_files: captureContext?.hot_files ?? [],
|
|
18050
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,
|
|
18051
18728
|
decisions_count: valueSignals.decisions_count,
|
|
18052
18729
|
lessons_count: valueSignals.lessons_count,
|
|
18053
18730
|
discoveries_count: valueSignals.discoveries_count,
|
|
@@ -18160,10 +18837,7 @@ function countPresentSections2(summary) {
|
|
|
18160
18837
|
].filter((value) => Boolean(value && value.trim())).length;
|
|
18161
18838
|
}
|
|
18162
18839
|
function extractSectionItems2(section) {
|
|
18163
|
-
|
|
18164
|
-
return [];
|
|
18165
|
-
return section.split(`
|
|
18166
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
|
|
18840
|
+
return extractSummaryItems(section, 4);
|
|
18167
18841
|
}
|
|
18168
18842
|
function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
18169
18843
|
const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
|
|
@@ -18171,11 +18845,18 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
|
18171
18845
|
const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
|
|
18172
18846
|
const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
|
|
18173
18847
|
const hotFiles = [...new Set(observations.flatMap((obs) => [
|
|
18174
|
-
...
|
|
18175
|
-
...
|
|
18848
|
+
...parseJsonArray3(obs.files_modified),
|
|
18849
|
+
...parseJsonArray3(obs.files_read)
|
|
18176
18850
|
]).filter(Boolean))].slice(0, 6);
|
|
18177
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);
|
|
18178
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;
|
|
18179
18860
|
return {
|
|
18180
18861
|
prompt_count: prompts.length,
|
|
18181
18862
|
tool_event_count: toolEvents.length,
|
|
@@ -18185,10 +18866,12 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
|
|
|
18185
18866
|
recent_tool_commands: recentToolCommands,
|
|
18186
18867
|
capture_state: captureState,
|
|
18187
18868
|
hot_files: hotFiles,
|
|
18188
|
-
recent_outcomes: recentOutcomes
|
|
18869
|
+
recent_outcomes: recentOutcomes,
|
|
18870
|
+
observation_source_tools: observationSourceTools,
|
|
18871
|
+
latest_observation_prompt_number: latestObservationPromptNumber
|
|
18189
18872
|
};
|
|
18190
18873
|
}
|
|
18191
|
-
function
|
|
18874
|
+
function parseJsonArray3(value) {
|
|
18192
18875
|
if (!value)
|
|
18193
18876
|
return [];
|
|
18194
18877
|
try {
|
|
@@ -18450,7 +19133,7 @@ async function backfillEmbeddings(db, batchSize = 50) {
|
|
|
18450
19133
|
}
|
|
18451
19134
|
|
|
18452
19135
|
// src/packs/recommender.ts
|
|
18453
|
-
import { existsSync as
|
|
19136
|
+
import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
18454
19137
|
import { join as join4, basename as basename2, dirname as dirname2 } from "node:path";
|
|
18455
19138
|
import { fileURLToPath } from "node:url";
|
|
18456
19139
|
function getPacksDir() {
|
|
@@ -18459,7 +19142,7 @@ function getPacksDir() {
|
|
|
18459
19142
|
}
|
|
18460
19143
|
function loadPack(name) {
|
|
18461
19144
|
const packPath = join4(getPacksDir(), `${name}.json`);
|
|
18462
|
-
if (!
|
|
19145
|
+
if (!existsSync6(packPath))
|
|
18463
19146
|
return null;
|
|
18464
19147
|
try {
|
|
18465
19148
|
const raw = readFileSync4(packPath, "utf-8");
|
|
@@ -18470,7 +19153,7 @@ function loadPack(name) {
|
|
|
18470
19153
|
}
|
|
18471
19154
|
|
|
18472
19155
|
// src/server.ts
|
|
18473
|
-
import { existsSync as
|
|
19156
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
18474
19157
|
import { join as join5 } from "node:path";
|
|
18475
19158
|
import { homedir as homedir3 } from "node:os";
|
|
18476
19159
|
|
|
@@ -18799,6 +19482,218 @@ function reduceGitDiffToMemory(input) {
|
|
|
18799
19482
|
};
|
|
18800
19483
|
}
|
|
18801
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
|
+
|
|
18802
19697
|
// src/server.ts
|
|
18803
19698
|
if (!configExists()) {
|
|
18804
19699
|
console.error("Engrm is not configured. Run: engrm init --manual");
|
|
@@ -18820,7 +19715,7 @@ var MCP_METRICS_PATH = join5(homedir3(), ".engrm", "mcp-session-metrics.json");
|
|
|
18820
19715
|
function persistSessionMetrics() {
|
|
18821
19716
|
try {
|
|
18822
19717
|
const dir = join5(homedir3(), ".engrm");
|
|
18823
|
-
if (!
|
|
19718
|
+
if (!existsSync7(dir))
|
|
18824
19719
|
mkdirSync2(dir, { recursive: true });
|
|
18825
19720
|
writeFileSync2(MCP_METRICS_PATH, JSON.stringify(sessionMetrics), "utf-8");
|
|
18826
19721
|
} catch {}
|
|
@@ -18869,7 +19764,7 @@ process.on("SIGTERM", () => {
|
|
|
18869
19764
|
});
|
|
18870
19765
|
var server = new McpServer({
|
|
18871
19766
|
name: "engrm",
|
|
18872
|
-
version: "0.4.
|
|
19767
|
+
version: "0.4.13"
|
|
18873
19768
|
});
|
|
18874
19769
|
server.tool("save_observation", "Save an observation to memory", {
|
|
18875
19770
|
type: exports_external.enum([
|
|
@@ -19045,6 +19940,164 @@ Facts: ${reduced.facts.join("; ")}` : "";
|
|
|
19045
19940
|
]
|
|
19046
19941
|
};
|
|
19047
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
|
+
});
|
|
19048
20101
|
server.tool("search", "Search memory for observations", {
|
|
19049
20102
|
query: exports_external.string().describe("Search query"),
|
|
19050
20103
|
project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
|
|
@@ -19390,7 +20443,22 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
19390
20443
|
return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
|
|
19391
20444
|
}).join(`
|
|
19392
20445
|
`) : "- (none)";
|
|
19393
|
-
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(`
|
|
19394
20462
|
`) : "- (none)";
|
|
19395
20463
|
const projectLine = result.project ? `Project: ${result.project}
|
|
19396
20464
|
|
|
@@ -19404,7 +20472,20 @@ server.tool("memory_console", "Show a high-signal local overview of what Engrm c
|
|
|
19404
20472
|
content: [
|
|
19405
20473
|
{
|
|
19406
20474
|
type: "text",
|
|
19407
|
-
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:
|
|
19408
20489
|
${sessionLines}
|
|
19409
20490
|
|
|
19410
20491
|
` + `Recent requests:
|
|
@@ -19455,6 +20536,106 @@ server.tool("capture_status", "Show whether Engrm hook registration and recent p
|
|
|
19455
20536
|
]
|
|
19456
20537
|
};
|
|
19457
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
|
+
});
|
|
19458
20639
|
server.tool("session_context", "Preview the exact project memory context Engrm would inject at session start", {
|
|
19459
20640
|
cwd: exports_external.string().optional(),
|
|
19460
20641
|
token_budget: exports_external.number().optional(),
|
|
@@ -19541,6 +20722,10 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
19541
20722
|
}).join(`
|
|
19542
20723
|
`) : "- (none)";
|
|
19543
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(`
|
|
19544
20729
|
`) : "- (none)";
|
|
19545
20730
|
const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
|
|
19546
20731
|
`) : "- (none)";
|
|
@@ -19555,15 +20740,25 @@ server.tool("project_memory_index", "Show a typed local memory index for the cur
|
|
|
19555
20740
|
|
|
19556
20741
|
` + `Raw chronology: ${result.raw_capture_active ? "active" : "observations-only so far"}
|
|
19557
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
|
+
|
|
19558
20747
|
` + `Observation counts:
|
|
19559
20748
|
${counts}
|
|
19560
20749
|
|
|
20750
|
+
` + `Top types:
|
|
20751
|
+
${topTypes}
|
|
20752
|
+
|
|
19561
20753
|
` + `Recent sessions:
|
|
19562
20754
|
${sessions}
|
|
19563
20755
|
|
|
19564
20756
|
` + `Hot files:
|
|
19565
20757
|
${hotFiles}
|
|
19566
20758
|
|
|
20759
|
+
` + `Observation provenance:
|
|
20760
|
+
${provenance}
|
|
20761
|
+
|
|
19567
20762
|
` + `Recent memory objects:
|
|
19568
20763
|
${topTitles}`
|
|
19569
20764
|
}
|
|
@@ -19634,17 +20829,22 @@ server.tool("workspace_memory_index", "Show a cross-project local memory index f
|
|
|
19634
20829
|
});
|
|
19635
20830
|
const projectLines = result.projects.length > 0 ? result.projects.map((project) => {
|
|
19636
20831
|
const when = new Date(project.last_active_epoch * 1000).toISOString().split("T")[0];
|
|
19637
|
-
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}`;
|
|
19638
20833
|
}).join(`
|
|
20834
|
+
`) : "- (none)";
|
|
20835
|
+
const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
19639
20836
|
`) : "- (none)";
|
|
19640
20837
|
return {
|
|
19641
20838
|
content: [
|
|
19642
20839
|
{
|
|
19643
20840
|
type: "text",
|
|
19644
|
-
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}
|
|
19645
20842
|
|
|
19646
20843
|
` + `Projects with raw chronology: ${result.projects_with_raw_capture}
|
|
19647
20844
|
|
|
20845
|
+
` + `Observation provenance:
|
|
20846
|
+
${provenanceLines}
|
|
20847
|
+
|
|
19648
20848
|
` + `Projects:
|
|
19649
20849
|
${projectLines}`
|
|
19650
20850
|
}
|
|
@@ -19752,10 +20952,19 @@ server.tool("session_story", "Show the full local memory story for one session",
|
|
|
19752
20952
|
return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
|
|
19753
20953
|
}).join(`
|
|
19754
20954
|
`) : "- (none)";
|
|
19755
|
-
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(`
|
|
19756
20963
|
`) : "- (none)";
|
|
19757
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";
|
|
19758
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(`
|
|
19759
20968
|
`) : "- none";
|
|
19760
20969
|
return {
|
|
19761
20970
|
content: [
|
|
@@ -19775,6 +20984,9 @@ ${promptLines}
|
|
|
19775
20984
|
` + `Tools:
|
|
19776
20985
|
${toolLines}
|
|
19777
20986
|
|
|
20987
|
+
` + `Provenance:
|
|
20988
|
+
${provenanceSummary}
|
|
20989
|
+
|
|
19778
20990
|
` + `Capture gaps:
|
|
19779
20991
|
${captureGaps}
|
|
19780
20992
|
|