@lossless-claude/lcm 0.2.0

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.
Files changed (292) hide show
  1. package/.claude-plugin/commands/lcm-compact.md +31 -0
  2. package/.claude-plugin/commands/lcm-curate.md +31 -0
  3. package/.claude-plugin/commands/lcm-diagnose.md +29 -0
  4. package/.claude-plugin/commands/lcm-doctor.md +23 -0
  5. package/.claude-plugin/commands/lcm-dogfood.md +101 -0
  6. package/.claude-plugin/commands/lcm-import.md +48 -0
  7. package/.claude-plugin/commands/lcm-promote.md +29 -0
  8. package/.claude-plugin/commands/lcm-sensitive.md +55 -0
  9. package/.claude-plugin/commands/lcm-stats.md +19 -0
  10. package/.claude-plugin/commands/lcm-status.md +27 -0
  11. package/.claude-plugin/hooks/README.md +47 -0
  12. package/.claude-plugin/marketplace.json +20 -0
  13. package/.claude-plugin/mcp.mjs +12 -0
  14. package/.claude-plugin/plugin.json +46 -0
  15. package/.claude-plugin/skills/lcm-context/SKILL.md +107 -0
  16. package/.claude-plugin/skills/lcm-dogfood/SKILL.md +102 -0
  17. package/.claude-plugin/skills/lcm-dogfood/references/checks.md +239 -0
  18. package/.claude-plugin/skills/lcm-dogfood/references/known-issues.md +11 -0
  19. package/.claude-plugin/skills/lcm-dogfood/scripts/db-integrity.js +40 -0
  20. package/.claude-plugin/skills/lcm-dogfood/scripts/prompt-search-test.js +35 -0
  21. package/.claude-plugin/skills/lcm-e2e/SKILL.md +61 -0
  22. package/.claude-plugin/skills/lcm-e2e/checklist.md +367 -0
  23. package/.claude-plugin/skills/lossless-claude-upgrade/SKILL.md +47 -0
  24. package/LICENSE +21 -0
  25. package/README.md +231 -0
  26. package/dist/bin/lcm.d.ts +2 -0
  27. package/dist/bin/lcm.js +461 -0
  28. package/dist/bin/lcm.js.map +1 -0
  29. package/dist/installer/dry-run-deps.d.ts +23 -0
  30. package/dist/installer/dry-run-deps.js +66 -0
  31. package/dist/installer/dry-run-deps.js.map +1 -0
  32. package/dist/installer/install.d.ts +39 -0
  33. package/dist/installer/install.js +236 -0
  34. package/dist/installer/install.js.map +1 -0
  35. package/dist/installer/uninstall.d.ts +11 -0
  36. package/dist/installer/uninstall.js +80 -0
  37. package/dist/installer/uninstall.js.map +1 -0
  38. package/dist/src/batch-compact.d.ts +16 -0
  39. package/dist/src/batch-compact.js +121 -0
  40. package/dist/src/batch-compact.js.map +1 -0
  41. package/dist/src/compaction.d.ts +198 -0
  42. package/dist/src/compaction.js +964 -0
  43. package/dist/src/compaction.js.map +1 -0
  44. package/dist/src/connectors/constants.d.ts +5 -0
  45. package/dist/src/connectors/constants.js +6 -0
  46. package/dist/src/connectors/constants.js.map +1 -0
  47. package/dist/src/connectors/installer.d.ts +16 -0
  48. package/dist/src/connectors/installer.js +200 -0
  49. package/dist/src/connectors/installer.js.map +1 -0
  50. package/dist/src/connectors/registry.d.ts +4 -0
  51. package/dist/src/connectors/registry.js +264 -0
  52. package/dist/src/connectors/registry.js.map +1 -0
  53. package/dist/src/connectors/template-service.d.ts +5 -0
  54. package/dist/src/connectors/template-service.js +54 -0
  55. package/dist/src/connectors/template-service.js.map +1 -0
  56. package/dist/src/connectors/templates/base.md +1 -0
  57. package/dist/src/connectors/templates/mcp-base.md +1 -0
  58. package/dist/src/connectors/templates/sections/command-reference.md +15 -0
  59. package/dist/src/connectors/templates/sections/mcp-workflow.md +18 -0
  60. package/dist/src/connectors/templates/sections/workflow.md +29 -0
  61. package/dist/src/connectors/templates/skill/SKILL.md +74 -0
  62. package/dist/src/connectors/types.d.ts +19 -0
  63. package/dist/src/connectors/types.js +10 -0
  64. package/dist/src/connectors/types.js.map +1 -0
  65. package/dist/src/daemon/client.d.ts +9 -0
  66. package/dist/src/daemon/client.js +28 -0
  67. package/dist/src/daemon/client.js.map +1 -0
  68. package/dist/src/daemon/config.d.ts +48 -0
  69. package/dist/src/daemon/config.js +67 -0
  70. package/dist/src/daemon/config.js.map +1 -0
  71. package/dist/src/daemon/lifecycle.d.ts +19 -0
  72. package/dist/src/daemon/lifecycle.js +102 -0
  73. package/dist/src/daemon/lifecycle.js.map +1 -0
  74. package/dist/src/daemon/orientation.d.ts +1 -0
  75. package/dist/src/daemon/orientation.js +9 -0
  76. package/dist/src/daemon/orientation.js.map +1 -0
  77. package/dist/src/daemon/project-queue.d.ts +1 -0
  78. package/dist/src/daemon/project-queue.js +17 -0
  79. package/dist/src/daemon/project-queue.js.map +1 -0
  80. package/dist/src/daemon/project.d.ts +7 -0
  81. package/dist/src/daemon/project.js +25 -0
  82. package/dist/src/daemon/project.js.map +1 -0
  83. package/dist/src/daemon/proxy-manager.d.ts +21 -0
  84. package/dist/src/daemon/proxy-manager.js +205 -0
  85. package/dist/src/daemon/proxy-manager.js.map +1 -0
  86. package/dist/src/daemon/routes/compact.d.ts +13 -0
  87. package/dist/src/daemon/routes/compact.js +195 -0
  88. package/dist/src/daemon/routes/compact.js.map +1 -0
  89. package/dist/src/daemon/routes/describe.d.ts +3 -0
  90. package/dist/src/daemon/routes/describe.js +39 -0
  91. package/dist/src/daemon/routes/describe.js.map +1 -0
  92. package/dist/src/daemon/routes/expand.d.ts +3 -0
  93. package/dist/src/daemon/routes/expand.js +41 -0
  94. package/dist/src/daemon/routes/expand.js.map +1 -0
  95. package/dist/src/daemon/routes/grep.d.ts +3 -0
  96. package/dist/src/daemon/routes/grep.js +43 -0
  97. package/dist/src/daemon/routes/grep.js.map +1 -0
  98. package/dist/src/daemon/routes/ingest.d.ts +3 -0
  99. package/dist/src/daemon/routes/ingest.js +101 -0
  100. package/dist/src/daemon/routes/ingest.js.map +1 -0
  101. package/dist/src/daemon/routes/promote.d.ts +4 -0
  102. package/dist/src/daemon/routes/promote.js +104 -0
  103. package/dist/src/daemon/routes/promote.js.map +1 -0
  104. package/dist/src/daemon/routes/prompt-search.d.ts +3 -0
  105. package/dist/src/daemon/routes/prompt-search.js +65 -0
  106. package/dist/src/daemon/routes/prompt-search.js.map +1 -0
  107. package/dist/src/daemon/routes/recent.d.ts +3 -0
  108. package/dist/src/daemon/routes/recent.js +37 -0
  109. package/dist/src/daemon/routes/recent.js.map +1 -0
  110. package/dist/src/daemon/routes/restore.d.ts +3 -0
  111. package/dist/src/daemon/routes/restore.js +120 -0
  112. package/dist/src/daemon/routes/restore.js.map +1 -0
  113. package/dist/src/daemon/routes/search.d.ts +2 -0
  114. package/dist/src/daemon/routes/search.js +66 -0
  115. package/dist/src/daemon/routes/search.js.map +1 -0
  116. package/dist/src/daemon/routes/status.d.ts +3 -0
  117. package/dist/src/daemon/routes/status.js +80 -0
  118. package/dist/src/daemon/routes/status.js.map +1 -0
  119. package/dist/src/daemon/routes/store.d.ts +2 -0
  120. package/dist/src/daemon/routes/store.js +46 -0
  121. package/dist/src/daemon/routes/store.js.map +1 -0
  122. package/dist/src/daemon/server.d.ts +19 -0
  123. package/dist/src/daemon/server.js +183 -0
  124. package/dist/src/daemon/server.js.map +1 -0
  125. package/dist/src/daemon/summarizer.d.ts +11 -0
  126. package/dist/src/daemon/summarizer.js +51 -0
  127. package/dist/src/daemon/summarizer.js.map +1 -0
  128. package/dist/src/db/config.d.ts +31 -0
  129. package/dist/src/db/config.js +83 -0
  130. package/dist/src/db/config.js.map +1 -0
  131. package/dist/src/db/connection.d.ts +3 -0
  132. package/dist/src/db/connection.js +62 -0
  133. package/dist/src/db/connection.js.map +1 -0
  134. package/dist/src/db/features.d.ts +11 -0
  135. package/dist/src/db/features.js +36 -0
  136. package/dist/src/db/features.js.map +1 -0
  137. package/dist/src/db/migration.d.ts +4 -0
  138. package/dist/src/db/migration.js +499 -0
  139. package/dist/src/db/migration.js.map +1 -0
  140. package/dist/src/db/promoted.d.ts +47 -0
  141. package/dist/src/db/promoted.js +96 -0
  142. package/dist/src/db/promoted.js.map +1 -0
  143. package/dist/src/db/redaction-stats.d.ts +6 -0
  144. package/dist/src/db/redaction-stats.js +16 -0
  145. package/dist/src/db/redaction-stats.js.map +1 -0
  146. package/dist/src/diagnose.d.ts +39 -0
  147. package/dist/src/diagnose.js +432 -0
  148. package/dist/src/diagnose.js.map +1 -0
  149. package/dist/src/doctor/doctor.d.ts +4 -0
  150. package/dist/src/doctor/doctor.js +378 -0
  151. package/dist/src/doctor/doctor.js.map +1 -0
  152. package/dist/src/doctor/types.d.ts +24 -0
  153. package/dist/src/doctor/types.js +2 -0
  154. package/dist/src/doctor/types.js.map +1 -0
  155. package/dist/src/expansion.d.ts +100 -0
  156. package/dist/src/expansion.js +268 -0
  157. package/dist/src/expansion.js.map +1 -0
  158. package/dist/src/hooks/auto-heal.d.ts +12 -0
  159. package/dist/src/hooks/auto-heal.js +49 -0
  160. package/dist/src/hooks/auto-heal.js.map +1 -0
  161. package/dist/src/hooks/compact.d.ts +5 -0
  162. package/dist/src/hooks/compact.js +22 -0
  163. package/dist/src/hooks/compact.js.map +1 -0
  164. package/dist/src/hooks/dispatch.d.ts +7 -0
  165. package/dist/src/hooks/dispatch.js +36 -0
  166. package/dist/src/hooks/dispatch.js.map +1 -0
  167. package/dist/src/hooks/probe-precompact.d.ts +2 -0
  168. package/dist/src/hooks/probe-precompact.js +17 -0
  169. package/dist/src/hooks/probe-precompact.js.map +1 -0
  170. package/dist/src/hooks/probe-sessionstart.d.ts +2 -0
  171. package/dist/src/hooks/probe-sessionstart.js +18 -0
  172. package/dist/src/hooks/probe-sessionstart.js.map +1 -0
  173. package/dist/src/hooks/restore.d.ts +5 -0
  174. package/dist/src/hooks/restore.js +19 -0
  175. package/dist/src/hooks/restore.js.map +1 -0
  176. package/dist/src/hooks/session-end.d.ts +16 -0
  177. package/dist/src/hooks/session-end.js +73 -0
  178. package/dist/src/hooks/session-end.js.map +1 -0
  179. package/dist/src/hooks/user-prompt.d.ts +5 -0
  180. package/dist/src/hooks/user-prompt.js +31 -0
  181. package/dist/src/hooks/user-prompt.js.map +1 -0
  182. package/dist/src/import.d.ts +24 -0
  183. package/dist/src/import.js +119 -0
  184. package/dist/src/import.js.map +1 -0
  185. package/dist/src/large-files.d.ts +28 -0
  186. package/dist/src/large-files.js +413 -0
  187. package/dist/src/large-files.js.map +1 -0
  188. package/dist/src/llm/anthropic.d.ts +9 -0
  189. package/dist/src/llm/anthropic.js +54 -0
  190. package/dist/src/llm/anthropic.js.map +1 -0
  191. package/dist/src/llm/claude-process.d.ts +2 -0
  192. package/dist/src/llm/claude-process.js +55 -0
  193. package/dist/src/llm/claude-process.js.map +1 -0
  194. package/dist/src/llm/codex-process.d.ts +15 -0
  195. package/dist/src/llm/codex-process.js +142 -0
  196. package/dist/src/llm/codex-process.js.map +1 -0
  197. package/dist/src/llm/mock-summarizer.d.ts +9 -0
  198. package/dist/src/llm/mock-summarizer.js +17 -0
  199. package/dist/src/llm/mock-summarizer.js.map +1 -0
  200. package/dist/src/llm/openai.d.ts +10 -0
  201. package/dist/src/llm/openai.js +52 -0
  202. package/dist/src/llm/openai.js.map +1 -0
  203. package/dist/src/llm/types.d.ts +6 -0
  204. package/dist/src/llm/types.js +2 -0
  205. package/dist/src/llm/types.js.map +1 -0
  206. package/dist/src/mcp/server.d.ts +9 -0
  207. package/dist/src/mcp/server.js +128 -0
  208. package/dist/src/mcp/server.js.map +1 -0
  209. package/dist/src/mcp/tools/lcm-describe.d.ts +14 -0
  210. package/dist/src/mcp/tools/lcm-describe.js +12 -0
  211. package/dist/src/mcp/tools/lcm-describe.js.map +1 -0
  212. package/dist/src/mcp/tools/lcm-doctor.d.ts +8 -0
  213. package/dist/src/mcp/tools/lcm-doctor.js +9 -0
  214. package/dist/src/mcp/tools/lcm-doctor.js.map +1 -0
  215. package/dist/src/mcp/tools/lcm-expand.d.ts +18 -0
  216. package/dist/src/mcp/tools/lcm-expand.js +13 -0
  217. package/dist/src/mcp/tools/lcm-expand.js.map +1 -0
  218. package/dist/src/mcp/tools/lcm-grep.d.ts +27 -0
  219. package/dist/src/mcp/tools/lcm-grep.js +15 -0
  220. package/dist/src/mcp/tools/lcm-grep.js.map +1 -0
  221. package/dist/src/mcp/tools/lcm-search.d.ts +33 -0
  222. package/dist/src/mcp/tools/lcm-search.js +15 -0
  223. package/dist/src/mcp/tools/lcm-search.js.map +1 -0
  224. package/dist/src/mcp/tools/lcm-stats.d.ts +14 -0
  225. package/dist/src/mcp/tools/lcm-stats.js +11 -0
  226. package/dist/src/mcp/tools/lcm-stats.js.map +1 -0
  227. package/dist/src/mcp/tools/lcm-store.d.ts +26 -0
  228. package/dist/src/mcp/tools/lcm-store.js +22 -0
  229. package/dist/src/mcp/tools/lcm-store.js.map +1 -0
  230. package/dist/src/memory/index.d.ts +22 -0
  231. package/dist/src/memory/index.js +21 -0
  232. package/dist/src/memory/index.js.map +1 -0
  233. package/dist/src/promotion/dedup.d.ts +19 -0
  234. package/dist/src/promotion/dedup.js +42 -0
  235. package/dist/src/promotion/dedup.js.map +1 -0
  236. package/dist/src/promotion/detector.d.ts +15 -0
  237. package/dist/src/promotion/detector.js +31 -0
  238. package/dist/src/promotion/detector.js.map +1 -0
  239. package/dist/src/prompts/condensed-d1.yaml +38 -0
  240. package/dist/src/prompts/condensed-d2.yaml +32 -0
  241. package/dist/src/prompts/condensed-d3plus.yaml +32 -0
  242. package/dist/src/prompts/leaf-aggressive.yaml +34 -0
  243. package/dist/src/prompts/leaf-normal.yaml +34 -0
  244. package/dist/src/prompts/loader.d.ts +9 -0
  245. package/dist/src/prompts/loader.js +37 -0
  246. package/dist/src/prompts/loader.js.map +1 -0
  247. package/dist/src/prompts/promoted-merge.yaml +18 -0
  248. package/dist/src/prompts/system.yaml +5 -0
  249. package/dist/src/retrieval.d.ts +122 -0
  250. package/dist/src/retrieval.js +214 -0
  251. package/dist/src/retrieval.js.map +1 -0
  252. package/dist/src/scrub.d.ts +46 -0
  253. package/dist/src/scrub.js +184 -0
  254. package/dist/src/scrub.js.map +1 -0
  255. package/dist/src/sensitive.d.ts +4 -0
  256. package/dist/src/sensitive.js +258 -0
  257. package/dist/src/sensitive.js.map +1 -0
  258. package/dist/src/stats.d.ts +34 -0
  259. package/dist/src/stats.js +260 -0
  260. package/dist/src/stats.js.map +1 -0
  261. package/dist/src/store/conversation-store.d.ts +115 -0
  262. package/dist/src/store/conversation-store.js +447 -0
  263. package/dist/src/store/conversation-store.js.map +1 -0
  264. package/dist/src/store/fts5-sanitize.d.ts +23 -0
  265. package/dist/src/store/fts5-sanitize.js +30 -0
  266. package/dist/src/store/fts5-sanitize.js.map +1 -0
  267. package/dist/src/store/full-text-fallback.d.ts +16 -0
  268. package/dist/src/store/full-text-fallback.js +60 -0
  269. package/dist/src/store/full-text-fallback.js.map +1 -0
  270. package/dist/src/store/index.d.ts +4 -0
  271. package/dist/src/store/index.js +3 -0
  272. package/dist/src/store/index.js.map +1 -0
  273. package/dist/src/store/summary-store.d.ts +118 -0
  274. package/dist/src/store/summary-store.js +570 -0
  275. package/dist/src/store/summary-store.js.map +1 -0
  276. package/dist/src/summarize.d.ts +59 -0
  277. package/dist/src/summarize.js +619 -0
  278. package/dist/src/summarize.js.map +1 -0
  279. package/dist/src/transcript.d.ts +7 -0
  280. package/dist/src/transcript.js +51 -0
  281. package/dist/src/transcript.js.map +1 -0
  282. package/dist/src/types.d.ts +116 -0
  283. package/dist/src/types.js +8 -0
  284. package/dist/src/types.js.map +1 -0
  285. package/docs/agent-tools.md +187 -0
  286. package/docs/architecture.md +224 -0
  287. package/docs/configuration.md +168 -0
  288. package/docs/fts5.md +161 -0
  289. package/docs/hook-protocol.md +41 -0
  290. package/docs/privacy.md +101 -0
  291. package/mcp.mjs +17 -0
  292. package/package.json +79 -0
