@pi-unipi/unipi 2.5.0 → 2.6.1

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 (62) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -0
  3. package/docs/prefix-cache-architecture.md +89 -0
  4. package/package.json +22 -21
  5. package/packages/ask-user/package.json +2 -2
  6. package/packages/autocomplete/package.json +1 -1
  7. package/packages/autocomplete/src/constants.ts +4 -0
  8. package/packages/btw/package.json +2 -2
  9. package/packages/cocoindex/README.md +2 -1
  10. package/packages/cocoindex/index.ts +6 -13
  11. package/packages/cocoindex/package.json +4 -3
  12. package/packages/cocoindex/tools.ts +45 -11
  13. package/packages/compactor/README.md +4 -2
  14. package/packages/compactor/package.json +3 -3
  15. package/packages/compactor/src/session/snapshot.ts +3 -2
  16. package/packages/compactor/src/tools/register.ts +6 -2
  17. package/packages/compactor/src/tools/vcc-recall.ts +18 -3
  18. package/packages/core/bounded-output.ts +106 -0
  19. package/packages/core/constants.ts +3 -0
  20. package/packages/core/index.ts +1 -0
  21. package/packages/core/model-cache.ts +18 -12
  22. package/packages/core/package.json +1 -1
  23. package/packages/footer/package.json +2 -2
  24. package/packages/image/package.json +2 -2
  25. package/packages/info-screen/package.json +2 -2
  26. package/packages/input-shortcuts/package.json +2 -2
  27. package/packages/kanboard/package.json +2 -2
  28. package/packages/mcp/README.md +4 -0
  29. package/packages/mcp/package.json +2 -2
  30. package/packages/mcp/src/bridge/translator.ts +19 -12
  31. package/packages/memory/index.ts +56 -34
  32. package/packages/memory/package.json +3 -3
  33. package/packages/milestone/hooks.ts +1 -1
  34. package/packages/milestone/package.json +2 -2
  35. package/packages/notify/README.md +1 -0
  36. package/packages/notify/commands.ts +61 -3
  37. package/packages/notify/package.json +2 -2
  38. package/packages/notify/tui/recap-model-selector.ts +72 -31
  39. package/packages/notify/tui/settings-overlay.ts +45 -32
  40. package/packages/ralph/index.ts +12 -18
  41. package/packages/ralph/package.json +6 -3
  42. package/packages/ralph/reminder.ts +40 -0
  43. package/packages/ralph/tools.ts +5 -1
  44. package/packages/subagents/README.md +2 -0
  45. package/packages/subagents/package.json +1 -1
  46. package/packages/subagents/src/agent-manager.ts +5 -1
  47. package/packages/subagents/src/agent-runner.ts +2 -2
  48. package/packages/subagents/src/core-compat.ts +73 -0
  49. package/packages/subagents/src/custom-agents.ts +10 -2
  50. package/packages/subagents/src/index.ts +14 -3
  51. package/packages/subagents/src/types.ts +2 -0
  52. package/packages/unipi/bundled.js +1002 -452
  53. package/packages/updater/package.json +2 -2
  54. package/packages/utility/README.md +9 -0
  55. package/packages/utility/package.json +2 -2
  56. package/packages/utility/src/index.ts +48 -0
  57. package/packages/utility/src/lifecycle/cleanup.ts +29 -0
  58. package/packages/utility/src/prefix-cache.ts +263 -0
  59. package/packages/utility/src/types.ts +1 -1
  60. package/packages/web-api/package.json +2 -2
  61. package/packages/workflow/index.ts +2 -2
  62. package/packages/workflow/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.6.1] — 2026-08-20
