@pi-unipi/compactor 2.5.0 → 2.6.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
@@ -108,6 +108,8 @@ Config-only legacy field: `fts5Index` remains in the schema for compatibility, b
108
108
 
109
109
  These settings control UniPi-managed percentage auto-compaction. They are separate from Pi core's own `compaction.reserveTokens` behavior.
110
110
 
111
+ **Prefix-cache policy:** every compaction replaces provider-visible history and starts a new cache epoch. UniPi therefore keeps this second percentage trigger **off by default** and does not compact repeatedly merely to shorten a cache-hit request. Pi core remains the default safety trigger: with its documented defaults it compacts when estimated context exceeds `contextWindow - 16,384` reserved response tokens, while keeping about 20,000 recent tokens. If you deliberately enable the UniPi percentage trigger, 80% is an earlier explicit boundary and the cooldown/growth guards prevent loops. UniPi's compiler is deterministic and zero-LLM; it does not issue a second summarizer request.
112
+
111
113
  | Setting | Default | Meaning |
112
114
  |---|---:|---|
113
115
  | `Percentage Trigger` | `off` | When on, UniPi checks context usage at `turn_end` and can call compaction when the threshold is reached. Disabled by default for backward compatibility. |
@@ -157,7 +159,7 @@ The extension registers tools during `session_start` after the session DB is ini
157
159
  | Tool | Purpose |
158
160
  |---|---|
159
161
  | `compact` | Agent-facing compaction request; supports `dryRun: true` preview. Slash command `/unipi:lossless-compact` is the user-facing immediate compaction path. |
160
- | `session_recall` | Search current session history with BM25 or regex. |
162
+ | `session_recall` | Search current session history with BM25 or regex. Defaults to 10 hits, hard-caps pages at 50, and caps expanded hit text at 16 KiB. |
161
163
  | `vcc_recall` | Deprecated alias for `session_recall`. |
162
164
  | `sandbox` | Run code in a sandboxed environment. Languages: JavaScript, TypeScript, Python, shell, Ruby, Go, Rust, PHP, Perl, R, Elixir. |
163
165
  | `sandbox_file` | Execute a file with its content injected as `FILE_CONTENT`. |
@@ -255,7 +257,7 @@ When UniPi handles `session_before_compact`, it transforms old messages through
255
257
  5. **Format** — emit a structured markdown summary.
256
258
  6. **Merge** — merge with the previous summary when Pi provides one.
257
259
 
258
- The resulting summary is optimized for continuity, not for perfect archival fidelity. Use `session_recall` when the agent needs details from the raw session branch.
260
+ The resulting summary is optimized for continuity, not for perfect archival fidelity. Use `session_recall` when the agent needs details from the raw session branch. Recall is deliberately paginated: request another offset page or narrow the query instead of injecting an unbounded result set. Even with `expand: true`, each hit is capped at 16 KiB to prevent a single historical message or tool result from dominating future cold-cache requests.
259
261
 
260
262
  ---
261
263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,8 +34,8 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.5.0",
38
- "@pi-unipi/info-screen": "2.5.0",
37
+ "@pi-unipi/core": "2.6.0",
38
+ "@pi-unipi/info-screen": "2.6.0",
39
39
  "@earendil-works/pi-agent-core": "^0.80.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -257,7 +257,6 @@ export function buildResumeSnapshot(