@@ -0,0 +1,964 @@
1
+ import { createHash } from "node:crypto";
2
+ import { extractFileIdsFromContent } from "./large-files.js";
3
+ // ── Helpers ──────────────────────────────────────────────────────────────────
4
+ /** Estimate token count from character length (~4 chars per token). */
5
+ function estimateTokens(content) {
6
+ return Math.ceil(content.length / 4);
7
+ }
8
+ /** Format a timestamp as `YYYY-MM-DD HH:mm TZ` for prompt source text. */
9
+ export function formatTimestamp(value, timezone = "UTC") {
10
+ try {
11
+ const fmt = new Intl.DateTimeFormat("en-CA", {
12
+ timeZone: timezone,
13
+ year: "numeric",
14
+ month: "2-digit",
15
+ day: "2-digit",
16
+ hour: "2-digit",
17
+ minute: "2-digit",
18
+ hour12: false,
19
+ });
20
+ const parts = Object.fromEntries(fmt.formatToParts(value).map((p) => [p.type, p.value]));
21
+ const tzAbbr = timezone === "UTC" ? "UTC" : shortTzAbbr(value, timezone);
22
+ return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute} ${tzAbbr}`;
23
+ }
24
+ catch {
25
+ // Fallback to UTC on invalid timezone
26
+ const year = value.getUTCFullYear();
27
+ const month = String(value.getUTCMonth() + 1).padStart(2, "0");
28
+ const day = String(value.getUTCDate()).padStart(2, "0");
29
+ const hours = String(value.getUTCHours()).padStart(2, "0");
30
+ const minutes = String(value.getUTCMinutes()).padStart(2, "0");
31
+ return `${year}-${month}-${day} ${hours}:${minutes} UTC`;
32
+ }
33
+ }
34
+ /** Extract short timezone abbreviation (e.g. "PST", "PDT", "EST"). */
35
+ function shortTzAbbr(value, timezone) {
36
+ try {
37
+ const abbr = new Intl.DateTimeFormat("en-US", {
38
+ timeZone: timezone,
39
+ timeZoneName: "short",
40
+ })
41
+ .formatToParts(value)
42
+ .find((p) => p.type === "timeZoneName")?.value;
43
+ return abbr ?? timezone;
44
+ }
45
+ catch {
46
+ return timezone;
47
+ }
48
+ }
49
+ /** Generate a deterministic summary ID from content + timestamp. */
50
+ function generateSummaryId(content) {
51
+ return ("sum_" +
52
+ createHash("sha256")
53
+ .update(content + Date.now().toString())
54
+ .digest("hex")
55
+ .slice(0, 16));
56
+ }
57
+ /** Maximum characters for the deterministic fallback truncation (512 tokens * 4 chars). */
58
+ const FALLBACK_MAX_CHARS = 512 * 4;
59
+ const DEFAULT_LEAF_CHUNK_TOKENS = 20_000;
60
+ const CONDENSED_MIN_INPUT_RATIO = 0.1;
61
+ function dedupeOrderedIds(ids) {
62
+ const seen = new Set();
63
+ const ordered = [];
64
+ for (const id of ids) {
65
+ if (!seen.has(id)) {
66
+ seen.add(id);
67
+ ordered.push(id);
68
+ }
69
+ }
70
+ return ordered;
71
+ }
72
+ // ── CompactionEngine ─────────────────────────────────────────────────────────
73
+ export class CompactionEngine {
74
+ conversationStore;
75
+ summaryStore;
76
+ config;
77
+ constructor(conversationStore, summaryStore, config) {
78
+ this.conversationStore = conversationStore;
79
+ this.summaryStore = summaryStore;
80
+ this.config = config;
81
+ }
82
+ // ── evaluate ─────────────────────────────────────────────────────────────
83
+ /** Evaluate whether compaction is needed. */
84
+ async evaluate(conversationId, tokenBudget, observedTokenCount) {
85
+ const storedTokens = await this.summaryStore.getContextTokenCount(conversationId);
86
+ const liveTokens = typeof observedTokenCount === "number" &&
87
+ Number.isFinite(observedTokenCount) &&
88
+ observedTokenCount > 0
89
+ ? Math.floor(observedTokenCount)
90
+ : 0;
91
+ const currentTokens = Math.max(storedTokens, liveTokens);
92
+ const threshold = Math.floor(this.config.contextThreshold * tokenBudget);
93
+ if (currentTokens > threshold) {
94
+ return {
95
+ shouldCompact: true,
96
+ reason: "threshold",
97
+ currentTokens,
98
+ threshold,
99
+ };
100
+ }
101
+ return {
102
+ shouldCompact: false,
103
+ reason: "none",
104
+ currentTokens,
105
+ threshold,
106
+ };
107
+ }
108
+ /**
109
+ * Evaluate whether the raw-message leaf trigger is active.
110
+ *
111
+ * Counts message tokens outside the protected fresh tail and compares against
112
+ * `leafChunkTokens`. This lets callers trigger a soft incremental leaf pass
113
+ * before the full context threshold is breached.
114
+ */
115
+ async evaluateLeafTrigger(conversationId) {
116
+ const rawTokensOutsideTail = await this.countRawTokensOutsideFreshTail(conversationId);
117
+ const threshold = this.resolveLeafChunkTokens();
118
+ return {
119
+ shouldCompact: rawTokensOutsideTail >= threshold,
120
+ rawTokensOutsideTail,
121
+ threshold,
122
+ };
123
+ }
124
+ // ── compact ──────────────────────────────────────────────────────────────
125
+ /** Run a full compaction sweep for a conversation. */
126
+ async compact(input) {
127
+ return this.compactFullSweep(input);
128
+ }
129
+ /**
130
+ * Run a single leaf pass against the oldest compactable raw chunk.
131
+ *
132
+ * This is the soft-trigger path used for incremental maintenance.
133
+ */
134
+ async compactLeaf(input) {
135
+ const { conversationId, tokenBudget, summarize, force } = input;
136
+ const tokensBefore = await this.summaryStore.getContextTokenCount(conversationId);
137
+ const threshold = Math.floor(this.config.contextThreshold * tokenBudget);
138
+ const leafTrigger = await this.evaluateLeafTrigger(conversationId);
139
+ if (!force && tokensBefore <= threshold && !leafTrigger.shouldCompact) {
140
+ return {
141
+ actionTaken: false,
142
+ tokensBefore,
143
+ tokensAfter: tokensBefore,
144
+ condensed: false,
145
+ };
146
+ }
147
+ const leafChunk = await this.selectOldestLeafChunk(conversationId);
148
+ if (leafChunk.items.length === 0) {
149
+ return {
150
+ actionTaken: false,
151
+ tokensBefore,
152
+ tokensAfter: tokensBefore,
153
+ condensed: false,
154
+ };
155
+ }
156
+ const previousSummaryContent = input.previousSummaryContent ??
157
+ (await this.resolvePriorLeafSummaryContext(conversationId, leafChunk.items));
158
+ const leafResult = await this.leafPass(conversationId, leafChunk.items, summarize, previousSummaryContent);
159
+ const tokensAfterLeaf = await this.summaryStore.getContextTokenCount(conversationId);
160
+ await this.persistCompactionEvents({
161
+ conversationId,
162
+ tokensBefore,
163
+ tokensAfterLeaf,
164
+ tokensAfterFinal: tokensAfterLeaf,
165
+ leafResult: { summaryId: leafResult.summaryId, level: leafResult.level },
166
+ condenseResult: null,
167
+ });
168
+ let tokensAfter = tokensAfterLeaf;
169
+ let condensed = false;
170
+ let createdSummaryId = leafResult.summaryId;
171
+ let level = leafResult.level;
172
+ const incrementalMaxDepth = this.resolveIncrementalMaxDepth();
173
+ const condensedMinChunkTokens = this.resolveCondensedMinChunkTokens();
174
+ if (incrementalMaxDepth > 0) {
175
+ for (let targetDepth = 0; targetDepth < incrementalMaxDepth; targetDepth++) {
176
+ const fanout = this.resolveFanoutForDepth(targetDepth, false);
177
+ const chunk = await this.selectOldestChunkAtDepth(conversationId, targetDepth);
178
+ if (chunk.items.length < fanout || chunk.summaryTokens < condensedMinChunkTokens) {
179
+ break;
180
+ }
181
+ const passTokensBefore = await this.summaryStore.getContextTokenCount(conversationId);
182
+ const condenseResult = await this.condensedPass(conversationId, chunk.items, targetDepth, summarize);
183
+ const passTokensAfter = await this.summaryStore.getContextTokenCount(conversationId);
184
+ await this.persistCompactionEvents({
185
+ conversationId,
186
+ tokensBefore: passTokensBefore,
187
+ tokensAfterLeaf: passTokensBefore,
188
+ tokensAfterFinal: passTokensAfter,
189
+ leafResult: null,
190
+ condenseResult,
191
+ });
192
+ tokensAfter = passTokensAfter;
193
+ condensed = true;
194
+ createdSummaryId = condenseResult.summaryId;
195
+ level = condenseResult.level;
196
+ if (passTokensAfter >= passTokensBefore) {
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ return {
202
+ actionTaken: true,
203
+ tokensBefore,
204
+ tokensAfter,
205
+ createdSummaryId,
206
+ condensed,
207
+ level,
208
+ };
209
+ }
210
+ /**
211
+ * Run a hard-trigger sweep:
212
+ *
213
+ * Phase 1: repeatedly compact raw-message chunks outside the fresh tail.
214
+ * Phase 2: repeatedly condense oldest summary chunks while chunk utilization
215
+ * remains high enough to be worthwhile.
216
+ */
217
+ async compactFullSweep(input) {
218
+ const { conversationId, tokenBudget, summarize, force, hardTrigger } = input;
219
+ const tokensBefore = await this.summaryStore.getContextTokenCount(conversationId);
220
+ const threshold = Math.floor(this.config.contextThreshold * tokenBudget);
221
+ const leafTrigger = await this.evaluateLeafTrigger(conversationId);
222
+ if (!force && tokensBefore <= threshold && !leafTrigger.shouldCompact) {
223
+ return {
224
+ actionTaken: false,
225
+ tokensBefore,
226
+ tokensAfter: tokensBefore,
227
+ condensed: false,
228
+ };
229
+ }
230
+ const contextItems = await this.summaryStore.getContextItems(conversationId);
231
+ if (contextItems.length === 0) {
232
+ return {
233
+ actionTaken: false,
234
+ tokensBefore,
235
+ tokensAfter: tokensBefore,
236
+ condensed: false,
237
+ };
238
+ }
239
+ let actionTaken = false;
240
+ let condensed = false;
241
+ let createdSummaryId;
242
+ let level;
243
+ let previousSummaryContent;
244
+ let previousTokens = tokensBefore;
245
+ // Phase 1: leaf passes over oldest raw chunks outside the protected tail.
246
+ while (true) {
247
+ const leafChunk = await this.selectOldestLeafChunk(conversationId);
248
+ if (leafChunk.items.length === 0) {
249
+ break;
250
+ }
251
+ const passTokensBefore = await this.summaryStore.getContextTokenCount(conversationId);
252
+ const leafResult = await this.leafPass(conversationId, leafChunk.items, summarize, previousSummaryContent);
253
+ const passTokensAfter = await this.summaryStore.getContextTokenCount(conversationId);
254
+ await this.persistCompactionEvents({
255
+ conversationId,
256
+ tokensBefore: passTokensBefore,
257
+ tokensAfterLeaf: passTokensAfter,
258
+ tokensAfterFinal: passTokensAfter,
259
+ leafResult: { summaryId: leafResult.summaryId, level: leafResult.level },
260
+ condenseResult: null,
261
+ });
262
+ actionTaken = true;
263
+ createdSummaryId = leafResult.summaryId;
264
+ level = leafResult.level;
265
+ previousSummaryContent = leafResult.content;
266
+ if (passTokensAfter >= passTokensBefore || passTokensAfter >= previousTokens) {
267
+ break;
268
+ }
269
+ previousTokens = passTokensAfter;
270
+ }
271
+ // Phase 2: depth-aware condensed passes, always processing shallowest depth first.
272
+ while (true) {
273
+ const candidate = await this.selectShallowestCondensationCandidate({
274
+ conversationId,
275
+ hardTrigger: hardTrigger === true,
276
+ });
277
+ if (!candidate) {
278
+ break;
279
+ }
280
+ const passTokensBefore = await this.summaryStore.getContextTokenCount(conversationId);
281
+ const condenseResult = await this.condensedPass(conversationId, candidate.chunk.items, candidate.targetDepth, summarize);
282
+ const passTokensAfter = await this.summaryStore.getContextTokenCount(conversationId);
283
+ await this.persistCompactionEvents({
284
+ conversationId,
285
+ tokensBefore: passTokensBefore,
286
+ tokensAfterLeaf: passTokensBefore,
287
+ tokensAfterFinal: passTokensAfter,
288
+ leafResult: null,
289
+ condenseResult,
290
+ });
291
+ actionTaken = true;
292
+ condensed = true;
293
+ createdSummaryId = condenseResult.summaryId;
294
+ level = condenseResult.level;
295
+ if (passTokensAfter >= passTokensBefore || passTokensAfter >= previousTokens) {
296
+ break;
297
+ }
298
+ previousTokens = passTokensAfter;
299
+ }
300
+ const tokensAfter = await this.summaryStore.getContextTokenCount(conversationId);
301
+ return {
302
+ actionTaken,
303
+ tokensBefore,
304
+ tokensAfter,
305
+ createdSummaryId,
306
+ condensed,
307
+ level,
308
+ };
309
+ }
310
+ // ── compactUntilUnder ────────────────────────────────────────────────────
311
+ /** Compact until under the requested target, running up to maxRounds. */
312
+ async compactUntilUnder(input) {
313
+ const { conversationId, tokenBudget, summarize } = input;
314
+ const targetTokens = typeof input.targetTokens === "number" &&
315
+ Number.isFinite(input.targetTokens) &&
316
+ input.targetTokens > 0
317
+ ? Math.floor(input.targetTokens)
318
+ : tokenBudget;
319
+ const storedTokens = await this.summaryStore.getContextTokenCount(conversationId);
320
+ const liveTokens = typeof input.currentTokens === "number" &&
321
+ Number.isFinite(input.currentTokens) &&
322
+ input.currentTokens > 0
323
+ ? Math.floor(input.currentTokens)
324
+ : 0;
325
+ let lastTokens = Math.max(storedTokens, liveTokens);
326
+ // For forced overflow recovery, callers may pass an observed count that
327
+ // equals the context budget. Treat equality as still needing a compaction
328
+ // attempt so we can create headroom for provider-side framing overhead.
329
+ if (lastTokens < targetTokens) {
330
+ return { success: true, rounds: 0, finalTokens: lastTokens };
331
+ }
332
+ for (let round = 1; round <= this.config.maxRounds; round++) {
333
+ const result = await this.compact({
334
+ conversationId,
335
+ tokenBudget,
336
+ summarize,
337
+ force: true,
338
+ });
339
+ if (result.tokensAfter <= targetTokens) {
340
+ return {
341
+ success: true,
342
+ rounds: round,
343
+ finalTokens: result.tokensAfter,
344
+ };
345
+ }
346
+ // No progress -- bail to avoid infinite loop
347
+ if (!result.actionTaken || result.tokensAfter >= lastTokens) {
348
+ return {
349
+ success: false,
350
+ rounds: round,
351
+ finalTokens: result.tokensAfter,
352
+ };
353
+ }
354
+ lastTokens = result.tokensAfter;
355
+ }
356
+ // Exhausted all rounds
357
+ const finalTokens = await this.summaryStore.getContextTokenCount(conversationId);
358
+ return {
359
+ success: finalTokens <= targetTokens,
360
+ rounds: this.config.maxRounds,
361
+ finalTokens,
362
+ };
363
+ }
364
+ // ── Private helpers ──────────────────────────────────────────────────────
365
+ /** Normalize configured leaf chunk size to a safe positive integer. */
366
+ resolveLeafChunkTokens() {
367
+ if (typeof this.config.leafChunkTokens === "number" &&
368
+ Number.isFinite(this.config.leafChunkTokens) &&
369
+ this.config.leafChunkTokens > 0) {
370
+ return Math.floor(this.config.leafChunkTokens);
371
+ }
372
+ return DEFAULT_LEAF_CHUNK_TOKENS;
373
+ }
374
+ /** Normalize configured fresh tail count to a safe non-negative integer. */
375
+ resolveFreshTailCount() {
376
+ if (typeof this.config.freshTailCount === "number" &&
377
+ Number.isFinite(this.config.freshTailCount) &&
378
+ this.config.freshTailCount > 0) {
379
+ return Math.floor(this.config.freshTailCount);
380
+ }
381
+ return 0;
382
+ }
383
+ /**
384
+ * Compute the ordinal boundary for protected fresh messages.
385
+ *
386
+ * Messages with ordinal >= returned value are preserved as fresh tail.
387
+ */
388
+ resolveFreshTailOrdinal(contextItems) {
389
+ const freshTailCount = this.resolveFreshTailCount();
390
+ if (freshTailCount <= 0) {
391
+ return Infinity;
392
+ }
393
+ const rawMessageItems = contextItems.filter((item) => item.itemType === "message" && item.messageId != null);
394
+ if (rawMessageItems.length === 0) {
395
+ return Infinity;
396
+ }
397
+ const tailStartIdx = Math.max(0, rawMessageItems.length - freshTailCount);
398
+ return rawMessageItems[tailStartIdx]?.ordinal ?? Infinity;
399
+ }
400
+ /** Resolve message token count with a content-length fallback. */
401
+ async getMessageTokenCount(messageId) {
402
+ const message = await this.conversationStore.getMessageById(messageId);
403
+ if (!message) {
404
+ return 0;
405
+ }
406
+ if (typeof message.tokenCount === "number" &&
407
+ Number.isFinite(message.tokenCount) &&
408
+ message.tokenCount > 0) {
409
+ return message.tokenCount;
410
+ }
411
+ return estimateTokens(message.content);
412
+ }
413
+ /** Sum raw message tokens outside the protected fresh tail. */
414
+ async countRawTokensOutsideFreshTail(conversationId) {
415
+ const contextItems = await this.summaryStore.getContextItems(conversationId);
416
+ const freshTailOrdinal = this.resolveFreshTailOrdinal(contextItems);
417
+ let rawTokens = 0;
418
+ for (const item of contextItems) {
419
+ if (item.ordinal >= freshTailOrdinal) {
420
+ break;
421
+ }
422
+ if (item.itemType !== "message" || item.messageId == null) {
423
+ continue;
424
+ }
425
+ rawTokens += await this.getMessageTokenCount(item.messageId);
426
+ }
427
+ return rawTokens;
428
+ }
429
+ /**
430
+ * Select the oldest contiguous raw-message chunk outside fresh tail.
431
+ *
432
+ * The selected chunk size is capped by `leafChunkTokens`, but we always pick
433
+ * at least one message when any compactable message exists.
434
+ */
435
+ async selectOldestLeafChunk(conversationId) {
436
+ const contextItems = await this.summaryStore.getContextItems(conversationId);
437
+ const freshTailOrdinal = this.resolveFreshTailOrdinal(contextItems);
438
+ const threshold = this.resolveLeafChunkTokens();
439
+ let rawTokensOutsideTail = 0;
440
+ for (const item of contextItems) {
441
+ if (item.ordinal >= freshTailOrdinal) {
442
+ break;
443
+ }
444
+ if (item.itemType !== "message" || item.messageId == null) {
445
+ continue;
446
+ }
447
+ rawTokensOutsideTail += await this.getMessageTokenCount(item.messageId);
448
+ }
449
+ const chunk = [];
450
+ let chunkTokens = 0;
451
+ let started = false;
452
+ for (const item of contextItems) {
453
+ if (item.ordinal >= freshTailOrdinal) {
454
+ break;
455
+ }
456
+ if (!started) {
457
+ if (item.itemType !== "message" || item.messageId == null) {
458
+ continue;
459
+ }
460
+ started = true;
461
+ }
462
+ else if (item.itemType !== "message" || item.messageId == null) {
463
+ break;
464
+ }
465
+ if (item.messageId == null) {
466
+ continue;
467
+ }
468
+ const messageTokens = await this.getMessageTokenCount(item.messageId);
469
+ if (chunk.length > 0 && chunkTokens + messageTokens > threshold) {
470
+ break;
471
+ }
472
+ chunk.push(item);
473
+ chunkTokens += messageTokens;
474
+ if (chunkTokens >= threshold) {
475
+ break;
476
+ }
477
+ }
478
+ return { items: chunk, rawTokensOutsideTail, threshold };
479
+ }
480
+ /**
481
+ * Resolve recent summary continuity for a leaf pass.
482
+ *
483
+ * Collects up to two most recent summary context items that precede the
484
+ * compacted raw-message chunk and returns their combined content.
485
+ */
486
+ async resolvePriorLeafSummaryContext(conversationId, messageItems) {
487
+ if (messageItems.length === 0) {
488
+ return undefined;
489
+ }
490
+ const startOrdinal = Math.min(...messageItems.map((item) => item.ordinal));
491
+ const priorSummaryItems = (await this.summaryStore.getContextItems(conversationId))
492
+ .filter((item) => item.ordinal < startOrdinal &&
493
+ item.itemType === "summary" &&
494
+ typeof item.summaryId === "string")
495
+ .slice(-2);
496
+ if (priorSummaryItems.length === 0) {
497
+ return undefined;
498
+ }
499
+ const summaryContents = [];
500
+ for (const item of priorSummaryItems) {
501
+ if (typeof item.summaryId !== "string") {
502
+ continue;
503
+ }
504
+ const summary = await this.summaryStore.getSummary(item.summaryId);
505
+ const content = summary?.content.trim();
506
+ if (content) {
507
+ summaryContents.push(content);
508
+ }
509
+ }
510
+ if (summaryContents.length === 0) {
511
+ return undefined;
512
+ }
513
+ return summaryContents.join("\n\n");
514
+ }
515
+ /** Resolve summary token count with content-length fallback. */
516
+ resolveSummaryTokenCount(summary) {
517
+ if (typeof summary.tokenCount === "number" &&
518
+ Number.isFinite(summary.tokenCount) &&
519
+ summary.tokenCount > 0) {
520
+ return summary.tokenCount;
521
+ }
522
+ return estimateTokens(summary.content);
523
+ }
524
+ /** Resolve message token count with content-length fallback. */
525
+ resolveMessageTokenCount(message) {
526
+ if (typeof message.tokenCount === "number" &&
527
+ Number.isFinite(message.tokenCount) &&
528
+ message.tokenCount > 0) {
529
+ return message.tokenCount;
530
+ }
531
+ return estimateTokens(message.content);
532
+ }
533
+ resolveLeafMinFanout() {
534
+ if (typeof this.config.leafMinFanout === "number" &&
535
+ Number.isFinite(this.config.leafMinFanout) &&
536
+ this.config.leafMinFanout > 0) {
537
+ return Math.floor(this.config.leafMinFanout);
538
+ }
539
+ return 8;
540
+ }
541
+ resolveCondensedMinFanout() {
542
+ if (typeof this.config.condensedMinFanout === "number" &&
543
+ Number.isFinite(this.config.condensedMinFanout) &&
544
+ this.config.condensedMinFanout > 0) {
545
+ return Math.floor(this.config.condensedMinFanout);
546
+ }
547
+ return 4;
548
+ }
549
+ resolveCondensedMinFanoutHard() {
550
+ if (typeof this.config.condensedMinFanoutHard === "number" &&
551
+ Number.isFinite(this.config.condensedMinFanoutHard) &&
552
+ this.config.condensedMinFanoutHard > 0) {
553
+ return Math.floor(this.config.condensedMinFanoutHard);
554
+ }
555
+ return 2;
556
+ }
557
+ resolveIncrementalMaxDepth() {
558
+ if (typeof this.config.incrementalMaxDepth === "number" &&
559
+ Number.isFinite(this.config.incrementalMaxDepth)) {
560
+ if (this.config.incrementalMaxDepth < 0)
561
+ return Infinity;
562
+ if (this.config.incrementalMaxDepth > 0)
563
+ return Math.floor(this.config.incrementalMaxDepth);
564
+ }
565
+ return 0;
566
+ }
567
+ resolveFanoutForDepth(targetDepth, hardTrigger) {
568
+ if (hardTrigger) {
569
+ return this.resolveCondensedMinFanoutHard();
570
+ }
571
+ if (targetDepth === 0) {
572
+ return this.resolveLeafMinFanout();
573
+ }
574
+ return this.resolveCondensedMinFanout();
575
+ }
576
+ /** Minimum condensed input size before we run another condensed pass. */
577
+ resolveCondensedMinChunkTokens() {
578
+ const chunkTarget = this.resolveLeafChunkTokens();
579
+ const ratioFloor = Math.floor(chunkTarget * CONDENSED_MIN_INPUT_RATIO);
580
+ return Math.max(this.config.condensedTargetTokens, ratioFloor);
581
+ }
582
+ /**
583
+ * Find the shallowest depth with an eligible same-depth summary chunk.
584
+ */
585
+ async selectShallowestCondensationCandidate(params) {
586
+ const { conversationId, hardTrigger } = params;
587
+ const contextItems = await this.summaryStore.getContextItems(conversationId);
588
+ const freshTailOrdinal = this.resolveFreshTailOrdinal(contextItems);
589
+ const minChunkTokens = this.resolveCondensedMinChunkTokens();
590
+ const depthLevels = await this.summaryStore.getDistinctDepthsInContext(conversationId, {
591
+ maxOrdinalExclusive: freshTailOrdinal,
592
+ });
593
+ for (const targetDepth of depthLevels) {
594
+ const fanout = this.resolveFanoutForDepth(targetDepth, hardTrigger);
595
+ const chunk = await this.selectOldestChunkAtDepth(conversationId, targetDepth, freshTailOrdinal);
596
+ if (chunk.items.length < fanout) {
597
+ continue;
598
+ }
599
+ if (chunk.summaryTokens < minChunkTokens) {
600
+ continue;
601
+ }
602
+ return { targetDepth, chunk };
603
+ }
604
+ return null;
605
+ }
606
+ /**
607
+ * Select the oldest contiguous summary chunk at a specific summary depth.
608
+ *
609
+ * Once selection starts, any non-summary item or depth mismatch terminates
610
+ * the chunk to prevent mixed-depth condensation.
611
+ */
612
+ async selectOldestChunkAtDepth(conversationId, targetDepth, freshTailOrdinalOverride) {
613
+ const contextItems = await this.summaryStore.getContextItems(conversationId);
614
+ const freshTailOrdinal = typeof freshTailOrdinalOverride === "number"
615
+ ? freshTailOrdinalOverride
616
+ : this.resolveFreshTailOrdinal(contextItems);
617
+ const chunkTokenBudget = this.resolveLeafChunkTokens();
618
+ const chunk = [];
619
+ let summaryTokens = 0;
620
+ for (const item of contextItems) {
621
+ if (item.ordinal >= freshTailOrdinal) {
622
+ break;
623
+ }
624
+ if (item.itemType !== "summary" || item.summaryId == null) {
625
+ if (chunk.length > 0) {
626
+ break;
627
+ }
628
+ continue;
629
+ }
630
+ const summary = await this.summaryStore.getSummary(item.summaryId);
631
+ if (!summary) {
632
+ if (chunk.length > 0) {
633
+ break;
634
+ }
635
+ continue;
636
+ }
637
+ if (summary.depth !== targetDepth) {
638
+ if (chunk.length > 0) {
639
+ break;
640
+ }
641
+ continue;
642
+ }
643
+ const tokenCount = this.resolveSummaryTokenCount(summary);
644
+ if (chunk.length > 0 && summaryTokens + tokenCount > chunkTokenBudget) {
645
+ break;
646
+ }
647
+ chunk.push(item);
648
+ summaryTokens += tokenCount;
649
+ if (summaryTokens >= chunkTokenBudget) {
650
+ break;
651
+ }
652
+ }
653
+ return { items: chunk, summaryTokens };
654
+ }
655
+ async resolvePriorSummaryContextAtDepth(conversationId, summaryItems, targetDepth) {
656
+ if (summaryItems.length === 0) {
657
+ return undefined;
658
+ }
659
+ const startOrdinal = Math.min(...summaryItems.map((item) => item.ordinal));
660
+ const priorSummaryItems = (await this.summaryStore.getContextItems(conversationId))
661
+ .filter((item) => item.ordinal < startOrdinal &&
662
+ item.itemType === "summary" &&
663
+ typeof item.summaryId === "string")
664
+ .slice(-4);
665
+ if (priorSummaryItems.length === 0) {
666
+ return undefined;
667
+ }
668
+ const summaryContents = [];
669
+ for (const item of priorSummaryItems) {
670
+ if (typeof item.summaryId !== "string") {
671
+ continue;
672
+ }
673
+ const summary = await this.summaryStore.getSummary(item.summaryId);
674
+ if (!summary || summary.depth !== targetDepth) {
675
+ continue;
676
+ }
677
+ const content = summary.content.trim();
678
+ if (content) {
679
+ summaryContents.push(content);
680
+ }
681
+ }
682
+ if (summaryContents.length === 0) {
683
+ return undefined;
684
+ }
685
+ return summaryContents.slice(-2).join("\n\n");
686
+ }
687
+ /**
688
+ * Run three-level summarization escalation:
689
+ * normal -> aggressive -> deterministic fallback.
690
+ */
691
+ async summarizeWithEscalation(params) {
692
+ const rawText = params.sourceText.trim();
693
+ const sourceText = this.config.scrubber ? this.config.scrubber.scrub(rawText) : rawText;
694
+ if (!sourceText) {
695
+ return {
696
+ content: "[Truncated from 0 tokens]",
697
+ level: "fallback",
698
+ };
699
+ }
700
+ const inputTokens = Math.max(1, estimateTokens(sourceText));
701
+ let summaryText = await params.summarize(sourceText, false, params.options);
702
+ let level = "normal";
703
+ if (estimateTokens(summaryText) >= inputTokens) {
704
+ summaryText = await params.summarize(sourceText, true, params.options);
705
+ level = "aggressive";
706
+ if (estimateTokens(summaryText) >= inputTokens) {
707
+ const truncated = sourceText.length > FALLBACK_MAX_CHARS
708
+ ? sourceText.slice(0, FALLBACK_MAX_CHARS)
709
+ : sourceText;
710
+ summaryText = `${truncated}\n[Truncated from ${inputTokens} tokens]`;
711
+ level = "fallback";
712
+ }
713
+ }
714
+ return { content: summaryText, level };
715
+ }
716
+ // ── Private: Leaf Pass ───────────────────────────────────────────────────
717
+ /**
718
+ * Summarize a chunk of messages into one leaf summary.
719
+ */
720
+ async leafPass(conversationId, messageItems, summarize, previousSummaryContent) {
721
+ // Fetch full message content for each context item
722
+ const messageContents = [];
723
+ for (const item of messageItems) {
724
+ if (item.messageId == null) {
725
+ continue;
726
+ }
727
+ const msg = await this.conversationStore.getMessageById(item.messageId);
728
+ if (msg) {
729
+ messageContents.push({
730
+ messageId: msg.messageId,
731
+ content: msg.content,
732
+ createdAt: msg.createdAt,
733
+ tokenCount: this.resolveMessageTokenCount(msg),
734
+ });
735
+ }
736
+ }
737
+ const concatenated = messageContents
738
+ .map((message) => `[${formatTimestamp(message.createdAt, this.config.timezone)}]\n${message.content}`)
739
+ .join("\n\n");
740
+ const fileIds = dedupeOrderedIds(messageContents.flatMap((message) => extractFileIdsFromContent(message.content)));
741
+ const summary = await this.summarizeWithEscalation({
742
+ sourceText: concatenated,
743
+ summarize,
744
+ options: {
745
+ previousSummary: previousSummaryContent,
746
+ isCondensed: false,
747
+ },
748
+ });
749
+ // Persist the leaf summary
750
+ const summaryId = generateSummaryId(summary.content);
751
+ const tokenCount = estimateTokens(summary.content);
752
+ await this.summaryStore.insertSummary({
753
+ summaryId,
754
+ conversationId,
755
+ kind: "leaf",
756
+ depth: 0,
757
+ content: summary.content,
758
+ tokenCount,
759
+ fileIds,
760
+ earliestAt: messageContents.length > 0
761
+ ? new Date(Math.min(...messageContents.map((message) => message.createdAt.getTime())))
762
+ : undefined,
763
+ latestAt: messageContents.length > 0
764
+ ? new Date(Math.max(...messageContents.map((message) => message.createdAt.getTime())))
765
+ : undefined,
766
+ descendantCount: 0,
767
+ descendantTokenCount: 0,
768
+ sourceMessageTokenCount: messageContents.reduce((sum, message) => sum + Math.max(0, Math.floor(message.tokenCount)), 0),
769
+ });
770
+ // Link to source messages
771
+ const messageIds = messageContents.map((m) => m.messageId);
772
+ await this.summaryStore.linkSummaryToMessages(summaryId, messageIds);
773
+ // Replace the message range in context with the new summary
774
+ const ordinals = messageItems.map((ci) => ci.ordinal);
775
+ const startOrdinal = Math.min(...ordinals);
776
+ const endOrdinal = Math.max(...ordinals);
777
+ await this.summaryStore.replaceContextRangeWithSummary({
778
+ conversationId,
779
+ startOrdinal,
780
+ endOrdinal,
781
+ summaryId,
782
+ });
783
+ return { summaryId, level: summary.level, content: summary.content };
784
+ }
785
+ // ── Private: Condensed Pass ──────────────────────────────────────────────
786
+ /**
787
+ * Condense one ratio-sized summary chunk into a single condensed summary.
788
+ */
789
+ async condensedPass(conversationId, summaryItems, targetDepth, summarize) {
790
+ // Fetch full summary records
791
+ const summaryRecords = [];
792
+ for (const item of summaryItems) {
793
+ if (item.summaryId == null) {
794
+ continue;
795
+ }
796
+ const rec = await this.summaryStore.getSummary(item.summaryId);
797
+ if (rec) {
798
+ summaryRecords.push(rec);
799
+ }
800
+ }
801
+ const concatenated = summaryRecords
802
+ .map((summary) => {
803
+ const earliestAt = summary.earliestAt ?? summary.createdAt;
804
+ const latestAt = summary.latestAt ?? summary.createdAt;
805
+ const tz = this.config.timezone;
806
+ const header = `[${formatTimestamp(earliestAt, tz)} - ${formatTimestamp(latestAt, tz)}]`;
807
+ return `${header}\n${summary.content}`;
808
+ })
809
+ .join("\n\n");
810
+ const fileIds = dedupeOrderedIds(summaryRecords.flatMap((summary) => [
811
+ ...summary.fileIds,
812
+ ...extractFileIdsFromContent(summary.content),
813
+ ]));
814
+ const previousSummaryContent = targetDepth === 0
815
+ ? await this.resolvePriorSummaryContextAtDepth(conversationId, summaryItems, targetDepth)
816
+ : undefined;
817
+ const condensed = await this.summarizeWithEscalation({
818
+ sourceText: concatenated,
819
+ summarize,
820
+ options: {
821
+ previousSummary: previousSummaryContent,
822
+ isCondensed: true,
823
+ depth: targetDepth + 1,
824
+ },
825
+ });
826
+ // Persist the condensed summary
827
+ const summaryId = generateSummaryId(condensed.content);
828
+ const tokenCount = estimateTokens(condensed.content);
829
+ await this.summaryStore.insertSummary({
830
+ summaryId,
831
+ conversationId,
832
+ kind: "condensed",
833
+ depth: targetDepth + 1,
834
+ content: condensed.content,
835
+ tokenCount,
836
+ fileIds,
837
+ earliestAt: summaryRecords.length > 0
838
+ ? new Date(Math.min(...summaryRecords.map((summary) => (summary.earliestAt ?? summary.createdAt).getTime())))
839
+ : undefined,
840
+ latestAt: summaryRecords.length > 0
841
+ ? new Date(Math.max(...summaryRecords.map((summary) => (summary.latestAt ?? summary.createdAt).getTime())))
842
+ : undefined,
843
+ descendantCount: summaryRecords.reduce((count, summary) => {
844
+ const childDescendants = typeof summary.descendantCount === "number" && Number.isFinite(summary.descendantCount)
845
+ ? Math.max(0, Math.floor(summary.descendantCount))
846
+ : 0;
847
+ return count + childDescendants + 1;
848
+ }, 0),
849
+ descendantTokenCount: summaryRecords.reduce((count, summary) => {
850
+ const childDescendantTokens = typeof summary.descendantTokenCount === "number" &&
851
+ Number.isFinite(summary.descendantTokenCount)
852
+ ? Math.max(0, Math.floor(summary.descendantTokenCount))
853
+ : 0;
854
+ return count + Math.max(0, Math.floor(summary.tokenCount)) + childDescendantTokens;
855
+ }, 0),
856
+ sourceMessageTokenCount: summaryRecords.reduce((count, summary) => {
857
+ const sourceTokens = typeof summary.sourceMessageTokenCount === "number" &&
858
+ Number.isFinite(summary.sourceMessageTokenCount)
859
+ ? Math.max(0, Math.floor(summary.sourceMessageTokenCount))
860
+ : 0;
861
+ return count + sourceTokens;
862
+ }, 0),
863
+ });
864
+ // Link to parent summaries
865
+ const parentSummaryIds = summaryRecords.map((s) => s.summaryId);
866
+ await this.summaryStore.linkSummaryToParents(summaryId, parentSummaryIds);
867
+ // Replace all summary items in context with the condensed summary
868
+ const ordinals = summaryItems.map((ci) => ci.ordinal);
869
+ const startOrdinal = Math.min(...ordinals);
870
+ const endOrdinal = Math.max(...ordinals);
871
+ await this.summaryStore.replaceContextRangeWithSummary({
872
+ conversationId,
873
+ startOrdinal,
874
+ endOrdinal,
875
+ summaryId,
876
+ });
877
+ return { summaryId, level: condensed.level };
878
+ }
879
+ /**
880
+ * Persist durable compaction events into canonical history as message parts.
881
+ *
882
+ * Event persistence is best-effort: failures are swallowed to avoid
883
+ * compromising the core compaction path.
884
+ */
885
+ async persistCompactionEvents(input) {
886
+ const { conversationId, tokensBefore, tokensAfterLeaf, tokensAfterFinal, leafResult, condenseResult, } = input;
887
+ if (!leafResult && !condenseResult) {
888
+ return;
889
+ }
890
+ const conversation = await this.conversationStore.getConversation(conversationId);
891
+ if (!conversation) {
892
+ return;
893
+ }
894
+ const createdSummaryIds = [leafResult?.summaryId, condenseResult?.summaryId].filter((id) => typeof id === "string" && id.length > 0);
895
+ const condensedPassOccurred = condenseResult !== null;
896
+ if (leafResult) {
897
+ await this.persistCompactionEvent({
898
+ conversationId,
899
+ sessionId: conversation.sessionId,
900
+ pass: "leaf",
901
+ level: leafResult.level,
902
+ tokensBefore,
903
+ tokensAfter: tokensAfterLeaf,
904
+ createdSummaryId: leafResult.summaryId,
905
+ createdSummaryIds,
906
+ condensedPassOccurred,
907
+ });
908
+ }
909
+ if (condenseResult) {
910
+ await this.persistCompactionEvent({
911
+ conversationId,
912
+ sessionId: conversation.sessionId,
913
+ pass: "condensed",
914
+ level: condenseResult.level,
915
+ tokensBefore: tokensAfterLeaf,
916
+ tokensAfter: tokensAfterFinal,
917
+ createdSummaryId: condenseResult.summaryId,
918
+ createdSummaryIds,
919
+ condensedPassOccurred,
920
+ });
921
+ }
922
+ }
923
+ /** Write one compaction event message + part atomically where possible. */
924
+ async persistCompactionEvent(input) {
925
+ const content = `LCM compaction ${input.pass} pass (${input.level}): ${input.tokensBefore} -> ${input.tokensAfter}`;
926
+ const metadata = JSON.stringify({
927
+ conversationId: input.conversationId,
928
+ pass: input.pass,
929
+ level: input.level,
930
+ tokensBefore: input.tokensBefore,
931
+ tokensAfter: input.tokensAfter,
932
+ createdSummaryId: input.createdSummaryId,
933
+ createdSummaryIds: input.createdSummaryIds,
934
+ condensedPassOccurred: input.condensedPassOccurred,
935
+ });
936
+ const writeEvent = async () => {
937
+ const seq = (await this.conversationStore.getMaxSeq(input.conversationId)) + 1;
938
+ const eventMessage = await this.conversationStore.createMessage({
939
+ conversationId: input.conversationId,
940
+ seq,
941
+ role: "system",
942
+ content,
943
+ tokenCount: estimateTokens(content),
944
+ });
945
+ const parts = [
946
+ {
947
+ sessionId: input.sessionId,
948
+ partType: "compaction",
949
+ ordinal: 0,
950
+ textContent: content,
951
+ metadata,
952
+ },
953
+ ];
954
+ await this.conversationStore.createMessageParts(eventMessage.messageId, parts);
955
+ };
956
+ try {
957
+ await this.conversationStore.withTransaction(() => writeEvent());
958
+ }
959
+ catch {
960
+ // Compaction should still succeed if event persistence fails.
961
+ }
962
+ }
963
+ }
964
+ //# sourceMappingURL=compaction.js.map