10
+
11
+ This release fixes the notify settings overlay and recap model selector in terminals using the kitty keyboard protocol (Ghostty under Herdr), and makes the model selector reflect Pi's live model registry.
12
+
13
+ ### Fixed
14
+
15
+ - `notify`: settings overlay and recap model selector match Up/Down/Escape/Enter/Tab/Space/Backspace via `matchesKey()` from `@earendil-works/pi-tui` instead of raw byte comparison — under the kitty keyboard protocol Escape arrives as `\x1b[27u` (Ctrl+C as `\x1b[99;5u`), so arrows/Escape silently did nothing and the selector could trap the user (issue #27).
16
+ - `notify`: Ctrl+C now always closes both overlays, even mid-filter.
17
+ - `notify`: recap model selector lists models injected from Pi's live `ctx.modelRegistry` instead of relying solely on `~/.unipi/config/models-cache.json`; when neither source has models it shows an actionable hint instead of a bare "No models found".
18
+ - `core`: model-cache paths are resolved per call (respects `HOME` changes; enables isolated tests).
19
+
20
+ ### Added
21
+
22
+ - `/unipi:notify-event <event> <on|off>` — toggle a notify event without the TUI (escape hatch for terminals with overlay input problems); reports the resulting value and reminds that `/reload` re-registers listeners.
23
+
24
+ ### Changed
25
+
26
+ - `notify`: arrows navigate inside selector filter mode without leaving it; `m` and `M` both open the model selector from the Recap tab.
27
+
28
+ ## [2.6.0] — 2026-08-14
29
+
30
+ This release completes UniPi's provider prefix-cache preservation rollout, making model-visible state append-only within explicit cache epochs while adding privacy-safe diagnostics and bounded external results.
31
+
32
+ ### Added
33
+
34
+ - **Provider-native prefix regression tests:** keyless tests capture real `pi-ai` OpenAI-compatible payloads before network I/O and verify ordinary turns plus Ralph, Memory, Milestone, Workflow, and Compactor snapshots extend the exact prior message prefix.
35
+ - **Privacy-safe cache diagnostics:** `/unipi:prefix-cache` reports session-local request/envelope HMAC fingerprints, structural prefix transitions, explicit epochs, and provider-reported cache-read/cache-write tokens without retaining raw prompts, arguments, schemas, or payloads.
36
+ - **Bounded result artifacts:** MCP and helper text has a hard 64 KiB model-visible ceiling. Raw results up to 16 MiB spill to private local artifacts with selective-read references and seven-day cleanup integration.
37
+ - **Prefix-cache architecture guide:** documents the append-only invariant, superseding snapshot semantics, cache-reset events, Pi/provider ownership boundaries, DeepSeek expectations, and compaction behavior.
38
+
39
+ ### Changed
40
+
41
+ - **Deterministic tool lifecycle:** Ralph and CocoIndex tools register at extension construction time; custom subagent types and filesystem discovery use locale-independent deterministic ordering.
42
+ - **Compaction continuity is byte-stable:** resume snapshots no longer include wall-clock generation text, and UniPi's optional percentage compaction trigger remains disabled by default.
43
+ - **Ralph reminders are superseding snapshots:** unchanged reminders deduplicate within an epoch and reappear once after compaction.
44
+ - **Search output is bounded:** CocoIndex and session recall default to 10 results and hard-cap pages at 50; expanded recall hits are capped at 16 KiB.
45
+
46
+ ### Fixed
47
+
48
+ - **Helper isolation recognizes current tool names:** child agents cannot recursively invoke `spawn_helper` or `get_helper_result` through stale exclusions.
49
+ - **Oversized external output no longer inflates every later cold request:** bounded previews retain useful head/tail context and fail safely if private artifact creation is unavailable.
50
+ - **Cache usage accounting counts distinct identical responses correctly** while deduplicating repeated active-context message objects.
51
+
9
52
  ## [2.5.0] — 2026-08-14
10
53
 
11
54
  This release makes persistent memory migration reliable across standalone and all-in-one installs, while stabilizing workflow tool schemas and MCP registration.
package/README.md CHANGED
@@ -17,6 +17,8 @@ pi install npm:@pi-unipi/unipi
17
17
 
18
18
  **[Compactor](./packages/compactor/README.md)** — Zero-LLM context engine. 6-stage pipeline hits 95%+ token reduction at zero API cost. Session continuity, percentage auto-compaction, session recall, and sandbox execution.
19
19
 
20
+ **[Prefix-cache architecture](./docs/prefix-cache-architecture.md)** — Append-only request discipline, explicit cache epochs, deterministic tools, privacy-safe diagnostics, provider limitations, and bounded cold-epoch output.
21
+
20
22
  **[CocoIndex](./packages/cocoindex/README.md)** — Project indexing and semantic code search backed by CocoIndex and LanceDB. Agent tools and slash commands for status, init, update, and search.
21
23
 
22
24
  **[Subagents](./packages/subagents/README.md)** — Parallel execution with file locking. Spawn background agents to research, fix, or build while the main agent keeps going.
@@ -0,0 +1,89 @@
1
+ # Provider Prefix-Cache Architecture
2
+
3
+ UniPi treats provider prefix caching as a request-shape invariant, not as a local cache. Within one **cache epoch**, each provider request must preserve the preceding request's model-visible prefix and add only new tail messages.
4
+
5
+ ```text
6
+ stable system prompt
7
+ + stable ordered tool definitions
8
+ + immutable prior messages
9
+ + newly appended messages
10
+ ```
11
+
12
+ A cache epoch is the period in which the provider, model, inference settings, system prompt, tool definitions and ordering, and already-serialized messages remain unchanged. UniPi cannot force a provider to retain or reuse its server-side cache, but it can avoid needlessly invalidating that cache.
13
+
14
+ ## Rules
15
+
16
+ 1. Dynamic operational state is appended as a persisted custom message. It does not rewrite the system prompt or an earlier message.
17
+ 2. A newer state snapshot explicitly supersedes older snapshots. Older snapshots stay immutable until compaction.
18
+ 3. Unchanged snapshots are not appended repeatedly.
19
+ 4. Tool definitions and registration order are deterministic for a fixed configuration.
20
+ 5. Execution policy is enforced when a tool is called rather than by changing the visible tool list mid-session.
21
+ 6. Model-visible content excludes avoidable clocks, random identifiers, and unstable filesystem or locale ordering unless they are genuine new results.
22
+ 7. Compaction is an explicit cache boundary. Post-compaction continuity extends the new compacted context; it does not pretend the old provider prefix survived.
23
+ 8. Large external results must have a bounded model-visible representation. Full output may be retained outside provider serialization when the host offers a safe durable channel.
24
+
25
+ `display: false` only hides a custom message in Pi's transcript UI. It does **not** hide it from the model. Prefix safety comes from append-only placement, not display state.
26
+
27
+ ## Gap matrix
28
+
29
+ This matrix reflects UniPi source and Pi 0.84.1 APIs as audited for the v2.5.x rollout.
30
+
31
+ | Surface | Status | Ownership and behavior |
32
+ |---|---|---|
33
+ | Ralph iteration state | Fixed | Hidden `unipi-ralph-loop-reminder` tail snapshots; no system-prompt mutation. |
34
+ | Milestone state | Fixed | Workspace-qualified, compaction-aware `unipi-milestone-snapshot` messages append only when state changes. |
35
+ | Workflow sandbox | Fixed | Stable tool schemas; `tool_call` blocks disallowed execution; active/inactive snapshots supersede prior state. |
36
+ | Memory reminders | Fixed | First-turn and retrospective reminders are tail messages. |
37
+ | Compactor resume | Fixed | One-shot hidden resume context extends the post-compaction epoch. Snapshot rendering is deterministic for fixed stored events. |
38
+ | Utility continuation | Fixed | `/continue` sends a hidden tail message. |
39
+ | BTW visible notes | Fixed | The `context` filter excludes the custom note type from its first provider projection onward; side threads use independent sessions. |
40
+ | MCP discovery and tool order | Fixed | Discovery is barriered and final names are sorted with locale-independent code-unit ordering before registration. |
41
+ | MCP JSON schemas | Fixed | Object keys and semantically unordered `required` members are canonicalized; order-sensitive literal arrays are preserved. |
42
+ | Subagent type descriptions | Fixed | Built-in, configured, and filesystem-discovered type names are code-unit sorted before entering tool descriptions. |
43
+ | Dynamic tool catalogs | Intentional boundary | MCP catalog changes and tool-enable settings require a new/restarted session when definitions change. Runtime removal is refused when Pi cannot unregister truthfully. |
44
+ | Compaction and branch summarization | Intentional boundary | History is replaced by a summary and retained tail. Pi uses separate one-shot routing behavior for summarization. |
45
+ | New/resume/fork/tree/reload | Intentional boundary | Session branch, resources, tools, or prompt inputs can change. A resumed process may reuse cache only if the provider still has an identical prefix; UniPi does not assume that. |
46
+ | Provider/model/thinking/sampling changes | Intentional boundary | These alter the request envelope and start a new epoch. Model and thinking changes are persisted by Pi. |
47
+ | Workspace/cwd and project instructions | Intentional boundary | Pi includes cwd and loaded resources in its base prompt. Switching or reloading them changes the epoch. |
48
+ | Helper and BTW sessions | Intentional boundary | Each is an independent model session with its own system prompt, tools, and cache lineage. |
49
+ | Base system prompt and cwd footer | Host-owned | Pi constructs these. UniPi avoids adding changing per-turn text to them. |
50
+ | Global extension/tool ordering | Host-owned | Pi owns extension loading and the combined registry. UniPi makes its own dynamic registration deterministic. |
51
+ | Session tree projection | Host-owned | Pi reconstructs the selected branch and compaction surface. |
52
+ | Provider-native conversion and wire serialization | Host-owned | `before_provider_request` exposes an untyped provider-native object before transport; later extensions and SDK serialization may still transform it. |
53
+ | Provider cache retention/hits | Provider-owned | Prefix identity is necessary, not sufficient. TTL, routing, load, and provider policy still determine reuse. |
54
+ | Request envelope observability | Fixed | Utility observes provider-native payloads without mutation, retains only session-local keyed HMAC fingerprints/counters, classifies structural transitions, and exposes provider-reported cache usage through `/unipi:prefix-cache`. |
55
+ | Oversized MCP/helper output | Fixed | MCP and helper results have a hard 64 KiB model-visible ceiling. Raw text up to 16 MiB is retained in random mode-0600 artifacts under an enforced mode-0700 directory and retrieved selectively with `read`; larger or failed spills return a non-retention warning. |
56
+ | Oversized paginated search/recall output | Fixed | CocoIndex and session recall default to 10 and hard-cap pages at 50 even when host validation is bypassed. CocoIndex renders 300-character excerpts; expanded recall hits are capped at 16 KiB and direct the model to narrow its query. |
57
+
58
+ ## Explicit cache boundaries
59
+
60
+ The following events are expected to lose all or part of prefix reuse:
61
+
62
+ - compaction or branch summarization;
63
+ - provider or model selection changes;
64
+ - thinking level, sampling parameters, stop sequences, or output-limit changes;
65
+ - a real system-prompt/resource/skill/project-instruction change;
66
+ - a meaningful tool definition, tool order, or active-tool change;
67
+ - workspace/cwd changes represented in Pi's base prompt;
68
+ - new, resumed, forked, or navigated session branches;
69
+ - independent helper, BTW, or other one-shot model sessions.
70
+
71
+ A boundary is not necessarily a defect. An unrecorded or avoidable boundary is.
72
+
73
+ ## Pi API boundary
74
+
75
+ The closest extension hook to a provider-native request body is `before_provider_request`. It is useful for structural tests and opt-in diagnostics, but it has important limits:
76
+
77
+ - `event.payload` is `unknown` and provider-specific;
78
+ - the event has no request ID, retry index, or purpose classification;
79
+ - later-loaded extensions can replace the payload afterward;
80
+ - final HTTP headers and transport serialization are separate;
81
+ - compaction and normal requests are not intrinsically distinguished.
82
+
83
+ Direct pi-ai `onPayload(payload, model)` is stronger for adapter-level tests. Exact wire correlation requires a custom provider, transport, or proxy. Parsed `cacheRead` and `cacheWrite` counters are available on final assistant or compaction usage when the provider reports them; a zero may also mean that the provider does not expose the metric.
84
+
85
+ UniPi's `/unipi:prefix-cache` diagnostic observes this provider-native boundary without replacing payloads. It classifies first request, identical retry, exact sequence extension, envelope change, payload-shape change, and history rewrite. Request and envelope fingerprints are HMAC-SHA-256 values truncated for display and keyed with a random process-memory secret. No raw payload is retained, persisted, or logged, and fingerprints intentionally cannot be correlated after reload. Model/thinking, compaction, and tree events mark explicit boundaries; subsequent payloads supply the canonical structural classification. Provider-reported cache read/write totals are deduplicated from successful assistant messages.
86
+
87
+ ## Compaction
88
+
89
+ UniPi currently performs deterministic, zero-LLM compaction. Therefore it does not issue a second summarizer request whose prefix needs warming. UniPi's additional percentage trigger is disabled by default; Pi core's reserve-token safety trigger remains active unless the user changes Pi settings. With Pi 0.84.1 defaults, core compaction triggers above `contextWindow - 16,384` estimated tokens and retains approximately 20,000 recent tokens. Enabling UniPi's optional 80% trigger intentionally trades earlier epoch resets for more headroom, with cooldown and minimum-growth guards against repeat compaction. If LLM summarization is introduced, the summarization call must replay the current conversation's exact system prompt, ordered tools, and leading messages, then append the compaction instruction as the final user message. Replacing the system prompt with a special summarizer prompt would throw away the warm prefix at the most expensive point in the session.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": [
34
34
  "CHANGELOG.md",
35
+ "docs/prefix-cache-architecture.md",
35
36
  "packages/*/package.json",
36
37
  "packages/*/*.ts",
37
38
  "packages/*/src/**/*.ts",
@@ -76,26 +77,26 @@
76
77
  "typebox": "^1.1.38"
77
78
  },
