engrm 0.4.23 → 0.4.26
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 -7
- package/dist/cli.js +103 -5
- package/dist/hooks/elicitation-result.js +90 -5
- package/dist/hooks/post-tool-use.js +680 -16
- package/dist/hooks/pre-compact.js +719 -27
- package/dist/hooks/sentinel.js +90 -5
- package/dist/hooks/session-start.js +1442 -170
- package/dist/hooks/stop.js +546 -8
- package/dist/hooks/user-prompt-submit.js +1390 -6
- package/dist/server.js +738 -76
- package/package.json +1 -1
|
@@ -473,6 +473,125 @@ function normalizeItem(value) {
|
|
|
473
473
|
return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
// src/tools/session-story.ts
|
|
477
|
+
function getSessionStory(db, input) {
|
|
478
|
+
const session = db.getSessionById(input.session_id);
|
|
479
|
+
const summary = db.getSessionSummary(input.session_id);
|
|
480
|
+
const prompts = db.getSessionUserPrompts(input.session_id, 50);
|
|
481
|
+
const chatMessages = db.getSessionChatMessages(input.session_id, 50);
|
|
482
|
+
const toolEvents = db.getSessionToolEvents(input.session_id, 100);
|
|
483
|
+
const allObservations = db.getObservationsBySession(input.session_id);
|
|
484
|
+
const handoffs = allObservations.filter((obs) => looksLikeHandoff(obs));
|
|
485
|
+
const rollingHandoffDrafts = handoffs.filter((obs) => isDraftHandoff(obs));
|
|
486
|
+
const savedHandoffs = handoffs.filter((obs) => !isDraftHandoff(obs));
|
|
487
|
+
const observations = allObservations.filter((obs) => !looksLikeHandoff(obs));
|
|
488
|
+
const metrics = db.getSessionMetrics(input.session_id);
|
|
489
|
+
const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
|
|
490
|
+
const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
|
|
491
|
+
return {
|
|
492
|
+
session,
|
|
493
|
+
project_name: projectName,
|
|
494
|
+
summary,
|
|
495
|
+
prompts,
|
|
496
|
+
chat_messages: chatMessages,
|
|
497
|
+
tool_events: toolEvents,
|
|
498
|
+
observations,
|
|
499
|
+
handoffs,
|
|
500
|
+
saved_handoffs: savedHandoffs,
|
|
501
|
+
rolling_handoff_drafts: rollingHandoffDrafts,
|
|
502
|
+
metrics,
|
|
503
|
+
capture_state: classifyCaptureState({
|
|
504
|
+
hasSummary: Boolean(summary?.request || summary?.completed),
|
|
505
|
+
promptCount: prompts.length,
|
|
506
|
+
toolEventCount: toolEvents.length
|
|
507
|
+
}),
|
|
508
|
+
capture_gaps: buildCaptureGaps({
|
|
509
|
+
promptCount: prompts.length,
|
|
510
|
+
toolEventCount: toolEvents.length,
|
|
511
|
+
toolCallsCount: metrics?.tool_calls_count ?? 0,
|
|
512
|
+
observationCount: observations.length,
|
|
513
|
+
hasSummary: Boolean(summary?.request || summary?.completed)
|
|
514
|
+
}),
|
|
515
|
+
latest_request: latestRequest,
|
|
516
|
+
recent_outcomes: collectRecentOutcomes(observations),
|
|
517
|
+
hot_files: collectHotFiles(observations),
|
|
518
|
+
provenance_summary: collectProvenanceSummary(observations)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function classifyCaptureState(input) {
|
|
522
|
+
if (input.promptCount > 0 && input.toolEventCount > 0)
|
|
523
|
+
return "rich";
|
|
524
|
+
if (input.promptCount > 0 || input.toolEventCount > 0)
|
|
525
|
+
return "partial";
|
|
526
|
+
if (input.hasSummary)
|
|
527
|
+
return "summary-only";
|
|
528
|
+
return "legacy";
|
|
529
|
+
}
|
|
530
|
+
function buildCaptureGaps(input) {
|
|
531
|
+
const gaps = [];
|
|
532
|
+
if (input.promptCount === 0)
|
|
533
|
+
gaps.push("missing prompts");
|
|
534
|
+
if (input.toolCallsCount > 0 && input.toolEventCount === 0) {
|
|
535
|
+
gaps.push("missing raw tool chronology");
|
|
536
|
+
} else if (input.toolEventCount === 0) {
|
|
537
|
+
gaps.push("no tool events");
|
|
538
|
+
}
|
|
539
|
+
if (input.observationCount === 0 && input.hasSummary) {
|
|
540
|
+
gaps.push("summary without reusable observations");
|
|
541
|
+
}
|
|
542
|
+
return gaps;
|
|
543
|
+
}
|
|
544
|
+
function collectRecentOutcomes(observations) {
|
|
545
|
+
const seen = new Set;
|
|
546
|
+
const outcomes = [];
|
|
547
|
+
for (const obs of observations) {
|
|
548
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
549
|
+
continue;
|
|
550
|
+
const title = obs.title.trim();
|
|
551
|
+
if (!title || looksLikeFileOperationTitle(title))
|
|
552
|
+
continue;
|
|
553
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
554
|
+
if (seen.has(normalized))
|
|
555
|
+
continue;
|
|
556
|
+
seen.add(normalized);
|
|
557
|
+
outcomes.push(title);
|
|
558
|
+
if (outcomes.length >= 6)
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
return outcomes;
|
|
562
|
+
}
|
|
563
|
+
function collectHotFiles(observations) {
|
|
564
|
+
const counts = new Map;
|
|
565
|
+
for (const obs of observations) {
|
|
566
|
+
for (const path of [...parseJsonArray(obs.files_modified), ...parseJsonArray(obs.files_read)]) {
|
|
567
|
+
counts.set(path, (counts.get(path) ?? 0) + 1);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
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);
|
|
571
|
+
}
|
|
572
|
+
function parseJsonArray(value) {
|
|
573
|
+
if (!value)
|
|
574
|
+
return [];
|
|
575
|
+
try {
|
|
576
|
+
const parsed = JSON.parse(value);
|
|
577
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
578
|
+
} catch {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function looksLikeFileOperationTitle(value) {
|
|
583
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
584
|
+
}
|
|
585
|
+
function collectProvenanceSummary(observations) {
|
|
586
|
+
const counts = new Map;
|
|
587
|
+
for (const obs of observations) {
|
|
588
|
+
if (!obs.source_tool)
|
|
589
|
+
continue;
|
|
590
|
+
counts.set(obs.source_tool, (counts.get(obs.source_tool) ?? 0) + 1);
|
|
591
|
+
}
|
|
592
|
+
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);
|
|
593
|
+
}
|
|
594
|
+
|
|
476
595
|
// src/tools/save.ts
|
|
477
596
|
import { relative, isAbsolute } from "node:path";
|
|
478
597
|
|
|
@@ -1136,71 +1255,996 @@ async function saveObservation(db, config, input) {
|
|
|
1136
1255
|
}
|
|
1137
1256
|
} catch {}
|
|
1138
1257
|
}
|
|
1139
|
-
return {
|
|
1140
|
-
success: true,
|
|
1141
|
-
observation_id: obs.id,
|
|
1142
|
-
quality_score: qualityScore,
|
|
1143
|
-
recall_hint: recallHint,
|
|
1144
|
-
conflict_warning: conflictWarning
|
|
1145
|
-
};
|
|
1258
|
+
return {
|
|
1259
|
+
success: true,
|
|
1260
|
+
observation_id: obs.id,
|
|
1261
|
+
quality_score: qualityScore,
|
|
1262
|
+
recall_hint: recallHint,
|
|
1263
|
+
conflict_warning: conflictWarning
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function toRelativePath(filePath, projectRoot) {
|
|
1267
|
+
if (!isAbsolute(filePath))
|
|
1268
|
+
return filePath;
|
|
1269
|
+
const rel = relative(projectRoot, filePath);
|
|
1270
|
+
if (rel.startsWith(".."))
|
|
1271
|
+
return filePath;
|
|
1272
|
+
return rel;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/tools/handoffs.ts
|
|
1276
|
+
async function upsertRollingHandoff(db, config, input) {
|
|
1277
|
+
const resolved = resolveTargetSession(db, input.cwd, config.user_id, input.session_id);
|
|
1278
|
+
if (!resolved.session) {
|
|
1279
|
+
return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
reason: "No recent session found to draft a handoff yet"
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const story = getSessionStory(db, { session_id: resolved.session.session_id });
|
|
1285
|
+
if (!story.session) {
|
|
1286
|
+
return {
|
|
1287
|
+
success: false,
|
|
1288
|
+
reason: `Session ${resolved.session.session_id} not found`
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const includeChat = input.include_chat === true || input.include_chat !== false && shouldAutoIncludeChat(story);
|
|
1292
|
+
const chatLimit = Math.max(1, Math.min(input.chat_limit ?? 3, 6));
|
|
1293
|
+
const title = `Handoff Draft: ${buildHandoffTitle(story.summary, story.latest_request)}`;
|
|
1294
|
+
const narrative = buildHandoffNarrative(story.summary, story, {
|
|
1295
|
+
includeChat,
|
|
1296
|
+
chatLimit
|
|
1297
|
+
});
|
|
1298
|
+
const facts = buildHandoffFacts(story.summary, story);
|
|
1299
|
+
const concepts = buildDraftHandoffConcepts(story.project_name, story.capture_state);
|
|
1300
|
+
const existing = getSessionRollingHandoff(db, story.session.session_id);
|
|
1301
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1302
|
+
if (existing) {
|
|
1303
|
+
const nextFacts = JSON.stringify(facts);
|
|
1304
|
+
const nextConcepts = JSON.stringify(concepts);
|
|
1305
|
+
const shouldRefresh = existing.title !== title || (existing.narrative ?? null) !== narrative || (existing.facts ?? null) !== nextFacts || (existing.concepts ?? null) !== nextConcepts || now - existing.created_at_epoch >= 120;
|
|
1306
|
+
if (!shouldRefresh) {
|
|
1307
|
+
return {
|
|
1308
|
+
success: true,
|
|
1309
|
+
observation_id: existing.id,
|
|
1310
|
+
session_id: story.session.session_id,
|
|
1311
|
+
title: existing.title
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
const updated = db.updateObservationContent(existing.id, {
|
|
1315
|
+
title,
|
|
1316
|
+
narrative,
|
|
1317
|
+
facts: nextFacts,
|
|
1318
|
+
concepts: nextConcepts,
|
|
1319
|
+
created_at_epoch: now
|
|
1320
|
+
});
|
|
1321
|
+
if (!updated) {
|
|
1322
|
+
return {
|
|
1323
|
+
success: false,
|
|
1324
|
+
reason: "Failed to update rolling handoff draft"
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
db.addToOutbox("observation", updated.id);
|
|
1328
|
+
return {
|
|
1329
|
+
success: true,
|
|
1330
|
+
observation_id: updated.id,
|
|
1331
|
+
session_id: story.session.session_id,
|
|
1332
|
+
title: updated.title
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
const result = await saveObservation(db, config, {
|
|
1336
|
+
type: "message",
|
|
1337
|
+
title,
|
|
1338
|
+
narrative,
|
|
1339
|
+
facts,
|
|
1340
|
+
concepts,
|
|
1341
|
+
session_id: story.session.session_id,
|
|
1342
|
+
cwd: input.cwd,
|
|
1343
|
+
agent: "engrm-handoff",
|
|
1344
|
+
source_tool: "rolling_handoff"
|
|
1345
|
+
});
|
|
1346
|
+
return {
|
|
1347
|
+
success: result.success,
|
|
1348
|
+
observation_id: result.observation_id,
|
|
1349
|
+
session_id: story.session.session_id,
|
|
1350
|
+
title,
|
|
1351
|
+
reason: result.reason
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function getRecentHandoffs(db, input) {
|
|
1355
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
|
|
1356
|
+
const queryLimit = input.current_device_id ? Math.max(limit, Math.min(limit * 5, 50)) : limit;
|
|
1357
|
+
const projectScoped = input.project_scoped !== false;
|
|
1358
|
+
let projectId = null;
|
|
1359
|
+
let projectName;
|
|
1360
|
+
if (projectScoped) {
|
|
1361
|
+
const cwd = input.cwd ?? process.cwd();
|
|
1362
|
+
const detected = detectProject(cwd);
|
|
1363
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
1364
|
+
if (project) {
|
|
1365
|
+
projectId = project.id;
|
|
1366
|
+
projectName = project.name;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
const conditions = [
|
|
1370
|
+
"o.type = 'message'",
|
|
1371
|
+
"o.lifecycle IN ('active', 'aging', 'pinned')",
|
|
1372
|
+
"o.superseded_by IS NULL",
|
|
1373
|
+
`(o.title LIKE 'Handoff:%' OR o.concepts LIKE '%"handoff"%')`
|
|
1374
|
+
];
|
|
1375
|
+
const params = [];
|
|
1376
|
+
if (input.user_id) {
|
|
1377
|
+
conditions.push("(o.sensitivity != 'personal' OR o.user_id = ?)");
|
|
1378
|
+
params.push(input.user_id);
|
|
1379
|
+
}
|
|
1380
|
+
if (projectId !== null) {
|
|
1381
|
+
conditions.push("o.project_id = ?");
|
|
1382
|
+
params.push(projectId);
|
|
1383
|
+
}
|
|
1384
|
+
params.push(queryLimit);
|
|
1385
|
+
const handoffs = db.db.query(`SELECT o.*, p.name AS project_name
|
|
1386
|
+
FROM observations o
|
|
1387
|
+
LEFT JOIN projects p ON p.id = o.project_id
|
|
1388
|
+
WHERE ${conditions.join(" AND ")}
|
|
1389
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
1390
|
+
LIMIT ?`).all(...params);
|
|
1391
|
+
handoffs.sort((a, b) => compareHandoffs(a, b, input.current_device_id));
|
|
1392
|
+
return {
|
|
1393
|
+
handoffs: handoffs.slice(0, limit),
|
|
1394
|
+
project: projectName
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function formatHandoffSource(handoff) {
|
|
1398
|
+
const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
|
|
1399
|
+
const ageLabel = ageSeconds < 3600 ? `${Math.max(1, Math.floor(ageSeconds / 60) || 1)}m ago` : ageSeconds < 86400 ? `${Math.floor(ageSeconds / 3600)}h ago` : `${Math.floor(ageSeconds / 86400)}d ago`;
|
|
1400
|
+
return `from ${handoff.device_id} · ${ageLabel}`;
|
|
1401
|
+
}
|
|
1402
|
+
function isDraftHandoff(obs) {
|
|
1403
|
+
if (obs.title.startsWith("Handoff Draft:"))
|
|
1404
|
+
return true;
|
|
1405
|
+
const concepts = parseJsonArray2(obs.concepts);
|
|
1406
|
+
return concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
|
|
1407
|
+
}
|
|
1408
|
+
function getSessionRollingHandoff(db, sessionId) {
|
|
1409
|
+
return db.db.query(`SELECT o.*, p.name AS project_name
|
|
1410
|
+
FROM observations o
|
|
1411
|
+
LEFT JOIN projects p ON p.id = o.project_id
|
|
1412
|
+
WHERE o.session_id = ?
|
|
1413
|
+
AND o.type = 'message'
|
|
1414
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
1415
|
+
AND o.superseded_by IS NULL
|
|
1416
|
+
AND (o.title LIKE 'Handoff Draft:%' OR o.concepts LIKE '%"draft-handoff"%')
|
|
1417
|
+
ORDER BY o.created_at_epoch DESC, o.id DESC
|
|
1418
|
+
LIMIT 1`).get(sessionId) ?? null;
|
|
1419
|
+
}
|
|
1420
|
+
function compareHandoffs(a, b, currentDeviceId) {
|
|
1421
|
+
const aDraft = isDraftHandoff(a) ? 1 : 0;
|
|
1422
|
+
const bDraft = isDraftHandoff(b) ? 1 : 0;
|
|
1423
|
+
if (aDraft !== bDraft)
|
|
1424
|
+
return aDraft - bDraft;
|
|
1425
|
+
if (currentDeviceId) {
|
|
1426
|
+
const aOther = a.device_id !== currentDeviceId ? 1 : 0;
|
|
1427
|
+
const bOther = b.device_id !== currentDeviceId ? 1 : 0;
|
|
1428
|
+
if (aOther !== bOther)
|
|
1429
|
+
return bOther - aOther;
|
|
1430
|
+
}
|
|
1431
|
+
if (b.created_at_epoch !== a.created_at_epoch) {
|
|
1432
|
+
return b.created_at_epoch - a.created_at_epoch;
|
|
1433
|
+
}
|
|
1434
|
+
return b.id - a.id;
|
|
1435
|
+
}
|
|
1436
|
+
function resolveTargetSession(db, cwd, userId, sessionId) {
|
|
1437
|
+
if (sessionId) {
|
|
1438
|
+
const session = db.getSessionById(sessionId);
|
|
1439
|
+
if (!session)
|
|
1440
|
+
return { session: null };
|
|
1441
|
+
const projectName = session.project_id ? db.getProjectById(session.project_id)?.name : undefined;
|
|
1442
|
+
return {
|
|
1443
|
+
session: {
|
|
1444
|
+
...session,
|
|
1445
|
+
project_name: projectName ?? null,
|
|
1446
|
+
request: db.getSessionSummary(sessionId)?.request ?? null,
|
|
1447
|
+
completed: db.getSessionSummary(sessionId)?.completed ?? null,
|
|
1448
|
+
current_thread: db.getSessionSummary(sessionId)?.current_thread ?? null,
|
|
1449
|
+
capture_state: db.getSessionSummary(sessionId)?.capture_state ?? null,
|
|
1450
|
+
recent_tool_names: db.getSessionSummary(sessionId)?.recent_tool_names ?? null,
|
|
1451
|
+
hot_files: db.getSessionSummary(sessionId)?.hot_files ?? null,
|
|
1452
|
+
recent_outcomes: db.getSessionSummary(sessionId)?.recent_outcomes ?? null,
|
|
1453
|
+
prompt_count: db.getSessionUserPrompts(sessionId, 200).length,
|
|
1454
|
+
tool_event_count: db.getSessionToolEvents(sessionId, 200).length
|
|
1455
|
+
},
|
|
1456
|
+
projectName: projectName ?? undefined
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
const detected = detectProject(cwd ?? process.cwd());
|
|
1460
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
1461
|
+
const sessions = db.getRecentSessions(project?.id ?? null, 10, userId);
|
|
1462
|
+
return {
|
|
1463
|
+
session: sessions[0] ?? null,
|
|
1464
|
+
projectName: project?.name
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function buildHandoffTitle(summary, latestRequest, explicit) {
|
|
1468
|
+
const chosen = explicit?.trim() || summary?.current_thread?.trim() || summary?.completed?.trim() || latestRequest?.trim() || "Current work";
|
|
1469
|
+
return compactLine(chosen) ?? "Current work";
|
|
1470
|
+
}
|
|
1471
|
+
function buildHandoffNarrative(summary, story, options) {
|
|
1472
|
+
const sections = [];
|
|
1473
|
+
if (summary?.request || story.latest_request) {
|
|
1474
|
+
sections.push(`Request: ${summary?.request ?? story.latest_request}`);
|
|
1475
|
+
}
|
|
1476
|
+
if (summary?.current_thread) {
|
|
1477
|
+
sections.push(`Current thread: ${summary.current_thread}`);
|
|
1478
|
+
}
|
|
1479
|
+
if (summary?.investigated) {
|
|
1480
|
+
sections.push(`Investigated: ${summary.investigated}`);
|
|
1481
|
+
}
|
|
1482
|
+
if (summary?.learned) {
|
|
1483
|
+
sections.push(`Learned: ${summary.learned}`);
|
|
1484
|
+
}
|
|
1485
|
+
if (summary?.completed) {
|
|
1486
|
+
sections.push(`Completed: ${summary.completed}`);
|
|
1487
|
+
}
|
|
1488
|
+
if (summary?.next_steps) {
|
|
1489
|
+
sections.push(`Next Steps: ${summary.next_steps}`);
|
|
1490
|
+
}
|
|
1491
|
+
if (story.recent_outcomes.length > 0) {
|
|
1492
|
+
sections.push(`Recent outcomes:
|
|
1493
|
+
${story.recent_outcomes.slice(0, 5).map((item) => `- ${item}`).join(`
|
|
1494
|
+
`)}`);
|
|
1495
|
+
}
|
|
1496
|
+
if (story.hot_files.length > 0) {
|
|
1497
|
+
sections.push(`Hot files:
|
|
1498
|
+
${story.hot_files.slice(0, 5).map((file) => `- ${file.path}`).join(`
|
|
1499
|
+
`)}`);
|
|
1500
|
+
}
|
|
1501
|
+
if (story.provenance_summary.length > 0) {
|
|
1502
|
+
sections.push(`Tool trail:
|
|
1503
|
+
${story.provenance_summary.slice(0, 5).map((item) => `- ${item.tool}: ${item.count}`).join(`
|
|
1504
|
+
`)}`);
|
|
1505
|
+
}
|
|
1506
|
+
if (options.includeChat && story.chat_messages.length > 0) {
|
|
1507
|
+
const chatLines = story.chat_messages.slice(-options.chatLimit).map((msg) => `- [${msg.role}] ${compactLine(msg.content) ?? msg.content.slice(0, 120)}`);
|
|
1508
|
+
sections.push(`Chat snippets:
|
|
1509
|
+
${chatLines.join(`
|
|
1510
|
+
`)}`);
|
|
1511
|
+
}
|
|
1512
|
+
return sections.filter(Boolean).join(`
|
|
1513
|
+
|
|
1514
|
+
`);
|
|
1515
|
+
}
|
|
1516
|
+
function shouldAutoIncludeChat(story) {
|
|
1517
|
+
if (story.chat_messages.length === 0)
|
|
1518
|
+
return false;
|
|
1519
|
+
const summary = story.summary;
|
|
1520
|
+
const thinSummary = !summary?.completed && !summary?.current_thread && story.recent_outcomes.length < 2;
|
|
1521
|
+
const thinChronology = story.capture_state !== "rich" || story.tool_events.length === 0;
|
|
1522
|
+
return thinSummary || thinChronology;
|
|
1523
|
+
}
|
|
1524
|
+
function buildHandoffFacts(summary, story) {
|
|
1525
|
+
const facts = [
|
|
1526
|
+
`session_id=${story.session?.session_id ?? "unknown"}`,
|
|
1527
|
+
`capture_state=${story.capture_state}`,
|
|
1528
|
+
story.project_name ? `project=${story.project_name}` : null,
|
|
1529
|
+
summary?.current_thread ? `current_thread=${summary.current_thread}` : null,
|
|
1530
|
+
story.hot_files[0] ? `hot_file=${story.hot_files[0].path}` : null,
|
|
1531
|
+
story.provenance_summary[0] ? `primary_tool=${story.provenance_summary[0].tool}` : null
|
|
1532
|
+
];
|
|
1533
|
+
return facts.filter((item) => Boolean(item));
|
|
1534
|
+
}
|
|
1535
|
+
function buildDraftHandoffConcepts(projectName, captureState) {
|
|
1536
|
+
return [
|
|
1537
|
+
"handoff",
|
|
1538
|
+
"draft-handoff",
|
|
1539
|
+
"auto-handoff",
|
|
1540
|
+
`capture:${captureState}`,
|
|
1541
|
+
...projectName ? [projectName] : []
|
|
1542
|
+
];
|
|
1543
|
+
}
|
|
1544
|
+
function looksLikeHandoff(obs) {
|
|
1545
|
+
if (obs.title.startsWith("Handoff:") || obs.title.startsWith("Handoff Draft:"))
|
|
1546
|
+
return true;
|
|
1547
|
+
const concepts = parseJsonArray2(obs.concepts);
|
|
1548
|
+
return concepts.includes("handoff") || concepts.includes("session-handoff") || concepts.includes("draft-handoff");
|
|
1549
|
+
}
|
|
1550
|
+
function parseJsonArray2(value) {
|
|
1551
|
+
if (!value)
|
|
1552
|
+
return [];
|
|
1553
|
+
try {
|
|
1554
|
+
const parsed = JSON.parse(value);
|
|
1555
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1556
|
+
} catch {
|
|
1557
|
+
return [];
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function compactLine(value) {
|
|
1561
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
1562
|
+
if (!trimmed)
|
|
1563
|
+
return null;
|
|
1564
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/context/inject.ts
|
|
1568
|
+
var FRESH_CONTINUITY_WINDOW_DAYS = 3;
|
|
1569
|
+
function tokenizeProjectHint(text) {
|
|
1570
|
+
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
1571
|
+
}
|
|
1572
|
+
function parseSummaryJsonList(value) {
|
|
1573
|
+
if (!value)
|
|
1574
|
+
return [];
|
|
1575
|
+
try {
|
|
1576
|
+
const parsed = JSON.parse(value);
|
|
1577
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1578
|
+
} catch {
|
|
1579
|
+
return [];
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function isObservationRelatedToProject(obs, detected) {
|
|
1583
|
+
const hints = new Set([
|
|
1584
|
+
...tokenizeProjectHint(detected.name),
|
|
1585
|
+
...tokenizeProjectHint(detected.canonical_id)
|
|
1586
|
+
]);
|
|
1587
|
+
if (hints.size === 0)
|
|
1588
|
+
return false;
|
|
1589
|
+
const haystack = [
|
|
1590
|
+
obs.title,
|
|
1591
|
+
obs.narrative ?? "",
|
|
1592
|
+
obs.facts ?? "",
|
|
1593
|
+
obs.concepts ?? "",
|
|
1594
|
+
obs.files_read ?? "",
|
|
1595
|
+
obs.files_modified ?? "",
|
|
1596
|
+
obs._source_project ?? ""
|
|
1597
|
+
].join(`
|
|
1598
|
+
`).toLowerCase();
|
|
1599
|
+
for (const hint of hints) {
|
|
1600
|
+
if (haystack.includes(hint))
|
|
1601
|
+
return true;
|
|
1602
|
+
}
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
function estimateTokens(text) {
|
|
1606
|
+
if (!text)
|
|
1607
|
+
return 0;
|
|
1608
|
+
return Math.ceil(text.length / 4);
|
|
1609
|
+
}
|
|
1610
|
+
function buildSessionContext(db, cwd, options = {}) {
|
|
1611
|
+
const opts = typeof options === "number" ? { maxCount: options } : options;
|
|
1612
|
+
const tokenBudget = opts.tokenBudget ?? 3000;
|
|
1613
|
+
const maxCount = opts.maxCount;
|
|
1614
|
+
const visibilityClause = opts.userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
1615
|
+
const visibilityParams = opts.userId ? [opts.userId] : [];
|
|
1616
|
+
const detected = detectProject(cwd);
|
|
1617
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
1618
|
+
const projectId = project?.id ?? -1;
|
|
1619
|
+
const isNewProject = !project;
|
|
1620
|
+
const totalActive = isNewProject ? (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
1621
|
+
WHERE lifecycle IN ('active', 'aging', 'pinned')
|
|
1622
|
+
${visibilityClause}
|
|
1623
|
+
AND superseded_by IS NULL`).get(...visibilityParams) ?? { c: 0 }).c : (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
1624
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging', 'pinned')
|
|
1625
|
+
${visibilityClause}
|
|
1626
|
+
AND superseded_by IS NULL`).get(projectId, ...visibilityParams) ?? { c: 0 }).c;
|
|
1627
|
+
const candidateLimit = maxCount ?? 50;
|
|
1628
|
+
let pinned = [];
|
|
1629
|
+
let recent = [];
|
|
1630
|
+
let candidates = [];
|
|
1631
|
+
if (!isNewProject) {
|
|
1632
|
+
const MAX_PINNED = 5;
|
|
1633
|
+
pinned = db.db.query(`SELECT * FROM observations
|
|
1634
|
+
WHERE project_id = ? AND lifecycle = 'pinned'
|
|
1635
|
+
AND superseded_by IS NULL
|
|
1636
|
+
${visibilityClause}
|
|
1637
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
1638
|
+
LIMIT ?`).all(projectId, ...visibilityParams, MAX_PINNED);
|
|
1639
|
+
const MAX_RECENT = 5;
|
|
1640
|
+
recent = db.db.query(`SELECT * FROM observations
|
|
1641
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
1642
|
+
AND superseded_by IS NULL
|
|
1643
|
+
${visibilityClause}
|
|
1644
|
+
ORDER BY created_at_epoch DESC
|
|
1645
|
+
LIMIT ?`).all(projectId, ...visibilityParams, MAX_RECENT);
|
|
1646
|
+
candidates = db.db.query(`SELECT * FROM observations
|
|
1647
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
1648
|
+
AND quality >= 0.3
|
|
1649
|
+
AND superseded_by IS NULL
|
|
1650
|
+
${visibilityClause}
|
|
1651
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
1652
|
+
LIMIT ?`).all(projectId, ...visibilityParams, candidateLimit);
|
|
1653
|
+
}
|
|
1654
|
+
let crossProjectCandidates = [];
|
|
1655
|
+
if (opts.scope === "all" || isNewProject) {
|
|
1656
|
+
const crossLimit = isNewProject ? Math.max(30, candidateLimit) : Math.max(10, Math.floor(candidateLimit / 3));
|
|
1657
|
+
const qualityThreshold = isNewProject ? 0.3 : 0.5;
|
|
1658
|
+
const rawCross = isNewProject ? db.db.query(`SELECT * FROM observations
|
|
1659
|
+
WHERE lifecycle IN ('active', 'aging', 'pinned')
|
|
1660
|
+
AND quality >= ?
|
|
1661
|
+
AND superseded_by IS NULL
|
|
1662
|
+
${visibilityClause}
|
|
1663
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
1664
|
+
LIMIT ?`).all(qualityThreshold, ...visibilityParams, crossLimit) : db.db.query(`SELECT * FROM observations
|
|
1665
|
+
WHERE project_id != ? AND lifecycle IN ('active', 'aging')
|
|
1666
|
+
AND quality >= ?
|
|
1667
|
+
AND superseded_by IS NULL
|
|
1668
|
+
${visibilityClause}
|
|
1669
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
1670
|
+
LIMIT ?`).all(projectId, qualityThreshold, ...visibilityParams, crossLimit);
|
|
1671
|
+
const projectNameCache = new Map;
|
|
1672
|
+
crossProjectCandidates = rawCross.map((obs) => {
|
|
1673
|
+
if (!projectNameCache.has(obs.project_id)) {
|
|
1674
|
+
const proj = db.getProjectById(obs.project_id);
|
|
1675
|
+
if (proj)
|
|
1676
|
+
projectNameCache.set(obs.project_id, proj.name);
|
|
1677
|
+
}
|
|
1678
|
+
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
1679
|
+
});
|
|
1680
|
+
if (isNewProject) {
|
|
1681
|
+
crossProjectCandidates = crossProjectCandidates.filter((obs) => isObservationRelatedToProject(obs, detected));
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
const seenIds = new Set(pinned.map((o) => o.id));
|
|
1685
|
+
const dedupedRecent = recent.filter((o) => {
|
|
1686
|
+
if (seenIds.has(o.id))
|
|
1687
|
+
return false;
|
|
1688
|
+
seenIds.add(o.id);
|
|
1689
|
+
return true;
|
|
1690
|
+
});
|
|
1691
|
+
const deduped = candidates.filter((o) => !seenIds.has(o.id));
|
|
1692
|
+
for (const obs of crossProjectCandidates) {
|
|
1693
|
+
if (!seenIds.has(obs.id)) {
|
|
1694
|
+
seenIds.add(obs.id);
|
|
1695
|
+
deduped.push(obs);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
1699
|
+
const sorted = [...deduped].sort((a, b) => {
|
|
1700
|
+
const scoreA = computeObservationPriority(a, nowEpoch);
|
|
1701
|
+
const scoreB = computeObservationPriority(b, nowEpoch);
|
|
1702
|
+
return scoreB - scoreA;
|
|
1703
|
+
});
|
|
1704
|
+
const projectName = project?.name ?? detected.name;
|
|
1705
|
+
const canonicalId = project?.canonical_id ?? detected.canonical_id;
|
|
1706
|
+
if (maxCount !== undefined) {
|
|
1707
|
+
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
1708
|
+
let all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
1709
|
+
const recentPrompts2 = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
1710
|
+
const recentToolEvents2 = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
1711
|
+
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
1712
|
+
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
1713
|
+
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions2);
|
|
1714
|
+
const recentHandoffs2 = isNewProject ? [] : getRecentHandoffs(db, {
|
|
1715
|
+
cwd,
|
|
1716
|
+
project_scoped: true,
|
|
1717
|
+
user_id: opts.userId,
|
|
1718
|
+
current_device_id: opts.currentDeviceId,
|
|
1719
|
+
limit: 3
|
|
1720
|
+
}).handoffs;
|
|
1721
|
+
const recentChatMessages2 = !isNewProject && project ? db.getRecentChatMessages(project.id, 4, opts.userId) : [];
|
|
1722
|
+
all = filterAutoLoadedObservationsForContinuity(all, pinned, isNewProject, recentPrompts2, recentToolEvents2, recentSessions2, recentHandoffs2, recentChatMessages2, summariesFromRecentSessions(db, projectId, recentSessions2));
|
|
1723
|
+
return {
|
|
1724
|
+
project_name: projectName,
|
|
1725
|
+
canonical_id: canonicalId,
|
|
1726
|
+
observations: all.map(toContextObservation),
|
|
1727
|
+
session_count: all.length,
|
|
1728
|
+
total_active: totalActive,
|
|
1729
|
+
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
1730
|
+
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
1731
|
+
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
1732
|
+
projectTypeCounts: projectTypeCounts2,
|
|
1733
|
+
recentOutcomes: recentOutcomes2,
|
|
1734
|
+
recentHandoffs: recentHandoffs2.length > 0 ? recentHandoffs2 : undefined,
|
|
1735
|
+
recentChatMessages: recentChatMessages2.length > 0 ? recentChatMessages2 : undefined
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
let remainingBudget = tokenBudget - 30;
|
|
1739
|
+
const selected = [];
|
|
1740
|
+
for (const obs of pinned) {
|
|
1741
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
1742
|
+
remainingBudget -= cost;
|
|
1743
|
+
selected.push(obs);
|
|
1744
|
+
}
|
|
1745
|
+
for (const obs of dedupedRecent) {
|
|
1746
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
1747
|
+
remainingBudget -= cost;
|
|
1748
|
+
selected.push(obs);
|
|
1749
|
+
}
|
|
1750
|
+
for (const obs of sorted) {
|
|
1751
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
1752
|
+
if (remainingBudget - cost < 0 && selected.length > 0)
|
|
1753
|
+
break;
|
|
1754
|
+
remainingBudget -= cost;
|
|
1755
|
+
selected.push(obs);
|
|
1756
|
+
}
|
|
1757
|
+
const summaries = isNewProject ? [] : db.getRecentSummaries(projectId, 5);
|
|
1758
|
+
const recentPrompts = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
1759
|
+
const recentToolEvents = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
1760
|
+
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
1761
|
+
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
1762
|
+
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions);
|
|
1763
|
+
const recentHandoffs = isNewProject ? [] : getRecentHandoffs(db, {
|
|
1764
|
+
cwd,
|
|
1765
|
+
project_scoped: true,
|
|
1766
|
+
user_id: opts.userId,
|
|
1767
|
+
current_device_id: opts.currentDeviceId,
|
|
1768
|
+
limit: 3
|
|
1769
|
+
}).handoffs;
|
|
1770
|
+
const recentChatMessages = !isNewProject ? db.getRecentChatMessages(projectId, 4, opts.userId) : [];
|
|
1771
|
+
const filteredSelected = filterAutoLoadedObservationsForContinuity(selected, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries);
|
|
1772
|
+
let securityFindings = [];
|
|
1773
|
+
if (!isNewProject) {
|
|
1774
|
+
try {
|
|
1775
|
+
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
1776
|
+
securityFindings = db.db.query(`SELECT * FROM security_findings
|
|
1777
|
+
WHERE project_id = ? AND created_at_epoch > ?
|
|
1778
|
+
ORDER BY severity DESC, created_at_epoch DESC
|
|
1779
|
+
LIMIT ?`).all(projectId, weekAgo, 10);
|
|
1780
|
+
} catch {}
|
|
1781
|
+
}
|
|
1782
|
+
let recentProjects;
|
|
1783
|
+
if (isNewProject) {
|
|
1784
|
+
try {
|
|
1785
|
+
const nowEpochSec = Math.floor(Date.now() / 1000);
|
|
1786
|
+
const projectRows = db.db.query(`SELECT p.name, p.canonical_id, p.last_active_epoch,
|
|
1787
|
+
(SELECT COUNT(*) FROM observations o
|
|
1788
|
+
WHERE o.project_id = p.id
|
|
1789
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
1790
|
+
${opts.userId ? "AND (o.sensitivity != 'personal' OR o.user_id = ?)" : ""}
|
|
1791
|
+
AND o.superseded_by IS NULL) as obs_count
|
|
1792
|
+
FROM projects p
|
|
1793
|
+
ORDER BY p.last_active_epoch DESC
|
|
1794
|
+
LIMIT 10`).all(...visibilityParams);
|
|
1795
|
+
if (projectRows.length > 0) {
|
|
1796
|
+
recentProjects = projectRows.map((r) => {
|
|
1797
|
+
const daysAgo = Math.max(0, Math.floor((nowEpochSec - r.last_active_epoch) / 86400));
|
|
1798
|
+
const lastActive = new Date(r.last_active_epoch * 1000).toISOString().split("T")[0];
|
|
1799
|
+
return {
|
|
1800
|
+
name: r.name,
|
|
1801
|
+
canonical_id: r.canonical_id,
|
|
1802
|
+
observation_count: r.obs_count,
|
|
1803
|
+
last_active: lastActive,
|
|
1804
|
+
days_ago: daysAgo
|
|
1805
|
+
};
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
} catch {}
|
|
1809
|
+
}
|
|
1810
|
+
let staleDecisions;
|
|
1811
|
+
try {
|
|
1812
|
+
const stale = isNewProject ? findStaleDecisionsGlobal(db) : findStaleDecisions(db, projectId);
|
|
1813
|
+
if (stale.length > 0)
|
|
1814
|
+
staleDecisions = stale;
|
|
1815
|
+
} catch {}
|
|
1816
|
+
return {
|
|
1817
|
+
project_name: projectName,
|
|
1818
|
+
canonical_id: canonicalId,
|
|
1819
|
+
observations: filteredSelected.map(toContextObservation),
|
|
1820
|
+
session_count: filteredSelected.length,
|
|
1821
|
+
total_active: totalActive,
|
|
1822
|
+
summaries: summaries.length > 0 ? summaries : undefined,
|
|
1823
|
+
securityFindings: securityFindings.length > 0 ? securityFindings : undefined,
|
|
1824
|
+
recentProjects,
|
|
1825
|
+
staleDecisions,
|
|
1826
|
+
recentPrompts: recentPrompts.length > 0 ? recentPrompts : undefined,
|
|
1827
|
+
recentToolEvents: recentToolEvents.length > 0 ? recentToolEvents : undefined,
|
|
1828
|
+
recentSessions: recentSessions.length > 0 ? recentSessions : undefined,
|
|
1829
|
+
projectTypeCounts,
|
|
1830
|
+
recentOutcomes,
|
|
1831
|
+
recentHandoffs: recentHandoffs.length > 0 ? recentHandoffs : undefined,
|
|
1832
|
+
recentChatMessages: recentChatMessages.length > 0 ? recentChatMessages : undefined
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function filterAutoLoadedObservationsForContinuity(observations, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
1836
|
+
if (isNewProject)
|
|
1837
|
+
return observations;
|
|
1838
|
+
if (hasFreshProjectContinuity(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries)) {
|
|
1839
|
+
return observations;
|
|
1840
|
+
}
|
|
1841
|
+
const pinnedIds = new Set(pinned.map((obs) => obs.id));
|
|
1842
|
+
return observations.filter((obs) => {
|
|
1843
|
+
if (pinnedIds.has(obs.id))
|
|
1844
|
+
return true;
|
|
1845
|
+
return observationAgeDays(obs.created_at_epoch) <= FRESH_CONTINUITY_WINDOW_DAYS;
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
function hasFreshProjectContinuity(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
1849
|
+
const freshEnough = (epoch) => typeof epoch === "number" && observationAgeDays(epoch) <= FRESH_CONTINUITY_WINDOW_DAYS;
|
|
1850
|
+
return recentPrompts.some((item) => freshEnough(item.created_at_epoch)) || recentToolEvents.some((item) => freshEnough(item.created_at_epoch)) || recentSessions.some((item) => freshEnough(item.completed_at_epoch ?? item.started_at_epoch)) || recentHandoffs.some((item) => freshEnough(item.created_at_epoch)) || recentChatMessages.some((item) => freshEnough(item.created_at_epoch)) || summaries.some((item) => freshEnough(item.created_at_epoch));
|
|
1851
|
+
}
|
|
1852
|
+
function summariesFromRecentSessions(db, projectId, recentSessions) {
|
|
1853
|
+
const seen = new Set;
|
|
1854
|
+
const rows = [];
|
|
1855
|
+
for (const session of recentSessions) {
|
|
1856
|
+
if (seen.has(session.session_id))
|
|
1857
|
+
continue;
|
|
1858
|
+
seen.add(session.session_id);
|
|
1859
|
+
const summary = db.getSessionSummary(session.session_id);
|
|
1860
|
+
if (summary && summary.project_id === projectId)
|
|
1861
|
+
rows.push(summary);
|
|
1862
|
+
}
|
|
1863
|
+
return rows;
|
|
1864
|
+
}
|
|
1865
|
+
function observationAgeDays(createdAtEpoch) {
|
|
1866
|
+
return Math.max(0, (Math.floor(Date.now() / 1000) - createdAtEpoch) / 86400);
|
|
1867
|
+
}
|
|
1868
|
+
function estimateObservationTokens(obs, index) {
|
|
1869
|
+
const DETAILED_THRESHOLD = 5;
|
|
1870
|
+
const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|
|
1871
|
+
if (index >= DETAILED_THRESHOLD) {
|
|
1872
|
+
return titleCost;
|
|
1873
|
+
}
|
|
1874
|
+
const detailText = formatObservationDetail(obs);
|
|
1875
|
+
return titleCost + estimateTokens(detailText);
|
|
1876
|
+
}
|
|
1877
|
+
function formatContextForInjection(context) {
|
|
1878
|
+
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)) {
|
|
1879
|
+
return `Project: ${context.project_name} (no prior observations)`;
|
|
1880
|
+
}
|
|
1881
|
+
const DETAILED_COUNT = 5;
|
|
1882
|
+
const isCrossProject = context.recentProjects && context.recentProjects.length > 0;
|
|
1883
|
+
const lines = [];
|
|
1884
|
+
if (isCrossProject) {
|
|
1885
|
+
lines.push(`## Engrm Memory — Workspace Overview`);
|
|
1886
|
+
lines.push(`This is a new project folder. Here is context from your recent work:`);
|
|
1887
|
+
lines.push("");
|
|
1888
|
+
lines.push("**Active projects in memory:**");
|
|
1889
|
+
for (const rp of context.recentProjects) {
|
|
1890
|
+
const activity = rp.days_ago === 0 ? "today" : rp.days_ago === 1 ? "yesterday" : `${rp.days_ago}d ago`;
|
|
1891
|
+
lines.push(`- **${rp.name}** — ${rp.observation_count} observations, last active ${activity}`);
|
|
1892
|
+
}
|
|
1893
|
+
lines.push("");
|
|
1894
|
+
lines.push(`${context.session_count} relevant observation(s) from across projects:`);
|
|
1895
|
+
lines.push("");
|
|
1896
|
+
} else {
|
|
1897
|
+
lines.push(`## Project Memory: ${context.project_name}`);
|
|
1898
|
+
lines.push(`${context.session_count} relevant observation(s) from prior sessions:`);
|
|
1899
|
+
lines.push("");
|
|
1900
|
+
}
|
|
1901
|
+
if (context.recentHandoffs && context.recentHandoffs.length > 0) {
|
|
1902
|
+
lines.push("## Recent Handoffs");
|
|
1903
|
+
for (const handoff of context.recentHandoffs.slice(0, 3)) {
|
|
1904
|
+
const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
1905
|
+
if (title) {
|
|
1906
|
+
lines.push(`- ${truncateText(`${title} (${formatHandoffSource(handoff)})`, 160)}`);
|
|
1907
|
+
}
|
|
1908
|
+
const narrative = handoff.narrative?.split(/\n{2,}/).map((part) => part.replace(/\s+/g, " ").trim()).find((part) => /^(Current thread:|Completed:|Next Steps:)/i.test(part));
|
|
1909
|
+
if (narrative) {
|
|
1910
|
+
lines.push(` ${truncateText(narrative, 180)}`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
lines.push("");
|
|
1914
|
+
}
|
|
1915
|
+
if (context.recentChatMessages && context.recentChatMessages.length > 0) {
|
|
1916
|
+
lines.push("## Recent Chat");
|
|
1917
|
+
for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
|
|
1918
|
+
lines.push(`- [${message.role}] ${truncateText(message.content.replace(/\s+/g, " ").trim(), 160)}`);
|
|
1919
|
+
}
|
|
1920
|
+
lines.push("");
|
|
1921
|
+
}
|
|
1922
|
+
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
1923
|
+
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt(prompt.prompt)).slice(0, 5);
|
|
1924
|
+
if (promptLines.length > 0) {
|
|
1925
|
+
lines.push("## Recent Requests");
|
|
1926
|
+
for (const prompt of promptLines) {
|
|
1927
|
+
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
1928
|
+
lines.push(`- ${label}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
1929
|
+
}
|
|
1930
|
+
lines.push("");
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
1934
|
+
lines.push("## Recent Tools");
|
|
1935
|
+
for (const tool of context.recentToolEvents.slice(0, 5)) {
|
|
1936
|
+
lines.push(`- ${tool.tool_name}: ${formatToolEventDetail(tool)}`);
|
|
1937
|
+
}
|
|
1938
|
+
lines.push("");
|
|
1939
|
+
}
|
|
1940
|
+
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
1941
|
+
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
1942
|
+
const summary = chooseMeaningfulSessionHeadline(session.request, session.completed);
|
|
1943
|
+
if (summary === "(no summary)")
|
|
1944
|
+
return null;
|
|
1945
|
+
return `- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
1946
|
+
}).filter((line) => Boolean(line));
|
|
1947
|
+
if (recentSessionLines.length > 0) {
|
|
1948
|
+
lines.push("## Recent Sessions");
|
|
1949
|
+
lines.push(...recentSessionLines);
|
|
1950
|
+
lines.push("");
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
if (context.recentOutcomes && context.recentOutcomes.length > 0) {
|
|
1954
|
+
lines.push("## Recent Outcomes");
|
|
1955
|
+
for (const outcome of context.recentOutcomes.slice(0, 5)) {
|
|
1956
|
+
lines.push(`- ${truncateText(outcome, 160)}`);
|
|
1957
|
+
}
|
|
1958
|
+
lines.push("");
|
|
1959
|
+
}
|
|
1960
|
+
if (context.projectTypeCounts && Object.keys(context.projectTypeCounts).length > 0) {
|
|
1961
|
+
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(" · ");
|
|
1962
|
+
if (topTypes) {
|
|
1963
|
+
lines.push(`## Project Signals`);
|
|
1964
|
+
lines.push(`Top memory types: ${topTypes}`);
|
|
1965
|
+
lines.push("");
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
for (let i = 0;i < context.observations.length; i++) {
|
|
1969
|
+
const obs = context.observations[i];
|
|
1970
|
+
const date = obs.created_at.split("T")[0];
|
|
1971
|
+
const fromLabel = obs.source_project ? ` [from: ${obs.source_project}]` : "";
|
|
1972
|
+
const fileLabel = formatObservationFiles(obs);
|
|
1973
|
+
lines.push(`- **#${obs.id} [${obs.type}]** ${obs.title} (${date}, q=${obs.quality.toFixed(1)})${fromLabel}${fileLabel}`);
|
|
1974
|
+
if (i < DETAILED_COUNT) {
|
|
1975
|
+
const detail = formatObservationDetailFromContext(obs);
|
|
1976
|
+
if (detail) {
|
|
1977
|
+
lines.push(detail);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (context.summaries && context.summaries.length > 0) {
|
|
1982
|
+
lines.push("");
|
|
1983
|
+
lines.push("## Recent Project Briefs");
|
|
1984
|
+
for (const summary of context.summaries.slice(0, 3)) {
|
|
1985
|
+
lines.push(...formatSessionBrief(summary));
|
|
1986
|
+
lines.push("");
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (context.securityFindings && context.securityFindings.length > 0) {
|
|
1990
|
+
lines.push("");
|
|
1991
|
+
lines.push("Security findings (recent):");
|
|
1992
|
+
for (const finding of context.securityFindings) {
|
|
1993
|
+
const date = new Date(finding.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
1994
|
+
const file = finding.file_path ? ` in ${finding.file_path}` : finding.tool_name ? ` via ${finding.tool_name}` : "";
|
|
1995
|
+
lines.push(`- [${finding.severity.toUpperCase()}] ${finding.pattern_name}${file} (${date})`);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (context.staleDecisions && context.staleDecisions.length > 0) {
|
|
1999
|
+
lines.push("");
|
|
2000
|
+
lines.push("Stale commitments (decided but no implementation observed):");
|
|
2001
|
+
for (const sd of context.staleDecisions) {
|
|
2002
|
+
const date = sd.created_at.split("T")[0];
|
|
2003
|
+
lines.push(`- [DECISION] ${sd.title} (${date}, ${sd.days_ago}d ago)`);
|
|
2004
|
+
if (sd.best_match_title) {
|
|
2005
|
+
lines.push(` Closest match: "${sd.best_match_title}" (${Math.round((sd.best_match_similarity ?? 0) * 100)}% similar — not enough to count as done)`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
const remaining = context.total_active - context.session_count;
|
|
2010
|
+
if (remaining > 0) {
|
|
2011
|
+
lines.push("");
|
|
2012
|
+
lines.push(`${remaining} more observation(s) available via search tool.`);
|
|
2013
|
+
}
|
|
2014
|
+
return lines.join(`
|
|
2015
|
+
`);
|
|
2016
|
+
}
|
|
2017
|
+
function formatSessionBrief(summary) {
|
|
2018
|
+
const lines = [];
|
|
2019
|
+
const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
2020
|
+
lines.push(heading);
|
|
2021
|
+
const sections = [
|
|
2022
|
+
["Investigated", summary.investigated, 180],
|
|
2023
|
+
["Learned", summary.learned, 180],
|
|
2024
|
+
["Completed", summary.completed, 180],
|
|
2025
|
+
["Next Steps", summary.next_steps, 140]
|
|
2026
|
+
];
|
|
2027
|
+
for (const [label, value, maxLen] of sections) {
|
|
2028
|
+
const formatted = formatSummarySection(value, maxLen);
|
|
2029
|
+
if (formatted) {
|
|
2030
|
+
lines.push(`${label}:`);
|
|
2031
|
+
lines.push(formatted);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return lines;
|
|
2035
|
+
}
|
|
2036
|
+
function chooseMeaningfulSessionHeadline(request, completed) {
|
|
2037
|
+
if (request && !looksLikeFileOperationTitle2(request))
|
|
2038
|
+
return request;
|
|
2039
|
+
const completedItems = extractMeaningfulLines(completed, 1);
|
|
2040
|
+
if (completedItems.length > 0)
|
|
2041
|
+
return completedItems[0];
|
|
2042
|
+
return request ?? completed ?? "(no summary)";
|
|
2043
|
+
}
|
|
2044
|
+
function formatSummarySection(value, maxLen) {
|
|
2045
|
+
return formatSummaryItems(value, maxLen);
|
|
2046
|
+
}
|
|
2047
|
+
function truncateText(text, maxLen) {
|
|
2048
|
+
if (text.length <= maxLen)
|
|
2049
|
+
return text;
|
|
2050
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
2051
|
+
}
|
|
2052
|
+
function isMeaningfulPrompt(value) {
|
|
2053
|
+
if (!value)
|
|
2054
|
+
return false;
|
|
2055
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
2056
|
+
if (compact.length < 8)
|
|
2057
|
+
return false;
|
|
2058
|
+
return /[a-z]{3,}/i.test(compact);
|
|
2059
|
+
}
|
|
2060
|
+
function looksLikeFileOperationTitle2(value) {
|
|
2061
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
2062
|
+
}
|
|
2063
|
+
function stripInlineSectionLabel(value) {
|
|
2064
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
2065
|
+
}
|
|
2066
|
+
function extractMeaningfulLines(value, limit) {
|
|
2067
|
+
if (!value)
|
|
2068
|
+
return [];
|
|
2069
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle2(line)).slice(0, limit);
|
|
2070
|
+
}
|
|
2071
|
+
function formatObservationDetailFromContext(obs) {
|
|
2072
|
+
if (obs.facts) {
|
|
2073
|
+
const bullets = parseFacts(obs.facts);
|
|
2074
|
+
if (bullets.length > 0) {
|
|
2075
|
+
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
2076
|
+
`);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (obs.narrative) {
|
|
2080
|
+
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
2081
|
+
return ` ${snippet}`;
|
|
2082
|
+
}
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
function formatObservationDetail(obs) {
|
|
2086
|
+
if (obs.facts) {
|
|
2087
|
+
const bullets = parseFacts(obs.facts);
|
|
2088
|
+
if (bullets.length > 0) {
|
|
2089
|
+
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
2090
|
+
`);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
if (obs.narrative) {
|
|
2094
|
+
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
2095
|
+
return ` ${snippet}`;
|
|
2096
|
+
}
|
|
2097
|
+
return "";
|
|
2098
|
+
}
|
|
2099
|
+
function parseFacts(facts) {
|
|
2100
|
+
if (!facts)
|
|
2101
|
+
return [];
|
|
2102
|
+
try {
|
|
2103
|
+
const parsed = JSON.parse(facts);
|
|
2104
|
+
if (Array.isArray(parsed)) {
|
|
2105
|
+
return parsed.filter((f) => typeof f === "string" && f.length > 0);
|
|
2106
|
+
}
|
|
2107
|
+
} catch {
|
|
2108
|
+
if (facts.trim().length > 0) {
|
|
2109
|
+
return [facts.trim()];
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return [];
|
|
2113
|
+
}
|
|
2114
|
+
function toContextObservation(obs) {
|
|
2115
|
+
return {
|
|
2116
|
+
id: obs.id,
|
|
2117
|
+
type: obs.type,
|
|
2118
|
+
title: obs.title,
|
|
2119
|
+
narrative: obs.narrative,
|
|
2120
|
+
facts: obs.facts,
|
|
2121
|
+
files_read: obs.files_read,
|
|
2122
|
+
files_modified: obs.files_modified,
|
|
2123
|
+
quality: obs.quality,
|
|
2124
|
+
created_at: obs.created_at,
|
|
2125
|
+
...obs._source_project ? { source_project: obs._source_project } : {}
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
function formatObservationFiles(obs) {
|
|
2129
|
+
const modified = parseJsonStringArray(obs.files_modified);
|
|
2130
|
+
if (modified.length > 0) {
|
|
2131
|
+
return ` · files: ${truncateText(modified.slice(0, 2).join(", "), 60)}`;
|
|
2132
|
+
}
|
|
2133
|
+
const read = parseJsonStringArray(obs.files_read);
|
|
2134
|
+
if (read.length > 0) {
|
|
2135
|
+
return ` · read: ${truncateText(read.slice(0, 2).join(", "), 60)}`;
|
|
2136
|
+
}
|
|
2137
|
+
return "";
|
|
2138
|
+
}
|
|
2139
|
+
function parseJsonStringArray(value) {
|
|
2140
|
+
if (!value)
|
|
2141
|
+
return [];
|
|
2142
|
+
try {
|
|
2143
|
+
const parsed = JSON.parse(value);
|
|
2144
|
+
if (!Array.isArray(parsed))
|
|
2145
|
+
return [];
|
|
2146
|
+
return parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
2147
|
+
} catch {
|
|
2148
|
+
return [];
|
|
2149
|
+
}
|
|
1146
2150
|
}
|
|
1147
|
-
function
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
const rel = relative(projectRoot, filePath);
|
|
1151
|
-
if (rel.startsWith(".."))
|
|
1152
|
-
return filePath;
|
|
1153
|
-
return rel;
|
|
2151
|
+
function formatToolEventDetail(tool) {
|
|
2152
|
+
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
2153
|
+
return truncateText(detail || "recent tool execution", 160);
|
|
1154
2154
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
2155
|
+
function getProjectTypeCounts(db, projectId, userId) {
|
|
2156
|
+
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
2157
|
+
const rows = db.db.query(`SELECT type, COUNT(*) as count
|
|
2158
|
+
FROM observations
|
|
2159
|
+
WHERE project_id = ?
|
|
2160
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
2161
|
+
AND superseded_by IS NULL
|
|
2162
|
+
${visibilityClause}
|
|
2163
|
+
GROUP BY type`).all(projectId, ...userId ? [userId] : []);
|
|
2164
|
+
const counts = {};
|
|
2165
|
+
for (const row of rows) {
|
|
2166
|
+
counts[row.type] = row.count;
|
|
2167
|
+
}
|
|
2168
|
+
return counts;
|
|
2169
|
+
}
|
|
2170
|
+
function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
2171
|
+
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
2172
|
+
const visibilityParams = userId ? [userId] : [];
|
|
2173
|
+
const summaries = db.db.query(`SELECT * FROM session_summaries
|
|
2174
|
+
WHERE project_id = ?
|
|
2175
|
+
ORDER BY created_at_epoch DESC
|
|
2176
|
+
LIMIT 6`).all(projectId);
|
|
2177
|
+
const picked = [];
|
|
2178
|
+
const seen = new Set;
|
|
2179
|
+
for (const summary of summaries) {
|
|
2180
|
+
for (const item of parseSummaryJsonList(summary.recent_outcomes)) {
|
|
2181
|
+
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2182
|
+
if (!normalized || seen.has(normalized))
|
|
2183
|
+
continue;
|
|
2184
|
+
seen.add(normalized);
|
|
2185
|
+
picked.push(item);
|
|
2186
|
+
if (picked.length >= 5)
|
|
2187
|
+
return picked;
|
|
2188
|
+
}
|
|
2189
|
+
for (const line of [
|
|
2190
|
+
...extractMeaningfulLines(summary.completed, 2),
|
|
2191
|
+
...extractMeaningfulLines(summary.learned, 1)
|
|
2192
|
+
]) {
|
|
2193
|
+
const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2194
|
+
if (!normalized || seen.has(normalized))
|
|
2195
|
+
continue;
|
|
2196
|
+
seen.add(normalized);
|
|
2197
|
+
picked.push(line);
|
|
2198
|
+
if (picked.length >= 5)
|
|
2199
|
+
return picked;
|
|
1169
2200
|
}
|
|
1170
2201
|
}
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
2202
|
+
for (const session of recentSessions ?? []) {
|
|
2203
|
+
for (const item of parseSummaryJsonList(session.recent_outcomes)) {
|
|
2204
|
+
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2205
|
+
if (!normalized || seen.has(normalized))
|
|
2206
|
+
continue;
|
|
2207
|
+
seen.add(normalized);
|
|
2208
|
+
picked.push(item);
|
|
2209
|
+
if (picked.length >= 5)
|
|
2210
|
+
return picked;
|
|
2211
|
+
}
|
|
1181
2212
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
2213
|
+
const rows = db.db.query(`SELECT * FROM observations
|
|
2214
|
+
WHERE project_id = ?
|
|
2215
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
2216
|
+
AND superseded_by IS NULL
|
|
2217
|
+
${visibilityClause}
|
|
2218
|
+
ORDER BY created_at_epoch DESC
|
|
2219
|
+
LIMIT 20`).all(projectId, ...visibilityParams);
|
|
2220
|
+
for (const obs of rows) {
|
|
2221
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
2222
|
+
continue;
|
|
2223
|
+
const title = stripInlineSectionLabel(obs.title);
|
|
2224
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2225
|
+
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle2(title))
|
|
2226
|
+
continue;
|
|
2227
|
+
seen.add(normalized);
|
|
2228
|
+
picked.push(title);
|
|
2229
|
+
if (picked.length >= 5)
|
|
2230
|
+
break;
|
|
1185
2231
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
return {
|
|
1194
|
-
handoffs,
|
|
1195
|
-
project: projectName
|
|
1196
|
-
};
|
|
2232
|
+
return picked;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/tools/handoffs.ts
|
|
2236
|
+
function formatHandoffSource2(handoff) {
|
|
2237
|
+
const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
|
|
2238
|
+
const ageLabel = ageSeconds < 3600 ? `${Math.max(1, Math.floor(ageSeconds / 60) || 1)}m ago` : ageSeconds < 86400 ? `${Math.floor(ageSeconds / 3600)}h ago` : `${Math.floor(ageSeconds / 86400)}d ago`;
|
|
2239
|
+
return `from ${handoff.device_id} · ${ageLabel}`;
|
|
1197
2240
|
}
|
|
1198
2241
|
|
|
1199
2242
|
// src/context/inject.ts
|
|
1200
|
-
|
|
2243
|
+
var FRESH_CONTINUITY_WINDOW_DAYS2 = 3;
|
|
2244
|
+
function tokenizeProjectHint2(text) {
|
|
1201
2245
|
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
1202
2246
|
}
|
|
1203
|
-
function
|
|
2247
|
+
function parseSummaryJsonList2(value) {
|
|
1204
2248
|
if (!value)
|
|
1205
2249
|
return [];
|
|
1206
2250
|
try {
|
|
@@ -1210,10 +2254,10 @@ function parseSummaryJsonList(value) {
|
|
|
1210
2254
|
return [];
|
|
1211
2255
|
}
|
|
1212
2256
|
}
|
|
1213
|
-
function
|
|
2257
|
+
function isObservationRelatedToProject2(obs, detected) {
|
|
1214
2258
|
const hints = new Set([
|
|
1215
|
-
...
|
|
1216
|
-
...
|
|
2259
|
+
...tokenizeProjectHint2(detected.name),
|
|
2260
|
+
...tokenizeProjectHint2(detected.canonical_id)
|
|
1217
2261
|
]);
|
|
1218
2262
|
if (hints.size === 0)
|
|
1219
2263
|
return false;
|
|
@@ -1233,12 +2277,12 @@ function isObservationRelatedToProject(obs, detected) {
|
|
|
1233
2277
|
}
|
|
1234
2278
|
return false;
|
|
1235
2279
|
}
|
|
1236
|
-
function
|
|
2280
|
+
function estimateTokens2(text) {
|
|
1237
2281
|
if (!text)
|
|
1238
2282
|
return 0;
|
|
1239
2283
|
return Math.ceil(text.length / 4);
|
|
1240
2284
|
}
|
|
1241
|
-
function
|
|
2285
|
+
function buildSessionContext2(db, cwd, options = {}) {
|
|
1242
2286
|
const opts = typeof options === "number" ? { maxCount: options } : options;
|
|
1243
2287
|
const tokenBudget = opts.tokenBudget ?? 3000;
|
|
1244
2288
|
const maxCount = opts.maxCount;
|
|
@@ -1309,7 +2353,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1309
2353
|
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
1310
2354
|
});
|
|
1311
2355
|
if (isNewProject) {
|
|
1312
|
-
crossProjectCandidates = crossProjectCandidates.filter((obs) =>
|
|
2356
|
+
crossProjectCandidates = crossProjectCandidates.filter((obs) => isObservationRelatedToProject2(obs, detected));
|
|
1313
2357
|
}
|
|
1314
2358
|
}
|
|
1315
2359
|
const seenIds = new Set(pinned.map((o) => o.id));
|
|
@@ -1336,22 +2380,25 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1336
2380
|
const canonicalId = project?.canonical_id ?? detected.canonical_id;
|
|
1337
2381
|
if (maxCount !== undefined) {
|
|
1338
2382
|
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
1339
|
-
|
|
1340
|
-
const recentPrompts2 =
|
|
1341
|
-
const recentToolEvents2 =
|
|
2383
|
+
let all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
2384
|
+
const recentPrompts2 = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
2385
|
+
const recentToolEvents2 = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
1342
2386
|
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
1343
|
-
const projectTypeCounts2 = isNewProject ? undefined :
|
|
1344
|
-
const recentOutcomes2 = isNewProject ? undefined :
|
|
1345
|
-
const recentHandoffs2 = getRecentHandoffs(db, {
|
|
2387
|
+
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts2(db, projectId, opts.userId);
|
|
2388
|
+
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes2(db, projectId, opts.userId, recentSessions2);
|
|
2389
|
+
const recentHandoffs2 = isNewProject ? [] : getRecentHandoffs(db, {
|
|
1346
2390
|
cwd,
|
|
1347
|
-
project_scoped:
|
|
2391
|
+
project_scoped: true,
|
|
1348
2392
|
user_id: opts.userId,
|
|
2393
|
+
current_device_id: opts.currentDeviceId,
|
|
1349
2394
|
limit: 3
|
|
1350
2395
|
}).handoffs;
|
|
2396
|
+
const recentChatMessages2 = !isNewProject && project ? db.getRecentChatMessages(project.id, 4, opts.userId) : [];
|
|
2397
|
+
all = filterAutoLoadedObservationsForContinuity2(all, pinned, isNewProject, recentPrompts2, recentToolEvents2, recentSessions2, recentHandoffs2, recentChatMessages2, summariesFromRecentSessions2(db, projectId, recentSessions2));
|
|
1351
2398
|
return {
|
|
1352
2399
|
project_name: projectName,
|
|
1353
2400
|
canonical_id: canonicalId,
|
|
1354
|
-
observations: all.map(
|
|
2401
|
+
observations: all.map(toContextObservation2),
|
|
1355
2402
|
session_count: all.length,
|
|
1356
2403
|
total_active: totalActive,
|
|
1357
2404
|
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
@@ -1359,40 +2406,44 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1359
2406
|
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
1360
2407
|
projectTypeCounts: projectTypeCounts2,
|
|
1361
2408
|
recentOutcomes: recentOutcomes2,
|
|
1362
|
-
recentHandoffs: recentHandoffs2.length > 0 ? recentHandoffs2 : undefined
|
|
2409
|
+
recentHandoffs: recentHandoffs2.length > 0 ? recentHandoffs2 : undefined,
|
|
2410
|
+
recentChatMessages: recentChatMessages2.length > 0 ? recentChatMessages2 : undefined
|
|
1363
2411
|
};
|
|
1364
2412
|
}
|
|
1365
2413
|
let remainingBudget = tokenBudget - 30;
|
|
1366
2414
|
const selected = [];
|
|
1367
2415
|
for (const obs of pinned) {
|
|
1368
|
-
const cost =
|
|
2416
|
+
const cost = estimateObservationTokens2(obs, selected.length);
|
|
1369
2417
|
remainingBudget -= cost;
|
|
1370
2418
|
selected.push(obs);
|
|
1371
2419
|
}
|
|
1372
2420
|
for (const obs of dedupedRecent) {
|
|
1373
|
-
const cost =
|
|
2421
|
+
const cost = estimateObservationTokens2(obs, selected.length);
|
|
1374
2422
|
remainingBudget -= cost;
|
|
1375
2423
|
selected.push(obs);
|
|
1376
2424
|
}
|
|
1377
2425
|
for (const obs of sorted) {
|
|
1378
|
-
const cost =
|
|
2426
|
+
const cost = estimateObservationTokens2(obs, selected.length);
|
|
1379
2427
|
if (remainingBudget - cost < 0 && selected.length > 0)
|
|
1380
2428
|
break;
|
|
1381
2429
|
remainingBudget -= cost;
|
|
1382
2430
|
selected.push(obs);
|
|
1383
2431
|
}
|
|
1384
2432
|
const summaries = isNewProject ? [] : db.getRecentSummaries(projectId, 5);
|
|
1385
|
-
const recentPrompts =
|
|
1386
|
-
const recentToolEvents =
|
|
2433
|
+
const recentPrompts = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
2434
|
+
const recentToolEvents = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
1387
2435
|
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
1388
|
-
const projectTypeCounts = isNewProject ? undefined :
|
|
1389
|
-
const recentOutcomes = isNewProject ? undefined :
|
|
1390
|
-
const recentHandoffs = getRecentHandoffs(db, {
|
|
2436
|
+
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts2(db, projectId, opts.userId);
|
|
2437
|
+
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes2(db, projectId, opts.userId, recentSessions);
|
|
2438
|
+
const recentHandoffs = isNewProject ? [] : getRecentHandoffs(db, {
|
|
1391
2439
|
cwd,
|
|
1392
|
-
project_scoped:
|
|
2440
|
+
project_scoped: true,
|
|
1393
2441
|
user_id: opts.userId,
|
|
2442
|
+
current_device_id: opts.currentDeviceId,
|
|
1394
2443
|
limit: 3
|
|
1395
2444
|
}).handoffs;
|
|
2445
|
+
const recentChatMessages = !isNewProject ? db.getRecentChatMessages(projectId, 4, opts.userId) : [];
|
|
2446
|
+
const filteredSelected = filterAutoLoadedObservationsForContinuity2(selected, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries);
|
|
1396
2447
|
let securityFindings = [];
|
|
1397
2448
|
if (!isNewProject) {
|
|
1398
2449
|
try {
|
|
@@ -1440,8 +2491,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1440
2491
|
return {
|
|
1441
2492
|
project_name: projectName,
|
|
1442
2493
|
canonical_id: canonicalId,
|
|
1443
|
-
observations:
|
|
1444
|
-
session_count:
|
|
2494
|
+
observations: filteredSelected.map(toContextObservation2),
|
|
2495
|
+
session_count: filteredSelected.length,
|
|
1445
2496
|
total_active: totalActive,
|
|
1446
2497
|
summaries: summaries.length > 0 ? summaries : undefined,
|
|
1447
2498
|
securityFindings: securityFindings.length > 0 ? securityFindings : undefined,
|
|
@@ -1452,19 +2503,53 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1452
2503
|
recentSessions: recentSessions.length > 0 ? recentSessions : undefined,
|
|
1453
2504
|
projectTypeCounts,
|
|
1454
2505
|
recentOutcomes,
|
|
1455
|
-
recentHandoffs: recentHandoffs.length > 0 ? recentHandoffs : undefined
|
|
2506
|
+
recentHandoffs: recentHandoffs.length > 0 ? recentHandoffs : undefined,
|
|
2507
|
+
recentChatMessages: recentChatMessages.length > 0 ? recentChatMessages : undefined
|
|
1456
2508
|
};
|
|
1457
2509
|
}
|
|
1458
|
-
function
|
|
2510
|
+
function filterAutoLoadedObservationsForContinuity2(observations, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
2511
|
+
if (isNewProject)
|
|
2512
|
+
return observations;
|
|
2513
|
+
if (hasFreshProjectContinuity2(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries)) {
|
|
2514
|
+
return observations;
|
|
2515
|
+
}
|
|
2516
|
+
const pinnedIds = new Set(pinned.map((obs) => obs.id));
|
|
2517
|
+
return observations.filter((obs) => {
|
|
2518
|
+
if (pinnedIds.has(obs.id))
|
|
2519
|
+
return true;
|
|
2520
|
+
return observationAgeDays2(obs.created_at_epoch) <= FRESH_CONTINUITY_WINDOW_DAYS2;
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
function hasFreshProjectContinuity2(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
2524
|
+
const freshEnough = (epoch) => typeof epoch === "number" && observationAgeDays2(epoch) <= FRESH_CONTINUITY_WINDOW_DAYS2;
|
|
2525
|
+
return recentPrompts.some((item) => freshEnough(item.created_at_epoch)) || recentToolEvents.some((item) => freshEnough(item.created_at_epoch)) || recentSessions.some((item) => freshEnough(item.completed_at_epoch ?? item.started_at_epoch)) || recentHandoffs.some((item) => freshEnough(item.created_at_epoch)) || recentChatMessages.some((item) => freshEnough(item.created_at_epoch)) || summaries.some((item) => freshEnough(item.created_at_epoch));
|
|
2526
|
+
}
|
|
2527
|
+
function summariesFromRecentSessions2(db, projectId, recentSessions) {
|
|
2528
|
+
const seen = new Set;
|
|
2529
|
+
const rows = [];
|
|
2530
|
+
for (const session of recentSessions) {
|
|
2531
|
+
if (seen.has(session.session_id))
|
|
2532
|
+
continue;
|
|
2533
|
+
seen.add(session.session_id);
|
|
2534
|
+
const summary = db.getSessionSummary(session.session_id);
|
|
2535
|
+
if (summary && summary.project_id === projectId)
|
|
2536
|
+
rows.push(summary);
|
|
2537
|
+
}
|
|
2538
|
+
return rows;
|
|
2539
|
+
}
|
|
2540
|
+
function observationAgeDays2(createdAtEpoch) {
|
|
2541
|
+
return Math.max(0, (Math.floor(Date.now() / 1000) - createdAtEpoch) / 86400);
|
|
2542
|
+
}
|
|
2543
|
+
function estimateObservationTokens2(obs, index) {
|
|
1459
2544
|
const DETAILED_THRESHOLD = 5;
|
|
1460
|
-
const titleCost =
|
|
2545
|
+
const titleCost = estimateTokens2(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|
|
1461
2546
|
if (index >= DETAILED_THRESHOLD) {
|
|
1462
2547
|
return titleCost;
|
|
1463
2548
|
}
|
|
1464
|
-
const detailText =
|
|
1465
|
-
return titleCost +
|
|
2549
|
+
const detailText = formatObservationDetail2(obs);
|
|
2550
|
+
return titleCost + estimateTokens2(detailText);
|
|
1466
2551
|
}
|
|
1467
|
-
function
|
|
2552
|
+
function formatContextForInjection2(context) {
|
|
1468
2553
|
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)) {
|
|
1469
2554
|
return `Project: ${context.project_name} (no prior observations)`;
|
|
1470
2555
|
}
|
|
@@ -1488,13 +2573,34 @@ function formatContextForInjection(context) {
|
|
|
1488
2573
|
lines.push(`${context.session_count} relevant observation(s) from prior sessions:`);
|
|
1489
2574
|
lines.push("");
|
|
1490
2575
|
}
|
|
2576
|
+
if (context.recentHandoffs && context.recentHandoffs.length > 0) {
|
|
2577
|
+
lines.push("## Recent Handoffs");
|
|
2578
|
+
for (const handoff of context.recentHandoffs.slice(0, 3)) {
|
|
2579
|
+
const title = handoff.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+·\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
2580
|
+
if (title) {
|
|
2581
|
+
lines.push(`- ${truncateText2(`${title} (${formatHandoffSource(handoff)})`, 160)}`);
|
|
2582
|
+
}
|
|
2583
|
+
const narrative = handoff.narrative?.split(/\n{2,}/).map((part) => part.replace(/\s+/g, " ").trim()).find((part) => /^(Current thread:|Completed:|Next Steps:)/i.test(part));
|
|
2584
|
+
if (narrative) {
|
|
2585
|
+
lines.push(` ${truncateText2(narrative, 180)}`);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
lines.push("");
|
|
2589
|
+
}
|
|
2590
|
+
if (context.recentChatMessages && context.recentChatMessages.length > 0) {
|
|
2591
|
+
lines.push("## Recent Chat");
|
|
2592
|
+
for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
|
|
2593
|
+
lines.push(`- [${message.role}] ${truncateText2(message.content.replace(/\s+/g, " ").trim(), 160)}`);
|
|
2594
|
+
}
|
|
2595
|
+
lines.push("");
|
|
2596
|
+
}
|
|
1491
2597
|
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
1492
|
-
const promptLines = context.recentPrompts.filter((prompt) =>
|
|
2598
|
+
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt2(prompt.prompt)).slice(0, 5);
|
|
1493
2599
|
if (promptLines.length > 0) {
|
|
1494
2600
|
lines.push("## Recent Requests");
|
|
1495
2601
|
for (const prompt of promptLines) {
|
|
1496
2602
|
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
1497
|
-
lines.push(`- ${label}: ${
|
|
2603
|
+
lines.push(`- ${label}: ${truncateText2(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
1498
2604
|
}
|
|
1499
2605
|
lines.push("");
|
|
1500
2606
|
}
|
|
@@ -1502,16 +2608,16 @@ function formatContextForInjection(context) {
|
|
|
1502
2608
|
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
1503
2609
|
lines.push("## Recent Tools");
|
|
1504
2610
|
for (const tool of context.recentToolEvents.slice(0, 5)) {
|
|
1505
|
-
lines.push(`- ${tool.tool_name}: ${
|
|
2611
|
+
lines.push(`- ${tool.tool_name}: ${formatToolEventDetail2(tool)}`);
|
|
1506
2612
|
}
|
|
1507
2613
|
lines.push("");
|
|
1508
2614
|
}
|
|
1509
2615
|
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
1510
2616
|
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
1511
|
-
const summary =
|
|
2617
|
+
const summary = chooseMeaningfulSessionHeadline2(session.request, session.completed);
|
|
1512
2618
|
if (summary === "(no summary)")
|
|
1513
2619
|
return null;
|
|
1514
|
-
return `- ${session.session_id}: ${
|
|
2620
|
+
return `- ${session.session_id}: ${truncateText2(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
1515
2621
|
}).filter((line) => Boolean(line));
|
|
1516
2622
|
if (recentSessionLines.length > 0) {
|
|
1517
2623
|
lines.push("## Recent Sessions");
|
|
@@ -1522,7 +2628,7 @@ function formatContextForInjection(context) {
|
|
|
1522
2628
|
if (context.recentOutcomes && context.recentOutcomes.length > 0) {
|
|
1523
2629
|
lines.push("## Recent Outcomes");
|
|
1524
2630
|
for (const outcome of context.recentOutcomes.slice(0, 5)) {
|
|
1525
|
-
lines.push(`- ${
|
|
2631
|
+
lines.push(`- ${truncateText2(outcome, 160)}`);
|
|
1526
2632
|
}
|
|
1527
2633
|
lines.push("");
|
|
1528
2634
|
}
|
|
@@ -1538,10 +2644,10 @@ function formatContextForInjection(context) {
|
|
|
1538
2644
|
const obs = context.observations[i];
|
|
1539
2645
|
const date = obs.created_at.split("T")[0];
|
|
1540
2646
|
const fromLabel = obs.source_project ? ` [from: ${obs.source_project}]` : "";
|
|
1541
|
-
const fileLabel =
|
|
2647
|
+
const fileLabel = formatObservationFiles2(obs);
|
|
1542
2648
|
lines.push(`- **#${obs.id} [${obs.type}]** ${obs.title} (${date}, q=${obs.quality.toFixed(1)})${fromLabel}${fileLabel}`);
|
|
1543
2649
|
if (i < DETAILED_COUNT) {
|
|
1544
|
-
const detail =
|
|
2650
|
+
const detail = formatObservationDetailFromContext2(obs);
|
|
1545
2651
|
if (detail) {
|
|
1546
2652
|
lines.push(detail);
|
|
1547
2653
|
}
|
|
@@ -1551,7 +2657,7 @@ function formatContextForInjection(context) {
|
|
|
1551
2657
|
lines.push("");
|
|
1552
2658
|
lines.push("## Recent Project Briefs");
|
|
1553
2659
|
for (const summary of context.summaries.slice(0, 3)) {
|
|
1554
|
-
lines.push(...
|
|
2660
|
+
lines.push(...formatSessionBrief2(summary));
|
|
1555
2661
|
lines.push("");
|
|
1556
2662
|
}
|
|
1557
2663
|
}
|
|
@@ -1583,9 +2689,9 @@ function formatContextForInjection(context) {
|
|
|
1583
2689
|
return lines.join(`
|
|
1584
2690
|
`);
|
|
1585
2691
|
}
|
|
1586
|
-
function
|
|
2692
|
+
function formatSessionBrief2(summary) {
|
|
1587
2693
|
const lines = [];
|
|
1588
|
-
const heading = summary.request ? `### ${
|
|
2694
|
+
const heading = summary.request ? `### ${truncateText2(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
1589
2695
|
lines.push(heading);
|
|
1590
2696
|
const sections = [
|
|
1591
2697
|
["Investigated", summary.investigated, 180],
|
|
@@ -1594,7 +2700,7 @@ function formatSessionBrief(summary) {
|
|
|
1594
2700
|
["Next Steps", summary.next_steps, 140]
|
|
1595
2701
|
];
|
|
1596
2702
|
for (const [label, value, maxLen] of sections) {
|
|
1597
|
-
const formatted =
|
|
2703
|
+
const formatted = formatSummarySection2(value, maxLen);
|
|
1598
2704
|
if (formatted) {
|
|
1599
2705
|
lines.push(`${label}:`);
|
|
1600
2706
|
lines.push(formatted);
|
|
@@ -1602,23 +2708,23 @@ function formatSessionBrief(summary) {
|
|
|
1602
2708
|
}
|
|
1603
2709
|
return lines;
|
|
1604
2710
|
}
|
|
1605
|
-
function
|
|
1606
|
-
if (request && !
|
|
2711
|
+
function chooseMeaningfulSessionHeadline2(request, completed) {
|
|
2712
|
+
if (request && !looksLikeFileOperationTitle3(request))
|
|
1607
2713
|
return request;
|
|
1608
|
-
const completedItems =
|
|
2714
|
+
const completedItems = extractMeaningfulLines2(completed, 1);
|
|
1609
2715
|
if (completedItems.length > 0)
|
|
1610
2716
|
return completedItems[0];
|
|
1611
2717
|
return request ?? completed ?? "(no summary)";
|
|
1612
2718
|
}
|
|
1613
|
-
function
|
|
2719
|
+
function formatSummarySection2(value, maxLen) {
|
|
1614
2720
|
return formatSummaryItems(value, maxLen);
|
|
1615
2721
|
}
|
|
1616
|
-
function
|
|
2722
|
+
function truncateText2(text, maxLen) {
|
|
1617
2723
|
if (text.length <= maxLen)
|
|
1618
2724
|
return text;
|
|
1619
2725
|
return text.slice(0, maxLen - 3) + "...";
|
|
1620
2726
|
}
|
|
1621
|
-
function
|
|
2727
|
+
function isMeaningfulPrompt2(value) {
|
|
1622
2728
|
if (!value)
|
|
1623
2729
|
return false;
|
|
1624
2730
|
const compact = value.replace(/\s+/g, " ").trim();
|
|
@@ -1626,20 +2732,20 @@ function isMeaningfulPrompt(value) {
|
|
|
1626
2732
|
return false;
|
|
1627
2733
|
return /[a-z]{3,}/i.test(compact);
|
|
1628
2734
|
}
|
|
1629
|
-
function
|
|
2735
|
+
function looksLikeFileOperationTitle3(value) {
|
|
1630
2736
|
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
1631
2737
|
}
|
|
1632
|
-
function
|
|
2738
|
+
function stripInlineSectionLabel2(value) {
|
|
1633
2739
|
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
1634
2740
|
}
|
|
1635
|
-
function
|
|
2741
|
+
function extractMeaningfulLines2(value, limit) {
|
|
1636
2742
|
if (!value)
|
|
1637
2743
|
return [];
|
|
1638
|
-
return extractSummaryItems(value).map((line) =>
|
|
2744
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel2(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle3(line)).slice(0, limit);
|
|
1639
2745
|
}
|
|
1640
|
-
function
|
|
2746
|
+
function formatObservationDetailFromContext2(obs) {
|
|
1641
2747
|
if (obs.facts) {
|
|
1642
|
-
const bullets =
|
|
2748
|
+
const bullets = parseFacts2(obs.facts);
|
|
1643
2749
|
if (bullets.length > 0) {
|
|
1644
2750
|
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
1645
2751
|
`);
|
|
@@ -1651,9 +2757,9 @@ function formatObservationDetailFromContext(obs) {
|
|
|
1651
2757
|
}
|
|
1652
2758
|
return null;
|
|
1653
2759
|
}
|
|
1654
|
-
function
|
|
2760
|
+
function formatObservationDetail2(obs) {
|
|
1655
2761
|
if (obs.facts) {
|
|
1656
|
-
const bullets =
|
|
2762
|
+
const bullets = parseFacts2(obs.facts);
|
|
1657
2763
|
if (bullets.length > 0) {
|
|
1658
2764
|
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
1659
2765
|
`);
|
|
@@ -1665,7 +2771,7 @@ function formatObservationDetail(obs) {
|
|
|
1665
2771
|
}
|
|
1666
2772
|
return "";
|
|
1667
2773
|
}
|
|
1668
|
-
function
|
|
2774
|
+
function parseFacts2(facts) {
|
|
1669
2775
|
if (!facts)
|
|
1670
2776
|
return [];
|
|
1671
2777
|
try {
|
|
@@ -1680,7 +2786,7 @@ function parseFacts(facts) {
|
|
|
1680
2786
|
}
|
|
1681
2787
|
return [];
|
|
1682
2788
|
}
|
|
1683
|
-
function
|
|
2789
|
+
function toContextObservation2(obs) {
|
|
1684
2790
|
return {
|
|
1685
2791
|
id: obs.id,
|
|
1686
2792
|
type: obs.type,
|
|
@@ -1694,18 +2800,18 @@ function toContextObservation(obs) {
|
|
|
1694
2800
|
...obs._source_project ? { source_project: obs._source_project } : {}
|
|
1695
2801
|
};
|
|
1696
2802
|
}
|
|
1697
|
-
function
|
|
1698
|
-
const modified =
|
|
2803
|
+
function formatObservationFiles2(obs) {
|
|
2804
|
+
const modified = parseJsonStringArray2(obs.files_modified);
|
|
1699
2805
|
if (modified.length > 0) {
|
|
1700
|
-
return ` · files: ${
|
|
2806
|
+
return ` · files: ${truncateText2(modified.slice(0, 2).join(", "), 60)}`;
|
|
1701
2807
|
}
|
|
1702
|
-
const read =
|
|
2808
|
+
const read = parseJsonStringArray2(obs.files_read);
|
|
1703
2809
|
if (read.length > 0) {
|
|
1704
|
-
return ` · read: ${
|
|
2810
|
+
return ` · read: ${truncateText2(read.slice(0, 2).join(", "), 60)}`;
|
|
1705
2811
|
}
|
|
1706
2812
|
return "";
|
|
1707
2813
|
}
|
|
1708
|
-
function
|
|
2814
|
+
function parseJsonStringArray2(value) {
|
|
1709
2815
|
if (!value)
|
|
1710
2816
|
return [];
|
|
1711
2817
|
try {
|
|
@@ -1717,11 +2823,11 @@ function parseJsonStringArray(value) {
|
|
|
1717
2823
|
return [];
|
|
1718
2824
|
}
|
|
1719
2825
|
}
|
|
1720
|
-
function
|
|
2826
|
+
function formatToolEventDetail2(tool) {
|
|
1721
2827
|
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
1722
|
-
return
|
|
2828
|
+
return truncateText2(detail || "recent tool execution", 160);
|
|
1723
2829
|
}
|
|
1724
|
-
function
|
|
2830
|
+
function getProjectTypeCounts2(db, projectId, userId) {
|
|
1725
2831
|
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
1726
2832
|
const rows = db.db.query(`SELECT type, COUNT(*) as count
|
|
1727
2833
|
FROM observations
|
|
@@ -1736,7 +2842,7 @@ function getProjectTypeCounts(db, projectId, userId) {
|
|
|
1736
2842
|
}
|
|
1737
2843
|
return counts;
|
|
1738
2844
|
}
|
|
1739
|
-
function
|
|
2845
|
+
function getRecentOutcomes2(db, projectId, userId, recentSessions) {
|
|
1740
2846
|
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
1741
2847
|
const visibilityParams = userId ? [userId] : [];
|
|
1742
2848
|
const summaries = db.db.query(`SELECT * FROM session_summaries
|
|
@@ -1746,7 +2852,7 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
1746
2852
|
const picked = [];
|
|
1747
2853
|
const seen = new Set;
|
|
1748
2854
|
for (const summary of summaries) {
|
|
1749
|
-
for (const item of
|
|
2855
|
+
for (const item of parseSummaryJsonList2(summary.recent_outcomes)) {
|
|
1750
2856
|
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1751
2857
|
if (!normalized || seen.has(normalized))
|
|
1752
2858
|
continue;
|
|
@@ -1756,8 +2862,8 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
1756
2862
|
return picked;
|
|
1757
2863
|
}
|
|
1758
2864
|
for (const line of [
|
|
1759
|
-
...
|
|
1760
|
-
...
|
|
2865
|
+
...extractMeaningfulLines2(summary.completed, 2),
|
|
2866
|
+
...extractMeaningfulLines2(summary.learned, 1)
|
|
1761
2867
|
]) {
|
|
1762
2868
|
const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1763
2869
|
if (!normalized || seen.has(normalized))
|
|
@@ -1769,7 +2875,7 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
1769
2875
|
}
|
|
1770
2876
|
}
|
|
1771
2877
|
for (const session of recentSessions ?? []) {
|
|
1772
|
-
for (const item of
|
|
2878
|
+
for (const item of parseSummaryJsonList2(session.recent_outcomes)) {
|
|
1773
2879
|
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1774
2880
|
if (!normalized || seen.has(normalized))
|
|
1775
2881
|
continue;
|
|
@@ -1789,9 +2895,9 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
1789
2895
|
for (const obs of rows) {
|
|
1790
2896
|
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
1791
2897
|
continue;
|
|
1792
|
-
const title =
|
|
2898
|
+
const title = stripInlineSectionLabel2(obs.title);
|
|
1793
2899
|
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1794
|
-
if (!normalized || seen.has(normalized) ||
|
|
2900
|
+
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle3(title))
|
|
1795
2901
|
continue;
|
|
1796
2902
|
seen.add(normalized);
|
|
1797
2903
|
picked.push(title);
|
|
@@ -1801,6 +2907,28 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
1801
2907
|
return picked;
|
|
1802
2908
|
}
|
|
1803
2909
|
|
|
2910
|
+
// src/tools/project-memory-index.ts
|
|
2911
|
+
function classifyContinuityState(recentRequestsCount, recentToolsCount, recentHandoffsCount, recentChatCount, recentSessions, recentOutcomesCount) {
|
|
2912
|
+
const hasRaw = recentRequestsCount > 0 || recentToolsCount > 0;
|
|
2913
|
+
const hasResume = recentHandoffsCount > 0 || recentChatCount > 0;
|
|
2914
|
+
const hasSessionThread = recentSessions.length > 0 || recentOutcomesCount > 0;
|
|
2915
|
+
if (hasRaw && (hasResume || hasSessionThread))
|
|
2916
|
+
return "fresh";
|
|
2917
|
+
if (hasRaw || hasResume || hasSessionThread)
|
|
2918
|
+
return "thin";
|
|
2919
|
+
return "cold";
|
|
2920
|
+
}
|
|
2921
|
+
function describeContinuityState(state) {
|
|
2922
|
+
switch (state) {
|
|
2923
|
+
case "fresh":
|
|
2924
|
+
return "Fresh repo-local continuity is available.";
|
|
2925
|
+
case "thin":
|
|
2926
|
+
return "Only partial continuity is available; recent prompts/chat are safer than older memory.";
|
|
2927
|
+
default:
|
|
2928
|
+
return "No fresh repo-local continuity yet; older memory should be treated cautiously.";
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
|
|
1804
2932
|
// src/telemetry/stack-detect.ts
|
|
1805
2933
|
import { existsSync as existsSync2 } from "node:fs";
|
|
1806
2934
|
import { join as join2, extname } from "node:path";
|
|
@@ -1921,7 +3049,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
1921
3049
|
import { join as join3 } from "node:path";
|
|
1922
3050
|
import { homedir } from "node:os";
|
|
1923
3051
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
1924
|
-
var CLIENT_VERSION = "0.4.
|
|
3052
|
+
var CLIENT_VERSION = "0.4.26";
|
|
1925
3053
|
function hashFile(filePath) {
|
|
1926
3054
|
try {
|
|
1927
3055
|
if (!existsSync3(filePath))
|
|
@@ -2448,7 +3576,9 @@ function mergeRemoteChat(db, config, change, projectId) {
|
|
|
2448
3576
|
device_id: (typeof change.metadata?.device_id === "string" ? change.metadata.device_id : null) ?? "remote",
|
|
2449
3577
|
agent: typeof change.metadata?.agent === "string" ? change.metadata.agent : "unknown",
|
|
2450
3578
|
created_at_epoch: typeof change.metadata?.created_at_epoch === "number" ? change.metadata.created_at_epoch : undefined,
|
|
2451
|
-
remote_source_id: change.source_id
|
|
3579
|
+
remote_source_id: change.source_id,
|
|
3580
|
+
source_kind: change.metadata?.source_kind === "transcript" ? "transcript" : "hook",
|
|
3581
|
+
transcript_index: typeof change.metadata?.transcript_index === "number" ? change.metadata.transcript_index : null
|
|
2452
3582
|
});
|
|
2453
3583
|
return true;
|
|
2454
3584
|
}
|
|
@@ -3101,6 +4231,19 @@ var MIGRATIONS = [
|
|
|
3101
4231
|
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
3102
4232
|
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
3103
4233
|
`
|
|
4234
|
+
},
|
|
4235
|
+
{
|
|
4236
|
+
version: 17,
|
|
4237
|
+
description: "Track transcript-backed chat messages separately from hook chat",
|
|
4238
|
+
sql: `
|
|
4239
|
+
ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook';
|
|
4240
|
+
ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER;
|
|
4241
|
+
CREATE INDEX IF NOT EXISTS idx_chat_messages_source_kind
|
|
4242
|
+
ON chat_messages(session_id, source_kind, transcript_index, created_at_epoch DESC, id DESC);
|
|
4243
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_session_transcript
|
|
4244
|
+
ON chat_messages(session_id, transcript_index)
|
|
4245
|
+
WHERE transcript_index IS NOT NULL;
|
|
4246
|
+
`
|
|
3104
4247
|
}
|
|
3105
4248
|
];
|
|
3106
4249
|
function isVecExtensionLoaded(db) {
|
|
@@ -3171,6 +4314,9 @@ function inferLegacySchemaVersion(db) {
|
|
|
3171
4314
|
if (syncOutboxSupportsChatMessages(db)) {
|
|
3172
4315
|
version = Math.max(version, 16);
|
|
3173
4316
|
}
|
|
4317
|
+
if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
|
|
4318
|
+
version = Math.max(version, 17);
|
|
4319
|
+
}
|
|
3174
4320
|
return version;
|
|
3175
4321
|
}
|
|
3176
4322
|
function runMigrations(db) {
|
|
@@ -3274,9 +4420,17 @@ function ensureChatMessageColumns(db) {
|
|
|
3274
4420
|
db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
|
|
3275
4421
|
}
|
|
3276
4422
|
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source ON chat_messages(remote_source_id) WHERE remote_source_id IS NOT NULL");
|
|
4423
|
+
if (!columnExists(db, "chat_messages", "source_kind")) {
|
|
4424
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN source_kind TEXT DEFAULT 'hook'");
|
|
4425
|
+
}
|
|
4426
|
+
if (!columnExists(db, "chat_messages", "transcript_index")) {
|
|
4427
|
+
db.exec("ALTER TABLE chat_messages ADD COLUMN transcript_index INTEGER");
|
|
4428
|
+
}
|
|
4429
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_chat_messages_source_kind ON chat_messages(session_id, source_kind, transcript_index, created_at_epoch DESC, id DESC)");
|
|
4430
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_session_transcript ON chat_messages(session_id, transcript_index) WHERE transcript_index IS NOT NULL");
|
|
3277
4431
|
const current = getSchemaVersion(db);
|
|
3278
|
-
if (current <
|
|
3279
|
-
db.exec("PRAGMA user_version =
|
|
4432
|
+
if (current < 17) {
|
|
4433
|
+
db.exec("PRAGMA user_version = 17");
|
|
3280
4434
|
}
|
|
3281
4435
|
}
|
|
3282
4436
|
function ensureSyncOutboxSupportsChatMessages(db) {
|
|
@@ -3481,6 +4635,22 @@ class MemDatabase {
|
|
|
3481
4635
|
getObservationById(id) {
|
|
3482
4636
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
3483
4637
|
}
|
|
4638
|
+
updateObservationContent(id, update) {
|
|
4639
|
+
const existing = this.getObservationById(id);
|
|
4640
|
+
if (!existing)
|
|
4641
|
+
return null;
|
|
4642
|
+
const createdAtEpoch = update.created_at_epoch ?? existing.created_at_epoch;
|
|
4643
|
+
const createdAt = new Date(createdAtEpoch * 1000).toISOString();
|
|
4644
|
+
this.db.query(`UPDATE observations
|
|
4645
|
+
SET title = ?, narrative = ?, facts = ?, concepts = ?, created_at = ?, created_at_epoch = ?
|
|
4646
|
+
WHERE id = ?`).run(update.title, update.narrative ?? null, update.facts ?? null, update.concepts ?? null, createdAt, createdAtEpoch, id);
|
|
4647
|
+
this.ftsDelete(existing);
|
|
4648
|
+
const refreshed = this.getObservationById(id);
|
|
4649
|
+
if (!refreshed)
|
|
4650
|
+
return null;
|
|
4651
|
+
this.ftsInsert(refreshed);
|
|
4652
|
+
return refreshed;
|
|
4653
|
+
}
|
|
3484
4654
|
getObservationsByIds(ids, userId) {
|
|
3485
4655
|
if (ids.length === 0)
|
|
3486
4656
|
return [];
|
|
@@ -3752,8 +4922,8 @@ class MemDatabase {
|
|
|
3752
4922
|
const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
|
|
3753
4923
|
const content = input.content.trim();
|
|
3754
4924
|
const result = this.db.query(`INSERT INTO chat_messages (
|
|
3755
|
-
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
|
|
3756
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.role, content, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt, input.remote_source_id ?? null);
|
|
4925
|
+
session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id, source_kind, transcript_index
|
|
4926
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.role, content, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt, input.remote_source_id ?? null, input.source_kind ?? "hook", input.transcript_index ?? null);
|
|
3757
4927
|
return this.getChatMessageById(Number(result.lastInsertRowid));
|
|
3758
4928
|
}
|
|
3759
4929
|
getChatMessageById(id) {
|
|
@@ -3765,7 +4935,17 @@ class MemDatabase {
|
|
|
3765
4935
|
getSessionChatMessages(sessionId, limit = 50) {
|
|
3766
4936
|
return this.db.query(`SELECT * FROM chat_messages
|
|
3767
4937
|
WHERE session_id = ?
|
|
3768
|
-
|
|
4938
|
+
AND (
|
|
4939
|
+
source_kind = 'transcript'
|
|
4940
|
+
OR NOT EXISTS (
|
|
4941
|
+
SELECT 1 FROM chat_messages t2
|
|
4942
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
4943
|
+
AND t2.source_kind = 'transcript'
|
|
4944
|
+
)
|
|
4945
|
+
)
|
|
4946
|
+
ORDER BY
|
|
4947
|
+
CASE WHEN transcript_index IS NULL THEN created_at_epoch ELSE transcript_index END ASC,
|
|
4948
|
+
id ASC
|
|
3769
4949
|
LIMIT ?`).all(sessionId, limit);
|
|
3770
4950
|
}
|
|
3771
4951
|
getRecentChatMessages(projectId, limit = 20, userId) {
|
|
@@ -3773,11 +4953,27 @@ class MemDatabase {
|
|
|
3773
4953
|
if (projectId !== null) {
|
|
3774
4954
|
return this.db.query(`SELECT * FROM chat_messages
|
|
3775
4955
|
WHERE project_id = ?${visibilityClause}
|
|
4956
|
+
AND (
|
|
4957
|
+
source_kind = 'transcript'
|
|
4958
|
+
OR NOT EXISTS (
|
|
4959
|
+
SELECT 1 FROM chat_messages t2
|
|
4960
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
4961
|
+
AND t2.source_kind = 'transcript'
|
|
4962
|
+
)
|
|
4963
|
+
)
|
|
3776
4964
|
ORDER BY created_at_epoch DESC, id DESC
|
|
3777
4965
|
LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
|
|
3778
4966
|
}
|
|
3779
4967
|
return this.db.query(`SELECT * FROM chat_messages
|
|
3780
4968
|
WHERE 1 = 1${visibilityClause}
|
|
4969
|
+
AND (
|
|
4970
|
+
source_kind = 'transcript'
|
|
4971
|
+
OR NOT EXISTS (
|
|
4972
|
+
SELECT 1 FROM chat_messages t2
|
|
4973
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
4974
|
+
AND t2.source_kind = 'transcript'
|
|
4975
|
+
)
|
|
4976
|
+
)
|
|
3781
4977
|
ORDER BY created_at_epoch DESC, id DESC
|
|
3782
4978
|
LIMIT ?`).all(...userId ? [userId] : [], limit);
|
|
3783
4979
|
}
|
|
@@ -3788,14 +4984,33 @@ class MemDatabase {
|
|
|
3788
4984
|
return this.db.query(`SELECT * FROM chat_messages
|
|
3789
4985
|
WHERE project_id = ?
|
|
3790
4986
|
AND lower(content) LIKE ?${visibilityClause}
|
|
4987
|
+
AND (
|
|
4988
|
+
source_kind = 'transcript'
|
|
4989
|
+
OR NOT EXISTS (
|
|
4990
|
+
SELECT 1 FROM chat_messages t2
|
|
4991
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
4992
|
+
AND t2.source_kind = 'transcript'
|
|
4993
|
+
)
|
|
4994
|
+
)
|
|
3791
4995
|
ORDER BY created_at_epoch DESC, id DESC
|
|
3792
4996
|
LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
|
|
3793
4997
|
}
|
|
3794
4998
|
return this.db.query(`SELECT * FROM chat_messages
|
|
3795
4999
|
WHERE lower(content) LIKE ?${visibilityClause}
|
|
5000
|
+
AND (
|
|
5001
|
+
source_kind = 'transcript'
|
|
5002
|
+
OR NOT EXISTS (
|
|
5003
|
+
SELECT 1 FROM chat_messages t2
|
|
5004
|
+
WHERE t2.session_id = chat_messages.session_id
|
|
5005
|
+
AND t2.source_kind = 'transcript'
|
|
5006
|
+
)
|
|
5007
|
+
)
|
|
3796
5008
|
ORDER BY created_at_epoch DESC, id DESC
|
|
3797
5009
|
LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
|
|
3798
5010
|
}
|
|
5011
|
+
getTranscriptChatMessage(sessionId, transcriptIndex) {
|
|
5012
|
+
return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
|
|
5013
|
+
}
|
|
3799
5014
|
addToOutbox(recordType, recordId) {
|
|
3800
5015
|
const now = Math.floor(Date.now() / 1000);
|
|
3801
5016
|
this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
|
|
@@ -4094,7 +5309,8 @@ async function main() {
|
|
|
4094
5309
|
const context = buildSessionContext(db, event.cwd, {
|
|
4095
5310
|
tokenBudget: 800,
|
|
4096
5311
|
scope: config.search.scope,
|
|
4097
|
-
userId: config.user_id
|
|
5312
|
+
userId: config.user_id,
|
|
5313
|
+
currentDeviceId: config.device_id
|
|
4098
5314
|
});
|
|
4099
5315
|
if (context) {
|
|
4100
5316
|
try {
|
|
@@ -4249,10 +5465,12 @@ function formatSplashScreen(data) {
|
|
|
4249
5465
|
}
|
|
4250
5466
|
function formatVisibleStartupBrief(context) {
|
|
4251
5467
|
const lines = [];
|
|
5468
|
+
const continuityState = getStartupContinuityState(context);
|
|
4252
5469
|
const latest = pickPrimarySummary(context);
|
|
4253
5470
|
const observationFallbacks = buildObservationFallbacks(context);
|
|
4254
5471
|
const promptFallback = buildPromptFallback(context);
|
|
4255
5472
|
const promptLines = buildPromptLines(context);
|
|
5473
|
+
const recentChatLines = buildRecentChatLines(context);
|
|
4256
5474
|
const latestPromptLine = promptLines[0] ?? null;
|
|
4257
5475
|
const currentRequest = latest ? chooseRequest(latest.request, promptFallback ?? sessionFallbacksFromContext(context)[0] ?? observationFallbacks.request) : promptFallback;
|
|
4258
5476
|
const toolFallbacks = buildToolFallbacks(context);
|
|
@@ -4262,6 +5480,7 @@ function formatVisibleStartupBrief(context) {
|
|
|
4262
5480
|
const projectSignals = buildProjectSignalLine(context);
|
|
4263
5481
|
const shownItems = new Set;
|
|
4264
5482
|
const latestHandoffLines = buildLatestHandoffLines(context);
|
|
5483
|
+
const freshContinuity = hasFreshContinuitySignal(context);
|
|
4265
5484
|
if (latestHandoffLines.length > 0) {
|
|
4266
5485
|
lines.push(`${c2.cyan}Latest handoff:${c2.reset}`);
|
|
4267
5486
|
for (const item of latestHandoffLines) {
|
|
@@ -4269,6 +5488,7 @@ function formatVisibleStartupBrief(context) {
|
|
|
4269
5488
|
rememberShownItem(shownItems, item);
|
|
4270
5489
|
}
|
|
4271
5490
|
}
|
|
5491
|
+
lines.push(`${c2.cyan}Continuity:${c2.reset} ${continuityState} \u2014 ${truncateInline(describeContinuityState(continuityState), 160)}`);
|
|
4272
5492
|
if (promptLines.length > 0) {
|
|
4273
5493
|
lines.push(`${c2.cyan}Asked recently:${c2.reset}`);
|
|
4274
5494
|
for (const item of promptLines) {
|
|
@@ -4276,6 +5496,13 @@ function formatVisibleStartupBrief(context) {
|
|
|
4276
5496
|
rememberShownItem(shownItems, item);
|
|
4277
5497
|
}
|
|
4278
5498
|
}
|
|
5499
|
+
if (promptLines.length === 0 && recentChatLines.length > 0) {
|
|
5500
|
+
lines.push(`${c2.cyan}Chat trail:${c2.reset}`);
|
|
5501
|
+
for (const item of recentChatLines) {
|
|
5502
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
5503
|
+
rememberShownItem(shownItems, item);
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
4279
5506
|
if (latest) {
|
|
4280
5507
|
const sanitizedNextSteps = sanitizeNextSteps(latest.next_steps, {
|
|
4281
5508
|
request: currentRequest,
|
|
@@ -4353,12 +5580,15 @@ function formatVisibleStartupBrief(context) {
|
|
|
4353
5580
|
lines.push(`${c2.cyan}Signal mix:${c2.reset}`);
|
|
4354
5581
|
lines.push(` - ${truncateInline(projectSignals, 160)}`);
|
|
4355
5582
|
}
|
|
5583
|
+
if (!freshContinuity && lines.length > 0 && (promptLines.length > 0 || recentChatLines.length > 0)) {
|
|
5584
|
+
lines.push(`${c2.dim}Fresh repo-local handoff is still thin; recent prompts/chat are more trustworthy than older memory here.${c2.reset}`);
|
|
5585
|
+
}
|
|
4356
5586
|
const stale = pickRelevantStaleDecision(context, latest);
|
|
4357
5587
|
if (stale) {
|
|
4358
5588
|
lines.push(`${c2.yellow}Watch:${c2.reset} ${truncateInline(`Decision still looks unfinished: ${stale.title}`, 170)}`);
|
|
4359
5589
|
}
|
|
4360
5590
|
if (lines.length === 0 && context.observations.length > 0) {
|
|
4361
|
-
const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => obs.type !== "decision").filter((obs) => !
|
|
5591
|
+
const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => obs.type !== "decision").filter((obs) => !looksLikeFileOperationTitle4(obs.title)).slice(0, 3);
|
|
4362
5592
|
for (const obs of top) {
|
|
4363
5593
|
lines.push(`${c2.cyan}${capitalize(obs.type)}:${c2.reset} ${truncateInline(obs.title, 170)}`);
|
|
4364
5594
|
}
|
|
@@ -4370,9 +5600,9 @@ function buildLatestHandoffLines(context) {
|
|
|
4370
5600
|
if (!latest)
|
|
4371
5601
|
return [];
|
|
4372
5602
|
const lines = [];
|
|
4373
|
-
const title = latest.title.replace(/^Handoff
|
|
5603
|
+
const title = latest.title.replace(/^Handoff(?: Draft)?:\s*/i, "").replace(/\s+\u00B7\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}Z$/, "").trim();
|
|
4374
5604
|
if (title)
|
|
4375
|
-
lines.push(title);
|
|
5605
|
+
lines.push(`${title} (${formatHandoffSource2(latest)})`);
|
|
4376
5606
|
const narrative = latest.narrative?.split(/\n{2,}/).map((part) => part.replace(/\s+/g, " ").trim()).find((part) => /^(Current thread:|Completed:|Next Steps:)/i.test(part));
|
|
4377
5607
|
if (narrative) {
|
|
4378
5608
|
lines.push(narrative.replace(/^(Current thread:|Completed:|Next Steps:)\s*/i, ""));
|
|
@@ -4402,6 +5632,9 @@ function formatLegend() {
|
|
|
4402
5632
|
}
|
|
4403
5633
|
function formatContextIndex(context, shownItems) {
|
|
4404
5634
|
const selected = pickContextIndexObservations(context, shownItems);
|
|
5635
|
+
if (!hasFreshContinuitySignal(context)) {
|
|
5636
|
+
return { lines: [], observationIds: [] };
|
|
5637
|
+
}
|
|
4405
5638
|
const rows = selected.map((obs) => {
|
|
4406
5639
|
const icon = observationIcon(obs.type);
|
|
4407
5640
|
const fileHint = extractPrimaryFileHint(obs);
|
|
@@ -4419,19 +5652,29 @@ function formatContextIndex(context, shownItems) {
|
|
|
4419
5652
|
}
|
|
4420
5653
|
function formatInspectHints(context, visibleObservationIds = []) {
|
|
4421
5654
|
const hints = [];
|
|
5655
|
+
const continuityState = getStartupContinuityState(context);
|
|
4422
5656
|
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
4423
5657
|
hints.push("recent_sessions");
|
|
4424
5658
|
hints.push("session_story");
|
|
4425
5659
|
hints.push("create_handoff");
|
|
4426
5660
|
}
|
|
4427
|
-
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
|
|
5661
|
+
if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0) {
|
|
4428
5662
|
hints.push("activity_feed");
|
|
4429
5663
|
}
|
|
4430
5664
|
if (context.observations.length > 0) {
|
|
4431
5665
|
hints.push("memory_console");
|
|
4432
5666
|
}
|
|
4433
|
-
if ((context.
|
|
5667
|
+
if ((context.recentHandoffs?.length ?? 0) > 0) {
|
|
5668
|
+
hints.push("load_handoff");
|
|
5669
|
+
hints.push("recent_handoffs");
|
|
5670
|
+
}
|
|
5671
|
+
if ((context.recentChatMessages?.length ?? 0) > 0) {
|
|
5672
|
+
hints.push("recent_chat");
|
|
5673
|
+
}
|
|
5674
|
+
if (continuityState !== "fresh") {
|
|
5675
|
+
hints.push("recent_chat");
|
|
4434
5676
|
hints.push("recent_handoffs");
|
|
5677
|
+
hints.push("refresh_chat_recall");
|
|
4435
5678
|
}
|
|
4436
5679
|
const unique = Array.from(new Set(hints)).slice(0, 4);
|
|
4437
5680
|
if (unique.length === 0)
|
|
@@ -4484,17 +5727,25 @@ function filterAdditiveToolFallbacks(toolFallbacks, shownItems) {
|
|
|
4484
5727
|
});
|
|
4485
5728
|
}
|
|
4486
5729
|
function buildPromptFallback(context) {
|
|
4487
|
-
const latest = (context.recentPrompts ?? []).find((prompt) =>
|
|
5730
|
+
const latest = (context.recentPrompts ?? []).find((prompt) => isMeaningfulPrompt3(prompt.prompt));
|
|
4488
5731
|
if (!latest?.prompt)
|
|
4489
5732
|
return null;
|
|
4490
5733
|
return latest.prompt.replace(/\s+/g, " ").trim();
|
|
4491
5734
|
}
|
|
4492
5735
|
function buildPromptLines(context) {
|
|
4493
|
-
return (context.recentPrompts ?? []).filter((prompt) =>
|
|
5736
|
+
return (context.recentPrompts ?? []).filter((prompt) => isMeaningfulPrompt3(prompt.prompt)).slice(0, 2).map((prompt) => {
|
|
4494
5737
|
const prefix = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : "request";
|
|
4495
5738
|
return `${prefix}: ${prompt.prompt.replace(/\s+/g, " ").trim()}`;
|
|
4496
5739
|
}).filter((item) => item.length > 0);
|
|
4497
5740
|
}
|
|
5741
|
+
function buildRecentChatLines(context) {
|
|
5742
|
+
return (context.recentChatMessages ?? []).slice(0, 2).map((message) => {
|
|
5743
|
+
const content = message.content.replace(/\s+/g, " ").trim();
|
|
5744
|
+
if (!content)
|
|
5745
|
+
return null;
|
|
5746
|
+
return `[${message.role}] ${content}`;
|
|
5747
|
+
}).filter((item) => Boolean(item));
|
|
5748
|
+
}
|
|
4498
5749
|
function duplicatesPromptLine(request, promptLine) {
|
|
4499
5750
|
if (!request || !promptLine)
|
|
4500
5751
|
return false;
|
|
@@ -4548,11 +5799,11 @@ function buildRecentOutcomeLines(context, summary) {
|
|
|
4548
5799
|
}
|
|
4549
5800
|
}
|
|
4550
5801
|
if (picked.length < 2) {
|
|
4551
|
-
for (const obs of context
|
|
5802
|
+
for (const obs of getFreshStartupObservations(context)) {
|
|
4552
5803
|
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
4553
5804
|
continue;
|
|
4554
|
-
const title =
|
|
4555
|
-
if (!title ||
|
|
5805
|
+
const title = stripInlineSectionLabel3(obs.title);
|
|
5806
|
+
if (!title || looksLikeFileOperationTitle4(title))
|
|
4556
5807
|
continue;
|
|
4557
5808
|
const normalized = normalizeStartupItem(title);
|
|
4558
5809
|
if (!normalized || seen.has(normalized))
|
|
@@ -4567,26 +5818,29 @@ function buildRecentOutcomeLines(context, summary) {
|
|
|
4567
5818
|
}
|
|
4568
5819
|
function buildCurrentThreadLine(context, summary) {
|
|
4569
5820
|
const explicit = summary?.current_thread ?? null;
|
|
4570
|
-
if (explicit && !
|
|
5821
|
+
if (explicit && !looksLikeFileOperationTitle4(explicit))
|
|
4571
5822
|
return explicit;
|
|
4572
5823
|
for (const session of context.recentSessions ?? []) {
|
|
4573
|
-
if (session.current_thread && !
|
|
5824
|
+
if (session.current_thread && !looksLikeFileOperationTitle4(session.current_thread)) {
|
|
4574
5825
|
return session.current_thread;
|
|
4575
5826
|
}
|
|
4576
5827
|
}
|
|
4577
5828
|
const request = buildPromptFallback(context);
|
|
4578
5829
|
const outcome = buildRecentOutcomeLines(context, summary)[0] ?? null;
|
|
4579
5830
|
const tool = buildToolFallbacks(context)[0] ?? null;
|
|
5831
|
+
const hasContinuity = hasFreshContinuitySignal(context);
|
|
4580
5832
|
if (outcome && tool)
|
|
4581
5833
|
return `${outcome} \xB7 ${tool}`;
|
|
5834
|
+
if (!hasContinuity && !outcome)
|
|
5835
|
+
return request;
|
|
4582
5836
|
return outcome ?? request ?? null;
|
|
4583
5837
|
}
|
|
4584
5838
|
function chooseMeaningfulSessionSummary(request, completed) {
|
|
4585
|
-
if (request && !
|
|
5839
|
+
if (request && !looksLikeFileOperationTitle4(request))
|
|
4586
5840
|
return request;
|
|
4587
5841
|
if (completed) {
|
|
4588
5842
|
const lines = completed.split(`
|
|
4589
|
-
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).map((line) =>
|
|
5843
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).map((line) => stripInlineSectionLabel3(line)).filter((line) => !looksLikeFileOperationTitle4(line));
|
|
4590
5844
|
if (lines.length > 0)
|
|
4591
5845
|
return lines[0] ?? null;
|
|
4592
5846
|
}
|
|
@@ -4695,7 +5949,7 @@ function pickContextIndexObservations(context, shownItems) {
|
|
|
4695
5949
|
score += 2.5;
|
|
4696
5950
|
return score;
|
|
4697
5951
|
};
|
|
4698
|
-
for (const obs of context.
|
|
5952
|
+
for (const obs of getFreshStartupObservations(context).filter((obs2) => obs2.type !== "digest").filter((obs2) => {
|
|
4699
5953
|
const normalized = normalizeStartupItem(obs2.title);
|
|
4700
5954
|
return normalized && !hidden.has(normalized);
|
|
4701
5955
|
}).sort((a, b) => {
|
|
@@ -4726,7 +5980,7 @@ function toSplashLines(value, maxItems) {
|
|
|
4726
5980
|
if (!value)
|
|
4727
5981
|
return [];
|
|
4728
5982
|
const lines = value.split(`
|
|
4729
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) =>
|
|
5983
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel3(line)).map((line) => dedupeFragments(line)).filter(Boolean).sort((a, b) => scoreSplashLine(b) - scoreSplashLine(a)).slice(0, maxItems).map((line) => `- ${truncateInline(line, 140)}`);
|
|
4730
5984
|
return dedupeFragmentsInLines(lines);
|
|
4731
5985
|
}
|
|
4732
5986
|
function pickPrimarySummary(context) {
|
|
@@ -4737,7 +5991,7 @@ function pickPrimarySummary(context) {
|
|
|
4737
5991
|
const request = summary.request?.trim();
|
|
4738
5992
|
const learned = summary.learned?.trim();
|
|
4739
5993
|
const completed = summary.completed?.trim();
|
|
4740
|
-
return Boolean(request && !
|
|
5994
|
+
return Boolean(request && !looksLikeFileOperationTitle4(request) || learned || hasMeaningfulCompleted(completed));
|
|
4741
5995
|
});
|
|
4742
5996
|
return meaningfulRecent ?? summaries[0] ?? null;
|
|
4743
5997
|
}
|
|
@@ -4745,7 +5999,7 @@ function hasMeaningfulCompleted(value) {
|
|
|
4745
5999
|
if (!value)
|
|
4746
6000
|
return false;
|
|
4747
6001
|
return value.split(`
|
|
4748
|
-
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).some((line) => !
|
|
6002
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).some((line) => !looksLikeFileOperationTitle4(stripInlineSectionLabel3(line)));
|
|
4749
6003
|
}
|
|
4750
6004
|
function sectionItemCount(value) {
|
|
4751
6005
|
if (!value)
|
|
@@ -4770,7 +6024,7 @@ function dedupeFragmentsInLines(lines) {
|
|
|
4770
6024
|
const seen = new Set;
|
|
4771
6025
|
const deduped = [];
|
|
4772
6026
|
for (const line of lines) {
|
|
4773
|
-
const normalized =
|
|
6027
|
+
const normalized = stripInlineSectionLabel3(line).toLowerCase().replace(/\s+/g, " ").trim();
|
|
4774
6028
|
if (!normalized || seen.has(normalized))
|
|
4775
6029
|
continue;
|
|
4776
6030
|
seen.add(normalized);
|
|
@@ -4782,7 +6036,7 @@ function hasRequestSection(lines) {
|
|
|
4782
6036
|
return lines.some((line) => line.includes("Request:"));
|
|
4783
6037
|
}
|
|
4784
6038
|
function normalizeStartupItem(value) {
|
|
4785
|
-
return
|
|
6039
|
+
return stripInlineSectionLabel3(value).replace(/^#?\d+:\s*/, "").replace(/^-\s*/, "").replace(/\([^)]*\)/g, " ").replace(/[^a-z0-9\s]/gi, " ").toLowerCase().replace(/\s+/g, " ").trim();
|
|
4786
6040
|
}
|
|
4787
6041
|
function titlesRoughlyMatch(left, right) {
|
|
4788
6042
|
const a = normalizeStartupItem(left ?? "");
|
|
@@ -4801,7 +6055,7 @@ function titlesRoughlyMatch(left, right) {
|
|
|
4801
6055
|
const minSize = Math.min(aTokens.length, bTokens.length);
|
|
4802
6056
|
return shared.length >= Math.max(3, Math.ceil(minSize * 0.6));
|
|
4803
6057
|
}
|
|
4804
|
-
function
|
|
6058
|
+
function isMeaningfulPrompt3(value) {
|
|
4805
6059
|
if (!value)
|
|
4806
6060
|
return false;
|
|
4807
6061
|
const compact = value.replace(/\s+/g, " ").trim();
|
|
@@ -4810,7 +6064,7 @@ function isMeaningfulPrompt2(value) {
|
|
|
4810
6064
|
return /[a-z]{3,}/i.test(compact);
|
|
4811
6065
|
}
|
|
4812
6066
|
function chooseRequest(primary, fallback) {
|
|
4813
|
-
if (primary && !
|
|
6067
|
+
if (primary && !looksLikeFileOperationTitle4(primary))
|
|
4814
6068
|
return primary;
|
|
4815
6069
|
return fallback;
|
|
4816
6070
|
}
|
|
@@ -4828,10 +6082,10 @@ function isWeakCompletedSection(value) {
|
|
|
4828
6082
|
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean);
|
|
4829
6083
|
if (!items.length)
|
|
4830
6084
|
return true;
|
|
4831
|
-
const weakCount = items.filter((item) =>
|
|
6085
|
+
const weakCount = items.filter((item) => looksLikeFileOperationTitle4(item)).length;
|
|
4832
6086
|
return weakCount === items.length;
|
|
4833
6087
|
}
|
|
4834
|
-
function
|
|
6088
|
+
function looksLikeFileOperationTitle4(value) {
|
|
4835
6089
|
const trimmed = value.trim();
|
|
4836
6090
|
if (/^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(trimmed)) {
|
|
4837
6091
|
return true;
|
|
@@ -4844,7 +6098,7 @@ function looksLikeGenericSummaryWrapper(value) {
|
|
|
4844
6098
|
}
|
|
4845
6099
|
function scoreSplashLine(value) {
|
|
4846
6100
|
let score = 0;
|
|
4847
|
-
if (!
|
|
6101
|
+
if (!looksLikeFileOperationTitle4(value))
|
|
4848
6102
|
score += 2;
|
|
4849
6103
|
if (/[:;]/.test(value))
|
|
4850
6104
|
score += 1;
|
|
@@ -4853,9 +6107,9 @@ function scoreSplashLine(value) {
|
|
|
4853
6107
|
return score;
|
|
4854
6108
|
}
|
|
4855
6109
|
function buildObservationFallbacks(context) {
|
|
4856
|
-
const request = context.
|
|
6110
|
+
const request = getFreshStartupObservations(context).find((obs) => obs.type !== "decision" && !looksLikeFileOperationTitle4(obs.title))?.title ?? null;
|
|
4857
6111
|
const investigated = collectObservationTitles(context, (obs) => obs.type === "discovery", 2);
|
|
4858
|
-
const completed = collectObservationTitles(context, (obs) => ["bugfix", "feature", "refactor", "change"].includes(obs.type) && !
|
|
6112
|
+
const completed = collectObservationTitles(context, (obs) => ["bugfix", "feature", "refactor", "change"].includes(obs.type) && !looksLikeFileOperationTitle4(obs.title), 2);
|
|
4859
6113
|
return {
|
|
4860
6114
|
request,
|
|
4861
6115
|
investigated,
|
|
@@ -4865,21 +6119,38 @@ function buildObservationFallbacks(context) {
|
|
|
4865
6119
|
function collectObservationTitles(context, predicate, limit) {
|
|
4866
6120
|
const seen = new Set;
|
|
4867
6121
|
const picked = [];
|
|
4868
|
-
for (const obs of context
|
|
6122
|
+
for (const obs of getFreshStartupObservations(context)) {
|
|
4869
6123
|
if (!predicate(obs))
|
|
4870
6124
|
continue;
|
|
4871
|
-
const normalized =
|
|
6125
|
+
const normalized = stripInlineSectionLabel3(obs.title).toLowerCase().replace(/\s+/g, " ").trim();
|
|
4872
6126
|
if (!normalized || seen.has(normalized))
|
|
4873
6127
|
continue;
|
|
4874
6128
|
seen.add(normalized);
|
|
4875
|
-
picked.push(`- ${
|
|
6129
|
+
picked.push(`- ${stripInlineSectionLabel3(obs.title)}`);
|
|
4876
6130
|
if (picked.length >= limit)
|
|
4877
6131
|
break;
|
|
4878
6132
|
}
|
|
4879
6133
|
return picked.length ? picked.join(`
|
|
4880
6134
|
`) : null;
|
|
4881
6135
|
}
|
|
4882
|
-
function
|
|
6136
|
+
function getFreshStartupObservations(context) {
|
|
6137
|
+
if (hasFreshContinuitySignal(context))
|
|
6138
|
+
return context.observations;
|
|
6139
|
+
return context.observations.filter((obs) => observationAgeDays3(obs) <= 3);
|
|
6140
|
+
}
|
|
6141
|
+
function hasFreshContinuitySignal(context) {
|
|
6142
|
+
return getStartupContinuityState(context) === "fresh";
|
|
6143
|
+
}
|
|
6144
|
+
function getStartupContinuityState(context) {
|
|
6145
|
+
return classifyContinuityState(context.recentPrompts?.length ?? 0, context.recentToolEvents?.length ?? 0, context.recentHandoffs?.length ?? 0, context.recentChatMessages?.length ?? 0, context.recentSessions ?? [], context.recentOutcomes?.length ?? 0);
|
|
6146
|
+
}
|
|
6147
|
+
function observationAgeDays3(obs) {
|
|
6148
|
+
const createdAt = new Date(obs.created_at).getTime();
|
|
6149
|
+
if (!Number.isFinite(createdAt))
|
|
6150
|
+
return Number.POSITIVE_INFINITY;
|
|
6151
|
+
return Math.max(0, (Date.now() - createdAt) / 86400000);
|
|
6152
|
+
}
|
|
6153
|
+
function stripInlineSectionLabel3(value) {
|
|
4883
6154
|
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
4884
6155
|
}
|
|
4885
6156
|
function pickRelevantStaleDecision(context, summary) {
|
|
@@ -4970,7 +6241,8 @@ function capitalize(value) {
|
|
|
4970
6241
|
}
|
|
4971
6242
|
var __testables = {
|
|
4972
6243
|
formatSplashScreen,
|
|
4973
|
-
formatVisibleStartupBrief
|
|
6244
|
+
formatVisibleStartupBrief,
|
|
6245
|
+
getStartupContinuityState
|
|
4974
6246
|
};
|
|
4975
6247
|
runHook("session-start", main);
|
|
4976
6248
|
export {
|