@monotykamary/pi-vcc 0.3.17 → 0.4.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
@@ -210,7 +210,7 @@ pi-vcc is one of four compaction approaches in the AI coding-agent ecosystem. He
210
210
  - **Error resolution detection** — tsc errors in `[Outstanding Context]` are tagged `[RESOLVED]` when the file they reference was subsequently edited, letting the model skip stale errors.
211
211
  - **Task-boundary-aware cut** — compaction splits at complete conversational turns, not mid-tool-call. If the assistant's response is in-flight (unmatched tool calls), the cut pushes back to keep the whole turn in the tail.
212
212
  - **Structured anchors** — `[Anchors]` section lists commit hashes, error IDs, and key file paths for zero-tool-call recall. The model can find references at a glance instead of calling `vcc_recall`.
213
- - **Per-model and global compaction thresholds** — configure different `reserveTokens` or `compactPercent` per model and globally, so models with different context windows compact at the right time. Works in both directions: compact earlier for small-context models, compact later for large-context ones. Applies to both pi-vcc and pi-core compaction. Proactive triggering on `agent_end` and `model_select` events.
213
+ - **Per-model and global compaction thresholds** — configure different `reserveTokens` or `compactPercent` per model and globally, so models with different context windows compact at the right time. Proactive triggering on `agent_end` and `model_select` events compacts earlier for small-context models. Applies to both pi-vcc and pi-core compaction.
214
214
 
215
215
  ## Install
216
216
 