78
79
  "dependencies": {
79
- "@pi-unipi/ask-user": "2.5.0",
80
- "@pi-unipi/btw": "2.5.0",
81
- "@pi-unipi/compactor": "2.5.0",
82
- "@pi-unipi/notify": "2.5.0",
83
- "@pi-unipi/command-enchantment": "2.5.0",
84
- "@pi-unipi/core": "2.5.0",
85
- "@pi-unipi/info-screen": "2.5.0",
86
- "@pi-unipi/mcp": "2.5.0",
87
- "@pi-unipi/memory": "2.5.0",
88
- "@pi-unipi/ralph": "2.5.0",
89
- "@pi-unipi/subagents": "2.5.0",
90
- "@pi-unipi/utility": "2.5.0",
91
- "@pi-unipi/milestone": "2.5.0",
92
- "@pi-unipi/kanboard": "2.5.0",
93
- "@pi-unipi/web-api": "2.5.0",
94
- "@pi-unipi/workflow": "2.5.0",
95
- "@pi-unipi/footer": "2.5.0",
96
- "@pi-unipi/updater": "2.5.0",
97
- "@pi-unipi/input-shortcuts": "2.5.0",
98
- "@pi-unipi/cocoindex": "2.5.0"
80
+ "@pi-unipi/ask-user": "2.6.1",
81
+ "@pi-unipi/btw": "2.6.1",
82
+ "@pi-unipi/compactor": "2.6.1",
83
+ "@pi-unipi/notify": "2.6.1",
84
+ "@pi-unipi/command-enchantment": "2.6.1",
85
+ "@pi-unipi/core": "2.6.1",
86
+ "@pi-unipi/info-screen": "2.6.1",
87
+ "@pi-unipi/mcp": "2.6.1",
88
+ "@pi-unipi/memory": "2.6.1",
89
+ "@pi-unipi/ralph": "2.6.1",
90
+ "@pi-unipi/subagents": "2.6.1",
91
+ "@pi-unipi/utility": "2.6.1",
92
+ "@pi-unipi/milestone": "2.6.1",
93
+ "@pi-unipi/kanboard": "2.6.1",
94
+ "@pi-unipi/web-api": "2.6.1",
95
+ "@pi-unipi/workflow": "2.6.1",
96
+ "@pi-unipi/footer": "2.6.1",
97
+ "@pi-unipi/updater": "2.6.1",
98
+ "@pi-unipi/input-shortcuts": "2.6.1",
99
+ "@pi-unipi/cocoindex": "2.6.1"
99
100
  },
