@monotykamary/pi-vcc 0.5.0 → 0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-vcc",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Algorithmic conversation compactor for pi - transcript-preserving structured summaries, no LLM calls",
5
5
  "main": "index.ts",
6
6
  "keywords": [
@@ -15,9 +15,9 @@
15
15
  "url": "git+https://github.com/monotykamary/pi-vcc.git"
16
16
  },
17
17
  "peerDependencies": {
18
- "@earendil-works/pi-ai": ">=0.74.0 <1.0.0",
19
- "@earendil-works/pi-agent-core": ">=0.74.0 <1.0.0",
20
- "@earendil-works/pi-coding-agent": ">=0.74.0 <1.0.0",
18
+ "@earendil-works/pi-ai": "^0.79.8",
19
+ "@earendil-works/pi-agent-core": "^0.79.8",
20
+ "@earendil-works/pi-coding-agent": "^0.79.8",
21
21
  "typebox": ">=1.1.24 <2.0.0"
22
22
  },
23
23
  "devDependencies": {
@@ -40,6 +40,10 @@ Agent.prototype.subscribe = function (this: Agent, ...args: any[]) {
40
40
  // "Cannot continue from message role: assistant" and the agent loop
41
41
  // exits — leaving mid-task work unfinished.
42
42
  //
43
+ // Note (pi 0.79+): Agent.continue() now drains queued steering/follow-up
44
+ // messages before throwing, so this throw path only fires when there are
45
+ // genuinely no queued messages — the prompt([]) fallback is still correct.
46
+ //
43
47
  // The fix: when continue() would throw because the last message is an
44
48
  // assistant, fall back to prompt([]) instead. prompt([]) doesn't check
45
49
  // the last message role — it starts a fresh agent loop with the current
@@ -2,10 +2,12 @@ import { Type } from "typebox";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { readFileSync } from "fs";
4
4
  import { loadAllMessages } from "../core/load-messages";
5
- import { searchEntries } from "../core/search-entries";
5
+ import { searchEntries, type SearchHit } from "../core/search-entries";
6
6
  import { formatRecallOutput } from "../core/format-recall";
7
7
  import { getActiveLineageEntryIds } from "../core/lineage";
8
8
  import { normalizeRecallScope } from "../core/recall-scope";
9
+ import { renderMessage } from "../core/render-entries";
10
+ import type { Message } from "@earendil-works/pi-ai";
9
11
  import type { PiVccCompactionDetails } from "../details";
10
12
 
11
13
  const DEFAULT_RECENT = 25;
@@ -87,13 +89,14 @@ export const registerRecallTool = (pi: ExtensionAPI) => {
87
89
  promptSnippet:
88
90
  "vcc_recall: Search history; default scope is active lineage. " +
89
91
  "Use scope:'all' for off-lineage branches. " +
90
- "Use scope:'compaction:N' or scope:'compaction:latest' for targeted search within a compaction segment.",
92
+ "Use scope:'compaction:N' or scope:'compaction:latest' for targeted search within a compaction segment. " +
93
+ "expand:[indices] returns full content for those entries, composable with query to expand matched results.",
91
94
  parameters: Type.Object({
92
95
  query: Type.Optional(
93
96
  Type.String({ description: "Search terms or regex pattern (e.g. 'hook|inject', 'fail.*build'). Multi-word = OR ranked by relevance." }),
94
97
  ),
95
98
  expand: Type.Optional(
96
- Type.Array(Type.Number(), { description: "Entry indices to return full untruncated content for" }),
99
+ Type.Array(Type.Number(), { description: "Entry indices to return full untruncated content for. Works alone (any index in scope) or alongside query (expands matching entries on the current page)." }),
97
100
  ),
98
101
  page: Type.Optional(
99
102
  Type.Number({ description: "Page number (1-based) for paginated search results. Default: 1." }),
@@ -177,15 +180,53 @@ export const registerRecallTool = (pi: ExtensionAPI) => {
177
180
  };
178
181
  }
179
182
  const start = (page - 1) * PAGE_SIZE;
180
- const pageResults = allResults.slice(start, start + PAGE_SIZE);
183
+ const pageResults = allResults.slice(start, start + PAGE_SIZE) as SearchHit[];
181
184
  const header = totalPages > 1
182
185
  ? `Page ${page}/${totalPages} (${allResults.length} total matches${searchScopeLabel})`
183
186
  : `${allResults.length} matches${searchScopeLabel}`;
184
- const footer = page < totalPages && page < MAX_PAGES
185
- ? `\n--- Use page:${page + 1} for more results ---`
186
- : totalPages > MAX_PAGES
187
- ? `\n--- Results truncated at ${MAX_PAGES} pages. Use a more specific query or scope to narrow results. ---`
188
- : "";
187
+
188
+ // Compose: when expand indices accompany a query, swap the truncated
189
+ // snippet for full untruncated content on any paged result whose
190
+ // index is in expandSet. rawMessages (loaded above with full=false)
191
+ // are parallel to `msgs`, so we re-render only the requested
192
+ // indices at full=true instead of re-reading the session file.
193
+ const expanded: number[] = [];
194
+ if (hasExpand) {
195
+ const msgByIndex = new Map<number, Message>();
196
+ for (let i = 0; i < msgs.length; i++) {
197
+ msgByIndex.set(msgs[i].index, rawMessages[i]);
198
+ }
199
+ for (const r of pageResults) {
200
+ if (!expandSet.has(r.index)) continue;
201
+ const raw = msgByIndex.get(r.index);
202
+ if (!raw) continue;
203
+ const full = renderMessage(raw, r.index, true);
204
+ // formatEntry prefers `snippet` for matched entries; set both
205
+ // so the full content renders regardless of match state.
206
+ r.snippet = full.summary;
207
+ r.summary = full.summary;
208
+ expanded.push(r.index);
209
+ }
210
+ }
211
+
212
+ const footerParts: string[] = [];
213
+ if (page < totalPages && page < MAX_PAGES) {
214
+ footerParts.push(`--- Use page:${page + 1} for more results ---`);
215
+ } else if (totalPages > MAX_PAGES) {
216
+ footerParts.push(`--- Results truncated at ${MAX_PAGES} pages. Use a more specific query or scope to narrow results. ---`);
217
+ }
218
+ if (hasExpand) {
219
+ const notExpanded = [...expandSet].filter((i) => !expanded.includes(i));
220
+ const noun = expanded.length === 1 ? "entry" : "entries";
221
+ if (expanded.length > 0 && notExpanded.length === 0) {
222
+ footerParts.push(`--- expanded ${expanded.length} ${noun} to full content ---`);
223
+ } else if (expanded.length > 0) {
224
+ footerParts.push(`--- expanded ${expanded.length} ${noun} to full content; not on this page: ${notExpanded.join(", ")} ---`);
225
+ } else if (notExpanded.length > 0) {
226
+ footerParts.push(`--- no expand indices on this page: ${notExpanded.join(", ")} ---`);
227
+ }
228
+ }
229
+ const footer = footerParts.length ? "\n" + footerParts.join("\n") : "";
189
230
  const output = formatRecallOutput(pageResults, params.query, header) + footer;
190
231
  return {
191
232
  content: [{ type: "text", text: output }],