pi-goosedump 0.1.3 → 0.2.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 (3) hide show
  1. package/README.md +14 -7
  2. package/index.ts +284 -237
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -20,19 +20,19 @@ Once installed, pi-goosedump registers a tool and a slash command:
20
20
 
21
21
  The agent can browse session history with:
22
22
 
23
- | Action | Description |
24
- | -------- | ------------------------------------------ |
25
- | `list` | List all available sessions |
26
- | `search` | Search within a session with regex pattern |
27
- | `expand` | Show full content for specific entry IDs |
28
- | `view` | View the full session transcript |
23
+ | Action | Description |
24
+ | -------- | ---------------------------------------- |
25
+ | `list` | List all available sessions |
26
+ | `search` | Rank messages by query relevance |
27
+ | `expand` | Show full content for specific entry IDs |
28
+ | `view` | View the full session transcript |
29
29
 
30
30
  Examples:
31
31
 
32
32
  ```
33
33
  goosedump({ action: "list" })
34
34
  goosedump({ action: "search", contextId: "abc123", query: "bug fix" })
35
- goosedump({ action: "expand", contextId: "abc123", ids: [0, 3, 5] })
35
+ goosedump({ action: "expand", contextId: "abc123", ids: ["entry-a", "entry-b"] })
36
36
  goosedump({ action: "view", contextId: "abc123" })
37
37
  ```
38
38
 
@@ -40,6 +40,13 @@ goosedump({ action: "view", contextId: "abc123" })
40
40
 
41
41
  Opens an interactive session browser.
42
42
 
43
+ ### Compaction
44
+
45
+ pi-goosedump hooks Pi's `/compact` and auto-compaction flow. It uses
46
+ `goosedump compact pi <session>` with Pi's compaction range (`--from`,
47
+ `--until`, and `--scope`) so the generated summary matches the entries Pi is
48
+ about to replace.
49
+
43
50
  ## License
44
51
 
45
52
  `pi-goosedump` is licensed under `MIT`. See [LICENSE](LICENSE) for more
package/index.ts CHANGED
@@ -3,34 +3,37 @@
3
3
 
4
4
  import type {
5
5
  AgentToolResult,
6
+ CompactionEntry,
7
+ CompactionResult,
6
8
  ExtensionAPI,
7
9
  ExtensionContext,
10
+ SessionEntry,
8
11
  } from '@earendil-works/pi-coding-agent';
9
12
 
10
13
  import { defineTool } from '@earendil-works/pi-coding-agent';
11
14
 
12
15
  import { execFileSync } from 'node:child_process';
13
- import { existsSync } from 'node:fs';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { createRequire } from 'node:module';
14
18
  import { homedir } from 'node:os';
15
- import { join } from 'node:path';
19
+ import { basename, join } from 'node:path';
16
20
 
