engrm 0.4.25 → 0.4.27
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 +9 -4
- package/dist/hooks/post-tool-use.js +8 -0
- package/dist/hooks/pre-compact.js +55 -11
- package/dist/hooks/session-start.js +909 -133
- package/dist/hooks/stop.js +9 -1
- package/dist/hooks/user-prompt-submit.js +8 -0
- package/dist/server.js +415 -68
- package/package.json +1 -1
|
@@ -494,6 +494,8 @@ function getSessionStory(db, input) {
|
|
|
494
494
|
summary,
|
|
495
495
|
prompts,
|
|
496
496
|
chat_messages: chatMessages,
|
|
497
|
+
chat_source_summary: summarizeChatSources(chatMessages),
|
|
498
|
+
chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
|
|
497
499
|
tool_events: toolEvents,
|
|
498
500
|
observations,
|
|
499
501
|
handoffs,
|
|
@@ -591,6 +593,12 @@ function collectProvenanceSummary(observations) {
|
|
|
591
593
|
}
|
|
592
594
|
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
595
|
}
|
|
596
|
+
function summarizeChatSources(messages) {
|
|
597
|
+
return messages.reduce((summary, message) => {
|
|
598
|
+
summary[message.source_kind] += 1;
|
|
599
|
+
return summary;
|
|
600
|
+
}, { transcript: 0, hook: 0 });
|
|
601
|
+
}
|
|
594
602
|
|
|
595
603
|
// src/tools/save.ts
|
|
596
604
|
import { relative, isAbsolute } from "node:path";
|
|
@@ -1532,43 +1540,719 @@ function buildHandoffFacts(summary, story) {
|
|
|
1532
1540
|
];
|
|
1533
1541
|
return facts.filter((item) => Boolean(item));
|
|
1534
1542
|
}
|
|
1535
|
-
function buildDraftHandoffConcepts(projectName, captureState) {
|
|
1536
|
-
return [
|
|
1537
|
-
"handoff",
|
|
1538
|
-
"draft-handoff",
|
|
1539
|
-
"auto-handoff",
|
|
1540
|
-
`capture:${captureState}`,
|
|
1541
|
-
...projectName ? [projectName] : []
|
|
1542
|
-
];
|
|
1543
|
+
function buildDraftHandoffConcepts(projectName, captureState) {
|
|
1544
|
+
return [
|
|
1545
|
+
"handoff",
|
|
1546
|
+
"draft-handoff",
|
|
1547
|
+
"auto-handoff",
|
|
1548
|
+
`capture:${captureState}`,
|
|
1549
|
+
...projectName ? [projectName] : []
|
|
1550
|
+
];
|
|
1551
|
+
}
|
|
1552
|
+
function looksLikeHandoff(obs) {
|
|
1553
|
+
if (obs.title.startsWith("Handoff:") || obs.title.startsWith("Handoff Draft:"))
|
|
1554
|
+
return true;
|
|
1555
|
+
const concepts = parseJsonArray2(obs.concepts);
|
|
1556
|
+
return concepts.includes("handoff") || concepts.includes("session-handoff") || concepts.includes("draft-handoff");
|
|
1557
|
+
}
|
|
1558
|
+
function parseJsonArray2(value) {
|
|
1559
|
+
if (!value)
|
|
1560
|
+
return [];
|
|
1561
|
+
try {
|
|
1562
|
+
const parsed = JSON.parse(value);
|
|
1563
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1564
|
+
} catch {
|
|
1565
|
+
return [];
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
function compactLine(value) {
|
|
1569
|
+
const trimmed = value?.replace(/\s+/g, " ").trim();
|
|
1570
|
+
if (!trimmed)
|
|
1571
|
+
return null;
|
|
1572
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/context/inject.ts
|
|
1576
|
+
var FRESH_CONTINUITY_WINDOW_DAYS = 3;
|
|
1577
|
+
function tokenizeProjectHint(text) {
|
|
1578
|
+
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
1579
|
+
}
|
|
1580
|
+
function parseSummaryJsonList(value) {
|
|
1581
|
+
if (!value)
|
|
1582
|
+
return [];
|
|
1583
|
+
try {
|
|
1584
|
+
const parsed = JSON.parse(value);
|
|
1585
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1586
|
+
} catch {
|
|
1587
|
+
return [];
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
function isObservationRelatedToProject(obs, detected) {
|
|
1591
|
+
const hints = new Set([
|
|
1592
|
+
...tokenizeProjectHint(detected.name),
|
|
1593
|
+
...tokenizeProjectHint(detected.canonical_id)
|
|
1594
|
+
]);
|
|
1595
|
+
if (hints.size === 0)
|
|
1596
|
+
return false;
|
|
1597
|
+
const haystack = [
|
|
1598
|
+
obs.title,
|
|
1599
|
+
obs.narrative ?? "",
|
|
1600
|
+
obs.facts ?? "",
|
|
1601
|
+
obs.concepts ?? "",
|
|
1602
|
+
obs.files_read ?? "",
|
|
1603
|
+
obs.files_modified ?? "",
|
|
1604
|
+
obs._source_project ?? ""
|
|
1605
|
+
].join(`
|
|
1606
|
+
`).toLowerCase();
|
|
1607
|
+
for (const hint of hints) {
|
|
1608
|
+
if (haystack.includes(hint))
|
|
1609
|
+
return true;
|
|
1610
|
+
}
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
function estimateTokens(text) {
|
|
1614
|
+
if (!text)
|
|
1615
|
+
return 0;
|
|
1616
|
+
return Math.ceil(text.length / 4);
|
|
1617
|
+
}
|
|
1618
|
+
function buildSessionContext(db, cwd, options = {}) {
|
|
1619
|
+
const opts = typeof options === "number" ? { maxCount: options } : options;
|
|
1620
|
+
const tokenBudget = opts.tokenBudget ?? 3000;
|
|
1621
|
+
const maxCount = opts.maxCount;
|
|
1622
|
+
const visibilityClause = opts.userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
1623
|
+
const visibilityParams = opts.userId ? [opts.userId] : [];
|
|
1624
|
+
const detected = detectProject(cwd);
|
|
1625
|
+
const project = db.getProjectByCanonicalId(detected.canonical_id);
|
|
1626
|
+
const projectId = project?.id ?? -1;
|
|
1627
|
+
const isNewProject = !project;
|
|
1628
|
+
const totalActive = isNewProject ? (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
1629
|
+
WHERE lifecycle IN ('active', 'aging', 'pinned')
|
|
1630
|
+
${visibilityClause}
|
|
1631
|
+
AND superseded_by IS NULL`).get(...visibilityParams) ?? { c: 0 }).c : (db.db.query(`SELECT COUNT(*) as c FROM observations
|
|
1632
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging', 'pinned')
|
|
1633
|
+
${visibilityClause}
|
|
1634
|
+
AND superseded_by IS NULL`).get(projectId, ...visibilityParams) ?? { c: 0 }).c;
|
|
1635
|
+
const candidateLimit = maxCount ?? 50;
|
|
1636
|
+
let pinned = [];
|
|
1637
|
+
let recent = [];
|
|
1638
|
+
let candidates = [];
|
|
1639
|
+
if (!isNewProject) {
|
|
1640
|
+
const MAX_PINNED = 5;
|
|
1641
|
+
pinned = db.db.query(`SELECT * FROM observations
|
|
1642
|
+
WHERE project_id = ? AND lifecycle = 'pinned'
|
|
1643
|
+
AND superseded_by IS NULL
|
|
1644
|
+
${visibilityClause}
|
|
1645
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
1646
|
+
LIMIT ?`).all(projectId, ...visibilityParams, MAX_PINNED);
|
|
1647
|
+
const MAX_RECENT = 5;
|
|
1648
|
+
recent = db.db.query(`SELECT * FROM observations
|
|
1649
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
1650
|
+
AND superseded_by IS NULL
|
|
1651
|
+
${visibilityClause}
|
|
1652
|
+
ORDER BY created_at_epoch DESC
|
|
1653
|
+
LIMIT ?`).all(projectId, ...visibilityParams, MAX_RECENT);
|
|
1654
|
+
candidates = db.db.query(`SELECT * FROM observations
|
|
1655
|
+
WHERE project_id = ? AND lifecycle IN ('active', 'aging')
|
|
1656
|
+
AND quality >= 0.3
|
|
1657
|
+
AND superseded_by IS NULL
|
|
1658
|
+
${visibilityClause}
|
|
1659
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
1660
|
+
LIMIT ?`).all(projectId, ...visibilityParams, candidateLimit);
|
|
1661
|
+
}
|
|
1662
|
+
let crossProjectCandidates = [];
|
|
1663
|
+
if (opts.scope === "all" || isNewProject) {
|
|
1664
|
+
const crossLimit = isNewProject ? Math.max(30, candidateLimit) : Math.max(10, Math.floor(candidateLimit / 3));
|
|
1665
|
+
const qualityThreshold = isNewProject ? 0.3 : 0.5;
|
|
1666
|
+
const rawCross = isNewProject ? db.db.query(`SELECT * FROM observations
|
|
1667
|
+
WHERE lifecycle IN ('active', 'aging', 'pinned')
|
|
1668
|
+
AND quality >= ?
|
|
1669
|
+
AND superseded_by IS NULL
|
|
1670
|
+
${visibilityClause}
|
|
1671
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
1672
|
+
LIMIT ?`).all(qualityThreshold, ...visibilityParams, crossLimit) : db.db.query(`SELECT * FROM observations
|
|
1673
|
+
WHERE project_id != ? AND lifecycle IN ('active', 'aging')
|
|
1674
|
+
AND quality >= ?
|
|
1675
|
+
AND superseded_by IS NULL
|
|
1676
|
+
${visibilityClause}
|
|
1677
|
+
ORDER BY quality DESC, created_at_epoch DESC
|
|
1678
|
+
LIMIT ?`).all(projectId, qualityThreshold, ...visibilityParams, crossLimit);
|
|
1679
|
+
const projectNameCache = new Map;
|
|
1680
|
+
crossProjectCandidates = rawCross.map((obs) => {
|
|
1681
|
+
if (!projectNameCache.has(obs.project_id)) {
|
|
1682
|
+
const proj = db.getProjectById(obs.project_id);
|
|
1683
|
+
if (proj)
|
|
1684
|
+
projectNameCache.set(obs.project_id, proj.name);
|
|
1685
|
+
}
|
|
1686
|
+
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
1687
|
+
});
|
|
1688
|
+
if (isNewProject) {
|
|
1689
|
+
crossProjectCandidates = crossProjectCandidates.filter((obs) => isObservationRelatedToProject(obs, detected));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const seenIds = new Set(pinned.map((o) => o.id));
|
|
1693
|
+
const dedupedRecent = recent.filter((o) => {
|
|
1694
|
+
if (seenIds.has(o.id))
|
|
1695
|
+
return false;
|
|
1696
|
+
seenIds.add(o.id);
|
|
1697
|
+
return true;
|
|
1698
|
+
});
|
|
1699
|
+
const deduped = candidates.filter((o) => !seenIds.has(o.id));
|
|
1700
|
+
for (const obs of crossProjectCandidates) {
|
|
1701
|
+
if (!seenIds.has(obs.id)) {
|
|
1702
|
+
seenIds.add(obs.id);
|
|
1703
|
+
deduped.push(obs);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
1707
|
+
const sorted = [...deduped].sort((a, b) => {
|
|
1708
|
+
const scoreA = computeObservationPriority(a, nowEpoch);
|
|
1709
|
+
const scoreB = computeObservationPriority(b, nowEpoch);
|
|
1710
|
+
return scoreB - scoreA;
|
|
1711
|
+
});
|
|
1712
|
+
const projectName = project?.name ?? detected.name;
|
|
1713
|
+
const canonicalId = project?.canonical_id ?? detected.canonical_id;
|
|
1714
|
+
if (maxCount !== undefined) {
|
|
1715
|
+
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
1716
|
+
let all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
1717
|
+
const recentPrompts2 = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
1718
|
+
const recentToolEvents2 = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
1719
|
+
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
1720
|
+
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
1721
|
+
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions2);
|
|
1722
|
+
const recentHandoffs2 = isNewProject ? [] : getRecentHandoffs(db, {
|
|
1723
|
+
cwd,
|
|
1724
|
+
project_scoped: true,
|
|
1725
|
+
user_id: opts.userId,
|
|
1726
|
+
current_device_id: opts.currentDeviceId,
|
|
1727
|
+
limit: 3
|
|
1728
|
+
}).handoffs;
|
|
1729
|
+
const recentChatMessages2 = !isNewProject && project ? db.getRecentChatMessages(project.id, 4, opts.userId) : [];
|
|
1730
|
+
all = filterAutoLoadedObservationsForContinuity(all, pinned, isNewProject, recentPrompts2, recentToolEvents2, recentSessions2, recentHandoffs2, recentChatMessages2, summariesFromRecentSessions(db, projectId, recentSessions2));
|
|
1731
|
+
return {
|
|
1732
|
+
project_name: projectName,
|
|
1733
|
+
canonical_id: canonicalId,
|
|
1734
|
+
observations: all.map(toContextObservation),
|
|
1735
|
+
session_count: all.length,
|
|
1736
|
+
total_active: totalActive,
|
|
1737
|
+
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
1738
|
+
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
1739
|
+
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
1740
|
+
projectTypeCounts: projectTypeCounts2,
|
|
1741
|
+
recentOutcomes: recentOutcomes2,
|
|
1742
|
+
recentHandoffs: recentHandoffs2.length > 0 ? recentHandoffs2 : undefined,
|
|
1743
|
+
recentChatMessages: recentChatMessages2.length > 0 ? recentChatMessages2 : undefined
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
let remainingBudget = tokenBudget - 30;
|
|
1747
|
+
const selected = [];
|
|
1748
|
+
for (const obs of pinned) {
|
|
1749
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
1750
|
+
remainingBudget -= cost;
|
|
1751
|
+
selected.push(obs);
|
|
1752
|
+
}
|
|
1753
|
+
for (const obs of dedupedRecent) {
|
|
1754
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
1755
|
+
remainingBudget -= cost;
|
|
1756
|
+
selected.push(obs);
|
|
1757
|
+
}
|
|
1758
|
+
for (const obs of sorted) {
|
|
1759
|
+
const cost = estimateObservationTokens(obs, selected.length);
|
|
1760
|
+
if (remainingBudget - cost < 0 && selected.length > 0)
|
|
1761
|
+
break;
|
|
1762
|
+
remainingBudget -= cost;
|
|
1763
|
+
selected.push(obs);
|
|
1764
|
+
}
|
|
1765
|
+
const summaries = isNewProject ? [] : db.getRecentSummaries(projectId, 5);
|
|
1766
|
+
const recentPrompts = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
1767
|
+
const recentToolEvents = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
1768
|
+
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
1769
|
+
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
1770
|
+
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions);
|
|
1771
|
+
const recentHandoffs = isNewProject ? [] : getRecentHandoffs(db, {
|
|
1772
|
+
cwd,
|
|
1773
|
+
project_scoped: true,
|
|
1774
|
+
user_id: opts.userId,
|
|
1775
|
+
current_device_id: opts.currentDeviceId,
|
|
1776
|
+
limit: 3
|
|
1777
|
+
}).handoffs;
|
|
1778
|
+
const recentChatMessages = !isNewProject ? db.getRecentChatMessages(projectId, 4, opts.userId) : [];
|
|
1779
|
+
const filteredSelected = filterAutoLoadedObservationsForContinuity(selected, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries);
|
|
1780
|
+
let securityFindings = [];
|
|
1781
|
+
if (!isNewProject) {
|
|
1782
|
+
try {
|
|
1783
|
+
const weekAgo = Math.floor(Date.now() / 1000) - 7 * 86400;
|
|
1784
|
+
securityFindings = db.db.query(`SELECT * FROM security_findings
|
|
1785
|
+
WHERE project_id = ? AND created_at_epoch > ?
|
|
1786
|
+
ORDER BY severity DESC, created_at_epoch DESC
|
|
1787
|
+
LIMIT ?`).all(projectId, weekAgo, 10);
|
|
1788
|
+
} catch {}
|
|
1789
|
+
}
|
|
1790
|
+
let recentProjects;
|
|
1791
|
+
if (isNewProject) {
|
|
1792
|
+
try {
|
|
1793
|
+
const nowEpochSec = Math.floor(Date.now() / 1000);
|
|
1794
|
+
const projectRows = db.db.query(`SELECT p.name, p.canonical_id, p.last_active_epoch,
|
|
1795
|
+
(SELECT COUNT(*) FROM observations o
|
|
1796
|
+
WHERE o.project_id = p.id
|
|
1797
|
+
AND o.lifecycle IN ('active', 'aging', 'pinned')
|
|
1798
|
+
${opts.userId ? "AND (o.sensitivity != 'personal' OR o.user_id = ?)" : ""}
|
|
1799
|
+
AND o.superseded_by IS NULL) as obs_count
|
|
1800
|
+
FROM projects p
|
|
1801
|
+
ORDER BY p.last_active_epoch DESC
|
|
1802
|
+
LIMIT 10`).all(...visibilityParams);
|
|
1803
|
+
if (projectRows.length > 0) {
|
|
1804
|
+
recentProjects = projectRows.map((r) => {
|
|
1805
|
+
const daysAgo = Math.max(0, Math.floor((nowEpochSec - r.last_active_epoch) / 86400));
|
|
1806
|
+
const lastActive = new Date(r.last_active_epoch * 1000).toISOString().split("T")[0];
|
|
1807
|
+
return {
|
|
1808
|
+
name: r.name,
|
|
1809
|
+
canonical_id: r.canonical_id,
|
|
1810
|
+
observation_count: r.obs_count,
|
|
1811
|
+
last_active: lastActive,
|
|
1812
|
+
days_ago: daysAgo
|
|
1813
|
+
};
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
} catch {}
|
|
1817
|
+
}
|
|
1818
|
+
let staleDecisions;
|
|
1819
|
+
try {
|
|
1820
|
+
const stale = isNewProject ? findStaleDecisionsGlobal(db) : findStaleDecisions(db, projectId);
|
|
1821
|
+
if (stale.length > 0)
|
|
1822
|
+
staleDecisions = stale;
|
|
1823
|
+
} catch {}
|
|
1824
|
+
return {
|
|
1825
|
+
project_name: projectName,
|
|
1826
|
+
canonical_id: canonicalId,
|
|
1827
|
+
observations: filteredSelected.map(toContextObservation),
|
|
1828
|
+
session_count: filteredSelected.length,
|
|
1829
|
+
total_active: totalActive,
|
|
1830
|
+
summaries: summaries.length > 0 ? summaries : undefined,
|
|
1831
|
+
securityFindings: securityFindings.length > 0 ? securityFindings : undefined,
|
|
1832
|
+
recentProjects,
|
|
1833
|
+
staleDecisions,
|
|
1834
|
+
recentPrompts: recentPrompts.length > 0 ? recentPrompts : undefined,
|
|
1835
|
+
recentToolEvents: recentToolEvents.length > 0 ? recentToolEvents : undefined,
|
|
1836
|
+
recentSessions: recentSessions.length > 0 ? recentSessions : undefined,
|
|
1837
|
+
projectTypeCounts,
|
|
1838
|
+
recentOutcomes,
|
|
1839
|
+
recentHandoffs: recentHandoffs.length > 0 ? recentHandoffs : undefined,
|
|
1840
|
+
recentChatMessages: recentChatMessages.length > 0 ? recentChatMessages : undefined
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
function filterAutoLoadedObservationsForContinuity(observations, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
1844
|
+
if (isNewProject)
|
|
1845
|
+
return observations;
|
|
1846
|
+
if (hasFreshProjectContinuity(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries)) {
|
|
1847
|
+
return observations;
|
|
1848
|
+
}
|
|
1849
|
+
const pinnedIds = new Set(pinned.map((obs) => obs.id));
|
|
1850
|
+
return observations.filter((obs) => {
|
|
1851
|
+
if (pinnedIds.has(obs.id))
|
|
1852
|
+
return true;
|
|
1853
|
+
return observationAgeDays(obs.created_at_epoch) <= FRESH_CONTINUITY_WINDOW_DAYS;
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
function hasFreshProjectContinuity(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
1857
|
+
const freshEnough = (epoch) => typeof epoch === "number" && observationAgeDays(epoch) <= FRESH_CONTINUITY_WINDOW_DAYS;
|
|
1858
|
+
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));
|
|
1859
|
+
}
|
|
1860
|
+
function summariesFromRecentSessions(db, projectId, recentSessions) {
|
|
1861
|
+
const seen = new Set;
|
|
1862
|
+
const rows = [];
|
|
1863
|
+
for (const session of recentSessions) {
|
|
1864
|
+
if (seen.has(session.session_id))
|
|
1865
|
+
continue;
|
|
1866
|
+
seen.add(session.session_id);
|
|
1867
|
+
const summary = db.getSessionSummary(session.session_id);
|
|
1868
|
+
if (summary && summary.project_id === projectId)
|
|
1869
|
+
rows.push(summary);
|
|
1870
|
+
}
|
|
1871
|
+
return rows;
|
|
1872
|
+
}
|
|
1873
|
+
function observationAgeDays(createdAtEpoch) {
|
|
1874
|
+
return Math.max(0, (Math.floor(Date.now() / 1000) - createdAtEpoch) / 86400);
|
|
1875
|
+
}
|
|
1876
|
+
function estimateObservationTokens(obs, index) {
|
|
1877
|
+
const DETAILED_THRESHOLD = 5;
|
|
1878
|
+
const titleCost = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|
|
1879
|
+
if (index >= DETAILED_THRESHOLD) {
|
|
1880
|
+
return titleCost;
|
|
1881
|
+
}
|
|
1882
|
+
const detailText = formatObservationDetail(obs);
|
|
1883
|
+
return titleCost + estimateTokens(detailText);
|
|
1884
|
+
}
|
|
1885
|
+
function formatContextForInjection(context) {
|
|
1886
|
+
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)) {
|
|
1887
|
+
return `Project: ${context.project_name} (no prior observations)`;
|
|
1888
|
+
}
|
|
1889
|
+
const DETAILED_COUNT = 5;
|
|
1890
|
+
const isCrossProject = context.recentProjects && context.recentProjects.length > 0;
|
|
1891
|
+
const lines = [];
|
|
1892
|
+
if (isCrossProject) {
|
|
1893
|
+
lines.push(`## Engrm Memory — Workspace Overview`);
|
|
1894
|
+
lines.push(`This is a new project folder. Here is context from your recent work:`);
|
|
1895
|
+
lines.push("");
|
|
1896
|
+
lines.push("**Active projects in memory:**");
|
|
1897
|
+
for (const rp of context.recentProjects) {
|
|
1898
|
+
const activity = rp.days_ago === 0 ? "today" : rp.days_ago === 1 ? "yesterday" : `${rp.days_ago}d ago`;
|
|
1899
|
+
lines.push(`- **${rp.name}** — ${rp.observation_count} observations, last active ${activity}`);
|
|
1900
|
+
}
|
|
1901
|
+
lines.push("");
|
|
1902
|
+
lines.push(`${context.session_count} relevant observation(s) from across projects:`);
|
|
1903
|
+
lines.push("");
|
|
1904
|
+
} else {
|
|
1905
|
+
lines.push(`## Project Memory: ${context.project_name}`);
|
|
1906
|
+
lines.push(`${context.session_count} relevant observation(s) from prior sessions:`);
|
|
1907
|
+
lines.push("");
|
|
1908
|
+
}
|
|
1909
|
+
if (context.recentHandoffs && context.recentHandoffs.length > 0) {
|
|
1910
|
+
lines.push("## Recent Handoffs");
|
|
1911
|
+
for (const handoff of context.recentHandoffs.slice(0, 3)) {
|
|
1912
|
+
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();
|
|
1913
|
+
if (title) {
|
|
1914
|
+
lines.push(`- ${truncateText(`${title} (${formatHandoffSource(handoff)})`, 160)}`);
|
|
1915
|
+
}
|
|
1916
|
+
const narrative = handoff.narrative?.split(/\n{2,}/).map((part) => part.replace(/\s+/g, " ").trim()).find((part) => /^(Current thread:|Completed:|Next Steps:)/i.test(part));
|
|
1917
|
+
if (narrative) {
|
|
1918
|
+
lines.push(` ${truncateText(narrative, 180)}`);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
lines.push("");
|
|
1922
|
+
}
|
|
1923
|
+
if (context.recentChatMessages && context.recentChatMessages.length > 0) {
|
|
1924
|
+
lines.push("## Recent Chat");
|
|
1925
|
+
for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
|
|
1926
|
+
lines.push(`- [${message.role}] ${truncateText(message.content.replace(/\s+/g, " ").trim(), 160)}`);
|
|
1927
|
+
}
|
|
1928
|
+
lines.push("");
|
|
1929
|
+
}
|
|
1930
|
+
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
1931
|
+
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt(prompt.prompt)).slice(0, 5);
|
|
1932
|
+
if (promptLines.length > 0) {
|
|
1933
|
+
lines.push("## Recent Requests");
|
|
1934
|
+
for (const prompt of promptLines) {
|
|
1935
|
+
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
1936
|
+
lines.push(`- ${label}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
1937
|
+
}
|
|
1938
|
+
lines.push("");
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
1942
|
+
lines.push("## Recent Tools");
|
|
1943
|
+
for (const tool of context.recentToolEvents.slice(0, 5)) {
|
|
1944
|
+
lines.push(`- ${tool.tool_name}: ${formatToolEventDetail(tool)}`);
|
|
1945
|
+
}
|
|
1946
|
+
lines.push("");
|
|
1947
|
+
}
|
|
1948
|
+
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
1949
|
+
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
1950
|
+
const summary = chooseMeaningfulSessionHeadline(session.request, session.completed);
|
|
1951
|
+
if (summary === "(no summary)")
|
|
1952
|
+
return null;
|
|
1953
|
+
return `- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
1954
|
+
}).filter((line) => Boolean(line));
|
|
1955
|
+
if (recentSessionLines.length > 0) {
|
|
1956
|
+
lines.push("## Recent Sessions");
|
|
1957
|
+
lines.push(...recentSessionLines);
|
|
1958
|
+
lines.push("");
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (context.recentOutcomes && context.recentOutcomes.length > 0) {
|
|
1962
|
+
lines.push("## Recent Outcomes");
|
|
1963
|
+
for (const outcome of context.recentOutcomes.slice(0, 5)) {
|
|
1964
|
+
lines.push(`- ${truncateText(outcome, 160)}`);
|
|
1965
|
+
}
|
|
1966
|
+
lines.push("");
|
|
1967
|
+
}
|
|
1968
|
+
if (context.projectTypeCounts && Object.keys(context.projectTypeCounts).length > 0) {
|
|
1969
|
+
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(" · ");
|
|
1970
|
+
if (topTypes) {
|
|
1971
|
+
lines.push(`## Project Signals`);
|
|
1972
|
+
lines.push(`Top memory types: ${topTypes}`);
|
|
1973
|
+
lines.push("");
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
for (let i = 0;i < context.observations.length; i++) {
|
|
1977
|
+
const obs = context.observations[i];
|
|
1978
|
+
const date = obs.created_at.split("T")[0];
|
|
1979
|
+
const fromLabel = obs.source_project ? ` [from: ${obs.source_project}]` : "";
|
|
1980
|
+
const fileLabel = formatObservationFiles(obs);
|
|
1981
|
+
lines.push(`- **#${obs.id} [${obs.type}]** ${obs.title} (${date}, q=${obs.quality.toFixed(1)})${fromLabel}${fileLabel}`);
|
|
1982
|
+
if (i < DETAILED_COUNT) {
|
|
1983
|
+
const detail = formatObservationDetailFromContext(obs);
|
|
1984
|
+
if (detail) {
|
|
1985
|
+
lines.push(detail);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (context.summaries && context.summaries.length > 0) {
|
|
1990
|
+
lines.push("");
|
|
1991
|
+
lines.push("## Recent Project Briefs");
|
|
1992
|
+
for (const summary of context.summaries.slice(0, 3)) {
|
|
1993
|
+
lines.push(...formatSessionBrief(summary));
|
|
1994
|
+
lines.push("");
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (context.securityFindings && context.securityFindings.length > 0) {
|
|
1998
|
+
lines.push("");
|
|
1999
|
+
lines.push("Security findings (recent):");
|
|
2000
|
+
for (const finding of context.securityFindings) {
|
|
2001
|
+
const date = new Date(finding.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
2002
|
+
const file = finding.file_path ? ` in ${finding.file_path}` : finding.tool_name ? ` via ${finding.tool_name}` : "";
|
|
2003
|
+
lines.push(`- [${finding.severity.toUpperCase()}] ${finding.pattern_name}${file} (${date})`);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
if (context.staleDecisions && context.staleDecisions.length > 0) {
|
|
2007
|
+
lines.push("");
|
|
2008
|
+
lines.push("Stale commitments (decided but no implementation observed):");
|
|
2009
|
+
for (const sd of context.staleDecisions) {
|
|
2010
|
+
const date = sd.created_at.split("T")[0];
|
|
2011
|
+
lines.push(`- [DECISION] ${sd.title} (${date}, ${sd.days_ago}d ago)`);
|
|
2012
|
+
if (sd.best_match_title) {
|
|
2013
|
+
lines.push(` Closest match: "${sd.best_match_title}" (${Math.round((sd.best_match_similarity ?? 0) * 100)}% similar — not enough to count as done)`);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
const remaining = context.total_active - context.session_count;
|
|
2018
|
+
if (remaining > 0) {
|
|
2019
|
+
lines.push("");
|
|
2020
|
+
lines.push(`${remaining} more observation(s) available via search tool.`);
|
|
2021
|
+
}
|
|
2022
|
+
return lines.join(`
|
|
2023
|
+
`);
|
|
2024
|
+
}
|
|
2025
|
+
function formatSessionBrief(summary) {
|
|
2026
|
+
const lines = [];
|
|
2027
|
+
const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
2028
|
+
lines.push(heading);
|
|
2029
|
+
const sections = [
|
|
2030
|
+
["Investigated", summary.investigated, 180],
|
|
2031
|
+
["Learned", summary.learned, 180],
|
|
2032
|
+
["Completed", summary.completed, 180],
|
|
2033
|
+
["Next Steps", summary.next_steps, 140]
|
|
2034
|
+
];
|
|
2035
|
+
for (const [label, value, maxLen] of sections) {
|
|
2036
|
+
const formatted = formatSummarySection(value, maxLen);
|
|
2037
|
+
if (formatted) {
|
|
2038
|
+
lines.push(`${label}:`);
|
|
2039
|
+
lines.push(formatted);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return lines;
|
|
2043
|
+
}
|
|
2044
|
+
function chooseMeaningfulSessionHeadline(request, completed) {
|
|
2045
|
+
if (request && !looksLikeFileOperationTitle2(request))
|
|
2046
|
+
return request;
|
|
2047
|
+
const completedItems = extractMeaningfulLines(completed, 1);
|
|
2048
|
+
if (completedItems.length > 0)
|
|
2049
|
+
return completedItems[0];
|
|
2050
|
+
return request ?? completed ?? "(no summary)";
|
|
2051
|
+
}
|
|
2052
|
+
function formatSummarySection(value, maxLen) {
|
|
2053
|
+
return formatSummaryItems(value, maxLen);
|
|
2054
|
+
}
|
|
2055
|
+
function truncateText(text, maxLen) {
|
|
2056
|
+
if (text.length <= maxLen)
|
|
2057
|
+
return text;
|
|
2058
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
2059
|
+
}
|
|
2060
|
+
function isMeaningfulPrompt(value) {
|
|
2061
|
+
if (!value)
|
|
2062
|
+
return false;
|
|
2063
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
2064
|
+
if (compact.length < 8)
|
|
2065
|
+
return false;
|
|
2066
|
+
return /[a-z]{3,}/i.test(compact);
|
|
2067
|
+
}
|
|
2068
|
+
function looksLikeFileOperationTitle2(value) {
|
|
2069
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
2070
|
+
}
|
|
2071
|
+
function stripInlineSectionLabel(value) {
|
|
2072
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
2073
|
+
}
|
|
2074
|
+
function extractMeaningfulLines(value, limit) {
|
|
2075
|
+
if (!value)
|
|
2076
|
+
return [];
|
|
2077
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle2(line)).slice(0, limit);
|
|
2078
|
+
}
|
|
2079
|
+
function formatObservationDetailFromContext(obs) {
|
|
2080
|
+
if (obs.facts) {
|
|
2081
|
+
const bullets = parseFacts(obs.facts);
|
|
2082
|
+
if (bullets.length > 0) {
|
|
2083
|
+
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
2084
|
+
`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (obs.narrative) {
|
|
2088
|
+
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
2089
|
+
return ` ${snippet}`;
|
|
2090
|
+
}
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
function formatObservationDetail(obs) {
|
|
2094
|
+
if (obs.facts) {
|
|
2095
|
+
const bullets = parseFacts(obs.facts);
|
|
2096
|
+
if (bullets.length > 0) {
|
|
2097
|
+
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
2098
|
+
`);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
if (obs.narrative) {
|
|
2102
|
+
const snippet = obs.narrative.length > 120 ? obs.narrative.slice(0, 117) + "..." : obs.narrative;
|
|
2103
|
+
return ` ${snippet}`;
|
|
2104
|
+
}
|
|
2105
|
+
return "";
|
|
2106
|
+
}
|
|
2107
|
+
function parseFacts(facts) {
|
|
2108
|
+
if (!facts)
|
|
2109
|
+
return [];
|
|
2110
|
+
try {
|
|
2111
|
+
const parsed = JSON.parse(facts);
|
|
2112
|
+
if (Array.isArray(parsed)) {
|
|
2113
|
+
return parsed.filter((f) => typeof f === "string" && f.length > 0);
|
|
2114
|
+
}
|
|
2115
|
+
} catch {
|
|
2116
|
+
if (facts.trim().length > 0) {
|
|
2117
|
+
return [facts.trim()];
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return [];
|
|
2121
|
+
}
|
|
2122
|
+
function toContextObservation(obs) {
|
|
2123
|
+
return {
|
|
2124
|
+
id: obs.id,
|
|
2125
|
+
type: obs.type,
|
|
2126
|
+
title: obs.title,
|
|
2127
|
+
narrative: obs.narrative,
|
|
2128
|
+
facts: obs.facts,
|
|
2129
|
+
files_read: obs.files_read,
|
|
2130
|
+
files_modified: obs.files_modified,
|
|
2131
|
+
quality: obs.quality,
|
|
2132
|
+
created_at: obs.created_at,
|
|
2133
|
+
...obs._source_project ? { source_project: obs._source_project } : {}
|
|
2134
|
+
};
|
|
1543
2135
|
}
|
|
1544
|
-
function
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
2136
|
+
function formatObservationFiles(obs) {
|
|
2137
|
+
const modified = parseJsonStringArray(obs.files_modified);
|
|
2138
|
+
if (modified.length > 0) {
|
|
2139
|
+
return ` · files: ${truncateText(modified.slice(0, 2).join(", "), 60)}`;
|
|
2140
|
+
}
|
|
2141
|
+
const read = parseJsonStringArray(obs.files_read);
|
|
2142
|
+
if (read.length > 0) {
|
|
2143
|
+
return ` · read: ${truncateText(read.slice(0, 2).join(", "), 60)}`;
|
|
2144
|
+
}
|
|
2145
|
+
return "";
|
|
1549
2146
|
}
|
|
1550
|
-
function
|
|
2147
|
+
function parseJsonStringArray(value) {
|
|
1551
2148
|
if (!value)
|
|
1552
2149
|
return [];
|
|
1553
2150
|
try {
|
|
1554
2151
|
const parsed = JSON.parse(value);
|
|
1555
|
-
|
|
2152
|
+
if (!Array.isArray(parsed))
|
|
2153
|
+
return [];
|
|
2154
|
+
return parsed.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
1556
2155
|
} catch {
|
|
1557
2156
|
return [];
|
|
1558
2157
|
}
|
|
1559
2158
|
}
|
|
1560
|
-
function
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
2159
|
+
function formatToolEventDetail(tool) {
|
|
2160
|
+
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
2161
|
+
return truncateText(detail || "recent tool execution", 160);
|
|
2162
|
+
}
|
|
2163
|
+
function getProjectTypeCounts(db, projectId, userId) {
|
|
2164
|
+
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
2165
|
+
const rows = db.db.query(`SELECT type, COUNT(*) as count
|
|
2166
|
+
FROM observations
|
|
2167
|
+
WHERE project_id = ?
|
|
2168
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
2169
|
+
AND superseded_by IS NULL
|
|
2170
|
+
${visibilityClause}
|
|
2171
|
+
GROUP BY type`).all(projectId, ...userId ? [userId] : []);
|
|
2172
|
+
const counts = {};
|
|
2173
|
+
for (const row of rows) {
|
|
2174
|
+
counts[row.type] = row.count;
|
|
2175
|
+
}
|
|
2176
|
+
return counts;
|
|
2177
|
+
}
|
|
2178
|
+
function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
2179
|
+
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
2180
|
+
const visibilityParams = userId ? [userId] : [];
|
|
2181
|
+
const summaries = db.db.query(`SELECT * FROM session_summaries
|
|
2182
|
+
WHERE project_id = ?
|
|
2183
|
+
ORDER BY created_at_epoch DESC
|
|
2184
|
+
LIMIT 6`).all(projectId);
|
|
2185
|
+
const picked = [];
|
|
2186
|
+
const seen = new Set;
|
|
2187
|
+
for (const summary of summaries) {
|
|
2188
|
+
for (const item of parseSummaryJsonList(summary.recent_outcomes)) {
|
|
2189
|
+
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2190
|
+
if (!normalized || seen.has(normalized))
|
|
2191
|
+
continue;
|
|
2192
|
+
seen.add(normalized);
|
|
2193
|
+
picked.push(item);
|
|
2194
|
+
if (picked.length >= 5)
|
|
2195
|
+
return picked;
|
|
2196
|
+
}
|
|
2197
|
+
for (const line of [
|
|
2198
|
+
...extractMeaningfulLines(summary.completed, 2),
|
|
2199
|
+
...extractMeaningfulLines(summary.learned, 1)
|
|
2200
|
+
]) {
|
|
2201
|
+
const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2202
|
+
if (!normalized || seen.has(normalized))
|
|
2203
|
+
continue;
|
|
2204
|
+
seen.add(normalized);
|
|
2205
|
+
picked.push(line);
|
|
2206
|
+
if (picked.length >= 5)
|
|
2207
|
+
return picked;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
for (const session of recentSessions ?? []) {
|
|
2211
|
+
for (const item of parseSummaryJsonList(session.recent_outcomes)) {
|
|
2212
|
+
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2213
|
+
if (!normalized || seen.has(normalized))
|
|
2214
|
+
continue;
|
|
2215
|
+
seen.add(normalized);
|
|
2216
|
+
picked.push(item);
|
|
2217
|
+
if (picked.length >= 5)
|
|
2218
|
+
return picked;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
const rows = db.db.query(`SELECT * FROM observations
|
|
2222
|
+
WHERE project_id = ?
|
|
2223
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
2224
|
+
AND superseded_by IS NULL
|
|
2225
|
+
${visibilityClause}
|
|
2226
|
+
ORDER BY created_at_epoch DESC
|
|
2227
|
+
LIMIT 20`).all(projectId, ...visibilityParams);
|
|
2228
|
+
for (const obs of rows) {
|
|
2229
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
2230
|
+
continue;
|
|
2231
|
+
const title = stripInlineSectionLabel(obs.title);
|
|
2232
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2233
|
+
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle2(title))
|
|
2234
|
+
continue;
|
|
2235
|
+
seen.add(normalized);
|
|
2236
|
+
picked.push(title);
|
|
2237
|
+
if (picked.length >= 5)
|
|
2238
|
+
break;
|
|
2239
|
+
}
|
|
2240
|
+
return picked;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// src/tools/handoffs.ts
|
|
2244
|
+
function formatHandoffSource2(handoff) {
|
|
2245
|
+
const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
|
|
2246
|
+
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`;
|
|
2247
|
+
return `from ${handoff.device_id} · ${ageLabel}`;
|
|
1565
2248
|
}
|
|
1566
2249
|
|
|
1567
2250
|
// src/context/inject.ts
|
|
1568
|
-
|
|
2251
|
+
var FRESH_CONTINUITY_WINDOW_DAYS2 = 3;
|
|
2252
|
+
function tokenizeProjectHint2(text) {
|
|
1569
2253
|
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
1570
2254
|
}
|
|
1571
|
-
function
|
|
2255
|
+
function parseSummaryJsonList2(value) {
|
|
1572
2256
|
if (!value)
|
|
1573
2257
|
return [];
|
|
1574
2258
|
try {
|
|
@@ -1578,10 +2262,10 @@ function parseSummaryJsonList(value) {
|
|
|
1578
2262
|
return [];
|
|
1579
2263
|
}
|
|
1580
2264
|
}
|
|
1581
|
-
function
|
|
2265
|
+
function isObservationRelatedToProject2(obs, detected) {
|
|
1582
2266
|
const hints = new Set([
|
|
1583
|
-
...
|
|
1584
|
-
...
|
|
2267
|
+
...tokenizeProjectHint2(detected.name),
|
|
2268
|
+
...tokenizeProjectHint2(detected.canonical_id)
|
|
1585
2269
|
]);
|
|
1586
2270
|
if (hints.size === 0)
|
|
1587
2271
|
return false;
|
|
@@ -1601,12 +2285,12 @@ function isObservationRelatedToProject(obs, detected) {
|
|
|
1601
2285
|
}
|
|
1602
2286
|
return false;
|
|
1603
2287
|
}
|
|
1604
|
-
function
|
|
2288
|
+
function estimateTokens2(text) {
|
|
1605
2289
|
if (!text)
|
|
1606
2290
|
return 0;
|
|
1607
2291
|
return Math.ceil(text.length / 4);
|
|
1608
2292
|
}
|
|
1609
|
-
function
|
|
2293
|
+
function buildSessionContext2(db, cwd, options = {}) {
|
|
1610
2294
|
const opts = typeof options === "number" ? { maxCount: options } : options;
|
|
1611
2295
|
const tokenBudget = opts.tokenBudget ?? 3000;
|
|
1612
2296
|
const maxCount = opts.maxCount;
|
|
@@ -1677,7 +2361,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1677
2361
|
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
1678
2362
|
});
|
|
1679
2363
|
if (isNewProject) {
|
|
1680
|
-
crossProjectCandidates = crossProjectCandidates.filter((obs) =>
|
|
2364
|
+
crossProjectCandidates = crossProjectCandidates.filter((obs) => isObservationRelatedToProject2(obs, detected));
|
|
1681
2365
|
}
|
|
1682
2366
|
}
|
|
1683
2367
|
const seenIds = new Set(pinned.map((o) => o.id));
|
|
@@ -1704,24 +2388,25 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1704
2388
|
const canonicalId = project?.canonical_id ?? detected.canonical_id;
|
|
1705
2389
|
if (maxCount !== undefined) {
|
|
1706
2390
|
const remaining = Math.max(0, maxCount - pinned.length - dedupedRecent.length);
|
|
1707
|
-
|
|
1708
|
-
const recentPrompts2 =
|
|
1709
|
-
const recentToolEvents2 =
|
|
2391
|
+
let all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
|
|
2392
|
+
const recentPrompts2 = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
2393
|
+
const recentToolEvents2 = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
1710
2394
|
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
1711
|
-
const projectTypeCounts2 = isNewProject ? undefined :
|
|
1712
|
-
const recentOutcomes2 = isNewProject ? undefined :
|
|
1713
|
-
const recentHandoffs2 = getRecentHandoffs(db, {
|
|
2395
|
+
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts2(db, projectId, opts.userId);
|
|
2396
|
+
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes2(db, projectId, opts.userId, recentSessions2);
|
|
2397
|
+
const recentHandoffs2 = isNewProject ? [] : getRecentHandoffs(db, {
|
|
1714
2398
|
cwd,
|
|
1715
|
-
project_scoped:
|
|
2399
|
+
project_scoped: true,
|
|
1716
2400
|
user_id: opts.userId,
|
|
1717
2401
|
current_device_id: opts.currentDeviceId,
|
|
1718
2402
|
limit: 3
|
|
1719
2403
|
}).handoffs;
|
|
1720
2404
|
const recentChatMessages2 = !isNewProject && project ? db.getRecentChatMessages(project.id, 4, opts.userId) : [];
|
|
2405
|
+
all = filterAutoLoadedObservationsForContinuity2(all, pinned, isNewProject, recentPrompts2, recentToolEvents2, recentSessions2, recentHandoffs2, recentChatMessages2, summariesFromRecentSessions2(db, projectId, recentSessions2));
|
|
1721
2406
|
return {
|
|
1722
2407
|
project_name: projectName,
|
|
1723
2408
|
canonical_id: canonicalId,
|
|
1724
|
-
observations: all.map(
|
|
2409
|
+
observations: all.map(toContextObservation2),
|
|
1725
2410
|
session_count: all.length,
|
|
1726
2411
|
total_active: totalActive,
|
|
1727
2412
|
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
@@ -1736,36 +2421,37 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1736
2421
|
let remainingBudget = tokenBudget - 30;
|
|
1737
2422
|
const selected = [];
|
|
1738
2423
|
for (const obs of pinned) {
|
|
1739
|
-
const cost =
|
|
2424
|
+
const cost = estimateObservationTokens2(obs, selected.length);
|
|
1740
2425
|
remainingBudget -= cost;
|
|
1741
2426
|
selected.push(obs);
|
|
1742
2427
|
}
|
|
1743
2428
|
for (const obs of dedupedRecent) {
|
|
1744
|
-
const cost =
|
|
2429
|
+
const cost = estimateObservationTokens2(obs, selected.length);
|
|
1745
2430
|
remainingBudget -= cost;
|
|
1746
2431
|
selected.push(obs);
|
|
1747
2432
|
}
|
|
1748
2433
|
for (const obs of sorted) {
|
|
1749
|
-
const cost =
|
|
2434
|
+
const cost = estimateObservationTokens2(obs, selected.length);
|
|
1750
2435
|
if (remainingBudget - cost < 0 && selected.length > 0)
|
|
1751
2436
|
break;
|
|
1752
2437
|
remainingBudget -= cost;
|
|
1753
2438
|
selected.push(obs);
|
|
1754
2439
|
}
|
|
1755
2440
|
const summaries = isNewProject ? [] : db.getRecentSummaries(projectId, 5);
|
|
1756
|
-
const recentPrompts =
|
|
1757
|
-
const recentToolEvents =
|
|
2441
|
+
const recentPrompts = isNewProject ? [] : db.getRecentUserPrompts(projectId, 6, opts.userId);
|
|
2442
|
+
const recentToolEvents = isNewProject ? [] : db.getRecentToolEvents(projectId, 6, opts.userId);
|
|
1758
2443
|
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
1759
|
-
const projectTypeCounts = isNewProject ? undefined :
|
|
1760
|
-
const recentOutcomes = isNewProject ? undefined :
|
|
1761
|
-
const recentHandoffs = getRecentHandoffs(db, {
|
|
2444
|
+
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts2(db, projectId, opts.userId);
|
|
2445
|
+
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes2(db, projectId, opts.userId, recentSessions);
|
|
2446
|
+
const recentHandoffs = isNewProject ? [] : getRecentHandoffs(db, {
|
|
1762
2447
|
cwd,
|
|
1763
|
-
project_scoped:
|
|
2448
|
+
project_scoped: true,
|
|
1764
2449
|
user_id: opts.userId,
|
|
1765
2450
|
current_device_id: opts.currentDeviceId,
|
|
1766
2451
|
limit: 3
|
|
1767
2452
|
}).handoffs;
|
|
1768
2453
|
const recentChatMessages = !isNewProject ? db.getRecentChatMessages(projectId, 4, opts.userId) : [];
|
|
2454
|
+
const filteredSelected = filterAutoLoadedObservationsForContinuity2(selected, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries);
|
|
1769
2455
|
let securityFindings = [];
|
|
1770
2456
|
if (!isNewProject) {
|
|
1771
2457
|
try {
|
|
@@ -1813,8 +2499,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1813
2499
|
return {
|
|
1814
2500
|
project_name: projectName,
|
|
1815
2501
|
canonical_id: canonicalId,
|
|
1816
|
-
observations:
|
|
1817
|
-
session_count:
|
|
2502
|
+
observations: filteredSelected.map(toContextObservation2),
|
|
2503
|
+
session_count: filteredSelected.length,
|
|
1818
2504
|
total_active: totalActive,
|
|
1819
2505
|
summaries: summaries.length > 0 ? summaries : undefined,
|
|
1820
2506
|
securityFindings: securityFindings.length > 0 ? securityFindings : undefined,
|
|
@@ -1829,16 +2515,49 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
1829
2515
|
recentChatMessages: recentChatMessages.length > 0 ? recentChatMessages : undefined
|
|
1830
2516
|
};
|
|
1831
2517
|
}
|
|
1832
|
-
function
|
|
2518
|
+
function filterAutoLoadedObservationsForContinuity2(observations, pinned, isNewProject, recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
2519
|
+
if (isNewProject)
|
|
2520
|
+
return observations;
|
|
2521
|
+
if (hasFreshProjectContinuity2(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries)) {
|
|
2522
|
+
return observations;
|
|
2523
|
+
}
|
|
2524
|
+
const pinnedIds = new Set(pinned.map((obs) => obs.id));
|
|
2525
|
+
return observations.filter((obs) => {
|
|
2526
|
+
if (pinnedIds.has(obs.id))
|
|
2527
|
+
return true;
|
|
2528
|
+
return observationAgeDays2(obs.created_at_epoch) <= FRESH_CONTINUITY_WINDOW_DAYS2;
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
function hasFreshProjectContinuity2(recentPrompts, recentToolEvents, recentSessions, recentHandoffs, recentChatMessages, summaries) {
|
|
2532
|
+
const freshEnough = (epoch) => typeof epoch === "number" && observationAgeDays2(epoch) <= FRESH_CONTINUITY_WINDOW_DAYS2;
|
|
2533
|
+
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));
|
|
2534
|
+
}
|
|
2535
|
+
function summariesFromRecentSessions2(db, projectId, recentSessions) {
|
|
2536
|
+
const seen = new Set;
|
|
2537
|
+
const rows = [];
|
|
2538
|
+
for (const session of recentSessions) {
|
|
2539
|
+
if (seen.has(session.session_id))
|
|
2540
|
+
continue;
|
|
2541
|
+
seen.add(session.session_id);
|
|
2542
|
+
const summary = db.getSessionSummary(session.session_id);
|
|
2543
|
+
if (summary && summary.project_id === projectId)
|
|
2544
|
+
rows.push(summary);
|
|
2545
|
+
}
|
|
2546
|
+
return rows;
|
|
2547
|
+
}
|
|
2548
|
+
function observationAgeDays2(createdAtEpoch) {
|
|
2549
|
+
return Math.max(0, (Math.floor(Date.now() / 1000) - createdAtEpoch) / 86400);
|
|
2550
|
+
}
|
|
2551
|
+
function estimateObservationTokens2(obs, index) {
|
|
1833
2552
|
const DETAILED_THRESHOLD = 5;
|
|
1834
|
-
const titleCost =
|
|
2553
|
+
const titleCost = estimateTokens2(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
|
|
1835
2554
|
if (index >= DETAILED_THRESHOLD) {
|
|
1836
2555
|
return titleCost;
|
|
1837
2556
|
}
|
|
1838
|
-
const detailText =
|
|
1839
|
-
return titleCost +
|
|
2557
|
+
const detailText = formatObservationDetail2(obs);
|
|
2558
|
+
return titleCost + estimateTokens2(detailText);
|
|
1840
2559
|
}
|
|
1841
|
-
function
|
|
2560
|
+
function formatContextForInjection2(context) {
|
|
1842
2561
|
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)) {
|
|
1843
2562
|
return `Project: ${context.project_name} (no prior observations)`;
|
|
1844
2563
|
}
|
|
@@ -1867,11 +2586,11 @@ function formatContextForInjection(context) {
|
|
|
1867
2586
|
for (const handoff of context.recentHandoffs.slice(0, 3)) {
|
|
1868
2587
|
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();
|
|
1869
2588
|
if (title) {
|
|
1870
|
-
lines.push(`- ${
|
|
2589
|
+
lines.push(`- ${truncateText2(`${title} (${formatHandoffSource(handoff)})`, 160)}`);
|
|
1871
2590
|
}
|
|
1872
2591
|
const narrative = handoff.narrative?.split(/\n{2,}/).map((part) => part.replace(/\s+/g, " ").trim()).find((part) => /^(Current thread:|Completed:|Next Steps:)/i.test(part));
|
|
1873
2592
|
if (narrative) {
|
|
1874
|
-
lines.push(` ${
|
|
2593
|
+
lines.push(` ${truncateText2(narrative, 180)}`);
|
|
1875
2594
|
}
|
|
1876
2595
|
}
|
|
1877
2596
|
lines.push("");
|
|
@@ -1879,17 +2598,17 @@ function formatContextForInjection(context) {
|
|
|
1879
2598
|
if (context.recentChatMessages && context.recentChatMessages.length > 0) {
|
|
1880
2599
|
lines.push("## Recent Chat");
|
|
1881
2600
|
for (const message of context.recentChatMessages.slice(0, 4).reverse()) {
|
|
1882
|
-
lines.push(`- [${message.role}] ${
|
|
2601
|
+
lines.push(`- [${message.role}] ${truncateText2(message.content.replace(/\s+/g, " ").trim(), 160)}`);
|
|
1883
2602
|
}
|
|
1884
2603
|
lines.push("");
|
|
1885
2604
|
}
|
|
1886
2605
|
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
1887
|
-
const promptLines = context.recentPrompts.filter((prompt) =>
|
|
2606
|
+
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt2(prompt.prompt)).slice(0, 5);
|
|
1888
2607
|
if (promptLines.length > 0) {
|
|
1889
2608
|
lines.push("## Recent Requests");
|
|
1890
2609
|
for (const prompt of promptLines) {
|
|
1891
2610
|
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
1892
|
-
lines.push(`- ${label}: ${
|
|
2611
|
+
lines.push(`- ${label}: ${truncateText2(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
1893
2612
|
}
|
|
1894
2613
|
lines.push("");
|
|
1895
2614
|
}
|
|
@@ -1897,16 +2616,16 @@ function formatContextForInjection(context) {
|
|
|
1897
2616
|
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
1898
2617
|
lines.push("## Recent Tools");
|
|
1899
2618
|
for (const tool of context.recentToolEvents.slice(0, 5)) {
|
|
1900
|
-
lines.push(`- ${tool.tool_name}: ${
|
|
2619
|
+
lines.push(`- ${tool.tool_name}: ${formatToolEventDetail2(tool)}`);
|
|
1901
2620
|
}
|
|
1902
2621
|
lines.push("");
|
|
1903
2622
|
}
|
|
1904
2623
|
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
1905
2624
|
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
1906
|
-
const summary =
|
|
2625
|
+
const summary = chooseMeaningfulSessionHeadline2(session.request, session.completed);
|
|
1907
2626
|
if (summary === "(no summary)")
|
|
1908
2627
|
return null;
|
|
1909
|
-
return `- ${session.session_id}: ${
|
|
2628
|
+
return `- ${session.session_id}: ${truncateText2(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
1910
2629
|
}).filter((line) => Boolean(line));
|
|
1911
2630
|
if (recentSessionLines.length > 0) {
|
|
1912
2631
|
lines.push("## Recent Sessions");
|
|
@@ -1917,7 +2636,7 @@ function formatContextForInjection(context) {
|
|
|
1917
2636
|
if (context.recentOutcomes && context.recentOutcomes.length > 0) {
|
|
1918
2637
|
lines.push("## Recent Outcomes");
|
|
1919
2638
|
for (const outcome of context.recentOutcomes.slice(0, 5)) {
|
|
1920
|
-
lines.push(`- ${
|
|
2639
|
+
lines.push(`- ${truncateText2(outcome, 160)}`);
|
|
1921
2640
|
}
|
|
1922
2641
|
lines.push("");
|
|
1923
2642
|
}
|
|
@@ -1933,10 +2652,10 @@ function formatContextForInjection(context) {
|
|
|
1933
2652
|
const obs = context.observations[i];
|
|
1934
2653
|
const date = obs.created_at.split("T")[0];
|
|
1935
2654
|
const fromLabel = obs.source_project ? ` [from: ${obs.source_project}]` : "";
|
|
1936
|
-
const fileLabel =
|
|
2655
|
+
const fileLabel = formatObservationFiles2(obs);
|
|
1937
2656
|
lines.push(`- **#${obs.id} [${obs.type}]** ${obs.title} (${date}, q=${obs.quality.toFixed(1)})${fromLabel}${fileLabel}`);
|
|
1938
2657
|
if (i < DETAILED_COUNT) {
|
|
1939
|
-
const detail =
|
|
2658
|
+
const detail = formatObservationDetailFromContext2(obs);
|
|
1940
2659
|
if (detail) {
|
|
1941
2660
|
lines.push(detail);
|
|
1942
2661
|
}
|
|
@@ -1946,7 +2665,7 @@ function formatContextForInjection(context) {
|
|
|
1946
2665
|
lines.push("");
|
|
1947
2666
|
lines.push("## Recent Project Briefs");
|
|
1948
2667
|
for (const summary of context.summaries.slice(0, 3)) {
|
|
1949
|
-
lines.push(...
|
|
2668
|
+
lines.push(...formatSessionBrief2(summary));
|
|
1950
2669
|
lines.push("");
|
|
1951
2670
|
}
|
|
1952
2671
|
}
|
|
@@ -1978,9 +2697,9 @@ function formatContextForInjection(context) {
|
|
|
1978
2697
|
return lines.join(`
|
|
1979
2698
|
`);
|
|
1980
2699
|
}
|
|
1981
|
-
function
|
|
2700
|
+
function formatSessionBrief2(summary) {
|
|
1982
2701
|
const lines = [];
|
|
1983
|
-
const heading = summary.request ? `### ${
|
|
2702
|
+
const heading = summary.request ? `### ${truncateText2(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
|
|
1984
2703
|
lines.push(heading);
|
|
1985
2704
|
const sections = [
|
|
1986
2705
|
["Investigated", summary.investigated, 180],
|
|
@@ -1989,7 +2708,7 @@ function formatSessionBrief(summary) {
|
|
|
1989
2708
|
["Next Steps", summary.next_steps, 140]
|
|
1990
2709
|
];
|
|
1991
2710
|
for (const [label, value, maxLen] of sections) {
|
|
1992
|
-
const formatted =
|
|
2711
|
+
const formatted = formatSummarySection2(value, maxLen);
|
|
1993
2712
|
if (formatted) {
|
|
1994
2713
|
lines.push(`${label}:`);
|
|
1995
2714
|
lines.push(formatted);
|
|
@@ -1997,23 +2716,23 @@ function formatSessionBrief(summary) {
|
|
|
1997
2716
|
}
|
|
1998
2717
|
return lines;
|
|
1999
2718
|
}
|
|
2000
|
-
function
|
|
2001
|
-
if (request && !
|
|
2719
|
+
function chooseMeaningfulSessionHeadline2(request, completed) {
|
|
2720
|
+
if (request && !looksLikeFileOperationTitle3(request))
|
|
2002
2721
|
return request;
|
|
2003
|
-
const completedItems =
|
|
2722
|
+
const completedItems = extractMeaningfulLines2(completed, 1);
|
|
2004
2723
|
if (completedItems.length > 0)
|
|
2005
2724
|
return completedItems[0];
|
|
2006
2725
|
return request ?? completed ?? "(no summary)";
|
|
2007
2726
|
}
|
|
2008
|
-
function
|
|
2727
|
+
function formatSummarySection2(value, maxLen) {
|
|
2009
2728
|
return formatSummaryItems(value, maxLen);
|
|
2010
2729
|
}
|
|
2011
|
-
function
|
|
2730
|
+
function truncateText2(text, maxLen) {
|
|
2012
2731
|
if (text.length <= maxLen)
|
|
2013
2732
|
return text;
|
|
2014
2733
|
return text.slice(0, maxLen - 3) + "...";
|
|
2015
2734
|
}
|
|
2016
|
-
function
|
|
2735
|
+
function isMeaningfulPrompt2(value) {
|
|
2017
2736
|
if (!value)
|
|
2018
2737
|
return false;
|
|
2019
2738
|
const compact = value.replace(/\s+/g, " ").trim();
|
|
@@ -2021,20 +2740,20 @@ function isMeaningfulPrompt(value) {
|
|
|
2021
2740
|
return false;
|
|
2022
2741
|
return /[a-z]{3,}/i.test(compact);
|
|
2023
2742
|
}
|
|
2024
|
-
function
|
|
2743
|
+
function looksLikeFileOperationTitle3(value) {
|
|
2025
2744
|
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
2026
2745
|
}
|
|
2027
|
-
function
|
|
2746
|
+
function stripInlineSectionLabel2(value) {
|
|
2028
2747
|
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
2029
2748
|
}
|
|
2030
|
-
function
|
|
2749
|
+
function extractMeaningfulLines2(value, limit) {
|
|
2031
2750
|
if (!value)
|
|
2032
2751
|
return [];
|
|
2033
|
-
return extractSummaryItems(value).map((line) =>
|
|
2752
|
+
return extractSummaryItems(value).map((line) => stripInlineSectionLabel2(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle3(line)).slice(0, limit);
|
|
2034
2753
|
}
|
|
2035
|
-
function
|
|
2754
|
+
function formatObservationDetailFromContext2(obs) {
|
|
2036
2755
|
if (obs.facts) {
|
|
2037
|
-
const bullets =
|
|
2756
|
+
const bullets = parseFacts2(obs.facts);
|
|
2038
2757
|
if (bullets.length > 0) {
|
|
2039
2758
|
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
2040
2759
|
`);
|
|
@@ -2046,9 +2765,9 @@ function formatObservationDetailFromContext(obs) {
|
|
|
2046
2765
|
}
|
|
2047
2766
|
return null;
|
|
2048
2767
|
}
|
|
2049
|
-
function
|
|
2768
|
+
function formatObservationDetail2(obs) {
|
|
2050
2769
|
if (obs.facts) {
|
|
2051
|
-
const bullets =
|
|
2770
|
+
const bullets = parseFacts2(obs.facts);
|
|
2052
2771
|
if (bullets.length > 0) {
|
|
2053
2772
|
return bullets.slice(0, 4).map((f) => ` - ${f}`).join(`
|
|
2054
2773
|
`);
|
|
@@ -2060,7 +2779,7 @@ function formatObservationDetail(obs) {
|
|
|
2060
2779
|
}
|
|
2061
2780
|
return "";
|
|
2062
2781
|
}
|
|
2063
|
-
function
|
|
2782
|
+
function parseFacts2(facts) {
|
|
2064
2783
|
if (!facts)
|
|
2065
2784
|
return [];
|
|
2066
2785
|
try {
|
|
@@ -2075,7 +2794,7 @@ function parseFacts(facts) {
|
|
|
2075
2794
|
}
|
|
2076
2795
|
return [];
|
|
2077
2796
|
}
|
|
2078
|
-
function
|
|
2797
|
+
function toContextObservation2(obs) {
|
|
2079
2798
|
return {
|
|
2080
2799
|
id: obs.id,
|
|
2081
2800
|
type: obs.type,
|
|
@@ -2089,18 +2808,18 @@ function toContextObservation(obs) {
|
|
|
2089
2808
|
...obs._source_project ? { source_project: obs._source_project } : {}
|
|
2090
2809
|
};
|
|
2091
2810
|
}
|
|
2092
|
-
function
|
|
2093
|
-
const modified =
|
|
2811
|
+
function formatObservationFiles2(obs) {
|
|
2812
|
+
const modified = parseJsonStringArray2(obs.files_modified);
|
|
2094
2813
|
if (modified.length > 0) {
|
|
2095
|
-
return ` · files: ${
|
|
2814
|
+
return ` · files: ${truncateText2(modified.slice(0, 2).join(", "), 60)}`;
|
|
2096
2815
|
}
|
|
2097
|
-
const read =
|
|
2816
|
+
const read = parseJsonStringArray2(obs.files_read);
|
|
2098
2817
|
if (read.length > 0) {
|
|
2099
|
-
return ` · read: ${
|
|
2818
|
+
return ` · read: ${truncateText2(read.slice(0, 2).join(", "), 60)}`;
|
|
2100
2819
|
}
|
|
2101
2820
|
return "";
|
|
2102
2821
|
}
|
|
2103
|
-
function
|
|
2822
|
+
function parseJsonStringArray2(value) {
|
|
2104
2823
|
if (!value)
|
|
2105
2824
|
return [];
|
|
2106
2825
|
try {
|
|
@@ -2112,11 +2831,11 @@ function parseJsonStringArray(value) {
|
|
|
2112
2831
|
return [];
|
|
2113
2832
|
}
|
|
2114
2833
|
}
|
|
2115
|
-
function
|
|
2834
|
+
function formatToolEventDetail2(tool) {
|
|
2116
2835
|
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
2117
|
-
return
|
|
2836
|
+
return truncateText2(detail || "recent tool execution", 160);
|
|
2118
2837
|
}
|
|
2119
|
-
function
|
|
2838
|
+
function getProjectTypeCounts2(db, projectId, userId) {
|
|
2120
2839
|
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
2121
2840
|
const rows = db.db.query(`SELECT type, COUNT(*) as count
|
|
2122
2841
|
FROM observations
|
|
@@ -2131,7 +2850,7 @@ function getProjectTypeCounts(db, projectId, userId) {
|
|
|
2131
2850
|
}
|
|
2132
2851
|
return counts;
|
|
2133
2852
|
}
|
|
2134
|
-
function
|
|
2853
|
+
function getRecentOutcomes2(db, projectId, userId, recentSessions) {
|
|
2135
2854
|
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
2136
2855
|
const visibilityParams = userId ? [userId] : [];
|
|
2137
2856
|
const summaries = db.db.query(`SELECT * FROM session_summaries
|
|
@@ -2141,7 +2860,7 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
2141
2860
|
const picked = [];
|
|
2142
2861
|
const seen = new Set;
|
|
2143
2862
|
for (const summary of summaries) {
|
|
2144
|
-
for (const item of
|
|
2863
|
+
for (const item of parseSummaryJsonList2(summary.recent_outcomes)) {
|
|
2145
2864
|
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2146
2865
|
if (!normalized || seen.has(normalized))
|
|
2147
2866
|
continue;
|
|
@@ -2151,8 +2870,8 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
2151
2870
|
return picked;
|
|
2152
2871
|
}
|
|
2153
2872
|
for (const line of [
|
|
2154
|
-
...
|
|
2155
|
-
...
|
|
2873
|
+
...extractMeaningfulLines2(summary.completed, 2),
|
|
2874
|
+
...extractMeaningfulLines2(summary.learned, 1)
|
|
2156
2875
|
]) {
|
|
2157
2876
|
const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2158
2877
|
if (!normalized || seen.has(normalized))
|
|
@@ -2164,7 +2883,7 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
2164
2883
|
}
|
|
2165
2884
|
}
|
|
2166
2885
|
for (const session of recentSessions ?? []) {
|
|
2167
|
-
for (const item of
|
|
2886
|
+
for (const item of parseSummaryJsonList2(session.recent_outcomes)) {
|
|
2168
2887
|
const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2169
2888
|
if (!normalized || seen.has(normalized))
|
|
2170
2889
|
continue;
|
|
@@ -2184,9 +2903,9 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
2184
2903
|
for (const obs of rows) {
|
|
2185
2904
|
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
2186
2905
|
continue;
|
|
2187
|
-
const title =
|
|
2906
|
+
const title = stripInlineSectionLabel2(obs.title);
|
|
2188
2907
|
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2189
|
-
if (!normalized || seen.has(normalized) ||
|
|
2908
|
+
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle3(title))
|
|
2190
2909
|
continue;
|
|
2191
2910
|
seen.add(normalized);
|
|
2192
2911
|
picked.push(title);
|
|
@@ -2196,11 +2915,26 @@ function getRecentOutcomes(db, projectId, userId, recentSessions) {
|
|
|
2196
2915
|
return picked;
|
|
2197
2916
|
}
|
|
2198
2917
|
|
|
2199
|
-
// src/tools/
|
|
2200
|
-
function
|
|
2201
|
-
const
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2918
|
+
// src/tools/project-memory-index.ts
|
|
2919
|
+
function classifyContinuityState(recentRequestsCount, recentToolsCount, recentHandoffsCount, recentChatCount, recentSessions, recentOutcomesCount) {
|
|
2920
|
+
const hasRaw = recentRequestsCount > 0 || recentToolsCount > 0;
|
|
2921
|
+
const hasResume = recentHandoffsCount > 0 || recentChatCount > 0;
|
|
2922
|
+
const hasSessionThread = recentSessions.length > 0 || recentOutcomesCount > 0;
|
|
2923
|
+
if (hasRaw && (hasResume || hasSessionThread))
|
|
2924
|
+
return "fresh";
|
|
2925
|
+
if (hasRaw || hasResume || hasSessionThread)
|
|
2926
|
+
return "thin";
|
|
2927
|
+
return "cold";
|
|
2928
|
+
}
|
|
2929
|
+
function describeContinuityState(state) {
|
|
2930
|
+
switch (state) {
|
|
2931
|
+
case "fresh":
|
|
2932
|
+
return "Fresh repo-local continuity is available.";
|
|
2933
|
+
case "thin":
|
|
2934
|
+
return "Only partial continuity is available; recent prompts/chat are safer than older memory.";
|
|
2935
|
+
default:
|
|
2936
|
+
return "No fresh repo-local continuity yet; older memory should be treated cautiously.";
|
|
2937
|
+
}
|
|
2204
2938
|
}
|
|
2205
2939
|
|
|
2206
2940
|
// src/telemetry/stack-detect.ts
|
|
@@ -2323,7 +3057,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
|
|
|
2323
3057
|
import { join as join3 } from "node:path";
|
|
2324
3058
|
import { homedir } from "node:os";
|
|
2325
3059
|
var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
|
|
2326
|
-
var CLIENT_VERSION = "0.4.
|
|
3060
|
+
var CLIENT_VERSION = "0.4.27";
|
|
2327
3061
|
function hashFile(filePath) {
|
|
2328
3062
|
try {
|
|
2329
3063
|
if (!existsSync3(filePath))
|
|
@@ -4739,6 +5473,7 @@ function formatSplashScreen(data) {
|
|
|
4739
5473
|
}
|
|
4740
5474
|
function formatVisibleStartupBrief(context) {
|
|
4741
5475
|
const lines = [];
|
|
5476
|
+
const continuityState = getStartupContinuityState(context);
|
|
4742
5477
|
const latest = pickPrimarySummary(context);
|
|
4743
5478
|
const observationFallbacks = buildObservationFallbacks(context);
|
|
4744
5479
|
const promptFallback = buildPromptFallback(context);
|
|
@@ -4753,6 +5488,7 @@ function formatVisibleStartupBrief(context) {
|
|
|
4753
5488
|
const projectSignals = buildProjectSignalLine(context);
|
|
4754
5489
|
const shownItems = new Set;
|
|
4755
5490
|
const latestHandoffLines = buildLatestHandoffLines(context);
|
|
5491
|
+
const freshContinuity = hasFreshContinuitySignal(context);
|
|
4756
5492
|
if (latestHandoffLines.length > 0) {
|
|
4757
5493
|
lines.push(`${c2.cyan}Latest handoff:${c2.reset}`);
|
|
4758
5494
|
for (const item of latestHandoffLines) {
|
|
@@ -4760,6 +5496,7 @@ function formatVisibleStartupBrief(context) {
|
|
|
4760
5496
|
rememberShownItem(shownItems, item);
|
|
4761
5497
|
}
|
|
4762
5498
|
}
|
|
5499
|
+
lines.push(`${c2.cyan}Continuity:${c2.reset} ${continuityState} \u2014 ${truncateInline(describeContinuityState(continuityState), 160)}`);
|
|
4763
5500
|
if (promptLines.length > 0) {
|
|
4764
5501
|
lines.push(`${c2.cyan}Asked recently:${c2.reset}`);
|
|
4765
5502
|
for (const item of promptLines) {
|
|
@@ -4851,12 +5588,15 @@ function formatVisibleStartupBrief(context) {
|
|
|
4851
5588
|
lines.push(`${c2.cyan}Signal mix:${c2.reset}`);
|
|
4852
5589
|
lines.push(` - ${truncateInline(projectSignals, 160)}`);
|
|
4853
5590
|
}
|
|
5591
|
+
if (!freshContinuity && lines.length > 0 && (promptLines.length > 0 || recentChatLines.length > 0)) {
|
|
5592
|
+
lines.push(`${c2.dim}Fresh repo-local handoff is still thin; recent prompts/chat are more trustworthy than older memory here.${c2.reset}`);
|
|
5593
|
+
}
|
|
4854
5594
|
const stale = pickRelevantStaleDecision(context, latest);
|
|
4855
5595
|
if (stale) {
|
|
4856
5596
|
lines.push(`${c2.yellow}Watch:${c2.reset} ${truncateInline(`Decision still looks unfinished: ${stale.title}`, 170)}`);
|
|
4857
5597
|
}
|
|
4858
5598
|
if (lines.length === 0 && context.observations.length > 0) {
|
|
4859
|
-
const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => obs.type !== "decision").filter((obs) => !
|
|
5599
|
+
const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => obs.type !== "decision").filter((obs) => !looksLikeFileOperationTitle4(obs.title)).slice(0, 3);
|
|
4860
5600
|
for (const obs of top) {
|
|
4861
5601
|
lines.push(`${c2.cyan}${capitalize(obs.type)}:${c2.reset} ${truncateInline(obs.title, 170)}`);
|
|
4862
5602
|
}
|
|
@@ -4900,6 +5640,9 @@ function formatLegend() {
|
|
|
4900
5640
|
}
|
|
4901
5641
|
function formatContextIndex(context, shownItems) {
|
|
4902
5642
|
const selected = pickContextIndexObservations(context, shownItems);
|
|
5643
|
+
if (!hasFreshContinuitySignal(context)) {
|
|
5644
|
+
return { lines: [], observationIds: [] };
|
|
5645
|
+
}
|
|
4903
5646
|
const rows = selected.map((obs) => {
|
|
4904
5647
|
const icon = observationIcon(obs.type);
|
|
4905
5648
|
const fileHint = extractPrimaryFileHint(obs);
|
|
@@ -4917,6 +5660,7 @@ function formatContextIndex(context, shownItems) {
|
|
|
4917
5660
|
}
|
|
4918
5661
|
function formatInspectHints(context, visibleObservationIds = []) {
|
|
4919
5662
|
const hints = [];
|
|
5663
|
+
const continuityState = getStartupContinuityState(context);
|
|
4920
5664
|
if ((context.recentSessions?.length ?? 0) > 0) {
|
|
4921
5665
|
hints.push("recent_sessions");
|
|
4922
5666
|
hints.push("session_story");
|
|
@@ -4935,6 +5679,13 @@ function formatInspectHints(context, visibleObservationIds = []) {
|
|
|
4935
5679
|
if ((context.recentChatMessages?.length ?? 0) > 0) {
|
|
4936
5680
|
hints.push("recent_chat");
|
|
4937
5681
|
}
|
|
5682
|
+
if (hasHookOnlyRecentChat(context)) {
|
|
5683
|
+
hints.push("refresh_chat_recall");
|
|
5684
|
+
}
|
|
5685
|
+
if (continuityState !== "fresh") {
|
|
5686
|
+
hints.push("recent_chat");
|
|
5687
|
+
hints.push("recent_handoffs");
|
|
5688
|
+
}
|
|
4938
5689
|
const unique = Array.from(new Set(hints)).slice(0, 4);
|
|
4939
5690
|
if (unique.length === 0)
|
|
4940
5691
|
return [];
|
|
@@ -4986,13 +5737,13 @@ function filterAdditiveToolFallbacks(toolFallbacks, shownItems) {
|
|
|
4986
5737
|
});
|
|
4987
5738
|
}
|
|
4988
5739
|
function buildPromptFallback(context) {
|
|
4989
|
-
const latest = (context.recentPrompts ?? []).find((prompt) =>
|
|
5740
|
+
const latest = (context.recentPrompts ?? []).find((prompt) => isMeaningfulPrompt3(prompt.prompt));
|
|
4990
5741
|
if (!latest?.prompt)
|
|
4991
5742
|
return null;
|
|
4992
5743
|
return latest.prompt.replace(/\s+/g, " ").trim();
|
|
4993
5744
|
}
|
|
4994
5745
|
function buildPromptLines(context) {
|
|
4995
|
-
return (context.recentPrompts ?? []).filter((prompt) =>
|
|
5746
|
+
return (context.recentPrompts ?? []).filter((prompt) => isMeaningfulPrompt3(prompt.prompt)).slice(0, 2).map((prompt) => {
|
|
4996
5747
|
const prefix = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : "request";
|
|
4997
5748
|
return `${prefix}: ${prompt.prompt.replace(/\s+/g, " ").trim()}`;
|
|
4998
5749
|
}).filter((item) => item.length > 0);
|
|
@@ -5058,11 +5809,11 @@ function buildRecentOutcomeLines(context, summary) {
|
|
|
5058
5809
|
}
|
|
5059
5810
|
}
|
|
5060
5811
|
if (picked.length < 2) {
|
|
5061
|
-
for (const obs of context
|
|
5812
|
+
for (const obs of getFreshStartupObservations(context)) {
|
|
5062
5813
|
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
5063
5814
|
continue;
|
|
5064
|
-
const title =
|
|
5065
|
-
if (!title ||
|
|
5815
|
+
const title = stripInlineSectionLabel3(obs.title);
|
|
5816
|
+
if (!title || looksLikeFileOperationTitle4(title))
|
|
5066
5817
|
continue;
|
|
5067
5818
|
const normalized = normalizeStartupItem(title);
|
|
5068
5819
|
if (!normalized || seen.has(normalized))
|
|
@@ -5077,26 +5828,29 @@ function buildRecentOutcomeLines(context, summary) {
|
|
|
5077
5828
|
}
|
|
5078
5829
|
function buildCurrentThreadLine(context, summary) {
|
|
5079
5830
|
const explicit = summary?.current_thread ?? null;
|
|
5080
|
-
if (explicit && !
|
|
5831
|
+
if (explicit && !looksLikeFileOperationTitle4(explicit))
|
|
5081
5832
|
return explicit;
|
|
5082
5833
|
for (const session of context.recentSessions ?? []) {
|
|
5083
|
-
if (session.current_thread && !
|
|
5834
|
+
if (session.current_thread && !looksLikeFileOperationTitle4(session.current_thread)) {
|
|
5084
5835
|
return session.current_thread;
|
|
5085
5836
|
}
|
|
5086
5837
|
}
|
|
5087
5838
|
const request = buildPromptFallback(context);
|
|
5088
5839
|
const outcome = buildRecentOutcomeLines(context, summary)[0] ?? null;
|
|
5089
5840
|
const tool = buildToolFallbacks(context)[0] ?? null;
|
|
5841
|
+
const hasContinuity = hasFreshContinuitySignal(context);
|
|
5090
5842
|
if (outcome && tool)
|
|
5091
5843
|
return `${outcome} \xB7 ${tool}`;
|
|
5844
|
+
if (!hasContinuity && !outcome)
|
|
5845
|
+
return request;
|
|
5092
5846
|
return outcome ?? request ?? null;
|
|
5093
5847
|
}
|
|
5094
5848
|
function chooseMeaningfulSessionSummary(request, completed) {
|
|
5095
|
-
if (request && !
|
|
5849
|
+
if (request && !looksLikeFileOperationTitle4(request))
|
|
5096
5850
|
return request;
|
|
5097
5851
|
if (completed) {
|
|
5098
5852
|
const lines = completed.split(`
|
|
5099
|
-
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).map((line) =>
|
|
5853
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).map((line) => stripInlineSectionLabel3(line)).filter((line) => !looksLikeFileOperationTitle4(line));
|
|
5100
5854
|
if (lines.length > 0)
|
|
5101
5855
|
return lines[0] ?? null;
|
|
5102
5856
|
}
|
|
@@ -5205,7 +5959,7 @@ function pickContextIndexObservations(context, shownItems) {
|
|
|
5205
5959
|
score += 2.5;
|
|
5206
5960
|
return score;
|
|
5207
5961
|
};
|
|
5208
|
-
for (const obs of context.
|
|
5962
|
+
for (const obs of getFreshStartupObservations(context).filter((obs2) => obs2.type !== "digest").filter((obs2) => {
|
|
5209
5963
|
const normalized = normalizeStartupItem(obs2.title);
|
|
5210
5964
|
return normalized && !hidden.has(normalized);
|
|
5211
5965
|
}).sort((a, b) => {
|
|
@@ -5236,7 +5990,7 @@ function toSplashLines(value, maxItems) {
|
|
|
5236
5990
|
if (!value)
|
|
5237
5991
|
return [];
|
|
5238
5992
|
const lines = value.split(`
|
|
5239
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) =>
|
|
5993
|
+
`).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)}`);
|
|
5240
5994
|
return dedupeFragmentsInLines(lines);
|
|
5241
5995
|
}
|
|
5242
5996
|
function pickPrimarySummary(context) {
|
|
@@ -5247,7 +6001,7 @@ function pickPrimarySummary(context) {
|
|
|
5247
6001
|
const request = summary.request?.trim();
|
|
5248
6002
|
const learned = summary.learned?.trim();
|
|
5249
6003
|
const completed = summary.completed?.trim();
|
|
5250
|
-
return Boolean(request && !
|
|
6004
|
+
return Boolean(request && !looksLikeFileOperationTitle4(request) || learned || hasMeaningfulCompleted(completed));
|
|
5251
6005
|
});
|
|
5252
6006
|
return meaningfulRecent ?? summaries[0] ?? null;
|
|
5253
6007
|
}
|
|
@@ -5255,7 +6009,7 @@ function hasMeaningfulCompleted(value) {
|
|
|
5255
6009
|
if (!value)
|
|
5256
6010
|
return false;
|
|
5257
6011
|
return value.split(`
|
|
5258
|
-
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).some((line) => !
|
|
6012
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).some((line) => !looksLikeFileOperationTitle4(stripInlineSectionLabel3(line)));
|
|
5259
6013
|
}
|
|
5260
6014
|
function sectionItemCount(value) {
|
|
5261
6015
|
if (!value)
|
|
@@ -5280,7 +6034,7 @@ function dedupeFragmentsInLines(lines) {
|
|
|
5280
6034
|
const seen = new Set;
|
|
5281
6035
|
const deduped = [];
|
|
5282
6036
|
for (const line of lines) {
|
|
5283
|
-
const normalized =
|
|
6037
|
+
const normalized = stripInlineSectionLabel3(line).toLowerCase().replace(/\s+/g, " ").trim();
|
|
5284
6038
|
if (!normalized || seen.has(normalized))
|
|
5285
6039
|
continue;
|
|
5286
6040
|
seen.add(normalized);
|
|
@@ -5292,7 +6046,7 @@ function hasRequestSection(lines) {
|
|
|
5292
6046
|
return lines.some((line) => line.includes("Request:"));
|
|
5293
6047
|
}
|
|
5294
6048
|
function normalizeStartupItem(value) {
|
|
5295
|
-
return
|
|
6049
|
+
return stripInlineSectionLabel3(value).replace(/^#?\d+:\s*/, "").replace(/^-\s*/, "").replace(/\([^)]*\)/g, " ").replace(/[^a-z0-9\s]/gi, " ").toLowerCase().replace(/\s+/g, " ").trim();
|
|
5296
6050
|
}
|
|
5297
6051
|
function titlesRoughlyMatch(left, right) {
|
|
5298
6052
|
const a = normalizeStartupItem(left ?? "");
|
|
@@ -5311,7 +6065,7 @@ function titlesRoughlyMatch(left, right) {
|
|
|
5311
6065
|
const minSize = Math.min(aTokens.length, bTokens.length);
|
|
5312
6066
|
return shared.length >= Math.max(3, Math.ceil(minSize * 0.6));
|
|
5313
6067
|
}
|
|
5314
|
-
function
|
|
6068
|
+
function isMeaningfulPrompt3(value) {
|
|
5315
6069
|
if (!value)
|
|
5316
6070
|
return false;
|
|
5317
6071
|
const compact = value.replace(/\s+/g, " ").trim();
|
|
@@ -5320,7 +6074,7 @@ function isMeaningfulPrompt2(value) {
|
|
|
5320
6074
|
return /[a-z]{3,}/i.test(compact);
|
|
5321
6075
|
}
|
|
5322
6076
|
function chooseRequest(primary, fallback) {
|
|
5323
|
-
if (primary && !
|
|
6077
|
+
if (primary && !looksLikeFileOperationTitle4(primary))
|
|
5324
6078
|
return primary;
|
|
5325
6079
|
return fallback;
|
|
5326
6080
|
}
|
|
@@ -5338,10 +6092,10 @@ function isWeakCompletedSection(value) {
|
|
|
5338
6092
|
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean);
|
|
5339
6093
|
if (!items.length)
|
|
5340
6094
|
return true;
|
|
5341
|
-
const weakCount = items.filter((item) =>
|
|
6095
|
+
const weakCount = items.filter((item) => looksLikeFileOperationTitle4(item)).length;
|
|
5342
6096
|
return weakCount === items.length;
|
|
5343
6097
|
}
|
|
5344
|
-
function
|
|
6098
|
+
function looksLikeFileOperationTitle4(value) {
|
|
5345
6099
|
const trimmed = value.trim();
|
|
5346
6100
|
if (/^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(trimmed)) {
|
|
5347
6101
|
return true;
|
|
@@ -5354,7 +6108,7 @@ function looksLikeGenericSummaryWrapper(value) {
|
|
|
5354
6108
|
}
|
|
5355
6109
|
function scoreSplashLine(value) {
|
|
5356
6110
|
let score = 0;
|
|
5357
|
-
if (!
|
|
6111
|
+
if (!looksLikeFileOperationTitle4(value))
|
|
5358
6112
|
score += 2;
|
|
5359
6113
|
if (/[:;]/.test(value))
|
|
5360
6114
|
score += 1;
|
|
@@ -5363,9 +6117,9 @@ function scoreSplashLine(value) {
|
|
|
5363
6117
|
return score;
|
|
5364
6118
|
}
|
|
5365
6119
|
function buildObservationFallbacks(context) {
|
|
5366
|
-
const request = context.
|
|
6120
|
+
const request = getFreshStartupObservations(context).find((obs) => obs.type !== "decision" && !looksLikeFileOperationTitle4(obs.title))?.title ?? null;
|
|
5367
6121
|
const investigated = collectObservationTitles(context, (obs) => obs.type === "discovery", 2);
|
|
5368
|
-
const completed = collectObservationTitles(context, (obs) => ["bugfix", "feature", "refactor", "change"].includes(obs.type) && !
|
|
6122
|
+
const completed = collectObservationTitles(context, (obs) => ["bugfix", "feature", "refactor", "change"].includes(obs.type) && !looksLikeFileOperationTitle4(obs.title), 2);
|
|
5369
6123
|
return {
|
|
5370
6124
|
request,
|
|
5371
6125
|
investigated,
|
|
@@ -5375,21 +6129,42 @@ function buildObservationFallbacks(context) {
|
|
|
5375
6129
|
function collectObservationTitles(context, predicate, limit) {
|
|
5376
6130
|
const seen = new Set;
|
|
5377
6131
|
const picked = [];
|
|
5378
|
-
for (const obs of context
|
|
6132
|
+
for (const obs of getFreshStartupObservations(context)) {
|
|
5379
6133
|
if (!predicate(obs))
|
|
5380
6134
|
continue;
|
|
5381
|
-
const normalized =
|
|
6135
|
+
const normalized = stripInlineSectionLabel3(obs.title).toLowerCase().replace(/\s+/g, " ").trim();
|
|
5382
6136
|
if (!normalized || seen.has(normalized))
|
|
5383
6137
|
continue;
|
|
5384
6138
|
seen.add(normalized);
|
|
5385
|
-
picked.push(`- ${
|
|
6139
|
+
picked.push(`- ${stripInlineSectionLabel3(obs.title)}`);
|
|
5386
6140
|
if (picked.length >= limit)
|
|
5387
6141
|
break;
|
|
5388
6142
|
}
|
|
5389
6143
|
return picked.length ? picked.join(`
|
|
5390
6144
|
`) : null;
|
|
5391
6145
|
}
|
|
5392
|
-
function
|
|
6146
|
+
function getFreshStartupObservations(context) {
|
|
6147
|
+
if (hasFreshContinuitySignal(context))
|
|
6148
|
+
return context.observations;
|
|
6149
|
+
return context.observations.filter((obs) => observationAgeDays3(obs) <= 3);
|
|
6150
|
+
}
|
|
6151
|
+
function hasFreshContinuitySignal(context) {
|
|
6152
|
+
return getStartupContinuityState(context) === "fresh";
|
|
6153
|
+
}
|
|
6154
|
+
function getStartupContinuityState(context) {
|
|
6155
|
+
return classifyContinuityState(context.recentPrompts?.length ?? 0, context.recentToolEvents?.length ?? 0, context.recentHandoffs?.length ?? 0, context.recentChatMessages?.length ?? 0, context.recentSessions ?? [], context.recentOutcomes?.length ?? 0);
|
|
6156
|
+
}
|
|
6157
|
+
function hasHookOnlyRecentChat(context) {
|
|
6158
|
+
const recentChat = context.recentChatMessages ?? [];
|
|
6159
|
+
return recentChat.length > 0 && !recentChat.some((message) => message.source_kind === "transcript");
|
|
6160
|
+
}
|
|
6161
|
+
function observationAgeDays3(obs) {
|
|
6162
|
+
const createdAt = new Date(obs.created_at).getTime();
|
|
6163
|
+
if (!Number.isFinite(createdAt))
|
|
6164
|
+
return Number.POSITIVE_INFINITY;
|
|
6165
|
+
return Math.max(0, (Date.now() - createdAt) / 86400000);
|
|
6166
|
+
}
|
|
6167
|
+
function stripInlineSectionLabel3(value) {
|
|
5393
6168
|
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
5394
6169
|
}
|
|
5395
6170
|
function pickRelevantStaleDecision(context, summary) {
|
|
@@ -5480,7 +6255,8 @@ function capitalize(value) {
|
|
|
5480
6255
|
}
|
|
5481
6256
|
var __testables = {
|
|
5482
6257
|
formatSplashScreen,
|
|
5483
|
-
formatVisibleStartupBrief
|
|
6258
|
+
formatVisibleStartupBrief,
|
|
6259
|
+
getStartupContinuityState
|
|
5484
6260
|
};
|
|
5485
6261
|
runHook("session-start", main);
|
|
5486
6262
|
export {
|