pi-goosedump 0.9.3 → 0.9.4
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/index.ts +148 -36
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -10,7 +10,12 @@ import type {
|
|
|
10
10
|
SessionEntry,
|
|
11
11
|
} from '@earendil-works/pi-coding-agent';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_COMPACTION_SETTINGS,
|
|
15
|
+
defineTool,
|
|
16
|
+
findCutPoint,
|
|
17
|
+
getSettingsListTheme,
|
|
18
|
+
} from '@earendil-works/pi-coding-agent';
|
|
14
19
|
|
|
15
20
|
import { execFile, execFileSync } from 'node:child_process';
|
|
16
21
|
import {
|
|
@@ -449,6 +454,56 @@ function isMessageEntry(entry: SessionEntry): entry is SessionEntry & {
|
|
|
449
454
|
return entry.type === 'message';
|
|
450
455
|
}
|
|
451
456
|
|
|
457
|
+
// Entry types that sessionEntryToContextMessages turns into context messages;
|
|
458
|
+
// only these count toward "is there anything to summarize".
|
|
459
|
+
const SUMMARIZABLE_ENTRY_TYPES = new Set(['message', 'custom_message', 'branch_summary']);
|
|
460
|
+
|
|
461
|
+
// Pi's compaction keepRecentTokens setting, project scope over global, matching
|
|
462
|
+
// SettingsManager's merge order.
|
|
463
|
+
function piKeepRecentTokens(cwd: string): number {
|
|
464
|
+
for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
|
|
465
|
+
const compaction = readJsonObject(path).compaction;
|
|
466
|
+
if (isRecord(compaction) && typeof compaction.keepRecentTokens === 'number') {
|
|
467
|
+
return compaction.keepRecentTokens;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return DEFAULT_COMPACTION_SETTINGS.keepRecentTokens;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Predict whether ctx.compact() would throw "Nothing to compact" or "Already
|
|
474
|
+
// compacted": mirrors prepareCompaction's guards over the active lineage so
|
|
475
|
+
// plugin-triggered compaction can skip the call instead of surfacing pi's
|
|
476
|
+
// error toast (and needlessly aborting the agent).
|
|
477
|
+
function hasCompactableHistory(ctx: ExtensionContext): boolean {
|
|
478
|
+
const branch = ctx.sessionManager.getBranch();
|
|
479
|
+
if (branch.length === 0) return false;
|
|
480
|
+
if (branch[branch.length - 1].type === 'compaction') return false;
|
|
481
|
+
|
|
482
|
+
let boundaryStart = 0;
|
|
483
|
+
for (let i = branch.length - 1; i >= 0; i -= 1) {
|
|
484
|
+
const entry = branch[i];
|
|
485
|
+
if (isCompactionEntry(entry)) {
|
|
486
|
+
const keptIndex = branch.findIndex((e) => e.id === entry.firstKeptEntryId);
|
|
487
|
+
boundaryStart = keptIndex >= 0 ? keptIndex : i + 1;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const cut = findCutPoint(branch, boundaryStart, branch.length, piKeepRecentTokens(ctx.cwd));
|
|
493
|
+
if (!branch[cut.firstKeptEntryIndex]?.id) return false;
|
|
494
|
+
|
|
495
|
+
const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
|
|
496
|
+
for (let i = boundaryStart; i < historyEnd; i += 1) {
|
|
497
|
+
if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type)) return true;
|
|
498
|
+
}
|
|
499
|
+
if (cut.isSplitTurn) {
|
|
500
|
+
for (let i = cut.turnStartIndex; i < cut.firstKeptEntryIndex; i += 1) {
|
|
501
|
+
if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type)) return true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
452
507
|
// Resolution of the `keep:N` boundary: scan the active lineage from the newest
|
|
453
508
|
// entry backward, count user turns (those whose message role is `user`), and
|
|
454
509
|
// return the id of the Nth-from-last user entry — the earliest entry to keep.
|
|
@@ -595,12 +650,16 @@ class NumberInputSubmenu implements Component {
|
|
|
595
650
|
handleInput(data: string): void {
|
|
596
651
|
const kb = getKeybindings();
|
|
597
652
|
if (kb.matches(data, 'tui.select.confirm') || data === '\n') {
|
|
598
|
-
const
|
|
599
|
-
if (
|
|
653
|
+
const value = this.input.getValue();
|
|
654
|
+
if (value === '') {
|
|
600
655
|
this.onCancelEdit();
|
|
601
656
|
return;
|
|
602
657
|
}
|
|
603
|
-
|
|
658
|
+
if (!/^\d+$/.test(value)) {
|
|
659
|
+
this.onInvalidValue(`Enter a whole number between ${this.min} and ${this.max}`);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const parsed = Number(value);
|
|
604
663
|
if (!Number.isSafeInteger(parsed) || parsed < this.min || parsed > this.max) {
|
|
605
664
|
this.onInvalidValue(`Enter a whole number between ${this.min} and ${this.max}`);
|
|
606
665
|
return;
|
|
@@ -1017,29 +1076,6 @@ export function createGoosedumpIntegration(
|
|
|
1017
1076
|
if (turnsRemaining <= 0) counterTriggered = true;
|
|
1018
1077
|
}
|
|
1019
1078
|
updateStatus(ctx);
|
|
1020
|
-
|
|
1021
|
-
if (!counterTriggered || compactionQueued) return;
|
|
1022
|
-
// ctx.compact() uses manual compaction semantics and aborts the current run.
|
|
1023
|
-
// Wait until no queued work remains before triggering plugin-driven compaction.
|
|
1024
|
-
if (ctx.hasPendingMessages()) return;
|
|
1025
|
-
|
|
1026
|
-
resetCompactCounters(ctx.cwd);
|
|
1027
|
-
updateStatus(ctx);
|
|
1028
|
-
compactionQueued = true;
|
|
1029
|
-
ctx.compact({
|
|
1030
|
-
onComplete: () => {
|
|
1031
|
-
compactionQueued = false;
|
|
1032
|
-
resetCompactCounters(ctx.cwd);
|
|
1033
|
-
updateStatus(ctx);
|
|
1034
|
-
},
|
|
1035
|
-
onError: (error) => {
|
|
1036
|
-
compactionQueued = false;
|
|
1037
|
-
resetCompactCounters(ctx.cwd);
|
|
1038
|
-
updateStatus(ctx);
|
|
1039
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1040
|
-
if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
|
|
1041
|
-
},
|
|
1042
|
-
});
|
|
1043
1079
|
}
|
|
1044
1080
|
|
|
1045
1081
|
function checkGoosedump(ctx?: ExtensionContext): boolean {
|
|
@@ -1061,18 +1097,28 @@ export function createGoosedumpIntegration(
|
|
|
1061
1097
|
// command and the goose_compact tool). Stashes a turn-based keep override and
|
|
1062
1098
|
// fires Pi's compaction flow; the session_before_compact hook reads pendingKeep
|
|
1063
1099
|
// and resets it on use, completion, or error.
|
|
1064
|
-
function requestCompaction(
|
|
1100
|
+
function requestCompaction(
|
|
1101
|
+
ctx: ExtensionContext,
|
|
1102
|
+
keep: number | null,
|
|
1103
|
+
): 'started' | 'busy' | 'nothing-to-compact' {
|
|
1104
|
+
if (compactionQueued) return 'busy';
|
|
1105
|
+
if (!hasCompactableHistory(ctx)) return 'nothing-to-compact';
|
|
1106
|
+
|
|
1107
|
+
compactionQueued = true;
|
|
1065
1108
|
pendingKeep = keep;
|
|
1066
1109
|
ctx.compact({
|
|
1067
1110
|
onComplete: () => {
|
|
1111
|
+
compactionQueued = false;
|
|
1068
1112
|
pendingKeep = null;
|
|
1069
1113
|
},
|
|
1070
1114
|
onError: (error) => {
|
|
1115
|
+
compactionQueued = false;
|
|
1071
1116
|
pendingKeep = null;
|
|
1072
1117
|
const message = error instanceof Error ? error.message : String(error);
|
|
1073
1118
|
if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
|
|
1074
1119
|
},
|
|
1075
1120
|
});
|
|
1121
|
+
return 'started';
|
|
1076
1122
|
}
|
|
1077
1123
|
|
|
1078
1124
|
function register(pi: ExtensionAPI): void {
|
|
@@ -1330,7 +1376,11 @@ export function createGoosedumpIntegration(
|
|
|
1330
1376
|
if (!goosedumpAvailable) return goosedumpUnavailableResult();
|
|
1331
1377
|
|
|
1332
1378
|
const keep = params.keep ?? null;
|
|
1333
|
-
requestCompaction(ctx, keep);
|
|
1379
|
+
const result = requestCompaction(ctx, keep);
|
|
1380
|
+
if (result === 'busy') return textResult('Compaction already in progress.');
|
|
1381
|
+
if (result === 'nothing-to-compact') {
|
|
1382
|
+
return textResult('Nothing to compact (session too small).');
|
|
1383
|
+
}
|
|
1334
1384
|
return textResult(
|
|
1335
1385
|
keep !== null
|
|
1336
1386
|
? `Compacting with goosedump; keeping the last ${keep} user turn${keep === 1 ? '' : 's'}.`
|
|
@@ -1341,14 +1391,62 @@ export function createGoosedumpIntegration(
|
|
|
1341
1391
|
);
|
|
1342
1392
|
}
|
|
1343
1393
|
|
|
1394
|
+
// Fire counter-driven compaction. ctx.compact() uses manual compaction
|
|
1395
|
+
// semantics and aborts the current run, so a mid-run trigger (resumeAfter)
|
|
1396
|
+
// re-triggers the agent with a synthetic continue message once the
|
|
1397
|
+
// compaction lands. If the session is still too small to compact, the
|
|
1398
|
+
// counter stays armed and retries at a later turn_end/agent_end.
|
|
1399
|
+
function maybeAutoCompact(ctx: ExtensionContext, resumeAfter: boolean): void {
|
|
1400
|
+
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
|
|
1401
|
+
if (!counterTriggered || compactionQueued) return;
|
|
1402
|
+
if (ctx.hasPendingMessages()) return;
|
|
1403
|
+
if (!hasCompactableHistory(ctx)) return;
|
|
1404
|
+
|
|
1405
|
+
resetCompactCounters(ctx.cwd);
|
|
1406
|
+
updateStatus(ctx);
|
|
1407
|
+
compactionQueued = true;
|
|
1408
|
+
ctx.compact({
|
|
1409
|
+
onComplete: () => {
|
|
1410
|
+
compactionQueued = false;
|
|
1411
|
+
resetCompactCounters(ctx.cwd);
|
|
1412
|
+
updateStatus(ctx);
|
|
1413
|
+
if (resumeAfter && !ctx.hasPendingMessages()) {
|
|
1414
|
+
pi.sendMessage(
|
|
1415
|
+
{
|
|
1416
|
+
customType: 'goosedump-autocompact-resume',
|
|
1417
|
+
content:
|
|
1418
|
+
'The session was compacted to reduce context size. Continue the task you were working on from where you left off.',
|
|
1419
|
+
display: false,
|
|
1420
|
+
},
|
|
1421
|
+
{ triggerTurn: true },
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
},
|
|
1425
|
+
onError: (error) => {
|
|
1426
|
+
compactionQueued = false;
|
|
1427
|
+
resetCompactCounters(ctx.cwd);
|
|
1428
|
+
updateStatus(ctx);
|
|
1429
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1430
|
+
if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
|
|
1431
|
+
},
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1344
1435
|
pi.on('session_start', async (_event, ctx) => {
|
|
1345
1436
|
goosedumpAvailable = checkGoosedump(ctx);
|
|
1346
1437
|
resetCompactCounters(ctx.cwd);
|
|
1347
1438
|
updateStatus(ctx);
|
|
1348
1439
|
});
|
|
1349
1440
|
|
|
1350
|
-
pi.on('turn_end', async (
|
|
1441
|
+
pi.on('turn_end', async (event, ctx) => {
|
|
1351
1442
|
noteCompletedTurn(ctx);
|
|
1443
|
+
// A turn with tool results means the agent loop would keep going; compact
|
|
1444
|
+
// between turns and resume so the run's progress is not lost.
|
|
1445
|
+
if (event.toolResults.length > 0) maybeAutoCompact(ctx, true);
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
1449
|
+
maybeAutoCompact(ctx, false);
|
|
1352
1450
|
});
|
|
1353
1451
|
|
|
1354
1452
|
pi.on('session_before_compact', async (event, ctx) => {
|
|
@@ -1371,11 +1469,9 @@ export function createGoosedumpIntegration(
|
|
|
1371
1469
|
keep,
|
|
1372
1470
|
);
|
|
1373
1471
|
if (compaction && 'cancel' in compaction) {
|
|
1374
|
-
pendingKeep = null;
|
|
1375
1472
|
if (ctx.hasUI) ctx.ui.notify(compaction.reason, 'info');
|
|
1376
1473
|
return { cancel: true };
|
|
1377
1474
|
}
|
|
1378
|
-
pendingKeep = null;
|
|
1379
1475
|
return compaction ? { compaction } : undefined;
|
|
1380
1476
|
} catch (err) {
|
|
1381
1477
|
if (ctx.hasUI) {
|
|
@@ -1386,15 +1482,26 @@ export function createGoosedumpIntegration(
|
|
|
1386
1482
|
);
|
|
1387
1483
|
}
|
|
1388
1484
|
return undefined;
|
|
1485
|
+
} finally {
|
|
1486
|
+
pendingKeep = null;
|
|
1389
1487
|
}
|
|
1390
1488
|
});
|
|
1391
1489
|
|
|
1392
1490
|
pi.on('session_compact', async (event, ctx) => {
|
|
1393
|
-
if (!goosedumpAvailable || !
|
|
1491
|
+
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
|
|
1394
1492
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1493
|
+
resetCompactCounters(ctx.cwd);
|
|
1494
|
+
updateStatus(ctx);
|
|
1397
1495
|
|
|
1496
|
+
const compactionEntry = event.compactionEntry;
|
|
1497
|
+
if (
|
|
1498
|
+
!event.fromExtension ||
|
|
1499
|
+
!compactionEntry ||
|
|
1500
|
+
!isRecord(compactionEntry.details) ||
|
|
1501
|
+
compactionEntry.details.source !== 'goosedump'
|
|
1502
|
+
) {
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1398
1505
|
const tokensBefore = compactionEntry.tokensBefore;
|
|
1399
1506
|
const summaryLen = compactionEntry.summary.length;
|
|
1400
1507
|
const tokenEstimate = Math.ceil(summaryLen / 4);
|
|
@@ -1451,7 +1558,12 @@ export function createGoosedumpIntegration(
|
|
|
1451
1558
|
}
|
|
1452
1559
|
}
|
|
1453
1560
|
|
|
1454
|
-
requestCompaction(ctx, keep);
|
|
1561
|
+
const result = requestCompaction(ctx, keep);
|
|
1562
|
+
if (result === 'busy') {
|
|
1563
|
+
ctx.ui.notify('Compaction already in progress', 'info');
|
|
1564
|
+
} else if (result === 'nothing-to-compact') {
|
|
1565
|
+
ctx.ui.notify('Nothing to compact (session too small)', 'info');
|
|
1566
|
+
}
|
|
1455
1567
|
},
|
|
1456
1568
|
});
|
|
1457
1569
|
|