257
257
  ): string {
258
258
  const compactCount = opts?.compactCount ?? 1;
259
259
  const searchTool = opts?.searchTool ?? "ctx_search";
260
- const now = new Date().toISOString();
261
260
 
262
261
  const fileEvents: StoredEvent[] = [];
263
262
  const taskEvents: StoredEvent[] = [];
@@ -316,7 +315,9 @@ export function buildResumeSnapshot(
316
315
  const intent = buildIntentSection(intentEvents);
317
316
  if (intent) sections.push(intent);
318
317
 
319
- const header = `<session_resume events="${events.length}" compact_count="${compactCount}" generated_at="${now}">`;
318
+ // Deliberately omit wall-clock time. This snapshot is model-visible on the
319
+ // first post-compaction request and must be reproducible from stored events.
320
+ const header = `<session_resume events="${events.length}" compact_count="${compactCount}">`;
320
321
  const footer = `</session_resume>`;
321
322
  const body = sections.join("\n\n");
322
323
  if (body) {
@@ -16,7 +16,7 @@
16
16
  import { Type } from "typebox";
17
17
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
18
18
  import { compactTool } from "./compact.js";
19
- import { vccRecall, type RecallInput } from "./vcc-recall.js";
19
+ import { MAX_RECALL_RESULTS, vccRecall, type RecallInput } from "./vcc-recall.js";
20
20
  import { ctxExecute, type CtxExecuteInput } from "./ctx-execute.js";
21
21
  import { ctxExecuteFile, type CtxExecuteFileInput } from "./ctx-execute-file.js";
22
22
  import { ctxBatchExecute, type BatchItem } from "./ctx-batch-execute.js";
@@ -55,7 +55,11 @@ const RecallParams = Type.Object({
55
55
  mode: Type.Optional(Type.Union([Type.Literal("bm25"), Type.Literal("regex")], {
56
56
  description: "Search mode: bm25 (default) or regex fallback",
57
57
  })),
58
- limit: Type.Optional(Type.Number({ description: "Max results to return (default 10)", minimum: 1 })),
58
+ limit: Type.Optional(Type.Number({
59
+ description: `Max results to return (default 10, hard cap ${MAX_RECALL_RESULTS})`,
60
+ minimum: 1,
61
+ maximum: MAX_RECALL_RESULTS,
62
+ })),
59
63
  offset: Type.Optional(Type.Number({ description: "Pagination offset", minimum: 0 })),
60
64
  expand: Type.Optional(Type.Boolean({ description: "Return full message content for hits" })),
61
65
  });
@@ -5,6 +5,9 @@
5
5
  import type { NormalizedBlock } from "../types.js";
6
6
  import { searchEntries } from "../compaction/search-entries.js";
7
7
 
8
+ export const MAX_RECALL_RESULTS = 50;
9
+ export const MAX_EXPANDED_HIT_BYTES = 16 * 1024;
10
+
8
11
  export interface RecallInput {
9
12
  query: string;
10
13
  mode?: "bm25" | "regex";
@@ -13,6 +16,14 @@ export interface RecallInput {
13
16
  expand?: boolean;
14
17
  }
15
18
 
19
+ function truncateExpandedHit(text: string): string {
20
+ const bytes = Buffer.from(text, "utf8");
21
+ if (bytes.byteLength <= MAX_EXPANDED_HIT_BYTES) return text;
22
+ const omitted = bytes.byteLength - MAX_EXPANDED_HIT_BYTES;
23
+ const visible = bytes.subarray(0, MAX_EXPANDED_HIT_BYTES).toString("utf8").replace(/\uFFFD+$/u, "");
24
+ return `${visible}\n… ${omitted} bytes omitted from this hit; narrow the query to inspect more specific context …`;
25
+ }
26
+
16
27
  export interface RecallResult {
17
28
  hits: Array<{
18
29
  index: number;
@@ -28,7 +39,11 @@ export function vccRecall(
28
39
  blocks: NormalizedBlock[],
29
40
  input: RecallInput,
30
41
  ): RecallResult {
31
- const { query, mode = "bm25", limit = 10, offset = 0, expand = false } = input;
42
+ const { query, mode = "bm25", expand = false } = input;
43
+ const requestedLimit = Number.isFinite(input.limit) ? Math.floor(input.limit!) : 10;
44
+ const requestedOffset = Number.isFinite(input.offset) ? Math.floor(input.offset!) : 0;
45
+ const limit = Math.min(MAX_RECALL_RESULTS, Math.max(1, requestedLimit));
46
+ const offset = Math.max(0, requestedOffset);
32
47
 
33
48
  let hits: Array<{ index: number; score: number; text: string; kind: string }> = [];
34
49
 
@@ -37,7 +52,7 @@ export function vccRecall(
37
52
  hits = results.map((r, i) => ({
38
53
  index: r.docId,
39
54
  score: r.score,
40
- text: expand ? r.text : r.text.slice(0, 200),
55
+ text: expand ? truncateExpandedHit(r.text) : r.text.slice(0, 200),
41
56
  kind: r.kind,
42
57
  }));
43
58
  } else {
@@ -50,7 +65,7 @@ export function vccRecall(
50
65
  hits.push({
51
66
  index: i,
52
67
  score: 1,
53
- text: expand ? text : text.slice(0, 200),
68
+ text: expand ? truncateExpandedHit(text) : text.slice(0, 200),
54
69
  kind: b.kind,
55
70
  });
56
71
  }