@pi-unipi/compactor 0.2.2 → 2.0.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.
package/README.md CHANGED
@@ -1,20 +1,14 @@
1
1
  # @pi-unipi/compactor
2
2
 
3
- Context engine for Pi coding agent. Fuses zero-LLM compaction, session continuity, sandbox execution, FTS5 search, and tool display optimization into a single cohesive package.
3
+ Context engine that keeps sessions lean. Compacts conversations, indexes code, searches history, and runs sandboxed code all without burning LLM tokens on compaction.
4
4
 
5
- ## Features
6
-
7
- - **Zero-LLM Compaction** — 6-stage pipeline (normalize → filter → build sections → brief → format → merge) achieves 95%+ token reduction with zero API cost
8
- - **Session Continuity** — XML resume snapshots survive compaction, preserving context across session boundaries
9
- - **Sandbox Execution** — 11 languages with process isolation, security hardening, and output capping
10
- - **FTS5 Search** — Full-text search over indexed content with auto-chunking
11
- - **Tool Display** — Mode-aware rendering for read, grep, find, ls, bash, edit, write tools
5
+ The zero-LLM pipeline compresses context through 6 stages (normalize, filter, build sections, brief, format, merge) to hit 95%+ token reduction at zero API cost. Session continuity preserves context across compaction boundaries with XML resume snapshots.
12
6
 
13
7
  ## Commands
14
8
 
15
9
  | Command | Description |
16
10
  |---------|-------------|
17
- | `/unipi:compact` | Manual compaction with stats |
11
+ | `/unipi:lossless-compact` | Immediate zero-LLM compaction with structured summary |
18
12
  | `/unipi:session-recall` | Search session history (BM25 or regex) |
19
13
  | `/unipi:content-index` | Index current project into FTS5 |
20
14
  | `/unipi:content-search` | Search indexed content |
@@ -25,6 +19,14 @@ Context engine for Pi coding agent. Fuses zero-LLM compaction, session continuit
25
19
  | `/unipi:compact-preset <name>` | Apply quick preset |
26
20
  | `/unipi:compact-help` | Show detailed documentation |
27
21
 
22
+ > **Note:** `/unipi:compact` still works as a deprecated alias for `/unipi:lossless-compact`.
23
+
24
+ ## Special Triggers
25
+
26
+ Compactor tools are available to the main agent when installed. All workflow skills can use compactor tools for context management.
27
+
28
+ Compactor registers with the info-screen dashboard, showing compaction count, tokens saved, compression ratio, and indexed documents. The footer subscribes to `COMPACTOR_STATSUPDATED` events to display compaction stats in the status bar.
29
+
28
30
  ## Agent Tools
29
31
 
30
32
  | Tool | Family | Description |
@@ -34,19 +36,19 @@ Context engine for Pi coding agent. Fuses zero-LLM compaction, session continuit
34
36
  | `sandbox` | sandbox | Run code in sandbox (11 languages) |
35
37
  | `sandbox_file` | sandbox | Execute file via FILE_CONTENT |
36
38
  | `sandbox_batch` | sandbox | Atomic batch of commands + searches |
37
- | `content_index` | content | Chunk content FTS5 index |
39
+ | `content_index` | content | Chunk content to FTS5 index |
38
40
  | `content_search` | content | Query indexed content |
39
- | `content_fetch` | content | Fetch URL index |
41
+ | `content_fetch` | content | Fetch URL and index |
40
42
  | `compactor_stats` | compactor | Context savings dashboard |
41
43
  | `compactor_doctor` | compactor | Diagnostics checklist |
42
44
  | `context_budget` | compactor | Estimate remaining context window |
43
45
 
44
46
  ## Two-Tier Skills
45
47
 
46
- - **Tier 1** (`compactor`): ~175 tokens, always loaded. Routing + critical rules + Ralph awareness.
48
+ - **Tier 1** (`compactor`): ~175 tokens, always loaded. Routing and critical rules.
47
49
  - **Tier 2** (`compactor-detail`): On-demand. Full tool reference, anti-patterns, sandbox languages, FTS5 modes, workflows.
48
50
 
