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.
@@ -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 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");
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 parseJsonArray2(value) {
2147
+ function parseJsonStringArray(value) {
1551
2148
  if (!value)
1552
2149
  return [];
1553
2150
  try {
1554
2151
  const parsed = JSON.parse(value);
1555
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
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 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;
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
- function tokenizeProjectHint(text) {
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 parseSummaryJsonList(value) {
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 isObservationRelatedToProject(obs, detected) {
2265
+ function isObservationRelatedToProject2(obs, detected) {
1582
2266
  const hints = new Set([
1583
- ...tokenizeProjectHint(detected.name),
1584
- ...tokenizeProjectHint(detected.canonical_id)
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 estimateTokens(text) {
2288
+ function estimateTokens2(text) {
1605
2289
  if (!text)
1606
2290
  return 0;
1607
2291
  return Math.ceil(text.length / 4);
1608
2292
  }
1609
- function buildSessionContext(db, cwd, options = {}) {
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) => isObservationRelatedToProject(obs, detected));
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
- const all = [...pinned, ...dedupedRecent, ...sorted.slice(0, remaining)];
1708
- const recentPrompts2 = db.getRecentUserPrompts(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
1709
- const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
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 : getProjectTypeCounts(db, projectId, opts.userId);
1712
- const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions2);
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: !isNewProject,
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(toContextObservation),
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 = estimateObservationTokens(obs, selected.length);
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 = estimateObservationTokens(obs, selected.length);
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 = estimateObservationTokens(obs, selected.length);
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 = db.getRecentUserPrompts(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
1757
- const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
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 : getProjectTypeCounts(db, projectId, opts.userId);
1760
- const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions);
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: !isNewProject,
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: selected.map(toContextObservation),
1817
- session_count: selected.length,
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 estimateObservationTokens(obs, index) {
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 = estimateTokens(`- **[${obs.type}]** ${obs.title} (2026-01-01, q=0.5)`);
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 = formatObservationDetail(obs);
1839
- return titleCost + estimateTokens(detailText);
2557
+ const detailText = formatObservationDetail2(obs);
2558
+ return titleCost + estimateTokens2(detailText);
1840
2559
  }
1841
- function formatContextForInjection(context) {
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(`- ${truncateText(`${title} (${formatHandoffSource(handoff)})`, 160)}`);
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(` ${truncateText(narrative, 180)}`);
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}] ${truncateText(message.content.replace(/\s+/g, " ").trim(), 160)}`);
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) => isMeaningfulPrompt(prompt.prompt)).slice(0, 5);
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}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
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}: ${formatToolEventDetail(tool)}`);
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 = chooseMeaningfulSessionHeadline(session.request, session.completed);
2625
+ const summary = chooseMeaningfulSessionHeadline2(session.request, session.completed);
1907
2626
  if (summary === "(no summary)")
1908
2627
  return null;
1909
- return `- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
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(`- ${truncateText(outcome, 160)}`);
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 = formatObservationFiles(obs);
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 = formatObservationDetailFromContext(obs);
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(...formatSessionBrief(summary));
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 formatSessionBrief(summary) {
2700
+ function formatSessionBrief2(summary) {
1982
2701
  const lines = [];
1983
- const heading = summary.request ? `### ${truncateText(summary.request, 120)}` : `### Session ${summary.session_id.slice(0, 8)}`;
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 = formatSummarySection(value, maxLen);
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 chooseMeaningfulSessionHeadline(request, completed) {
2001
- if (request && !looksLikeFileOperationTitle2(request))
2719
+ function chooseMeaningfulSessionHeadline2(request, completed) {
2720
+ if (request && !looksLikeFileOperationTitle3(request))
2002
2721
  return request;
2003
- const completedItems = extractMeaningfulLines(completed, 1);
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 formatSummarySection(value, maxLen) {
2727
+ function formatSummarySection2(value, maxLen) {
2009
2728
  return formatSummaryItems(value, maxLen);
2010
2729
  }
2011
- function truncateText(text, maxLen) {
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 isMeaningfulPrompt(value) {
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 looksLikeFileOperationTitle2(value) {
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 stripInlineSectionLabel(value) {
2746
+ function stripInlineSectionLabel2(value) {
2028
2747
  return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
2029
2748
  }
2030
- function extractMeaningfulLines(value, limit) {
2749
+ function extractMeaningfulLines2(value, limit) {
2031
2750
  if (!value)
2032
2751
  return [];
2033
- return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle2(line)).slice(0, limit);
2752
+ return extractSummaryItems(value).map((line) => stripInlineSectionLabel2(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle3(line)).slice(0, limit);
2034
2753
  }
2035
- function formatObservationDetailFromContext(obs) {
2754
+ function formatObservationDetailFromContext2(obs) {
2036
2755
  if (obs.facts) {
2037
- const bullets = parseFacts(obs.facts);
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 formatObservationDetail(obs) {
2768
+ function formatObservationDetail2(obs) {
2050
2769
  if (obs.facts) {
2051
- const bullets = parseFacts(obs.facts);
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 parseFacts(facts) {
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 toContextObservation(obs) {
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 formatObservationFiles(obs) {
2093
- const modified = parseJsonStringArray(obs.files_modified);
2811
+ function formatObservationFiles2(obs) {
2812
+ const modified = parseJsonStringArray2(obs.files_modified);
2094
2813
  if (modified.length > 0) {
2095
- return ` · files: ${truncateText(modified.slice(0, 2).join(", "), 60)}`;
2814
+ return ` · files: ${truncateText2(modified.slice(0, 2).join(", "), 60)}`;
2096
2815
  }
2097
- const read = parseJsonStringArray(obs.files_read);
2816
+ const read = parseJsonStringArray2(obs.files_read);
2098
2817
  if (read.length > 0) {
2099
- return ` · read: ${truncateText(read.slice(0, 2).join(", "), 60)}`;
2818
+ return ` · read: ${truncateText2(read.slice(0, 2).join(", "), 60)}`;
2100
2819
  }
2101
2820
  return "";
2102
2821
  }
2103
- function parseJsonStringArray(value) {
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 formatToolEventDetail(tool) {
2834
+ function formatToolEventDetail2(tool) {
2116
2835
  const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
2117
- return truncateText(detail || "recent tool execution", 160);
2836
+ return truncateText2(detail || "recent tool execution", 160);
2118
2837
  }
2119
- function getProjectTypeCounts(db, projectId, userId) {
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 getRecentOutcomes(db, projectId, userId, recentSessions) {
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 parseSummaryJsonList(summary.recent_outcomes)) {
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
- ...extractMeaningfulLines(summary.completed, 2),
2155
- ...extractMeaningfulLines(summary.learned, 1)
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 parseSummaryJsonList(session.recent_outcomes)) {
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 = stripInlineSectionLabel(obs.title);
2906
+ const title = stripInlineSectionLabel2(obs.title);
2188
2907
  const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
2189
- if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle2(title))
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/handoffs.ts
2200
- function formatHandoffSource2(handoff) {
2201
- const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
2202
- 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`;
2203
- return `from ${handoff.device_id} · ${ageLabel}`;
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.25";
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) => !looksLikeFileOperationTitle3(obs.title)).slice(0, 3);
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) => isMeaningfulPrompt2(prompt.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) => isMeaningfulPrompt2(prompt.prompt)).slice(0, 2).map((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.observations) {
5812
+ for (const obs of getFreshStartupObservations(context)) {
5062
5813
  if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
5063
5814
  continue;
5064
- const title = stripInlineSectionLabel2(obs.title);
5065
- if (!title || looksLikeFileOperationTitle3(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 && !looksLikeFileOperationTitle3(explicit))
5831
+ if (explicit && !looksLikeFileOperationTitle4(explicit))
5081
5832
  return explicit;
5082
5833
  for (const session of context.recentSessions ?? []) {
5083
- if (session.current_thread && !looksLikeFileOperationTitle3(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 && !looksLikeFileOperationTitle3(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) => stripInlineSectionLabel2(line)).filter((line) => !looksLikeFileOperationTitle3(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.observations.filter((obs2) => obs2.type !== "digest").filter((obs2) => {
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) => stripInlineSectionLabel2(line)).map((line) => dedupeFragments(line)).filter(Boolean).sort((a, b) => scoreSplashLine(b) - scoreSplashLine(a)).slice(0, maxItems).map((line) => `- ${truncateInline(line, 140)}`);
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 && !looksLikeFileOperationTitle3(request) || learned || hasMeaningfulCompleted(completed));
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) => !looksLikeFileOperationTitle3(stripInlineSectionLabel2(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 = stripInlineSectionLabel2(line).toLowerCase().replace(/\s+/g, " ").trim();
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 stripInlineSectionLabel2(value).replace(/^#?\d+:\s*/, "").replace(/^-\s*/, "").replace(/\([^)]*\)/g, " ").replace(/[^a-z0-9\s]/gi, " ").toLowerCase().replace(/\s+/g, " ").trim();
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 isMeaningfulPrompt2(value) {
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 && !looksLikeFileOperationTitle3(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) => looksLikeFileOperationTitle3(item)).length;
6095
+ const weakCount = items.filter((item) => looksLikeFileOperationTitle4(item)).length;
5342
6096
  return weakCount === items.length;
5343
6097
  }
5344
- function looksLikeFileOperationTitle3(value) {
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 (!looksLikeFileOperationTitle3(value))
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.observations.find((obs) => obs.type !== "decision" && !looksLikeFileOperationTitle3(obs.title))?.title ?? null;
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) && !looksLikeFileOperationTitle3(obs.title), 2);
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.observations) {
6132
+ for (const obs of getFreshStartupObservations(context)) {
5379
6133
  if (!predicate(obs))
5380
6134
  continue;
5381
- const normalized = stripInlineSectionLabel2(obs.title).toLowerCase().replace(/\s+/g, " ").trim();
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(`- ${stripInlineSectionLabel2(obs.title)}`);
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 stripInlineSectionLabel2(value) {
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 {