pi-goosedump 0.10.5 → 0.10.7
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 +201 -45
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -50,6 +50,9 @@ import {
|
|
|
50
50
|
import { Type } from '@sinclair/typebox';
|
|
51
51
|
|
|
52
52
|
const require = createRequire(import.meta.url);
|
|
53
|
+
const BRANCH_SUMMARY_PREFIX =
|
|
54
|
+
'The following is a summary of a branch that this conversation came back from:\n\n<summary>\n';
|
|
55
|
+
const BRANCH_SUMMARY_SUFFIX = '</summary>';
|
|
53
56
|
|
|
54
57
|
// One rendered message after normalizing a context into pi's native JSONL.
|
|
55
58
|
interface GoosedumpMessage {
|
|
@@ -91,6 +94,7 @@ interface PiEntryJson {
|
|
|
91
94
|
type?: string;
|
|
92
95
|
id?: string;
|
|
93
96
|
message?: { role?: string; content?: PiPartJson[] | string };
|
|
97
|
+
content?: PiPartJson[] | string;
|
|
94
98
|
summary?: string;
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -108,6 +112,15 @@ const DEFAULT_GOOSEDUMP_SETTINGS: GoosedumpSettings = {
|
|
|
108
112
|
const TURNS_BEFORE_COMPACT_MIN = 0;
|
|
109
113
|
const TURNS_BEFORE_COMPACT_MAX = 255;
|
|
110
114
|
|
|
115
|
+
function isTurnsBeforeCompact(value: unknown): value is number {
|
|
116
|
+
return (
|
|
117
|
+
typeof value === 'number' &&
|
|
118
|
+
Number.isSafeInteger(value) &&
|
|
119
|
+
value >= TURNS_BEFORE_COMPACT_MIN &&
|
|
120
|
+
value <= TURNS_BEFORE_COMPACT_MAX
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
function piAgentDir(): string {
|
|
112
125
|
return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
113
126
|
}
|
|
@@ -138,7 +151,13 @@ function readJsonObject(path: string): Record<string, unknown> {
|
|
|
138
151
|
function readJsonObjectStrict(path: string): Record<string, unknown> {
|
|
139
152
|
if (!existsSync(path)) return {};
|
|
140
153
|
|
|
141
|
-
|
|
154
|
+
let parsed: unknown;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
159
|
+
throw new Error(`${path}: ${message}`);
|
|
160
|
+
}
|
|
142
161
|
if (!isRecord(parsed)) throw new Error(`${path} must contain a JSON object`);
|
|
143
162
|
return parsed;
|
|
144
163
|
}
|
|
@@ -165,17 +184,26 @@ function readGoosedumpSettingsFrom(path: string): Partial<GoosedumpSettings> {
|
|
|
165
184
|
|
|
166
185
|
const settings: Partial<GoosedumpSettings> = {};
|
|
167
186
|
const value = raw.turnsBeforeCompact;
|
|
168
|
-
if (
|
|
169
|
-
typeof value === 'number' &&
|
|
170
|
-
Number.isSafeInteger(value) &&
|
|
171
|
-
value >= TURNS_BEFORE_COMPACT_MIN &&
|
|
172
|
-
value <= TURNS_BEFORE_COMPACT_MAX
|
|
173
|
-
) {
|
|
174
|
-
settings.turnsBeforeCompact = value;
|
|
175
|
-
}
|
|
187
|
+
if (isTurnsBeforeCompact(value)) settings.turnsBeforeCompact = value;
|
|
176
188
|
return settings;
|
|
177
189
|
}
|
|
178
190
|
|
|
191
|
+
function validateGoosedumpSettings(path: string): void {
|
|
192
|
+
const root = readJsonObjectStrict(path);
|
|
193
|
+
if (root.goosedump === undefined) return;
|
|
194
|
+
if (!isRecord(root.goosedump)) {
|
|
195
|
+
throw new Error(`${path}: goosedump must be a JSON object`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const value = root.goosedump.turnsBeforeCompact;
|
|
199
|
+
if (value !== undefined && !isTurnsBeforeCompact(value)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${path}: goosedump.turnsBeforeCompact must be an integer from ` +
|
|
202
|
+
`${TURNS_BEFORE_COMPACT_MIN} to ${TURNS_BEFORE_COMPACT_MAX}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
179
207
|
function resolveGoosedumpSettings(cwd: string): GoosedumpSettings {
|
|
180
208
|
return {
|
|
181
209
|
...DEFAULT_GOOSEDUMP_SETTINGS,
|
|
@@ -187,7 +215,7 @@ function resolveGoosedumpSettings(cwd: string): GoosedumpSettings {
|
|
|
187
215
|
function notifyInvalidSettings(ctx: ExtensionContext): void {
|
|
188
216
|
for (const path of [globalSettingsPath(), projectSettingsPath(ctx.cwd)]) {
|
|
189
217
|
try {
|
|
190
|
-
|
|
218
|
+
validateGoosedumpSettings(path);
|
|
191
219
|
} catch (error) {
|
|
192
220
|
const message = error instanceof Error ? error.message : String(error);
|
|
193
221
|
if (ctx.hasUI) ctx.ui.notify(`goosedump settings error: ${message}`, 'error');
|
|
@@ -202,14 +230,7 @@ function readScopedGoosedumpSettings(cwd: string, scope: SettingsScope): Goosedu
|
|
|
202
230
|
|
|
203
231
|
const settings: Partial<GoosedumpSettings> = {};
|
|
204
232
|
const value = raw.turnsBeforeCompact;
|
|
205
|
-
if (
|
|
206
|
-
typeof value === 'number' &&
|
|
207
|
-
Number.isSafeInteger(value) &&
|
|
208
|
-
value >= TURNS_BEFORE_COMPACT_MIN &&
|
|
209
|
-
value <= TURNS_BEFORE_COMPACT_MAX
|
|
210
|
-
) {
|
|
211
|
-
settings.turnsBeforeCompact = value;
|
|
212
|
-
}
|
|
233
|
+
if (isTurnsBeforeCompact(value)) settings.turnsBeforeCompact = value;
|
|
213
234
|
return { ...DEFAULT_GOOSEDUMP_SETTINGS, ...settings };
|
|
214
235
|
}
|
|
215
236
|
|
|
@@ -250,6 +271,33 @@ function resolveGoosedumpBinary(): string {
|
|
|
250
271
|
return require.resolve('@jarkkojs/goosedump/bin/goosedump.js');
|
|
251
272
|
}
|
|
252
273
|
|
|
274
|
+
function resolveNativeGoosedumpBinary(): string {
|
|
275
|
+
const target = `${process.platform}-${process.arch}`;
|
|
276
|
+
let packageName: string;
|
|
277
|
+
let binaryName = 'goosedump';
|
|
278
|
+
|
|
279
|
+
switch (target) {
|
|
280
|
+
case 'linux-x64':
|
|
281
|
+
packageName = '@jarkkojs/goosedump-linux-x64';
|
|
282
|
+
break;
|
|
283
|
+
case 'darwin-arm64':
|
|
284
|
+
packageName = '@jarkkojs/goosedump-darwin-arm64';
|
|
285
|
+
break;
|
|
286
|
+
case 'win32-x64':
|
|
287
|
+
packageName = '@jarkkojs/goosedump-win32-x64';
|
|
288
|
+
binaryName = 'goosedump.exe';
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
throw new Error(`unsupported platform architecture: ${target}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const wrapperRequire = createRequire(resolveGoosedumpBinary());
|
|
295
|
+
const packageDir = dirname(wrapperRequire.resolve(`${packageName}/package.json`));
|
|
296
|
+
const binaryPath = join(packageDir, 'bin', binaryName);
|
|
297
|
+
if (!existsSync(binaryPath)) throw new Error(`binary not found: ${binaryPath}`);
|
|
298
|
+
return binaryPath;
|
|
299
|
+
}
|
|
300
|
+
|
|
253
301
|
function clip(value: string, max: number): string {
|
|
254
302
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
255
303
|
}
|
|
@@ -296,11 +344,10 @@ function flattenPiContent(content: PiPartJson[] | string | undefined): string {
|
|
|
296
344
|
return content.map(flattenPiPart).filter(Boolean).join('\n');
|
|
297
345
|
}
|
|
298
346
|
|
|
299
|
-
// goosedump
|
|
300
|
-
// Passing `--as-provider pi` normalizes every provider's output to
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
function parsePiMessages(output: string): GoosedumpMessage[] {
|
|
347
|
+
// goosedump renders `show`/`grep`/`search` in a provider's native format.
|
|
348
|
+
// Passing `--as-provider pi` normalizes every provider's output to Pi JSONL, so
|
|
349
|
+
// one parser covers all sources, including Pi-native context entries.
|
|
350
|
+
export function parsePiMessages(output: string): GoosedumpMessage[] {
|
|
304
351
|
const messages: GoosedumpMessage[] = [];
|
|
305
352
|
for (const line of output.split('\n')) {
|
|
306
353
|
if (!line) continue;
|
|
@@ -310,9 +357,22 @@ function parsePiMessages(output: string): GoosedumpMessage[] {
|
|
|
310
357
|
} catch {
|
|
311
358
|
continue;
|
|
312
359
|
}
|
|
313
|
-
if (entry.type
|
|
314
|
-
|
|
315
|
-
|
|
360
|
+
if (entry.type === 'message' && entry.message) {
|
|
361
|
+
const content = flattenPiContent(entry.message.content);
|
|
362
|
+
messages.push({ id: entry.id ?? '', role: entry.message.role ?? 'unknown', content });
|
|
363
|
+
} else if (entry.type === 'custom_message') {
|
|
364
|
+
messages.push({
|
|
365
|
+
id: entry.id ?? '',
|
|
366
|
+
role: 'user',
|
|
367
|
+
content: flattenPiContent(entry.content),
|
|
368
|
+
});
|
|
369
|
+
} else if (entry.type === 'branch_summary') {
|
|
370
|
+
messages.push({
|
|
371
|
+
id: entry.id ?? '',
|
|
372
|
+
role: 'user',
|
|
373
|
+
content: `${BRANCH_SUMMARY_PREFIX}${entry.summary ?? ''}${BRANCH_SUMMARY_SUFFIX}`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
316
376
|
}
|
|
317
377
|
return messages;
|
|
318
378
|
}
|
|
@@ -404,8 +464,8 @@ function runGoosedump(args: string[]): string {
|
|
|
404
464
|
function runGoosedumpAsync(args: string[], signal?: AbortSignal): Promise<GoosedumpCommandOutput> {
|
|
405
465
|
return new Promise((resolve, reject) => {
|
|
406
466
|
execFile(
|
|
407
|
-
|
|
408
|
-
|
|
467
|
+
resolveNativeGoosedumpBinary(),
|
|
468
|
+
args,
|
|
409
469
|
{
|
|
410
470
|
encoding: 'utf-8',
|
|
411
471
|
maxBuffer: 16 * 1024 * 1024,
|
|
@@ -620,11 +680,19 @@ function isCompactionEntry(entry: SessionEntry): entry is CompactionEntry {
|
|
|
620
680
|
|
|
621
681
|
function isMessageEntry(entry: SessionEntry): entry is SessionEntry & {
|
|
622
682
|
type: 'message';
|
|
623
|
-
message: { role?: string };
|
|
683
|
+
message: { role?: string; stopReason?: string };
|
|
624
684
|
} {
|
|
625
685
|
return entry.type === 'message';
|
|
626
686
|
}
|
|
627
687
|
|
|
688
|
+
function isCompletedAssistant(message: { role?: string; stopReason?: string }): boolean {
|
|
689
|
+
return (
|
|
690
|
+
message.role === 'assistant' &&
|
|
691
|
+
message.stopReason !== 'error' &&
|
|
692
|
+
message.stopReason !== 'aborted'
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
628
696
|
// Entry types that sessionEntryToContextMessages turns into context messages;
|
|
629
697
|
// only these count toward "is there anything to summarize".
|
|
630
698
|
const SUMMARIZABLE_ENTRY_TYPES = new Set(['message', 'custom_message', 'branch_summary']);
|
|
@@ -641,6 +709,38 @@ function piKeepRecentTokens(cwd: string): number {
|
|
|
641
709
|
return DEFAULT_COMPACTION_SETTINGS.keepRecentTokens;
|
|
642
710
|
}
|
|
643
711
|
|
|
712
|
+
function piCompactionEnabled(cwd: string): boolean {
|
|
713
|
+
for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
|
|
714
|
+
const compaction = readJsonObject(path).compaction;
|
|
715
|
+
if (isRecord(compaction) && typeof compaction.enabled === 'boolean') {
|
|
716
|
+
return compaction.enabled;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return DEFAULT_COMPACTION_SETTINGS.enabled;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function piReserveTokens(cwd: string): number {
|
|
723
|
+
for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
|
|
724
|
+
const compaction = readJsonObject(path).compaction;
|
|
725
|
+
if (isRecord(compaction) && typeof compaction.reserveTokens === 'number') {
|
|
726
|
+
return compaction.reserveTokens;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return DEFAULT_COMPACTION_SETTINGS.reserveTokens;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const SUMMARY_TOKEN_BUDGET_RATIO = 0.8;
|
|
733
|
+
const OMITTED_SUMMARY_CONTEXT = '\n\n[Earlier compaction context omitted]\n\n';
|
|
734
|
+
|
|
735
|
+
function piAutoCompactionPending(ctx: ExtensionContext): boolean {
|
|
736
|
+
const usage = ctx.getContextUsage();
|
|
737
|
+
if (!usage || usage.tokens === null) return false;
|
|
738
|
+
|
|
739
|
+
return (
|
|
740
|
+
piCompactionEnabled(ctx.cwd) && usage.tokens > usage.contextWindow - piReserveTokens(ctx.cwd)
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
644
744
|
// Predict whether ctx.compact() would throw "Nothing to compact" or "Already
|
|
645
745
|
// compacted": mirrors prepareCompaction's guards over the active lineage so
|
|
646
746
|
// plugin-triggered compaction can skip the call instead of surfacing pi's
|
|
@@ -704,7 +804,7 @@ function completedTurnsSinceCompaction(branchEntries: SessionEntry[]): number {
|
|
|
704
804
|
let completed = 0;
|
|
705
805
|
for (let i = start; i < branchEntries.length; i += 1) {
|
|
706
806
|
const entry = branchEntries[i];
|
|
707
|
-
if (isMessageEntry(entry) && entry.message
|
|
807
|
+
if (isMessageEntry(entry) && isCompletedAssistant(entry.message)) completed += 1;
|
|
708
808
|
}
|
|
709
809
|
return completed;
|
|
710
810
|
}
|
|
@@ -763,12 +863,59 @@ function buildCompactRange(
|
|
|
763
863
|
: { scope: 'lineage', until: firstKeptEntryId };
|
|
764
864
|
}
|
|
765
865
|
|
|
866
|
+
function utf8Prefix(text: string, maxBytes: number): string {
|
|
867
|
+
const prefix: string[] = [];
|
|
868
|
+
let bytes = 0;
|
|
869
|
+
for (const char of text) {
|
|
870
|
+
const charBytes = Buffer.byteLength(char);
|
|
871
|
+
if (bytes + charBytes > maxBytes) break;
|
|
872
|
+
prefix.push(char);
|
|
873
|
+
bytes += charBytes;
|
|
874
|
+
}
|
|
875
|
+
return prefix.join('');
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function utf8Suffix(text: string, maxBytes: number): string {
|
|
879
|
+
const suffix: string[] = [];
|
|
880
|
+
let bytes = 0;
|
|
881
|
+
for (const char of Array.from(text).reverse()) {
|
|
882
|
+
const charBytes = Buffer.byteLength(char);
|
|
883
|
+
if (bytes + charBytes > maxBytes) break;
|
|
884
|
+
suffix.push(char);
|
|
885
|
+
bytes += charBytes;
|
|
886
|
+
}
|
|
887
|
+
return suffix.reverse().join('');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function truncateSummary(summary: string, maxBytes: number): string {
|
|
891
|
+
const text = summary.trim();
|
|
892
|
+
if (Buffer.byteLength(text) <= maxBytes) return text;
|
|
893
|
+
|
|
894
|
+
const markerBytes = Buffer.byteLength(OMITTED_SUMMARY_CONTEXT);
|
|
895
|
+
if (maxBytes <= markerBytes) return utf8Prefix(text, maxBytes);
|
|
896
|
+
|
|
897
|
+
const retainedBytes = maxBytes - markerBytes;
|
|
898
|
+
const headBytes = Math.ceil(retainedBytes / 2);
|
|
899
|
+
const tailBytes = Math.floor(retainedBytes / 2);
|
|
900
|
+
return `${utf8Prefix(text, headBytes)}${OMITTED_SUMMARY_CONTEXT}${utf8Suffix(text, tailBytes)}`;
|
|
901
|
+
}
|
|
902
|
+
|
|
766
903
|
function mergeCompactionSummary(
|
|
767
904
|
previousSummary: string | undefined,
|
|
768
905
|
generatedSummary: string,
|
|
906
|
+
reserveTokens: number,
|
|
769
907
|
): string {
|
|
908
|
+
// A token always represents at least one UTF-8 byte. Capping bytes is
|
|
909
|
+
// conservative, but unlike a chars/token estimate it cannot undercount text.
|
|
910
|
+
const maxBytes = Math.max(1, Math.floor(reserveTokens * SUMMARY_TOKEN_BUDGET_RATIO));
|
|
911
|
+
const generated = truncateSummary(generatedSummary, maxBytes);
|
|
770
912
|
const previous = previousSummary?.trim();
|
|
771
|
-
|
|
913
|
+
const separator = '\n\n';
|
|
914
|
+
const generatedBytes = Buffer.byteLength(generated);
|
|
915
|
+
if (!previous || generatedBytes + Buffer.byteLength(separator) >= maxBytes) return generated;
|
|
916
|
+
|
|
917
|
+
const previousBytes = maxBytes - generatedBytes - Buffer.byteLength(separator);
|
|
918
|
+
return `${truncateSummary(previous, previousBytes)}${separator}${generated}`;
|
|
772
919
|
}
|
|
773
920
|
|
|
774
921
|
async function buildGoosedumpCompaction(
|
|
@@ -780,6 +927,7 @@ async function buildGoosedumpCompaction(
|
|
|
780
927
|
fileOps: unknown;
|
|
781
928
|
},
|
|
782
929
|
branchEntries: SessionEntry[],
|
|
930
|
+
reserveTokens: number,
|
|
783
931
|
keep?: number,
|
|
784
932
|
signal?: AbortSignal,
|
|
785
933
|
): Promise<
|
|
@@ -801,7 +949,11 @@ async function buildGoosedumpCompaction(
|
|
|
801
949
|
|
|
802
950
|
const generated = await goosedumpCompact(target, range, signal);
|
|
803
951
|
if (!generated.summary) return undefined;
|
|
804
|
-
const summary = mergeCompactionSummary(
|
|
952
|
+
const summary = mergeCompactionSummary(
|
|
953
|
+
preparation.previousSummary,
|
|
954
|
+
generated.summary,
|
|
955
|
+
reserveTokens,
|
|
956
|
+
);
|
|
805
957
|
|
|
806
958
|
return {
|
|
807
959
|
summary,
|
|
@@ -935,11 +1087,7 @@ async function runSettingsEditor(ctx: ExtensionContext): Promise<void> {
|
|
|
935
1087
|
const projectPath = projectSettingsPath(location);
|
|
936
1088
|
const projectRaw = readJsonObjectStrict(projectPath).goosedump;
|
|
937
1089
|
const projectValue =
|
|
938
|
-
isRecord(projectRaw) &&
|
|
939
|
-
typeof projectRaw.turnsBeforeCompact === 'number' &&
|
|
940
|
-
Number.isSafeInteger(projectRaw.turnsBeforeCompact) &&
|
|
941
|
-
projectRaw.turnsBeforeCompact >= TURNS_BEFORE_COMPACT_MIN &&
|
|
942
|
-
projectRaw.turnsBeforeCompact <= TURNS_BEFORE_COMPACT_MAX
|
|
1090
|
+
isRecord(projectRaw) && isTurnsBeforeCompact(projectRaw.turnsBeforeCompact)
|
|
943
1091
|
? projectRaw.turnsBeforeCompact
|
|
944
1092
|
: undefined;
|
|
945
1093
|
|
|
@@ -990,12 +1138,7 @@ async function runSettingsEditor(ctx: ExtensionContext): Promise<void> {
|
|
|
990
1138
|
}
|
|
991
1139
|
|
|
992
1140
|
const parsed = Number(newValue);
|
|
993
|
-
if (
|
|
994
|
-
!Number.isSafeInteger(parsed) ||
|
|
995
|
-
parsed < TURNS_BEFORE_COMPACT_MIN ||
|
|
996
|
-
parsed > TURNS_BEFORE_COMPACT_MAX
|
|
997
|
-
)
|
|
998
|
-
return;
|
|
1141
|
+
if (!isTurnsBeforeCompact(parsed)) return;
|
|
999
1142
|
try {
|
|
1000
1143
|
writeScopedGoosedumpSetting(location, scope, key, parsed);
|
|
1001
1144
|
settingsList.updateValue(id, newValue);
|
|
@@ -1289,6 +1432,7 @@ export function createGoosedumpIntegration(
|
|
|
1289
1432
|
let turnsRemaining = 0;
|
|
1290
1433
|
let counterTriggered = false;
|
|
1291
1434
|
let compactionQueued = false;
|
|
1435
|
+
let compactionHookActive = false;
|
|
1292
1436
|
let pendingKeep: number | null = null;
|
|
1293
1437
|
|
|
1294
1438
|
function syncCompactCounters(ctx: ExtensionContext, clearPending = false): void {
|
|
@@ -1339,7 +1483,7 @@ export function createGoosedumpIntegration(
|
|
|
1339
1483
|
ctx: ExtensionContext,
|
|
1340
1484
|
keep: number | null,
|
|
1341
1485
|
): 'started' | 'busy' | 'nothing-to-compact' {
|
|
1342
|
-
if (compactionQueued) return 'busy';
|
|
1486
|
+
if (compactionQueued || compactionHookActive) return 'busy';
|
|
1343
1487
|
if (keep === null && !hasCompactableHistory(ctx)) return 'nothing-to-compact';
|
|
1344
1488
|
|
|
1345
1489
|
compactionQueued = true;
|
|
@@ -1613,6 +1757,10 @@ export function createGoosedumpIntegration(
|
|
|
1613
1757
|
if (!counterTriggered || compactionQueued) return;
|
|
1614
1758
|
if (ctx.hasPendingMessages()) return;
|
|
1615
1759
|
if (!hasCompactableHistory(ctx)) return;
|
|
1760
|
+
// Pi checks this threshold immediately after agent_end. Let that native
|
|
1761
|
+
// path invoke our session_before_compact hook instead of starting a
|
|
1762
|
+
// competing manual compaction.
|
|
1763
|
+
if (!resumeAfter && piAutoCompactionPending(ctx)) return;
|
|
1616
1764
|
|
|
1617
1765
|
syncCompactCounters(ctx, true);
|
|
1618
1766
|
updateStatus(ctx);
|
|
@@ -1652,7 +1800,7 @@ export function createGoosedumpIntegration(
|
|
|
1652
1800
|
});
|
|
1653
1801
|
|
|
1654
1802
|
pi.on('turn_end', async (event, ctx) => {
|
|
1655
|
-
noteCompletedTurn(ctx);
|
|
1803
|
+
if (isCompletedAssistant(event.message)) noteCompletedTurn(ctx);
|
|
1656
1804
|
// A turn with tool results means the agent loop would keep going; compact
|
|
1657
1805
|
// between turns and resume so the run's progress is not lost.
|
|
1658
1806
|
if (event.toolResults.length > 0) maybeAutoCompact(ctx, true);
|
|
@@ -1679,12 +1827,19 @@ export function createGoosedumpIntegration(
|
|
|
1679
1827
|
const target = currentSessionTarget(ctx);
|
|
1680
1828
|
if (!target) return;
|
|
1681
1829
|
|
|
1830
|
+
// Pi checks reserveTokens immediately after agent_end. A turn-driven
|
|
1831
|
+
// ctx.compact() can reach this hook at the same time; let the first
|
|
1832
|
+
// compaction proceed and cancel the duplicate before both append entries.
|
|
1833
|
+
if (compactionHookActive) return { cancel: true };
|
|
1834
|
+
compactionHookActive = true;
|
|
1835
|
+
|
|
1682
1836
|
try {
|
|
1683
1837
|
const keep = pendingKeep ?? undefined;
|
|
1684
1838
|
const compaction = await buildGoosedumpCompaction(
|
|
1685
1839
|
target,
|
|
1686
1840
|
event.preparation,
|
|
1687
1841
|
event.branchEntries,
|
|
1842
|
+
piReserveTokens(ctx.cwd),
|
|
1688
1843
|
keep,
|
|
1689
1844
|
event.signal,
|
|
1690
1845
|
);
|
|
@@ -1708,6 +1863,7 @@ export function createGoosedumpIntegration(
|
|
|
1708
1863
|
}
|
|
1709
1864
|
return undefined;
|
|
1710
1865
|
} finally {
|
|
1866
|
+
compactionHookActive = false;
|
|
1711
1867
|
pendingKeep = null;
|
|
1712
1868
|
}
|
|
1713
1869
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goosedump",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"description": "Pi extension for goosedump-backed session-history search, entry expansion, and compaction",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"compaction",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
36
|
-
"@jarkkojs/goosedump": "^0.10.
|
|
36
|
+
"@jarkkojs/goosedump": "^0.10.7",
|
|
37
37
|
"@sinclair/typebox": "^0.34.49"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|