100
101
  "devDependencies": {
101
102
  "@earendil-works/pi-agent-core": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@pi-unipi/core": "2.5.0"
43
+ "@pi-unipi/core": "2.6.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -73,6 +73,7 @@ export const COMMAND_REGISTRY: Record<string, string> = {
73
73
  "unipi:worktree-list": "workflow",
74
74
  "unipi:worktree-merge": "workflow",
75
75
  "unipi:consultant": "workflow",
76
+ "unipi:prefix-cache": "utility",
76
77
  "unipi:quick-work": "workflow",
77
78
  "unipi:gather-context": "workflow",
78
79
  "unipi:document": "workflow",
@@ -168,6 +169,7 @@ export const COMMAND_REGISTRY: Record<string, string> = {
168
169
  "unipi:notify-set-ntfy": "notify",
169
170
  "unipi:notify-test": "notify",
170
171
  "unipi:notify-recap-model": "notify",
172
+ "unipi:notify-event": "notify",
171
173
 
172
174
  // kanboard (2 commands)
173
175
  "unipi:kanboard": "kanboard",
@@ -199,6 +201,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
199
201
  "unipi:worktree-list": "List all unipi worktrees",
200
202
  "unipi:worktree-merge": "Merge worktree branches back to main",
201
203
  "unipi:consultant": "Expert consultation — advisory analysis",
204
+ "unipi:prefix-cache": "Show privacy-safe provider prefix-cache diagnostics",
202
205
  "unipi:quick-work": "Fast single-task execution — one shot",
203
206
  "unipi:gather-context": "Research codebase — surface patterns",
204
207
  "unipi:document": "Generate documentation — README, API docs",
@@ -280,6 +283,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
280
283
  "unipi:notify-set-ntfy": "Set up ntfy push notifications",
281
284
  "unipi:notify-test": "Test all enabled notification platforms",
282
285
  "unipi:notify-recap-model": "Select model for notification recaps",
286
+ "unipi:notify-event": "Toggle a notify event without the TUI: <event> <on|off>",
283
287
 
284
288
  "unipi:milestone-onboard": "Create MILESTONES.md from existing workflow docs",
285
289
  "unipi:milestone-update": "Sync MILESTONES.md with completed work",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/btw.ts",
@@ -37,7 +37,7 @@
37
37
  "@earendil-works/pi-tui": "^0.80.0"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "2.5.0"
40
+ "@pi-unipi/core": "2.6.1"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [],
@@ -58,7 +58,7 @@ Uses cocoindex v1.0+ App/fn/mount API with:
58
58
 
59
59
  | Tool | Description |
60
60
  |------|-------------|
61
- | `cocoindex_search` | Search indexed content (semantic vector when available, LanceDB FTS when available, lexical fallback for text-only indexes) |
61
+ | `cocoindex_search` | Search indexed content (semantic vector when available, LanceDB FTS when available, lexical fallback for text-only indexes); defaults to 10 results and hard-caps each page at 50 |
62
62
  | `cocoindex_status` | Check indexing status, freshness, doc count |
63
63
 
64
64
  ## Commands
@@ -76,6 +76,7 @@ Uses cocoindex v1.0+ App/fn/mount API with:
76
76
  - **Data store**: `.unipi/cocoindex/.lancedb/`
77
77
  - **Embeddings**: `~/.unipi/memory/config.json` (shared with memory package)
78
78
  - **Search fallback**: Existing text-only LanceDB tables remain searchable through a lexical scan fallback when no vector column or FTS index exists
79
+ - **Provider context bound**: Search pages are limited to 50 results, each rendered with a 300-character excerpt. Request another offset page instead of placing an unbounded result set into conversation history.
79
80
 
80
81
  ## What Changed from FTS5
81
82
 
@@ -15,22 +15,15 @@ import { registerCocoindexCommands } from "./commands.js";
15
15
  import * as bridge from "./bridge.js";
16
16
 
17
17
  export default function cocoindexExtension(pi: ExtensionAPI): void {
18
- // Register commands at extension load time (synchronous).
19
- // Commands resolve projectDir from ctx.cwd at handler invocation time.
18
+ // Register commands and static tool definitions at extension load. Tool
19
+ // executors resolve ctx.cwd per call, so session_start never changes schemas.
20
20
  registerCocoindexCommands(pi);
21
+ registerCocoindexTools(pi, {
22
+ getProjectDir: (ctx) => ctx.cwd ?? process.cwd(),
23
+ });
21
24
 
22
25
  pi.on("session_start", async (_event, ctx) => {
23
- const projectDir = (ctx as any).cwd ?? process.cwd();
24
-
25
- // Register tools — these need projectDir for search context
26
- const pipelineDir = bridge.getPipelineDir(projectDir);
27
- const initialized = await bridge.isPipelineInitialized(pipelineDir);
28
-
29
- registerCocoindexTools(pi, {
30
- projectDir,
31
- pipelineDir,
32
- initialized,
33
- });
26
+ const projectDir = ctx.cwd ?? process.cwd();
34
27
 
35
28
  emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
36
29
  name: MODULES.COCOINDEX,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/cocoindex",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "CocoIndex integration for Pi — AST-aware content indexing, semantic vector search, and incremental pipeline management",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -16,7 +16,8 @@
16
16
  "vector-search"
17
17
  ],
18
18
  "scripts": {
19
- "typecheck": "tsc --noEmit"
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "npx tsx --test tools.test.ts"
20
21
  },
21
22
  "peerDependencies": {
22
23
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -25,7 +26,7 @@
25
26
  "typebox": "^1.1.38"
26
27
  },
27
28
  "dependencies": {
28
- "@pi-unipi/core": "2.5.0"
29
+ "@pi-unipi/core": "2.6.1"
29
30
  },
30
31
  "optionalDependencies": {
31
32
  "@lancedb/lancedb": "^0.21.0"
@@ -7,18 +7,36 @@
7
7
  */
8
8
 
9
9
  import { Type } from "typebox";
10
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
11
  import { COCOINDEX_PACKAGE_SPEC, COCOINDEX_TOOLS } from "@pi-unipi/core";
12
12
  import * as bridge from "./bridge.js";
13
- import type { CocoindexDeps } from "./bridge.js";
13
+
14
+ export interface CocoindexToolDeps {
15
+ getProjectDir(ctx: ExtensionContext): string;
16
+ }
14
17
 
15
18
  // ─────────────────────────────────────────────────────────
16
19
  // Schemas
17
20
  // ─────────────────────────────────────────────────────────
18
21
 
22
+ export const MAX_SEARCH_RESULTS = 50;
23
+
24
+ export function normalizeSearchPage(limit?: number, offset?: number): { limit: number; offset: number } {
25
+ const requestedLimit = Number.isFinite(limit) ? Math.floor(limit!) : 10;
26
+ const requestedOffset = Number.isFinite(offset) ? Math.floor(offset!) : 0;
27
+ return {
28
+ limit: Math.min(MAX_SEARCH_RESULTS, Math.max(1, requestedLimit)),
29
+ offset: Math.max(0, requestedOffset),
30
+ };
31
+ }
32
+
19
33
  const SearchParams = Type.Object({
20
34
  query: Type.String({ description: "Search query against indexed content" }),
21
- limit: Type.Optional(Type.Number({ description: "Max results (default 10)", minimum: 1 })),
35
+ limit: Type.Optional(Type.Number({
36
+ description: `Max results (default 10, hard cap ${MAX_SEARCH_RESULTS})`,
37
+ minimum: 1,
38
+ maximum: MAX_SEARCH_RESULTS,
39
+ })),
22
40
  offset: Type.Optional(Type.Number({ description: "Pagination offset", minimum: 0 })),
23
41
  });
24
42
 
@@ -39,7 +57,7 @@ function textResult(text: string, details?: Record<string, unknown>): any {
39
57
  // Registration
40
58
  // ─────────────────────────────────────────────────────────
41
59
 
42
- export function registerCocoindexTools(pi: ExtensionAPI, deps: CocoindexDeps): void {
60
+ export function registerCocoindexTools(pi: ExtensionAPI, deps: CocoindexToolDeps): void {
43
61
  // cocoindex_search — search indexed content via LanceDB
44
62
  pi.registerTool({
45
63
  name: COCOINDEX_TOOLS.SEARCH,
@@ -49,7 +67,13 @@ export function registerCocoindexTools(pi: ExtensionAPI, deps: CocoindexDeps): v
49
67
  "Diagnostic/search only: this tool never installs CocoIndex. " +
50
68
  "Use /unipi:cocoindex-init to set up/install, then /unipi:cocoindex-update to index.",
51
69
  parameters: SearchParams,
52
- async execute(_toolCallId: string, params: any): Promise<any> {
70
+ async execute(
71
+ _toolCallId: string,
72
+ params: any,
73
+ _signal: AbortSignal | undefined,
74
+ _onUpdate: unknown,
75
+ ctx: ExtensionContext,
76
+ ): Promise<any> {
53
77
  try {
54
78
  const available = await bridge.isAvailable();
55
79
  if (!available) {
@@ -61,9 +85,12 @@ export function registerCocoindexTools(pi: ExtensionAPI, deps: CocoindexDeps): v
61
85
  );
62
86
  }
63
87
 
64
- const results = await bridge.search(deps.projectDir, params.query, {
65
- limit: params.limit,
66
- offset: params.offset,
88
+ // Clamp in execution as well as schema validation: Pi extensions may
89
+ // mutate tool inputs after validation and custom hosts may skip it.
90
+ const { limit, offset } = normalizeSearchPage(params.limit, params.offset);
91
+ const results = await bridge.search(deps.getProjectDir(ctx), params.query, {
92
+ limit,
93
+ offset,
67
94
  });
68
95
 
69
96
  if (results.length === 0) {
@@ -79,7 +106,8 @@ export function registerCocoindexTools(pi: ExtensionAPI, deps: CocoindexDeps): v
79
106
  );
80
107
  return textResult(
81
108
  `Found ${results.length} results for "${params.query}":\n\n${lines.join("\n\n")}`,
82
- { results } as unknown as Record<string, unknown>,
109
+ // Avoid duplicating full result content in model-visible details.
110
+ { count: results.length, query: params.query, limit, offset },
83
111
  );
84
112
  } catch (err) {
85
113
  return textResult(`CocoIndex search error: ${err}`, { error: true });
@@ -93,9 +121,15 @@ export function registerCocoindexTools(pi: ExtensionAPI, deps: CocoindexDeps): v
93
121
  label: "CocoIndex Status",
94
122
  description: "Check CocoIndex indexing status. Diagnostic only; use commands for interactive install/update.",
95
123
  parameters: StatusParams,
96
- async execute(): Promise<any> {
124
+ async execute(
125
+ _toolCallId: string,
126
+ _params: Record<string, never>,
127
+ _signal: AbortSignal | undefined,
128
+ _onUpdate: unknown,
129
+ ctx: ExtensionContext,
130
+ ): Promise<any> {
97
131
  try {
98
- const info = await bridge.status(deps.projectDir);
132
+ const info = await bridge.status(deps.getProjectDir(ctx));
99
133
  const lines = [
100
134
  `📦 CocoIndex Status`,
101
135
  `CLI available: ${info.cliAvailable ? "✅" : "❌"}`,
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
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.1",
38
+ "@pi-unipi/info-screen": "2.6.1",
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
  }