49
- ## Configuration
51
+ ## Configurables
50
52
 
51
53
  Config lives at `~/.unipi/config/compactor/config.json`. Per-project overrides at `<project>/.unipi/config/compactor.json`.
52
54
 
@@ -78,7 +80,7 @@ Tabbed settings interface (Presets / Strategies / Pipeline):
78
80
  - `/` key opens search filter in Strategies tab
79
81
  - Preset selection shows 3-line preview
80
82
  - Per-project override checkbox (`o` key)
81
- - Keyboard: `←→` cycle modes, `Space` toggle, `s` save, `Esc` cancel
83
+ - Keyboard: left/right cycle modes, Space toggle, `s` save, Esc cancel
82
84
 
83
85
  ## Architecture
84
86
 
@@ -95,18 +97,6 @@ Tabbed settings interface (Presets / Strategies / Pipeline):
95
97
  └─────────────────────┘
96
98
  ```
97
99
 
98
- ## Installation
99
-
100
- Included in `@pi-unipi/unipi` metapackage. To use standalone:
101
-
102
- ```json
103
- {
104
- "pi": {
105
- "extensions": ["node_modules/@pi-unipi/compactor/src/index.ts"]
106
- }
107
- }
108
- ```
109
-
110
100
  ## License
111
101
 
112
102
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "0.2.2",
4
- "description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, FTS5 search, and tool display optimization",
3
+ "version": "2.0.0",
4
+ "description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Neuron Mr White",
@@ -2,33 +2,22 @@
2
2
  * All /unipi:compact-* commands
3
3
  *
4
4
  * Commands perform real work by calling tool implementations directly.
5
- * Dependencies (sessionDB, contentStore, sessionId) are injected at registration time.
6
- *
7
- * New command names (v0.2.0):
8
- * /unipi:session-recall (was /unipi:compact-recall)
9
- * /unipi:content-index (was /unipi:compact-index)
10
- * /unipi:content-search (was /unipi:compact-search)
11
- * /unipi:content-purge (was /unipi:compact-purge)
12
- *
13
- * Old names kept as deprecated aliases for backward compatibility.
5
+ * Dependencies (sessionDB, sessionId) are injected at registration time.
14
6
  */
15
7
 
16
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
17
9
  import { loadConfig, saveConfig } from "../config/manager.js";
18
10
  import { applyPreset, parsePreset } from "../config/presets.js";
11
+ import { COMPACTOR_INSTRUCTION } from "@pi-unipi/core";
19
12
  import { getLastCompactionStats } from "../compaction/hooks.js";
20
- import { compactTool } from "../tools/compact.js";
21
13
  import { vccRecall } from "../tools/vcc-recall.js";
22
14
  import { ctxStats } from "../tools/ctx-stats.js";
23
15
  import { ctxDoctor } from "../tools/ctx-doctor.js";
24
- import { ctxSearch } from "../tools/ctx-search.js";
25
- import { ContentStore } from "../store/index.js";
26
16
  import type { SessionDB } from "../session/db.js";
27
17
  import type { NormalizedBlock, RuntimeCounters } from "../types.js";
28
18
 
29
19
  export interface CommandDeps {
30
20
  sessionDB: SessionDB | null;
31
- contentStore: ContentStore | null;
32
21
  getSessionId: () => string;
33
22
  getBlocks: () => NormalizedBlock[];
34
23
  getCounters?: () => RuntimeCounters;
@@ -38,22 +27,70 @@ function deprecationLog(_oldName: string, _newName: string): void {
38
27
  // Deprecation logging disabled — was writing to stdout causing TUI rendering issues.
39
28
  }
40
29
 
30
+ const formatTokens = (n: number): string => {
31
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
32
+ return String(n);
33
+ };
34
+
41
35
  export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
42
- // ── /unipi:compact ──────────────────────────────────
36
+ // ── /unipi:lossless-compact ──────────────────────────
37
+ pi.registerCommand("unipi:lossless-compact", {
38
+ description: "Immediate zero-LLM compaction — structured summary with full recall",
39
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
40
+ ctx.compact({
41
+ customInstructions: COMPACTOR_INSTRUCTION,
42
+ onComplete: () => {
43
+ const stats = getLastCompactionStats();
44
+ if (stats) {
45
+ ctx.ui.notify(
46
+ `Compacted ${stats.totalMessages} messages (~${formatTokens(stats.tokensBefore)} tokens) → ${stats.kept} messages (~${formatTokens(stats.tokensAfterEst)} tokens)`,
47
+ "info",
48
+ );
49
+ } else {
50
+ ctx.ui.notify("Compaction completed.", "info");
51
+ }
52
+ },
53
+ onError: (err: Error) => {
54
+ if (err.message === "Compaction cancelled" || err.message === "Already compacted") {
55
+ ctx.ui.notify("Nothing to compact.", "info");
56
+ } else {
57
+ ctx.ui.notify(`Compaction failed: ${err.message}`, "error");
58
+ }
59
+ },
60
+ });
61
+ },
62
+ });
63
+ // Deprecated alias — old name
43
64
  pi.registerCommand("unipi:compact", {
44
- description: "Trigger manual compaction with stats",
45
- handler: async (_args: string, ctx: any) => {
46
- const result = compactTool();
47
- const stats = getLastCompactionStats();
48
- const msg = stats
49
- ? `🗜️ Compaction: ${stats.summarized} summarized, ${stats.kept} kept (~${stats.keptTokensEst} tok)\n${result.message}`
50
- : `🗜️ ${result.message}`;
51
- ctx.ui.notify(msg, "info");
65
+ description: "(DEPRECATED) Use /unipi:lossless-compact instead",
66
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
67
+ deprecationLog("/unipi:compact", "/unipi:lossless-compact");
68
+ ctx.compact({
69
+ customInstructions: COMPACTOR_INSTRUCTION,
70
+ onComplete: () => {
71
+ const stats = getLastCompactionStats();
72
+ if (stats) {
73
+ ctx.ui.notify(
74
+ `Compacted ${stats.totalMessages} messages (~${formatTokens(stats.tokensBefore)} tokens) → ${stats.kept} messages (~${formatTokens(stats.tokensAfterEst)} tokens)`,
75
+ "info",
76
+ );
77
+ } else {
78
+ ctx.ui.notify("Compaction completed.", "info");
79
+ }
80
+ },
81
+ onError: (err: Error) => {
82
+ if (err.message === "Compaction cancelled" || err.message === "Already compacted") {
83
+ ctx.ui.notify("Nothing to compact.", "info");
84
+ } else {
85
+ ctx.ui.notify(`Compaction failed: ${err.message}`, "error");
86
+ }
87
+ },
88
+ });
52
89
  },
53
90
  });
54
91
 
55
92
  // ── /unipi:session-recall (new) ─────────────────────
56
- const sessionRecallHandler = async (args: string, ctx: any) => {
93
+ const sessionRecallHandler = async (args: string, ctx: ExtensionCommandContext) => {
57
94
  const query = args.trim();
58
95
  if (!query) {
59
96
  ctx.ui.notify("Usage: /unipi:session-recall <query>", "warning");
@@ -81,7 +118,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
81
118
  // Deprecated alias
82
119
  pi.registerCommand("unipi:compact-recall", {
83
120
  description: "(DEPRECATED) Search session history — use /unipi:session-recall instead",
84
- handler: async (args: string, ctx: any) => {
121
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
85
122
  deprecationLog("/unipi:compact-recall", "/unipi:session-recall");
86
123
  return sessionRecallHandler(args, ctx);
87
124
  },
@@ -90,19 +127,18 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
90
127
  // ── /unipi:compact-stats ─────────────────────────────
91
128
  pi.registerCommand("unipi:compact-stats", {
92
129
  description: "Show context savings dashboard",
93
- handler: async (_args: string, ctx: any) => {
94
- if (!deps?.sessionDB || !deps?.contentStore) {
130
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
131
+ if (!deps?.sessionDB) {
95
132
  ctx.ui.notify("Compactor services not initialized.", "error");
96
133
  return;
97
134
  }
98
135
  try {
99
- const stats = await ctxStats(deps.sessionDB, deps.contentStore, deps.getSessionId(), deps.getCounters?.());
136
+ const stats = await ctxStats(deps.sessionDB, deps.getSessionId(), deps.getCounters?.());
100
137
  const lines = [
101
138
  "📊 Compactor Stats",
102
139
  `Session events: ${stats.sessionEvents}`,
103
140
  `Compactions: ${stats.compactions}`,
104
141
  `Tokens saved: ${stats.tokensSaved}`,
105
- `Indexed docs: ${stats.indexedDocs} (${stats.indexedChunks} chunks)`,
106
142
  `Sandbox runs: ${stats.sandboxRuns}`,
107
143
  `Search queries: ${stats.searchQueries}`,
108
144
  ];
@@ -116,13 +152,13 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
116
152
  // ── /unipi:compact-doctor ────────────────────────────
117
153
  pi.registerCommand("unipi:compact-doctor", {
118
154
  description: "Run diagnostics checklist",
119
- handler: async (_args: string, ctx: any) => {
120
- if (!deps?.sessionDB || !deps?.contentStore) {
155
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
156
+ if (!deps?.sessionDB) {
121
157
  ctx.ui.notify("Compactor services not initialized.", "error");
122
158
  return;
123
159
  }
124
160
  try {
125
- const result = await ctxDoctor(deps.sessionDB, deps.contentStore);
161
+ const result = await ctxDoctor(deps.sessionDB);
126
162
  const icon = (s: string) => (s === "pass" ? "✅" : s === "warn" ? "⚠️" : "❌");
127
163
  const lines = [
128
164
  result.healthy ? "🩺 All checks passed" : "🩺 Issues found",
@@ -139,7 +175,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
139
175
  // ── /unipi:compact-settings ──────────────────────────
140
176
  pi.registerCommand("unipi:compact-settings", {
141
177
  description: "Open TUI settings overlay",
142
- handler: async (_args: string, ctx: any) => {
178
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
143
179
  try {
144
180
  const cwd = (ctx as any).cwd ?? process.cwd();
145
181
  const { renderSettingsOverlay } = await import("../tui/settings-overlay.js");
@@ -158,7 +194,7 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
158
194
  // ── /unipi:compact-preset ────────────────────────────
159
195
  pi.registerCommand("unipi:compact-preset", {
160
196
  description: "Apply quick preset (precise/balanced/thorough/lean)",
161
- handler: async (args: string, ctx: any) => {
197
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
162
198
  const presetName = parsePreset(args.trim());
163
199
  if (!presetName) {
164
200
  ctx.ui.notify("Unknown preset. Use: precise, balanced, thorough, lean", "error");
@@ -174,150 +210,24 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
174
210
  },
175
211
  });
176
212
 
177
- // ── /unipi:content-index (new) / /unipi:compact-index (deprecated) ──
178
- const contentIndexHandler = async (_args: string, ctx: any) => {
179
- if (!deps?.contentStore) {
180
- ctx.ui.notify("Content store not initialized. Enable fts5Index in config.", "warning");
181
- return;
182
- }
183
- try {
184
- const cwd = (ctx as any).cwd ?? process.cwd();
185
- const { readdirSync, readFileSync, statSync } = await import("node:fs");
186
- const { join, relative, extname } = await import("node:path");
187
-
188
- const indexable = [".md", ".txt", ".ts", ".js", ".json", ".py", ".sh"];
189
- const files: string[] = [];
190
-
191
- const walk = (dir: string, depth = 0) => {
192
- if (depth > 3) return;
193
- try {
194
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
195
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
196
- const full = join(dir, entry.name);
197
- if (entry.isDirectory()) {
198
- walk(full, depth + 1);
199
- } else if (indexable.includes(extname(entry.name))) {
200
- files.push(full);
201
- }
202
- }
203
- } catch {
204
- // skip unreadable dirs
205
- }
206
- };
207
-
208
- walk(cwd);
209
- let totalChunks = 0;
210
- for (const file of files.slice(0, 100)) {
211
- try {
212
- const content = readFileSync(file, "utf-8");
213
- if (content.length < 50) continue;
214
- const ext = extname(file);
215
- const ct = ext === ".md" ? "markdown" : ext === ".json" ? "json" : "plain";
216
- const result = await deps.contentStore!.index(relative(cwd, file), content, {
217
- contentType: ct,
218
- source: file,
219
- });
220
- totalChunks += result.totalChunks;
221
- } catch {
222
- // skip unreadable files
223
- }
224
- }
225
- ctx.ui.notify(`Indexed ${Math.min(files.length, 100)} files (${totalChunks} chunks).`, "info");
226
- } catch (err) {
227
- ctx.ui.notify(`Index error: ${err}`, "error");
228
- }
229
- };
230
- pi.registerCommand("unipi:content-index", {
231
- description: "Index current project files into FTS5",
232
- handler: contentIndexHandler,
233
- });
234
- pi.registerCommand("unipi:compact-index", {
235
- description: "(DEPRECATED) Index project files — use /unipi:content-index instead",
236
- handler: async (args: string, ctx: any) => {
237
- deprecationLog("/unipi:compact-index", "/unipi:content-index");
238
- return contentIndexHandler(args, ctx);
239
- },
240
- });
241
-
242
- // ── /unipi:content-search (new) / /unipi:compact-search (deprecated) ──
243
- const contentSearchHandler = async (args: string, ctx: any) => {
244
- const query = args.trim();
245
- if (!query) {
246
- ctx.ui.notify("Usage: /unipi:content-search <query>", "warning");
247
- return;
248
- }
249
- if (!deps?.contentStore) {
250
- ctx.ui.notify("Content store not initialized.", "warning");
251
- return;
252
- }
253
- try {
254
- const results = await ctxSearch(deps.contentStore!, { query, limit: 10 });
255
- if (results.length === 0) {
256
- ctx.ui.notify(`No results for "${query}".`, "info");
257
- return;
258
- }
259
- const lines = results.map(
260
- (r, i) => `[${i + 1}] ${r.title} (rank: ${r.rank.toFixed(3)})\n${r.content.slice(0, 200)}`,
261
- );
262
- ctx.ui.notify(`Found ${results.length} results:\n${lines.join("\n\n")}`, "info");
263
- } catch (err) {
264
- ctx.ui.notify(`Search error: ${err}`, "error");
265
- }
266
- };
267
- pi.registerCommand("unipi:content-search", {
268
- description: "Search indexed content",
269
- handler: contentSearchHandler,
270
- });
271
- pi.registerCommand("unipi:compact-search", {
272
- description: "(DEPRECATED) Search indexed content — use /unipi:content-search instead",
273
- handler: async (args: string, ctx: any) => {
274
- deprecationLog("/unipi:compact-search", "/unipi:content-search");
275
- return contentSearchHandler(args, ctx);
276
- },
277
- });
278
-
279
- // ── /unipi:content-purge (new) / /unipi:compact-purge (deprecated) ──
280
- const contentPurgeHandler = async (_args: string, ctx: any) => {
281
- if (!deps?.contentStore) {
282
- ctx.ui.notify("Content store not initialized.", "warning");
283
- return;
284
- }
285
- try {
286
- await deps.contentStore!.purge();
287
- ctx.ui.notify("All indexed content purged.", "info");
288
- } catch (err) {
289
- ctx.ui.notify(`Purge error: ${err}`, "error");
290
- }
291
- };
292
- pi.registerCommand("unipi:content-purge", {
293
- description: "Wipe all indexed content from FTS5",
294
- handler: contentPurgeHandler,
295
- });
296
- pi.registerCommand("unipi:compact-purge", {
297
- description: "(DEPRECATED) Wipe indexed content — use /unipi:content-purge instead",
298
- handler: async (args: string, ctx: any) => {
299
- deprecationLog("/unipi:compact-purge", "/unipi:content-purge");
300
- return contentPurgeHandler(args, ctx);
301
- },
302
- });
303
-
304
213
  // ── /unipi:compact-help ──────────────────────────────
305
214
  pi.registerCommand("unipi:compact-help", {
306
215
  description: "Show detailed compactor documentation (tier-2 skill)",
307
- handler: async (_args: string, ctx: any) => {
308
- // Load tier-2 skill content — delegates to skill loading system
216
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
309
217
  ctx.ui.notify(
310
- "🗜️ Compactor Help — Use your compactor-detail skill for full documentation.\n" +
218
+ "🗜️ Compactor Help\n" +
311
219
  "Quick commands:\n" +
312
- " /unipi:compact — trigger compaction\n" +
220
+ " /unipi:lossless-compact — trigger immediate compaction\n" +
313
221
  " /unipi:session-recall <query> — search session history\n" +
314
- " /unipi:content-index — index project files\n" +
315
- " /unipi:content-search <query> — search indexed content\n" +
316
- " /unipi:content-purge — wipe indexed content\n" +
317
222
  " /unipi:compact-stats — view stats\n" +
318
223
  " /unipi:compact-doctor — run diagnostics\n" +
319
224
  " /unipi:compact-settings — TUI settings\n" +
320
- " /unipi:compact-preset <name> — apply preset",
225
+ " /unipi:compact-preset <name> — apply preset\n" +
226
+ "\n" +
227
+ "Content indexing has moved to @pi-unipi/cocoindex:\n" +
228
+ " /unipi:cocoindex-init — initialize pipeline\n" +
229
+ " /unipi:cocoindex-update — index project files\n" +
230
+ " cocoindex_search — search indexed content",
321
231
  "info",
322
232
  );
323
233
  },
@@ -7,8 +7,8 @@ export function textOf(content: unknown): string {
7
7
  if (typeof content === "string") return content;
8
8
  if (Array.isArray(content)) {
9
9
  return content
10
- .map((c: any) => {
11
- if (c?.type === "text") return c.text ?? "";
10
+ .map((c: Record<string, unknown>) => {
11
+ if (c?.type === "text") return (c.text as string) ?? "";
12
12
  if (c?.type === "toolCall") return `[toolCall:${c.name}]`;
13
13
  if (c?.type === "thinking") return "[thinking]";
14
14
  if (c?.type === "image") return `[image:${c.mimeType}]`;
@@ -7,28 +7,32 @@ export type OwnCutCancelReason =
7
7
  | "too_few_live_messages"
8
8
  | "no_user_message";
9
9
 
10
+ import type { SessionEntry, SessionMessageEntry, CompactionEntry } from "@mariozechner/pi-coding-agent";
11
+ import type { AgentMessage } from "@mariozechner/pi-agent-core";
12
+
10
13
  export type OwnCutResult =
11
- | { ok: true; messages: any[]; firstKeptEntryId: string; compactAll: boolean }
14
+ | { ok: true; messages: AgentMessage[]; firstKeptEntryId: string; compactAll: boolean }
12
15
  | { ok: false; reason: OwnCutCancelReason };
13
16
 
14
17
  interface EntryWithMessage {
15
- entry: { id: string; type: string };
16
- message: { role: string; content: unknown };
18
+ entry: SessionEntry;
19
+ message: AgentMessage;
17
20
  }
18
21
 
19
- export function buildOwnCut(branchEntries: any[]): OwnCutResult {
22
+ export function buildOwnCut(branchEntries: SessionEntry[]): OwnCutResult {
20
23
  let lastCompactionIdx = -1;
21
24
  let lastKeptId: string | undefined;
22
25
  for (let i = branchEntries.length - 1; i >= 0; i--) {
23
26
  if (branchEntries[i].type === "compaction") {
24
27
  lastCompactionIdx = i;
25
- lastKeptId = branchEntries[i].firstKeptEntryId;
28
+ const ce = branchEntries[i] as CompactionEntry;
29
+ lastKeptId = ce.firstKeptEntryId;
26
30
  break;
27
31
  }
28
32
  }
29
33
 
30
34
  const hasPriorCompaction = lastCompactionIdx >= 0;
31
- const hasValidKeptId = !!lastKeptId && branchEntries.some((e: any) => e.id === lastKeptId);
35
+ const hasValidKeptId = !!lastKeptId && branchEntries.some((e) => e.id === lastKeptId);
32
36
  const orphanRecovery = hasPriorCompaction && !hasValidKeptId;
33
37
 
34
38
  const liveMessages: EntryWithMessage[] = [];