17
21
  import { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
18
22
  import { Type } from '@sinclair/typebox';
19
23
 
20
- const GOOSEDUMP_VERSION = [0, 1, 0] as const;
24
+ const require = createRequire(import.meta.url);
25
+
26
+ const GOOSEDUMP_VERSION = [0, 2, 1] as const;
21
27
 
22
28
  interface GoosedumpListing {
23
29
  id: string;
24
- created: string;
25
- modified: string;
26
- entryCount: number;
27
30
  }
28
31
 
29
32
  interface GoosedumpMessage {
30
33
  id: string;
31
34
  role: string;
32
35
  content: string;
33
- timestamp: string;
36
+ score?: number;
34
37
  }
35
38
 
36
39
  interface GoosedumpContextResult {
@@ -91,17 +94,14 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
91
94
 
92
95
  function parseListings(output: string): GoosedumpListing[] {
93
96
  const listings: GoosedumpListing[] = [];
94
- // Format: <id> <created> <entryCount> entries <modified>
95
- const regex = /^(\S+)\s+(\S+\s+\S+)\s+(\d+)\s+entries?\s+(.+)$/gm;
96
- let match: RegExpExecArray | null;
97
-
98
- while ((match = regex.exec(output)) !== null) {
99
- listings.push({
100
- id: match[1],
101
- created: match[2],
102
- entryCount: parseInt(match[3], 10),
103
- modified: match[4],
104
- });
97
+ const lines = output.split('\n');
98
+
99
+ for (const line of lines) {
100
+ if (line === 'Sessions:' || line.trim() === '') continue;
101
+ const match = line.match(/^ (\S+)/);
102
+ if (match) {
103
+ listings.push({ id: match[1] });
104
+ }
105
105
  }
106
106
 
107
107
  return listings;
@@ -109,41 +109,46 @@ function parseListings(output: string): GoosedumpListing[] {
109
109
 
110
110
  function parseMessages(output: string): GoosedumpMessage[] {
111
111
  const messages: GoosedumpMessage[] = [];
112
- // Format from goosedump plain output: [id] role: content
113
112
  const lines = output.split('\n');
114
- let currentId = '';
115
- let currentRole = '';
113
+ let currentMsg: GoosedumpMessage | null = null;
116
114
  let contentLines: string[] = [];
117
- let currentTimestamp = '';
118
115
 
119
116
  for (const line of lines) {
120
- const headerMatch = line.match(/^\[([^\]]+)\]\s+(user|assistant|tool|system)\s*(\([^)]+\))?:/);
121
- if (headerMatch) {
122
- if (currentId) {
123
- messages.push({
124
- id: currentId,
125
- role: currentRole,
126
- content: contentLines.join('\n').trim(),
127
- timestamp: currentTimestamp,
128
- });
117
+ if (line.startsWith('results for ')) continue;
118
+
119
+ const entryMatch = line.match(/^entryId:\s+(\S+)\s+\((.+)\)$/);
120
+ if (entryMatch) {
121
+ if (currentMsg) {
122
+ currentMsg.content = contentLines.join('\n').trim();
123
+ messages.push(currentMsg);
124
+ }
125
+ currentMsg = {
126
+ id: entryMatch[1],
127
+ role: entryMatch[2],
128
+ content: '',
129
+ };
130
+ contentLines = [];
131
+ continue;
132
+ }
133
+
134
+ if (currentMsg) {
135
+ const indentedMatch = line.match(/^ (.+)$/);
136
+ if (indentedMatch) {
137
+ const content = indentedMatch[1];
138
+ if (content.startsWith('score: ')) {
139
+ const scoreVal = parseFloat(content.slice(7));
140
+ if (!isNaN(scoreVal)) currentMsg.score = scoreVal;
141
+ continue;
142
+ }
143
+ if (/^\.\.\.\(\d+ lines (?:above|below)\)$/.test(content)) continue;
144
+ contentLines.push(content);
129
145
  }
130
- currentId = headerMatch[1];
131
- currentRole = headerMatch[2];
132
- currentTimestamp = headerMatch[3]?.replace(/[()]/g, '') ?? '';
133
- contentLines = [line.slice(headerMatch[0].length).trim()];
134
- } else if (currentId) {
135
- if (line.startsWith('---') && messages.length > 0) continue;
136
- contentLines.push(line);
137
146
  }
138
147
  }
139
148
 
140
- if (currentId) {
141
- messages.push({
142
- id: currentId,
143
- role: currentRole,
144
- content: contentLines.join('\n').trim(),
145
- timestamp: currentTimestamp,
146
- });
149
+ if (currentMsg) {
150
+ currentMsg.content = contentLines.join('\n').trim();
151
+ messages.push(currentMsg);
147
152
  }
148
153
 
149
154
  return messages;
@@ -161,10 +166,26 @@ function formatMessagesFull(messages: GoosedumpMessage[]): string {
161
166
  return messages.map((m) => `[${m.id}] ${m.role}:\n${m.content}`).join('\n\n---\n\n');
162
167
  }
163
168
 
164
- function goosedumpList(): GoosedumpListing[] {
165
- const output = execFileSync(process.execPath, [binaryPath(), 'pi'], {
169
+ function runGoosedump(args: string[]): string {
170
+ return execFileSync(process.execPath, [binaryPath(), ...args], {
166
171
  encoding: 'utf-8',
167
172
  });
173
+ }
174
+
175
+ function parsePageInfo(output: string, fallbackPage: number): { page: number; totalPages: number } {
176
+ const match = output.match(/\(page\s+(\d+)\s+of\s+(\d+)\):/i);
177
+ if (!match) {
178
+ return { page: fallbackPage, totalPages: 1 };
179
+ }
180
+
181
+ return {
182
+ page: Number(match[1]),
183
+ totalPages: Number(match[2]),
184
+ };
185
+ }
186
+
187
+ function goosedumpList(): GoosedumpListing[] {
188
+ const output = runGoosedump(['list', 'pi']);
168
189
  return parseListings(output);
169
190
  }
170
191
 
@@ -179,56 +200,76 @@ function goosedumpContext(
179
200
  page?: number;
180
201
  } = {},
181
202
  ): GoosedumpContextResult {
182
- const args = ['pi', contextId];
203
+ const scope = options.scope ?? 'lineage';
204
+ const hasQuery = typeof options.grep === 'string';
205
+ const args = hasQuery
206
+ ? options.rank === false
207
+ ? ['grep', 'pi', contextId, options.grep ?? '']
208
+ : ['search', 'pi', contextId, options.grep ?? '']
209
+ : ['show', 'pi', contextId];
183
210
 
184
- if (options.scope && options.scope !== 'lineage') {
185
- args.push('--scope', options.scope);
186
- }
187
- if (options.grep) {
188
- args.push('--grep', options.grep);
189
- }
190
211
  if (options.compact) {
191
- args.push('--compact');
212
+ args.push('--clip');
192
213
  }
193
214
  if (options.ids) {
194
215
  args.push('--ids', options.ids);
195
216
  }
196
- if (options.rank && options.grep) {
197
- args.push('--rank');
198
- if (options.page && options.page > 1) {
199
- args.push('--page', String(options.page));
200
- }
217
+ if (hasQuery && options.rank !== false && options.page) {
218
+ args.push('--page', String(options.page));
201
219
  }
220
+ args.push('--scope', scope);
202
221
 
203
- const output = execFileSync(process.execPath, [binaryPath(), ...args], {
204
- encoding: 'utf-8',
205
- });
206
-
222
+ const output = runGoosedump(args);
207
223
  const messages = parseMessages(output);
224
+ const pageInfo = parsePageInfo(output, options.page ?? 1);
225
+
208
226
  return {
209
227
  messages,
210
228
  totalCount: messages.length,
211
- page: options.page ?? 1,
212
- totalPages: 1,
229
+ page: pageInfo.page,
230
+ totalPages: pageInfo.totalPages,
213
231
  };
214
232
  }
215
233
 
216
234
  function goosedumpExpand(
217
235
  contextId: string,
218
- entryIds: number[],
236
+ entryIds: string[],
219
237
  scope: string = 'lineage',
220
238
  ): GoosedumpMessage[] {
221
- const ids = entryIds.join(',');
222
- const args = ['pi', contextId, '--ids', ids];
223
- if (scope !== 'lineage') {
224
- args.push('--scope', scope);
225
- }
239
+ if (entryIds.length === 0) return [];
240
+
241
+ const output = runGoosedump([
242
+ 'show',
243
+ 'pi',
244
+ contextId,
245
+ '--ids',
246
+ entryIds.join(','),
247
+ '--scope',
248
+ scope,
249
+ ]);
250
+ return parseMessages(output);
251
+ }
226
252
 
227
- const output = execFileSync(process.execPath, [binaryPath(), ...args], {
228
- encoding: 'utf-8',
229
- });
253
+ function goosedumpCompact(
254
+ contextId: string,
255
+ options: {
256
+ scope?: string;
257
+ from?: string;
258
+ until?: string;
259
+ ids?: string[];
260
+ },
261
+ ): string {
262
+ const scope = options.scope ?? 'lineage';
263
+ const args = ['compact', 'pi', contextId, '--scope', scope];
264
+
265
+ if (options.ids && options.ids.length > 0) {
266
+ args.push('--ids', options.ids.join(','));
267
+ } else {
268
+ if (options.from) args.push('--from', options.from);
269
+ if (options.until) args.push('--until', options.until);
270
+ }
230
271
 
231
- return parseMessages(output);
272
+ return runGoosedump(args).trim();
232
273
  }
233
274
 
234
275
  export interface GoosedumpIntegrationOptions {
@@ -236,18 +277,23 @@ export interface GoosedumpIntegrationOptions {
236
277
  overrideDefaultCompaction?: boolean;
237
278
  }
238
279
 
239
- interface MessageLike {
240
- role?: string;
241
- content?: string | { type: string; text?: string; source?: unknown }[];
280
+ interface GoosedumpCompactRange {
281
+ scope: string;
282
+ from?: string;
283
+ until: string;
284
+ ids?: string[];
242
285
  }
243
286
 
244
- interface ExtractedContext {
245
- goal: string;
246
- outstanding: string[];
247
- references: string[];
248
- decisions: string[];
249
- constraints: string[];
250
- status: string;
287
+ interface GoosedumpCompactionDetails {
288
+ source: 'goosedump';
289
+ goosedumpVersion: string | null;
290
+ contextId: string;
291
+ scope: string;
292
+ from?: string;
293
+ until?: string;
294
+ ids?: string[];
295
+ previousSummaryIncluded: boolean;
296
+ fileOps: unknown;
251
297
  }
252
298
 
253
299
  export interface GoosedumpIntegration {
@@ -258,143 +304,138 @@ export default function (pi: ExtensionAPI) {
258
304
  createGoosedumpIntegration().register(pi);
259
305
  }
260
306
 
261
- function getMessageText(m: MessageLike): string {
262
- if (typeof m.content === 'string') return m.content;
263
- if (Array.isArray(m.content)) {
264
- return m.content
265
- .filter((c) => c.type === 'text' && typeof c.text === 'string')
266
- .map((c) => c.text!)
267
- .join(' ');
268
- }
269
- return '';
270
- }
271
-
272
- function extractContextFromMessages(messages: MessageLike[]): ExtractedContext {
273
- const goalPatterns = [
274
- /(?:goal|objective|task|aim|purpose)[\s:]*([^.\n]+)/gi,
275
- /(?:working on|implementing|building|creating|fixing|adding)\s+([^.\n]+)/gi,
276
- /#\s*(?:Goal|Task|Objective)[\s:]*([^\n]+)/gi,
277
- ];
278
-
279
- const referencePatterns = [
280
- /(?:https?:\/\/[^\s]+)/gi,
281
- /(?:[\w./-]+\/[@\w.-]+(?:#\S+)?)/g,
282
- /(?:`[^`]+\.[a-z]{1,6}`)/g,
283
- /(?:v?\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)/g,
284
- /(?:#[1-9]\d*)/g,
285
- ];
286
-
287
- const decisionPatterns = [
288
- /(?:decided|decision|chosen?|opted|settled|agreed)\s+(?:to\s+)?([^.\n]+)/gi,
289
- /(?:will|should|must|going to)\s+(?:use|switch|change|keep|stay|go with)\s+([^.\n]+)/gi,
290
- /(?:let's|lets|we'll)\s+([^.\n]+)/gi,
291
- ];
292
-
293
- const constraintPatterns = [
294
- /(?:cannot|can't|must not|should not|don't|do not|never)\s+([^.\n]+)/gi,
295
- /(?:constraint|restriction|limitation|requirement)[\s:]*([^.\n]+)/gi,
296
- /(?:need to|have to|required to)\s+(?:keep|maintain|preserve|ensure|avoid)\s+([^.\n]+)/gi,
297
- ];
298
-
299
- const statusPatterns = [
300
- /(?:status|state|progress)[\s:]*([^.\n]+)/gi,
301
- /(?:done|completed|finished|resolved|fixed)\s+([^.\n]+)/gi,
302
- /(?:todo|pending|remaining|left|in progress|wip)\s*:?\s*([^.\n]+)/gi,
303
- ];
304
-
305
- const allText = messages.map((m) => getMessageText(m)).join('\n');
306
-
307
- const recentText = messages
308
- .slice(-30)
309
- .map((m) => getMessageText(m))
310
- .join('\n');
311
-
312
- const extractMatches = (text: string, patterns: RegExp[]): Set<string> => {
313
- const matches = new Set<string>();
314
- for (const pattern of patterns) {
315
- pattern.lastIndex = 0;
316
- let match: RegExpExecArray | null;
317
- while ((match = pattern.exec(text)) !== null) {
318
- const extracted = (match[1] ?? '').trim().slice(0, 200);
319
- if (extracted.length > 3) matches.add(extracted);
320
- }
321
- }
322
- return matches;
323
- };
307
+ function sessionFileContextId(sessionFile: string | null | undefined): string | null {
308
+ if (!sessionFile) return null;
324
309
 
325
- const goalMatches = extractMatches(allText, goalPatterns);
326
- const goal = goalMatches.size > 0 ? [...goalMatches][0] : 'Ongoing development work';
310
+ const name = basename(sessionFile);
311
+ const nameMatch = name.match(
312
+ /(?:^|_)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i,
313
+ );
314
+ if (nameMatch) return nameMatch[1];
327
315
 
328
- const outstandingItems = messages
329
- .filter((m: MessageLike) => {
330
- const text = getMessageText(m);
331
- return /todo|pending|remaining|left|wip|bug|issue|fixme/gi.test(text);
332
- })
333
- .slice(-10)
334
- .map((m: MessageLike) => {
335
- const content = getMessageText(m);
336
- const lines = content.split('\n').filter((l: string) => l.trim().length > 10);
337
- return lines.length > 0 ? lines[0].trim().slice(0, 120) : null;
338
- })
339
- .filter((l: string | null): l is string => l !== null)
340
- .slice(0, 8);
316
+ try {
317
+ const firstLine = readFileSync(sessionFile, 'utf-8').split(/\r?\n/, 1)[0];
318
+ const header = JSON.parse(firstLine) as { id?: unknown };
319
+ return typeof header.id === 'string' && header.id.length > 0 ? header.id : null;
320
+ } catch {
321
+ return null;
322
+ }
323
+ }
341
324
 
342
- const referenceMatches = extractMatches(allText, referencePatterns);
343
- const references = [...referenceMatches].slice(0, 15);
325
+ function currentSessionContextId(ctx: ExtensionContext): string | null {
326
+ return sessionFileContextId(ctx.sessionManager.getSessionFile());
327
+ }
344
328
 
345
- const decisionMatches = extractMatches(recentText, decisionPatterns);
346
- const decisions = [...decisionMatches].slice(0, 6);
329
+ function isCompactionEntry(entry: SessionEntry): entry is CompactionEntry {
330
+ return entry.type === 'compaction';
331
+ }
347
332
 
348
- const constraintMatches = extractMatches(allText, constraintPatterns);
349
- const constraints = [...constraintMatches].slice(0, 6);
333
+ function latestCompactionBefore(
334
+ branchEntries: SessionEntry[],
335
+ untilIndex: number,
336
+ ): CompactionEntry | undefined {
337
+ for (let i = untilIndex - 1; i >= 0; i -= 1) {
338
+ const entry = branchEntries[i];
339
+ if (isCompactionEntry(entry)) return entry;
340
+ }
341
+ return undefined;
342
+ }
350
343
 
351
- const statusMatches = extractMatches(recentText, statusPatterns);
352
- const status = statusMatches.size > 0 ? [...statusMatches][0] : 'In progress';
344
+ function compactionStartId(
345
+ branchEntries: SessionEntry[],
346
+ previousCompaction: CompactionEntry,
347
+ untilIndex: number,
348
+ ): string | undefined {
349
+ const keptIndex = branchEntries.findIndex(
350
+ (entry) => entry.id === previousCompaction.firstKeptEntryId,
351
+ );
352
+ if (keptIndex >= 0 && keptIndex < untilIndex) return previousCompaction.firstKeptEntryId;
353
+
354
+ const compactionIndex = branchEntries.findIndex((entry) => entry.id === previousCompaction.id);
355
+ if (compactionIndex >= 0 && compactionIndex + 1 < untilIndex) {
356
+ return branchEntries[compactionIndex + 1].id;
357
+ }
353
358
 
354
- return {
355
- goal,
356
- outstanding:
357
- outstandingItems.length > 0 ? outstandingItems : ['No outstanding items identified'],
358
- references,
359
- decisions,
360
- constraints: constraints.length > 0 ? constraints : ['No specific constraints identified'],
361
- status,
362
- };
359
+ return undefined;
363
360
  }
364
361
 
365
- function formatCompactionSummary(ctx: ExtractedContext): string {
366
- const sections: string[] = [];
362
+ function buildCompactRange(
363
+ branchEntries: SessionEntry[],
364
+ firstKeptEntryId: string,
365
+ ): GoosedumpCompactRange | undefined {
366
+ const untilIndex = branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
367
+ if (untilIndex < 0) return undefined;
368
+
369
+ const previousCompaction = latestCompactionBefore(branchEntries, untilIndex);
370
+ const from = previousCompaction
371
+ ? compactionStartId(branchEntries, previousCompaction, untilIndex)
372
+ : undefined;
373
+
374
+ return from && from !== firstKeptEntryId
375
+ ? { scope: 'lineage', from, until: firstKeptEntryId }
376
+ : { scope: 'lineage', until: firstKeptEntryId };
377
+ }
367
378
 
368
- sections.push(`## Session Goal\n${ctx.goal}`);
379
+ function mergeWithPreviousSummary(
380
+ summary: string,
381
+ previousSummary: string | undefined,
382
+ ): { summary: string; previousSummaryIncluded: boolean } {
383
+ const nextSummary = summary.trim();
384
+ const priorSummary = previousSummary?.trim();
369
385
 
370
- if (ctx.status) {
371
- sections.push(`## Status\n${ctx.status}`);
386
+ if (!priorSummary) {
387
+ return { summary: nextSummary, previousSummaryIncluded: false };
372
388
  }
373
-
374
- if (ctx.outstanding.length > 0) {
375
- sections.push(`## Outstanding Context\n${ctx.outstanding.map((o) => `- ${o}`).join('\n')}`);
376
- }
377
-
378
- if (ctx.decisions.length > 0) {
379
- sections.push(`## Key Decisions\n${ctx.decisions.map((d) => `- ${d}`).join('\n')}`);
389
+ if (!nextSummary) {
390
+ return { summary: priorSummary, previousSummaryIncluded: true };
380
391
  }
381
392
 
382
- if (ctx.constraints.length > 0) {
383
- sections.push(`## Constraints\n${ctx.constraints.map((c) => `- ${c}`).join('\n')}`);
384
- }
393
+ return {
394
+ summary: `${priorSummary}\n\n## Recent Compaction Update\n${nextSummary}`,
395
+ previousSummaryIncluded: true,
396
+ };
397
+ }
385
398
 
386
- if (ctx.references.length > 0) {
387
- sections.push(`## References\n${ctx.references.map((r) => `- ${r}`).join('\n')}`);
388
- }
399
+ function buildGoosedumpCompaction(
400
+ contextId: string,
401
+ preparation: {
402
+ firstKeptEntryId: string;
403
+ tokensBefore: number;
404
+ previousSummary?: string;
405
+ fileOps: unknown;
406
+ },
407
+ branchEntries: SessionEntry[],
408
+ ): CompactionResult<GoosedumpCompactionDetails> | undefined {
409
+ const range = buildCompactRange(branchEntries, preparation.firstKeptEntryId);
410
+ if (!range) return undefined;
411
+
412
+ const summary = goosedumpCompact(contextId, range);
413
+ const merged = mergeWithPreviousSummary(summary, preparation.previousSummary);
414
+ if (!merged.summary) return undefined;
389
415
 
390
- return sections.join('\n\n');
416
+ return {
417
+ summary: merged.summary,
418
+ firstKeptEntryId: preparation.firstKeptEntryId,
419
+ tokensBefore: preparation.tokensBefore,
420
+ details: {
421
+ source: 'goosedump',
422
+ goosedumpVersion: goosedumpVersion(),
423
+ contextId,
424
+ scope: range.scope,
425
+ from: range.from,
426
+ until: range.until,
427
+ ids: range.ids,
428
+ previousSummaryIncluded: merged.previousSummaryIncluded,
429
+ fileOps: preparation.fileOps,
430
+ },
431
+ };
391
432
  }
392
433
 
393
434
  export function createGoosedumpIntegration(
394
435
  options: GoosedumpIntegrationOptions = {},
395
436
  ): GoosedumpIntegration {
396
437
  const shouldRegisterTool = options.registerTool ?? true;
397
- const _overrideDefaultCompaction = options.overrideDefaultCompaction ?? false;
438
+ const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
398
439
 
399
440
  let goosedumpReady = false;
400
441
  let goosedumpEnabled = false;
@@ -431,12 +472,13 @@ export function createGoosedumpIntegration(
431
472
  name: 'goosedump',
432
473
  label: 'goosedump',
433
474
  description:
434
- 'Browse coding agent session history. List all sessions, or view/search within a session. Supports regex search, compact overview, and result expansion. Default scope is active lineage.',
435
- promptSnippet: 'goosedump({ query, scope?, page? }) - search session history',
475
+ 'Browse coding agent session history. List all sessions, or view/search within a session. Supports ranked search, compact overview, and result expansion. Default scope is active lineage.',
476
+ promptSnippet:
477
+ 'goosedump({ action: "search", contextId, query, scope?, page? }) - search session history',
436
478
  promptGuidelines: [
437
479
  'When researching past conversation history, use goosedump to find relevant context.',
438
- 'Start with compact search (compact: true) for quick overview, then expand interesting entries.',
439
- 'Default scope is "lineage" (current branch); use scope: "all" to search all sessions.',
480
+ 'Start with compact ranked search (compact: true) for quick overview, then expand interesting entries.',
481
+ 'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
440
482
  ],
441
483
  parameters: Type.Object({
442
484
  action: Type.Union(
@@ -457,12 +499,12 @@ export function createGoosedumpIntegration(
457
499
  }),
458
500
  ),
459
501
  query: Type.Optional(
460
- Type.String({ description: 'Regex pattern to search for in messages' }),
502
+ Type.String({ description: 'Search query to rank messages by relevance' }),
461
503
  ),
462
504
  scope: Type.Optional(
463
505
  Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
464
506
  default: 'lineage',
465
- description: 'Scope: lineage (current branch) or all (all sessions)',
507
+ description: 'Scope: lineage (current branch) or all entries in the session',
466
508
  }),
467
509
  ),
468
510
  compact: Type.Optional(
@@ -472,7 +514,7 @@ export function createGoosedumpIntegration(
472
514
  }),
473
515
  ),
474
516
  ids: Type.Optional(
475
- Type.Array(Type.Integer(), {
517
+ Type.Array(Type.String(), {
476
518
  description: 'Entry IDs to expand (show full content)',
477
519
  }),
478
520
  ),
@@ -510,9 +552,7 @@ export function createGoosedumpIntegration(
510
552
 
511
553
  const lines = ['Available sessions:', ''];
512
554
  for (const listing of listings) {
513
- lines.push(
514
- ` ${listing.id} — ${listing.entryCount} entries (modified ${listing.modified})`,
515
- );
555
+ lines.push(` ${listing.id}`);
516
556
  }
517
557
 
518
558
  return {
@@ -551,7 +591,8 @@ export function createGoosedumpIntegration(
551
591
  };
552
592
  }
553
593
 
554
- const text = params.compact
594
+ const useCompact = params.compact ?? true;
595
+ const text = useCompact
555
596
  ? formatMessagesCompact(result.messages)
556
597
  : formatMessagesFull(result.messages);
557
598
 
@@ -656,34 +697,41 @@ export function createGoosedumpIntegration(
656
697
  }
657
698
  });
658
699
 
659
- pi.on('session_before_compact', async (event, _ctx) => {
660
- if (!goosedumpEnabled || !goosedumpReady) return;
700
+ pi.on('session_before_compact', async (event, ctx) => {
701
+ if (!shouldOverrideDefaultCompaction || !goosedumpEnabled || !goosedumpReady) return;
702
+ if (event.customInstructions?.trim()) return;
703
+ if (event.signal.aborted) return;
661
704
 
662
- const { messagesToSummarize, firstKeptEntryId, tokensBefore } = event.preparation;
705
+ const { messagesToSummarize, turnPrefixMessages } = event.preparation;
706
+ if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) return;
663
707
 
664
- if (messagesToSummarize.length === 0) return;
708
+ const contextId = currentSessionContextId(ctx);
709
+ if (!contextId) return;
665
710
 
666
- const extracted = extractContextFromMessages(messagesToSummarize as MessageLike[]);
667
- const summary = formatCompactionSummary(extracted);
668
-
669
- return {
670
- compaction: {
671
- summary,
672
- firstKeptEntryId,
673
- tokensBefore,
674
- details: {
675
- source: 'goosedump',
676
- extracted,
677
- },
678
- },
679
- };
711
+ try {
712
+ const compaction = buildGoosedumpCompaction(
713
+ contextId,
714
+ event.preparation,
715
+ event.branchEntries,
716
+ );
717
+ return compaction ? { compaction } : undefined;
718
+ } catch (err) {
719
+ if (ctx.hasUI) {
720
+ const message = err instanceof Error ? err.message : String(err);
721
+ ctx.ui.notify(
722
+ `goosedump compact failed; falling back to Pi compaction: ${message}`,
723
+ 'error',
724
+ );
725
+ }
726
+ return undefined;
727
+ }
680
728
  });
681
729
 
682
- pi.on('session_compact', async (event, _ctx) => {
683
- if (!goosedumpEnabled || !goosedumpReady) return;
730
+ pi.on('session_compact', async (event, ctx) => {
731
+ if (!goosedumpEnabled || !goosedumpReady || !event.fromExtension) return;
684
732
 
685
733
  const compactionEntry = event.compactionEntry;
686
- if (!compactionEntry || compactionEntry.fromHook) return;
734
+ if (!compactionEntry) return;
687
735
 
688
736
  const tokensBefore = compactionEntry.tokensBefore;
689
737
  const summaryLen = compactionEntry.summary.length;
@@ -691,7 +739,7 @@ export function createGoosedumpIntegration(
691
739
  const tokensSaved =
692
740
  tokensBefore > 0 ? Math.round((1 - tokenEstimate / tokensBefore) * 100) : 0;
693
741
 
694
- _ctx.ui.notify(
742
+ ctx.ui.notify(
695
743
  `Goosedump compacted session: ~${tokensSaved}% token reduction (${tokensBefore} → ~${tokenEstimate} tokens)`,
696
744
  'info',
697
745
  );
@@ -755,9 +803,8 @@ export function createGoosedumpIntegration(
755
803
  const listing = listings[i];
756
804
  const isSelected = i === selectedIndex;
757
805
  const cursor = isSelected ? accent('▶') : ' ';
758
- const entryInfo = dim(`${listing.entryCount} entries`);
759
- const idText = isSelected ? accent(`[${listing.id}]`) : text(`[${listing.id}]`);
760
- const line = ` ${cursor} ${idText} ${entryInfo}`;
806
+ const idText = isSelected ? accent(listing.id) : text(listing.id);
807
+ const line = ` ${cursor} ${idText}`;
761
808
  lines.push(makeRow(truncateToWidth(line, innerW), innerW, border));
762
809
  }
763
810
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Coding agent context data browser plugin for pi",
5
5
  "keywords": [
6
6
  "goosedump",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@earendil-works/pi-tui": "^0.78.0",
32
- "@jarkkojs/goosedump": "^0.1.2",
32
+ "@jarkkojs/goosedump": "^0.2.1",
33
33
  "@sinclair/typebox": "^0.34.49"
34
34
  },
35
35
  "devDependencies": {