@@ -579,14 +579,15 @@ Config lives at `~/.pi/agent/pi-vcc-config.json` (auto-scaffolded on first load
579
579
 
580
580
  Pi-core's auto-compaction triggers when `contextTokens > contextWindow − reserveTokens`. The global `reserveTokens` (default 16384) is one-size-fits-all — but different models have very different context windows and cost profiles.
581
581
 
582
- Pi-vcc's thresholds control compaction timing in **both directions**, and work at both the per-model and global level:
582
+ Pi-vcc's thresholds provide proactive compaction at both the per-model and global level:
583
583
 
584
584
  | Direction | How it works |
585
585
  |---|---|
586
- | **Compact later** (model can handle more context) | `session_before_compact` cancels compaction when context hasn't crossed the model's threshold. The global threshold might trigger compaction prematurely for a model with a large context window. |
587
586
  | **Compact earlier** (model needs compaction sooner) | `agent_end` and `model_select` proactively trigger compaction when context exceeds the model's threshold but hasn't hit the global threshold yet. The `globalThreshold` also proactively triggers for unmatched models. |
588
587
 
589
- This works regardless of whether pi-vcc or pi-core handles the actual summary (`overrideDefaultCompaction` true or false) — compaction thresholds control the *when*, not the *how*.
588
+ Previously, a "compact later" direction was implemented by cancelling compaction in `session_before_compact` when context was below the per-model threshold. This guard was removed because `session_before_compact` carries no reason field — manual `/compact` and auto-compaction are indistinguishable (both have `customInstructions: undefined`), so the guard was blocking explicit user compaction requests.
589
+
590
+ The proactive trigger handles the "compact earlier" direction. If pi-core's global threshold fires before the per-model threshold is crossed, the compaction proceeds — slightly premature from the per-model threshold's perspective, but preferable to blocking an explicit user action.
590
591
 
591
592
  Key matching order: exact `"provider/modelId"` → `"modelId"` → `globalThreshold` → pi-core's global setting.
592
593
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-vcc",
3
- "version": "0.3.17",
3
+ "version": "0.4.0",
4
4
  "description": "Algorithmic conversation compactor for pi - transcript-preserving structured summaries, no LLM calls",
5
5
  "main": "index.ts",
6
6
  "keywords": [
@@ -224,34 +224,16 @@ const extractOutstandingContext = (blocks: NormalizedBlock[]): string[] => {
224
224
 
225
225
  const formatFileActivityFromUnified = (data: import("../extract/shared-symbols").UnifiedExtractResult): string[] => {
226
226
  const act = data.fileActivity;
227
- const maxSymbolsPerFile = 4;
228
-
229
- const cap = (set: Set<string>, limit: number) => {
230
- const arr = [...set];
231
- if (arr.length <= limit) return arr.join(", ");
232
- return arr.slice(0, limit).join(", ") + ` (+${arr.length - limit} more)`;
233
- };
234
-
235
227
  const formatCategory = (label: string, set: Set<string>): string | null => {
236
228
  if (set.size === 0) return null;
237
229
  const arr = [...set];
238
- const annotated: string[] = [];
239
-
240
- for (const p of arr.slice(0, 10)) {
241
- const syms = act.symbols.get(p);
242
- if (syms && syms.length > 0) {
243
- const sigs = syms.slice(0, maxSymbolsPerFile).join(", ");
244
- const suffix = syms.length > maxSymbolsPerFile ? `, +${syms.length - maxSymbolsPerFile} more` : "";
245
- annotated.push(`${p} (${sigs}${suffix})`);
246
- } else {
247
- annotated.push(p);
248
- }
249
- }
230
+ const kept = arr.slice(0, 10);
250
231
 
251
232
  if (arr.length > 10) {
252
- return `${label}: ${annotated.join(", ")} (+${arr.length - 10} more)`;
233
+ const omitted = arr.slice(10);
234
+ return `${label}: ${kept.join(", ")}, +recall: ${omitted.join(", ")}`;
253
235
  }
254
- return `${label}: ${annotated.join(", ")}`;
236
+ return `${label}: ${kept.join(", ")}`;
255
237
  };
256
238
 
257
239
  const lines: string[] = [];
@@ -271,118 +253,22 @@ const formatTypeCatalogFromUnified = (data: import("../extract/shared-symbols").
271
253
  let totalSigs = 0;
272
254
  const MAX_TOTAL_SIGS = 30;
273
255
 
274
- for (const entry of catalog) {
256
+ const omittedFiles: string[] = [];
257
+ for (let i = 0; i < catalog.length; i++) {
258
+ const entry = catalog[i];
275
259
  if (totalSigs >= MAX_TOTAL_SIGS) {
276
- lines.push("(more signatures omitted)");
277
- break;
260
+ omittedFiles.push(entry.file);
261
+ continue;
278
262
  }
279
- const tag = entry.modified ? "[modified]" : "[read]";
280
- lines.push(`${entry.file} ${tag}:`);
263
+ lines.push(`${entry.file}:`);
281
264
  for (const sig of entry.signatures) {
282
265
  if (totalSigs >= MAX_TOTAL_SIGS) break;
283
266
  lines.push(` ${sig}`);
284
267
  totalSigs++;
285
268
  }
286
269
  }
287
-
288
- return lines;
289
- };
290
-
291
- // User messages that are purely confirmatory — skip these for "Working on"
292
- const CONFIRMATORY_USER_RE =
293
- /^(ok|okay|yes|yeah|yep|sure|great|thanks|thx|nice|looks? good|works?|perfect|done|thanks!*|got it|i see|lgtm|awesome)\b/i;
294
-
295
- /**
296
- * Extract current working status from the tail of the conversation.
297
- * Returns up to 3 lines: current focus, last action, next steps.
298
- */
299
- const extractCurrentStatus = (blocks: NormalizedBlock[]): string[] => {
300
- const items: string[] = [];
301
- const tail = blocks.slice(-20);
302
-
303
- // 1. Current focus: last substantive (non-confirmatory) user message
304
- for (let i = tail.length - 1; i >= 0; i--) {
305
- const b = tail[i];
306
- if (b.kind !== "user") continue;
307
- const text = b.text.trim();
308
- if (text.length < 10 || CONFIRMATORY_USER_RE.test(text)) continue;
309
- items.push(`Working on: ${clip(text, 120)}`);
310
- break;
311
- }
312
-
313
- // 2. Last action: last tool call that modified/read a file
314
- for (let i = tail.length - 1; i >= 0; i--) {
315
- const b = tail[i];
316
- if (b.kind === "tool_call") {
317
- const path = extractPath(b.args);
318
- if (path) {
319
- const cmd = b.name.length > 80 ? `${b.name.slice(0, 77)}...` : b.name;
320
- items.push(`Last action: ${cmd} "${clip(path, 80)}"`);
321
- break;
322
- }
323
- }
324
- }
325
-
326
- // 3. Next steps: last agent text that mentions what to do next
327
- for (let i = tail.length - 1; i >= 0; i--) {
328
- const b = tail[i];
329
- if (b.kind === "assistant" && b.text.trim().length > 20) {
330
- const nextMatch = b.text.match(/(?:next|remaining|todo|still need|what.*left|following)/i);
331
- if (nextMatch) {
332
- items.push(`Next: ${clip(b.text.trim(), 120)}`);
333
- break;
334
- }
335
- }
336
- }
337
-
338
- return items.slice(0, 3);
339
- };
340
-
341
- /**
342
- * Extract structured reference anchors from already-built section data.
343
- * These let the model self-serve common lookups without calling vcc_recall.
344
- */
345
- const extractAnchors = (data: SectionData): string[] => {
346
- const lines: string[] = [];
347
-
348
- // Commit hashes
349
- const commitHashes: string[] = [];
350
- for (const line of data.commits) {
351
- const hashMatch = line.match(/^-\s*([a-f0-9]{7,40}):/);
352
- if (hashMatch) commitHashes.push(hashMatch[1]);
353
- }
354
- if (commitHashes.length > 0) {
355
- lines.push(`commits: ${commitHashes.join(", ")}`);
356
- }
357
-
358
- // Error IDs from outstanding context
359
- const errorIds: string[] = [];
360
- for (const line of data.outstandingContext) {
361
- const tscMatch = line.match(/TS(\d{4,5})/);
362
- if (tscMatch) errorIds.push(`TS${tscMatch[1]}`);
363
- }
364
- if (errorIds.length > 0) {
365
- lines.push(`errors: ${[...new Set(errorIds)].join(", ")}`);
366
- }
367
-
368
- // Key file paths from Files And Changes
369
- const filePaths: string[] = [];
370
- for (const line of data.filesAndChanges) {
371
- const categoryMatch = line.match(/^-\s*(?:Modified|Created|Read):\s*(.*)/);
372
- if (!categoryMatch) continue;
373
- const pathPart = categoryMatch[1]
374
- .replace(/\s*\([^)]*\)/g, "")
375
- .replace(/\s*\(\+\d+ more\)\s*$/, "");
376
- for (const p of pathPart.split(",")) {
377
- const trimmed = p.trim();
378
- if (trimmed) filePaths.push(trimmed);
379
- }
380
- }
381
- if (filePaths.length > 0) {
382
- const display = filePaths.length <= 15
383
- ? filePaths.join(", ")
384
- : `${filePaths.slice(0, 12).join(", ")} (+${filePaths.length - 12} more)`;
385
- lines.push(`files: ${display}`);
270
+ if (omittedFiles.length > 0) {
271
+ lines.push(`(${omittedFiles.length} more files with signatures omitted)`);
386
272
  }
387
273
 
388
274
  return lines;
@@ -417,15 +303,10 @@ export const buildSections = (input: BuildSectionsInput): SectionData => {
417
303
  userPreferences,
418
304
  typeCatalog: formatTypeCatalogFromUnified(fileAndSymbols),
419
305
  symbolChanges: fileAndSymbols.symbolChanges,
420
- currentStatus: extractCurrentStatus(blocks),
421
306
  turnSummaries,
422
- anchors: [], // populated after initial section data is built
423
307
  briefTranscript: stringifyBrief(briefSections),
424
308
  transcriptEntries: sectionsToTranscript(briefSections),
425
309
  };
426
310
 
427
- // Anchors depend on other sections, so extract after building them
428
- result.anchors = extractAnchors(result);
429
-
430
311
  return result;
431
312
  };
@@ -43,20 +43,15 @@ export const capBrief = (text: string): string => {
43
43
  // Find first section header to avoid cutting mid-section
44
44
  const firstHeader = kept.findIndex((l) => /^\[.+\]/.test(l));
45
45
  const clean = firstHeader > 0 ? kept.slice(firstHeader) : kept;
46
- return `...(${omitted} earlier lines omitted)\n\n${clean.join("\n")}`;
46
+ const crumbLine = `...(${omitted} earlier lines omitted)`;
47
+ return `${crumbLine}\n\n${clean.join("\n")}`;
47
48
  };
48
49
 
49
- export const RECALL_NOTE =
50
- "Use `vcc_recall` to search for prior work, decisions, and context from before this summary. " +
51
- "Do not redo work already completed.";
52
-
53
- /**
54
- * Format the summary with cache-friendly section ordering.
50
+ /** Format the summary with cache-friendly section ordering.
55
51
  *
56
52
  * Stable (merged/accumulated) sections come first so the prompt prefix
57
53
  * stays cacheable across compactions. Volatile (always-fresh) sections
58
- * come last.
59
- */
54
+ * come last. */
60
55
  export const formatSummary = (
61
56
  data: SectionData,
62
57
  ): string => {
@@ -66,14 +61,12 @@ export const formatSummary = (
66
61
  section("User Preferences", data.userPreferences),
67
62
  section("Files And Changes", data.filesAndChanges),
68
63
  section("Commits", data.commits),
69
- section("Anchors", data.anchors),
70
64
  ].filter(Boolean);
71
65
 
72
66
  const volatileSections = [
73
67
  section("Type Catalog", data.typeCatalog),
74
68
  section("Outstanding Context", data.outstandingContext),
75
69
  section("Earlier Turns", data.turnSummaries),
76
- section("Current Status", data.currentStatus),
77
70
  ].filter(Boolean);
78
71
 
79
72
  // All header sections (stable + volatile) form the header block
@@ -91,8 +84,5 @@ export const formatSummary = (
91
84
 
92
85
  let result = wrapLongLines(parts.join("\n\n---\n\n"));
93
86
 
94
- // NOTE: RECALL_NOTE is intentionally NOT appended here.
95
- // It is appended once by `compile()` at the very end, after merge-with-previous,
96
- // to avoid the note compounding inside the brief transcript across compactions.
97
87
  return result;
98
88
  };
@@ -3,7 +3,7 @@ import type { FileOps } from "../types";
3
3
  import { normalize } from "./normalize";
4
4
  import { filterNoise } from "./filter-noise";
5
5
  import { buildSections } from "./build-sections";
6
- import { formatSummary, capBrief, RECALL_NOTE, wrapLongLines } from "./format";
6
+ import { formatSummary, capBrief, wrapLongLines } from "./format";
7
7
 
8
8
  export interface CompileInput {
9
9
  messages: Message[];
@@ -17,11 +17,9 @@ const HEADER_NAMES = [
17
17
  "User Preferences",
18
18
  "Files And Changes",
19
19
  "Commits",
20
- "Anchors",
21
20
  "Type Catalog",
22
21
  "Outstanding Context",
23
22
  "Earlier Turns",
24
- "Current Status",
25
23
  ];
26
24
 
27
25
  const SEPARATOR = "\n\n---\n\n";
@@ -31,7 +29,8 @@ const HANDFOFF_PREAMBLE =
31
29
  "This summary captures work done before the most recent messages in this session. " +
32
30
  "Read it to pick up context — this is work already in progress. " +
33
31
  "Do not recap what was done, do not ask what to do next. " +
34
- "Continue directly where you left off.";
32
+ "Continue directly where you left off. " +
33
+ "Use `vcc_recall` to search for prior work, decisions, and context from before this summary.";
35
34
 
36
35
  /** Extract a named section from summary text */
37
36
  const sectionOf = (text: string, header: string): string => {
@@ -57,27 +56,67 @@ const briefOf = (text: string): string => {
57
56
  return text.slice(idx + SEPARATOR.length).trim();
58
57
  };
59
58
 
59
+ /**
60
+ * Extract searchable keywords from a section line for breadcrumb trails.
61
+ *
62
+ * Deterministic: same input always produces the same breadcrumb.
63
+ * Idempotent: the breadcrumb is a pure function of the line text.
64
+ */
65
+ const extractBreadcrumb = (line: string): string => {
66
+ const text = line.replace(/^\s*-\s*/, "").trim();
67
+ if (!text) return "";
68
+ // "edited auth.ts" → "auth.ts"
69
+ const fileMatch = text.match(/(?:edited |read |wrote |created |deleted )?(\S+\.\w{1,12})/);
70
+ if (fileMatch) return fileMatch[1];
71
+ // "Fix login bug → ..." → first few words before →
72
+ const beforeArrow = text.split("\u2192")[0].trim();
73
+ const words = beforeArrow.split(/\s+/).filter(w => w.length > 2).slice(0, 3);
74
+ if (words.length > 0) return words.join(" ");
75
+ // fallback: first content word
76
+ const first = text.split(/\s+/).find(w => w.length > 2);
77
+ return first ?? "";
78
+ };
79
+
60
80
  /** Merge a header section */
61
81
  const mergeHeaderSection = (header: string, prev: string, fresh: string): string => {
62
- // Outstanding Context, Type Catalog, and Current Status are volatile -- always use fresh only
63
- if (header === "Outstanding Context" || header === "Type Catalog" || header === "Current Status" || header === "Anchors") return fresh;
64
- if (!prev) return fresh;
65
- if (!fresh) return prev;
66
-
67
- // Files And Changes: merge by category (Modified/Created/Read), dedup paths
82
+ // Outstanding Context, Type Catalog are volatile -- always use fresh only
83
+ if (header === "Outstanding Context" || header === "Type Catalog") return fresh;
84
+ if (!prev && !fresh) return "";
85
+ // Files And Changes: always run through merge (even when only prev
86
+ // has content) so the cap +recall breadcrumbs are applied.
68
87
  if (header === "Files And Changes") {
69
88
  return mergeFileLines(prev, fresh);
70
89
  }
71
90
 
72
- // Session Goal, User Preferences: line-level dedup, cap
91
+ // Session Goal, User Preferences, Commits, Earlier Turns: line-level dedup, cap
73
92
  const isClean = (l: string) => l.startsWith("- ") && !l.includes("<skill") && !l.includes("</skill");
93
+ // ...recall: breadcrumb lines from prior compactions are preserved as-is,
94
+ // not re-processed or capped. They survive across compactions.
95
+ const isRecallBreadcrumb = (l: string) => l.startsWith("- ...recall:");
74
96
  const prevLines = prev.split("\n").filter(isClean);
75
97
  const freshLines = fresh.split("\n").filter(isClean);
76
- const combined = [...new Set([...prevLines, ...freshLines])];
98
+ const prevBreadcrumbs = prev.split("\n").filter(isRecallBreadcrumb);
99
+ const freshBreadcrumbs = fresh.split("\n").filter(isRecallBreadcrumb);
100
+ const allBreadcrumbs = [...new Set([...prevBreadcrumbs, ...freshBreadcrumbs])];
101
+ const contentLines = [...new Set([...prevLines.filter(l => !isRecallBreadcrumb(l)), ...freshLines.filter(l => !isRecallBreadcrumb(l))])];
77
102
  const CAP = header === "Session Goal" ? 8 : header === "Commits" ? 8 : header === "Earlier Turns" ? 15 : 15;
78
- const capped = combined.length > CAP ? combined.slice(-CAP) : combined;
79
- if (capped.length === 0) return "";
80
- return `[${header}]\n${capped.join("\n")}`;
103
+ if (contentLines.length > CAP) {
104
+ const kept = contentLines.slice(-CAP);
105
+ const dropped = contentLines.slice(0, contentLines.length - CAP);
106
+ const crumbs = dropped.map(extractBreadcrumb).filter(Boolean);
107
+ const headerLine = `[${header}]`;
108
+ // Merge new crumbs with existing breadcrumbs (dedup by extracting terms)
109
+ const allCrumbs = crumbs.length > 0 ? [...allBreadcrumbs, `- ...recall: ${crumbs.join(", ")}`] : allBreadcrumbs;
110
+ if (allCrumbs.length > 0) {
111
+ return `${headerLine}\n${allCrumbs.join("\n")}\n${kept.join("\n")}`;
112
+ }
113
+ return `${headerLine}\n${kept.join("\n")}`;
114
+ }
115
+ if (contentLines.length === 0 && allBreadcrumbs.length === 0) return "";
116
+ const parts: string[] = [];
117
+ if (allBreadcrumbs.length > 0) parts.push(...allBreadcrumbs);
118
+ if (contentLines.length > 0) parts.push(...contentLines);
119
+ return `[${header}]\n${parts.join("\n")}`;
81
120
  };
82
121
 
83
122
  /** Merge Files And Changes by category, dedup paths across compactions */
@@ -88,7 +127,9 @@ const mergeFileLines = (prev: string, fresh: string): string => {
88
127
 
89
128
  // Parse "- Modified: a, b, c (+N more)" lines from both prev and fresh
90
129
  // Also handle symbol-annotated format: "- Modified: a (fn1, fn2), b"
130
+ // Also handle breadcrumb format: "path1, +recall: path2, path3"
91
131
  for (const text of [prev, fresh]) {
132
+ if (!text) continue;
92
133
  for (const line of text.split("\n")) {
93
134
  for (const cat of categories) {
94
135
  const prefix = `- ${cat}: `;
@@ -98,8 +139,12 @@ const mergeFileLines = (prev: string, fresh: string): string => {
98
139
  rest = rest.replace(/\s*\([^)]*\)/g, "");
99
140
  // Strip "(+N more)" suffix
100
141
  rest = rest.replace(/\s*\(\+\d+ more\)\s*$/, "");
142
+ // Strip breadcrumb marker so paths after it are parsed
143
+ rest = rest.replace(/,\s*\+recall:\s*/, ", ");
101
144
  for (const p of rest.split(",")) {
102
145
  const trimmed = p.trim();
146
+ // Skip breadcrumb markers that survived the replace
147
+ if (trimmed.startsWith("+recall:")) continue;
103
148
  if (trimmed) merged[cat].add(trimmed);
104
149
  }
105
150
  }
@@ -112,7 +157,9 @@ const mergeFileLines = (prev: string, fresh: string): string => {
112
157
  const cap = (set: Set<string>, limit: number) => {
113
158
  const arr = [...set];
114
159
  if (arr.length <= limit) return arr.join(", ");
115
- return arr.slice(0, limit).join(", ") + ` (+${arr.length - limit} more)`;
160
+ const kept = arr.slice(0, limit);
161
+ const omitted = arr.slice(limit);
162
+ return kept.join(", ") + `, +recall: ${omitted.join(", ")}`;
116
163
  };
117
164
 
118
165
  const lines: string[] = [];
@@ -166,14 +213,43 @@ export const compile = (input: CompileInput): string => {
166
213
  : undefined;
167
214
  const merged = prev ? mergePrevious(prev, fresh) : fresh;
168
215
  if (!merged) return "";
169
- const body = merged + SEPARATOR + RECALL_NOTE;
216
+ const body = merged;
170
217
  return wrapLongLines(HANDFOFF_PREAMBLE + "\n\n" + body);
171
218
  };
172
219
 
173
220
  const stripRecallNote = (text: string): string => {
174
- // Remove trailing RECALL_NOTE (and any separators surrounding it) if present.
175
- // Handles both current format (---\n\nNOTE) and bare trailing NOTE.
176
- const idx = text.lastIndexOf(RECALL_NOTE);
177
- if (idx < 0) return text;
178
- return text.slice(0, idx).replace(/\s*(?:\n\n---\n\n)?\s*$/, "").trimEnd();
221
+ // Strip leading preamble (HANDFOFF_PREAMBLE) that compile() prepends to every output.
222
+ // When the output from one compaction becomes the previousSummary for the next,
223
+ // the preamble must be removed so mergePrevious only sees the actual sections.
224
+ //
225
+ // Also handles legacy format where RECALL_NOTE was a trailing --- block
226
+ // (before the preamble merge). Both shapes can appear in the same text
227
+ // (old format had a leading preamble AND a trailing RECALL_NOTE). Both must
228
+ // be stripped so the merge only sees clean section data.
229
+
230
+ let result = text;
231
+
232
+ // 1. Leading preamble (current format): "This summary captures..."
233
+ if (result.startsWith("This summary captures work done before")) {
234
+ // Find the end of the preamble paragraph (first section header or double newline)
235
+ const headerStart = result.indexOf("[");
236
+ if (headerStart > 0) {
237
+ result = result.slice(headerStart).trim();
238
+ } else {
239
+ const doubleNL = result.indexOf("\n\n");
240
+ if (doubleNL > 0) result = result.slice(doubleNL + 2).trim();
241
+ }
242
+ }
243
+
244
+ // 2. Trailing legacy RECALL_NOTE (pre-preamble-merge format)
245
+ // Old format appended RECALL_NOTE as a separate --- block at the end.
246
+ // After step 1 strips the leading preamble, the trailing block survives.
247
+ // Must be stripped so it doesn't get merged into the brief transcript.
248
+ const legacyRecall = "Use `vcc_recall` to search for prior work, decisions, and context from before this summary.";
249
+ const legacyIdx = result.lastIndexOf(legacyRecall);
250
+ if (legacyIdx > 0) {
251
+ result = result.slice(0, legacyIdx).replace(/\s*(?:\n\n---\n\n)?\s*$/, "").trimEnd();
252
+ }
253
+
254
+ return result;
179
255
  };
package/src/sections.ts CHANGED
@@ -11,12 +11,8 @@ export interface SectionData {
11
11
  typeCatalog: string[];
12
12
  /** Symbol-level changes (function/type/class names per file) */
13
13
  symbolChanges: SymbolRef[];
14
- /** Current working context: what's being worked on, last action, next steps */
15
- currentStatus: string[];
16
14
  /** Per-turn one-liner summaries for the HCA zone (heaviest compression, oldest turns) */
17
15
  turnSummaries: string[];
18
- /** Structured reference anchors for zero-tool-call recall */
19
- anchors: string[];
20
16
  briefTranscript: string;
21
17
  /** Structured transcript entries (verbose object format) */
22
18
  transcriptEntries: TranscriptEntry[];