pi-goosedump 0.1.4 → 0.2.2

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 +238 -187
  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,21 +3,27 @@
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, 4] as const;
24
+ const require = createRequire(import.meta.url);
25
+
26
+ const GOOSEDUMP_VERSION = [0, 2, 2] as const;
21
27
 
22
28
  interface GoosedumpListing {
23
29
  id: string;
@@ -160,10 +166,26 @@ function formatMessagesFull(messages: GoosedumpMessage[]): string {
160
166
  return messages.map((m) => `[${m.id}] ${m.role}:\n${m.content}`).join('\n\n---\n\n');
161
167
  }
162
168
 
163
- function goosedumpList(): GoosedumpListing[] {
164
- const output = execFileSync(process.execPath, [binaryPath(), 'pi'], {
169
+ function runGoosedump(args: string[]): string {
170
+ return execFileSync(process.execPath, [binaryPath(), ...args], {
165
171
  encoding: 'utf-8',
166
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']);
167
189
  return parseListings(output);
168
190
  }
169
191
 
@@ -178,37 +200,34 @@ function goosedumpContext(
178
200
  page?: number;
179
201
  } = {},
180
202
  ): GoosedumpContextResult {
181
- 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];
182
210
 
183
- if (options.scope && options.scope !== 'lineage') {
184
- args.push('--scope', options.scope);
185
- }
186
- if (options.grep) {
187
- args.push('--grep', options.grep);
188
- }
189
211
  if (options.compact) {
190
- args.push('--compact');
212
+ args.push('--clip');
191
213
  }
192
214
  if (options.ids) {
193
215
  args.push('--ids', options.ids);
194
216
  }
195
- if (options.rank && options.grep) {
196
- args.push('--rank');
197
- if (options.page && options.page > 1) {
198
- args.push('--page', String(options.page));
199
- }
217
+ if (hasQuery && options.rank !== false && options.page) {
218
+ args.push('--page', String(options.page));
200
219
  }
220
+ args.push('--scope', scope);
201
221
 
202
- const output = execFileSync(process.execPath, [binaryPath(), ...args], {
203
- encoding: 'utf-8',
204
- });
205
-
222
+ const output = runGoosedump(args);
206
223
  const messages = parseMessages(output);
224
+ const pageInfo = parsePageInfo(output, options.page ?? 1);
225
+
207
226
  return {
208
227
  messages,
209
228
  totalCount: messages.length,
210
- page: options.page ?? 1,
211
- totalPages: 1,
229
+ page: pageInfo.page,
230
+ totalPages: pageInfo.totalPages,
212
231
  };
213
232
  }
214
233
 
@@ -217,17 +236,40 @@ function goosedumpExpand(
217
236
  entryIds: string[],
218
237
  scope: string = 'lineage',
219
238
  ): GoosedumpMessage[] {
220
- const ids = entryIds.join(',');
221
- const args = ['pi', contextId, '--ids', ids];
222
- if (scope !== 'lineage') {
223
- args.push('--scope', scope);
224
- }
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
+ }
225
252
 
226
- const output = execFileSync(process.execPath, [binaryPath(), ...args], {
227
- encoding: 'utf-8',
228
- });
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
+ }
229
271
 
230
- return parseMessages(output);
272
+ return runGoosedump(args).trim();
231
273
  }
232
274
 
233
275
  export interface GoosedumpIntegrationOptions {
@@ -235,18 +277,23 @@ export interface GoosedumpIntegrationOptions {
235
277
  overrideDefaultCompaction?: boolean;
236
278
  }
237
279
 
238
- interface MessageLike {
239
- role?: string;
240
- content?: string | { type: string; text?: string; source?: unknown }[];
280
+ interface GoosedumpCompactRange {
281
+ scope: string;
282
+ from?: string;
283
+ until: string;
284
+ ids?: string[];
241
285
  }
242
286
 
243
- interface ExtractedContext {
244
- goal: string;
245
- outstanding: string[];
246
- references: string[];
247
- decisions: string[];
248
- constraints: string[];
249
- 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;
250
297
  }
251
298
 
252
299
  export interface GoosedumpIntegration {
@@ -257,143 +304,138 @@ export default function (pi: ExtensionAPI) {
257
304
  createGoosedumpIntegration().register(pi);
258
305
  }
259
306
 
260
- function getMessageText(m: MessageLike): string {
261
- if (typeof m.content === 'string') return m.content;
262
- if (Array.isArray(m.content)) {
263
- return m.content
264
- .filter((c) => c.type === 'text' && typeof c.text === 'string')
265
- .map((c) => c.text!)
266
- .join(' ');
267
- }
268
- return '';
269
- }
270
-
271
- function extractContextFromMessages(messages: MessageLike[]): ExtractedContext {
272
- const goalPatterns = [
273
- /(?:goal|objective|task|aim|purpose)[\s:]*([^.\n]+)/gi,
274
- /(?:working on|implementing|building|creating|fixing|adding)\s+([^.\n]+)/gi,
275
- /#\s*(?:Goal|Task|Objective)[\s:]*([^\n]+)/gi,
276
- ];
277
-
278
- const referencePatterns = [
279
- /(?:https?:\/\/[^\s]+)/gi,
280
- /(?:[\w./-]+\/[@\w.-]+(?:#\S+)?)/g,
281
- /(?:`[^`]+\.[a-z]{1,6}`)/g,
282
- /(?:v?\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)/g,
283
- /(?:#[1-9]\d*)/g,
284
- ];
285
-
286
- const decisionPatterns = [
287
- /(?:decided|decision|chosen?|opted|settled|agreed)\s+(?:to\s+)?([^.\n]+)/gi,
288
- /(?:will|should|must|going to)\s+(?:use|switch|change|keep|stay|go with)\s+([^.\n]+)/gi,
289
- /(?:let's|lets|we'll)\s+([^.\n]+)/gi,
290
- ];
291
-
292
- const constraintPatterns = [
293
- /(?:cannot|can't|must not|should not|don't|do not|never)\s+([^.\n]+)/gi,
294
- /(?:constraint|restriction|limitation|requirement)[\s:]*([^.\n]+)/gi,
295
- /(?:need to|have to|required to)\s+(?:keep|maintain|preserve|ensure|avoid)\s+([^.\n]+)/gi,
296
- ];
297
-
298
- const statusPatterns = [
299
- /(?:status|state|progress)[\s:]*([^.\n]+)/gi,
300
- /(?:done|completed|finished|resolved|fixed)\s+([^.\n]+)/gi,
301
- /(?:todo|pending|remaining|left|in progress|wip)\s*:?\s*([^.\n]+)/gi,
302
- ];
303
-
304
- const allText = messages.map((m) => getMessageText(m)).join('\n');
305
-
306
- const recentText = messages
307
- .slice(-30)
308
- .map((m) => getMessageText(m))
309
- .join('\n');
310
-
311
- const extractMatches = (text: string, patterns: RegExp[]): Set<string> => {
312
- const matches = new Set<string>();
313
- for (const pattern of patterns) {
314
- pattern.lastIndex = 0;
315
- let match: RegExpExecArray | null;
316
- while ((match = pattern.exec(text)) !== null) {
317
- const extracted = (match[1] ?? '').trim().slice(0, 200);
318
- if (extracted.length > 3) matches.add(extracted);
319
- }
320
- }
321
- return matches;
322
- };
307
+ function sessionFileContextId(sessionFile: string | null | undefined): string | null {
308
+ if (!sessionFile) return null;
323
309
 
324
- const goalMatches = extractMatches(allText, goalPatterns);
325
- 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];
326
315
 
327
- const outstandingItems = messages
328
- .filter((m: MessageLike) => {
329
- const text = getMessageText(m);
330
- return /todo|pending|remaining|left|wip|bug|issue|fixme/gi.test(text);
331
- })
332
- .slice(-10)
333
- .map((m: MessageLike) => {
334
- const content = getMessageText(m);
335
- const lines = content.split('\n').filter((l: string) => l.trim().length > 10);
336
- return lines.length > 0 ? lines[0].trim().slice(0, 120) : null;
337
- })
338
- .filter((l: string | null): l is string => l !== null)
339
- .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
+ }
340
324
 
341
- const referenceMatches = extractMatches(allText, referencePatterns);
342
- const references = [...referenceMatches].slice(0, 15);
325
+ function currentSessionContextId(ctx: ExtensionContext): string | null {
326
+ return sessionFileContextId(ctx.sessionManager.getSessionFile());
327
+ }
343
328
 
344
- const decisionMatches = extractMatches(recentText, decisionPatterns);
345
- const decisions = [...decisionMatches].slice(0, 6);
329
+ function isCompactionEntry(entry: SessionEntry): entry is CompactionEntry {
330
+ return entry.type === 'compaction';
331
+ }
346
332
 
347
- const constraintMatches = extractMatches(allText, constraintPatterns);
348
- 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
+ }
349
343
 
350
- const statusMatches = extractMatches(recentText, statusPatterns);
351
- 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
+ }
352
358
 
353
- return {
354
- goal,
355
- outstanding:
356
- outstandingItems.length > 0 ? outstandingItems : ['No outstanding items identified'],
357
- references,
358
- decisions,
359
- constraints: constraints.length > 0 ? constraints : ['No specific constraints identified'],
360
- status,
361
- };
359
+ return undefined;
362
360
  }
363
361
 
364
- function formatCompactionSummary(ctx: ExtractedContext): string {
365
- const sections: string[] = [];
366
-
367
- sections.push(`## Session Goal\n${ctx.goal}`);
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
+ }
368
378
 
369
- if (ctx.status) {
370
- sections.push(`## Status\n${ctx.status}`);
371
- }
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();
372
385
 
373
- if (ctx.outstanding.length > 0) {
374
- sections.push(`## Outstanding Context\n${ctx.outstanding.map((o) => `- ${o}`).join('\n')}`);
386
+ if (!priorSummary) {
387
+ return { summary: nextSummary, previousSummaryIncluded: false };
375
388
  }
376
-
377
- if (ctx.decisions.length > 0) {
378
- sections.push(`## Key Decisions\n${ctx.decisions.map((d) => `- ${d}`).join('\n')}`);
389
+ if (!nextSummary) {
390
+ return { summary: priorSummary, previousSummaryIncluded: true };
379
391
  }
380
392
 
381
- if (ctx.constraints.length > 0) {
382
- sections.push(`## Constraints\n${ctx.constraints.map((c) => `- ${c}`).join('\n')}`);
383
- }
393
+ return {
394
+ summary: `${priorSummary}\n\n## Recent Compaction Update\n${nextSummary}`,
395
+ previousSummaryIncluded: true,
396
+ };
397
+ }
384
398
 
385
- if (ctx.references.length > 0) {
386
- sections.push(`## References\n${ctx.references.map((r) => `- ${r}`).join('\n')}`);
387
- }
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;
388
415
 
389
- 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
+ };
390
432
  }
391
433
 
392
434
  export function createGoosedumpIntegration(
393
435
  options: GoosedumpIntegrationOptions = {},
394
436
  ): GoosedumpIntegration {
395
437
  const shouldRegisterTool = options.registerTool ?? true;
396
- const _overrideDefaultCompaction = options.overrideDefaultCompaction ?? false;
438
+ const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
397
439
 
398
440
  let goosedumpReady = false;
399
441
  let goosedumpEnabled = false;
@@ -430,12 +472,13 @@ export function createGoosedumpIntegration(
430
472
  name: 'goosedump',
431
473
  label: 'goosedump',
432
474
  description:
433
- '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.',
434
- 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',
435
478
  promptGuidelines: [
436
479
  'When researching past conversation history, use goosedump to find relevant context.',
437
- 'Start with compact search (compact: true) for quick overview, then expand interesting entries.',
438
- '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.',
439
482
  ],
440
483
  parameters: Type.Object({
441
484
  action: Type.Union(
@@ -456,12 +499,12 @@ export function createGoosedumpIntegration(
456
499
  }),
457
500
  ),
458
501
  query: Type.Optional(
459
- Type.String({ description: 'Regex pattern to search for in messages' }),
502
+ Type.String({ description: 'Search query to rank messages by relevance' }),
460
503
  ),
461
504
  scope: Type.Optional(
462
505
  Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
463
506
  default: 'lineage',
464
- description: 'Scope: lineage (current branch) or all (all sessions)',
507
+ description: 'Scope: lineage (current branch) or all entries in the session',
465
508
  }),
466
509
  ),
467
510
  compact: Type.Optional(
@@ -548,7 +591,8 @@ export function createGoosedumpIntegration(
548
591
  };
549
592
  }
550
593
 
551
- const text = params.compact
594
+ const useCompact = params.compact ?? true;
595
+ const text = useCompact
552
596
  ? formatMessagesCompact(result.messages)
553
597
  : formatMessagesFull(result.messages);
554
598
 
@@ -653,34 +697,41 @@ export function createGoosedumpIntegration(
653
697
  }
654
698
  });
655
699
 
656
- pi.on('session_before_compact', async (event, _ctx) => {
657
- 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;
658
704
 
659
- const { messagesToSummarize, firstKeptEntryId, tokensBefore } = event.preparation;
705
+ const { messagesToSummarize, turnPrefixMessages } = event.preparation;
706
+ if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) return;
660
707
 
661
- if (messagesToSummarize.length === 0) return;
708
+ const contextId = currentSessionContextId(ctx);
709
+ if (!contextId) return;
662
710
 
663
- const extracted = extractContextFromMessages(messagesToSummarize as MessageLike[]);
664
- const summary = formatCompactionSummary(extracted);
665
-
666
- return {
667
- compaction: {
668
- summary,
669
- firstKeptEntryId,
670
- tokensBefore,
671
- details: {
672
- source: 'goosedump',
673
- extracted,
674
- },
675
- },
676
- };
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
+ }
677
728
  });
678
729
 
679
- pi.on('session_compact', async (event, _ctx) => {
680
- if (!goosedumpEnabled || !goosedumpReady) return;
730
+ pi.on('session_compact', async (event, ctx) => {
731
+ if (!goosedumpEnabled || !goosedumpReady || !event.fromExtension) return;
681
732
 
682
733
  const compactionEntry = event.compactionEntry;
683
- if (!compactionEntry || compactionEntry.fromHook) return;
734
+ if (!compactionEntry) return;
684
735
 
685
736
  const tokensBefore = compactionEntry.tokensBefore;
686
737
  const summaryLen = compactionEntry.summary.length;
@@ -688,7 +739,7 @@ export function createGoosedumpIntegration(
688
739
  const tokensSaved =
689
740
  tokensBefore > 0 ? Math.round((1 - tokenEstimate / tokensBefore) * 100) : 0;
690
741
 
691
- _ctx.ui.notify(
742
+ ctx.ui.notify(
692
743
  `Goosedump compacted session: ~${tokensSaved}% token reduction (${tokensBefore} → ~${tokenEstimate} tokens)`,
693
744
  'info',
694
745
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.1.4",
3
+ "version": "0.2.2",
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.4",
32
+ "@jarkkojs/goosedump": "^0.2.2",
33
33
  "@sinclair/typebox": "^0.34.49"
34
34
  },
35
35
  "devDependencies": {