pi-goosedump 0.10.0 → 0.10.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.
- package/README.md +14 -10
- package/index.ts +232 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,9 +74,12 @@ section and are optional (defaults shown):
|
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
`turnsBeforeCompact` sets how many LLM turns elapse before goosedump triggers
|
|
77
|
-
compaction, as an integer 0–255. `0` disables the counter
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
compaction, as an integer 0–255. `0` disables the counter. The settings editor
|
|
78
|
+
can clear the project value to inherit Global again, and writes updates atomically.
|
|
79
|
+
The countdown is reconstructed from the active session after Pi restarts or
|
|
80
|
+
resumes, rather than restarting at the configured value. When enabled, the footer
|
|
81
|
+
shows `GD: XX` counting down in hex (e.g. `GD: 0A`); `GD: 00` is armed and waiting
|
|
82
|
+
to compact, while `GD: --` means disabled.
|
|
80
83
|
|
|
81
84
|
### Compaction flow
|
|
82
85
|
|
|
@@ -91,13 +94,14 @@ header line, then runs `goosedump compact` over the replacement range:
|
|
|
91
94
|
compaction.
|
|
92
95
|
- `--until` is the first entry Pi is keeping (`firstKeptEntryId`).
|
|
93
96
|
|
|
94
|
-
goosedump composes the structured summary itself
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
goosedump composes the structured summary itself and emits a native Pi
|
|
98
|
+
compaction entry, from which the extension extracts the `summary` field. The
|
|
99
|
+
extension prepends Pi's previous compaction summary when present, so prior goals,
|
|
100
|
+
progress, and context survive incremental compactions. A `keep:N` override (via
|
|
101
|
+
`/goose-compact` or the `goose_compact` tool) replaces Pi's token-based cut with
|
|
102
|
+
a turn-based one: summarize everything before the Nth-from-last user entry in the
|
|
103
|
+
active, uncompacted history and keep the tail verbatim. Fewer than N active user
|
|
104
|
+
turns cancels with a notice. On completion Pi notifies the token reduction.
|
|
101
105
|
|
|
102
106
|
## License
|
|
103
107
|
|
package/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from '@earendil-works/pi-coding-agent';
|
|
19
19
|
|
|
20
20
|
import { execFile, execFileSync } from 'node:child_process';
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
21
22
|
import {
|
|
22
23
|
closeSync,
|
|
23
24
|
existsSync,
|
|
@@ -25,6 +26,8 @@ import {
|
|
|
25
26
|
openSync,
|
|
26
27
|
readFileSync,
|
|
27
28
|
readSync,
|
|
29
|
+
renameSync,
|
|
30
|
+
unlinkSync,
|
|
28
31
|
writeFileSync,
|
|
29
32
|
} from 'node:fs';
|
|
30
33
|
import { createRequire } from 'node:module';
|
|
@@ -126,6 +129,30 @@ function readJsonObject(path: string): Record<string, unknown> {
|
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
function readJsonObjectStrict(path: string): Record<string, unknown> {
|
|
133
|
+
if (!existsSync(path)) return {};
|
|
134
|
+
|
|
135
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;
|
|
136
|
+
if (!isRecord(parsed)) throw new Error(`${path} must contain a JSON object`);
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function writeJsonObjectAtomically(path: string, value: Record<string, unknown>): void {
|
|
141
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
142
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
143
|
+
try {
|
|
144
|
+
writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
|
|
145
|
+
renameSync(temporaryPath, path);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
try {
|
|
148
|
+
unlinkSync(temporaryPath);
|
|
149
|
+
} catch {
|
|
150
|
+
// The temporary file was either never created or has already been renamed.
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
129
156
|
function readGoosedumpSettingsFrom(path: string): Partial<GoosedumpSettings> {
|
|
130
157
|
const raw = readJsonObject(path).goosedump;
|
|
131
158
|
if (!isRecord(raw)) return {};
|
|
@@ -151,9 +178,33 @@ function resolveGoosedumpSettings(cwd: string): GoosedumpSettings {
|
|
|
151
178
|
};
|
|
152
179
|
}
|
|
153
180
|
|
|
181
|
+
function notifyInvalidSettings(ctx: ExtensionContext): void {
|
|
182
|
+
for (const path of [globalSettingsPath(), projectSettingsPath(ctx.cwd)]) {
|
|
183
|
+
try {
|
|
184
|
+
readJsonObjectStrict(path);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
if (ctx.hasUI) ctx.ui.notify(`goosedump settings error: ${message}`, 'error');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
154
192
|
function readScopedGoosedumpSettings(cwd: string, scope: SettingsScope): GoosedumpSettings {
|
|
155
193
|
const path = scope === 'global' ? globalSettingsPath() : projectSettingsPath(cwd);
|
|
156
|
-
|
|
194
|
+
const raw = readJsonObjectStrict(path).goosedump;
|
|
195
|
+
if (!isRecord(raw)) return { ...DEFAULT_GOOSEDUMP_SETTINGS };
|
|
196
|
+
|
|
197
|
+
const settings: Partial<GoosedumpSettings> = {};
|
|
198
|
+
const value = raw.turnsBeforeCompact;
|
|
199
|
+
if (
|
|
200
|
+
typeof value === 'number' &&
|
|
201
|
+
Number.isSafeInteger(value) &&
|
|
202
|
+
value >= TURNS_BEFORE_COMPACT_MIN &&
|
|
203
|
+
value <= TURNS_BEFORE_COMPACT_MAX
|
|
204
|
+
) {
|
|
205
|
+
settings.turnsBeforeCompact = value;
|
|
206
|
+
}
|
|
207
|
+
return { ...DEFAULT_GOOSEDUMP_SETTINGS, ...settings };
|
|
157
208
|
}
|
|
158
209
|
|
|
159
210
|
function writeScopedGoosedumpSetting(
|
|
@@ -163,11 +214,26 @@ function writeScopedGoosedumpSetting(
|
|
|
163
214
|
value: number,
|
|
164
215
|
): void {
|
|
165
216
|
const path = scope === 'global' ? globalSettingsPath() : projectSettingsPath(cwd);
|
|
166
|
-
const root =
|
|
217
|
+
const root = readJsonObjectStrict(path);
|
|
167
218
|
const goosedump = isRecord(root.goosedump) ? root.goosedump : {};
|
|
168
219
|
root.goosedump = { ...goosedump, [key]: value };
|
|
169
|
-
|
|
170
|
-
|
|
220
|
+
writeJsonObjectAtomically(path, root);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function clearScopedGoosedumpSetting(
|
|
224
|
+
cwd: string,
|
|
225
|
+
scope: SettingsScope,
|
|
226
|
+
key: GoosedumpSettingKey,
|
|
227
|
+
): void {
|
|
228
|
+
const path = scope === 'global' ? globalSettingsPath() : projectSettingsPath(cwd);
|
|
229
|
+
const root = readJsonObjectStrict(path);
|
|
230
|
+
if (!isRecord(root.goosedump)) return;
|
|
231
|
+
|
|
232
|
+
const goosedump = { ...root.goosedump };
|
|
233
|
+
delete goosedump[key];
|
|
234
|
+
if (Object.keys(goosedump).length === 0) delete root.goosedump;
|
|
235
|
+
else root.goosedump = goosedump;
|
|
236
|
+
writeJsonObjectAtomically(path, root);
|
|
171
237
|
}
|
|
172
238
|
|
|
173
239
|
function formatCompactSetting(value: number): string {
|
|
@@ -352,8 +418,9 @@ function runGoosedumpAsync(args: string[], signal?: AbortSignal): Promise<string
|
|
|
352
418
|
|
|
353
419
|
// Read only the first line of a file without loading the whole transcript.
|
|
354
420
|
function readFirstLine(path: string): string | null {
|
|
355
|
-
|
|
421
|
+
let fd: number | undefined;
|
|
356
422
|
try {
|
|
423
|
+
fd = openSync(path, 'r');
|
|
357
424
|
const buf = Buffer.alloc(8192);
|
|
358
425
|
const bytesRead = readSync(fd, buf, 0, buf.length, 0);
|
|
359
426
|
if (bytesRead < 0) return null;
|
|
@@ -363,7 +430,7 @@ function readFirstLine(path: string): string | null {
|
|
|
363
430
|
} catch {
|
|
364
431
|
return null;
|
|
365
432
|
} finally {
|
|
366
|
-
closeSync(fd);
|
|
433
|
+
if (fd !== undefined) closeSync(fd);
|
|
367
434
|
}
|
|
368
435
|
}
|
|
369
436
|
|
|
@@ -573,17 +640,10 @@ function hasCompactableHistory(ctx: ExtensionContext): boolean {
|
|
|
573
640
|
if (branch.length === 0) return false;
|
|
574
641
|
if (branch[branch.length - 1].type === 'compaction') return false;
|
|
575
642
|
|
|
576
|
-
|
|
577
|
-
for (let i = branch.length - 1; i >= 0; i -= 1) {
|
|
578
|
-
const entry = branch[i];
|
|
579
|
-
if (isCompactionEntry(entry)) {
|
|
580
|
-
const keptIndex = branch.findIndex((e) => e.id === entry.firstKeptEntryId);
|
|
581
|
-
boundaryStart = keptIndex >= 0 ? keptIndex : i + 1;
|
|
582
|
-
break;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
643
|
+
const boundaryStart = activeHistoryStartIndex(branch);
|
|
585
644
|
|
|
586
645
|
const cut = findCutPoint(branch, boundaryStart, branch.length, piKeepRecentTokens(ctx.cwd));
|
|
646
|
+
|
|
587
647
|
if (!branch[cut.firstKeptEntryIndex]?.id) return false;
|
|
588
648
|
|
|
589
649
|
const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
|
|
@@ -598,32 +658,71 @@ function hasCompactableHistory(ctx: ExtensionContext): boolean {
|
|
|
598
658
|
return false;
|
|
599
659
|
}
|
|
600
660
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
661
|
+
function latestCompaction(branchEntries: SessionEntry[]): CompactionEntry | undefined {
|
|
662
|
+
for (let i = branchEntries.length - 1; i >= 0; i -= 1) {
|
|
663
|
+
const entry = branchEntries[i];
|
|
664
|
+
if (isCompactionEntry(entry)) return entry;
|
|
665
|
+
}
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function compactionBoundaryStartIndex(
|
|
670
|
+
branchEntries: SessionEntry[],
|
|
671
|
+
compaction: CompactionEntry,
|
|
672
|
+
): number {
|
|
673
|
+
const keptIndex = branchEntries.findIndex((entry) => entry.id === compaction.firstKeptEntryId);
|
|
674
|
+
if (keptIndex >= 0) return keptIndex;
|
|
675
|
+
|
|
676
|
+
const compactionIndex = branchEntries.findIndex((entry) => entry.id === compaction.id);
|
|
677
|
+
return compactionIndex >= 0 ? compactionIndex + 1 : 0;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function activeHistoryStartIndex(branchEntries: SessionEntry[]): number {
|
|
681
|
+
const compaction = latestCompaction(branchEntries);
|
|
682
|
+
return compaction ? compactionBoundaryStartIndex(branchEntries, compaction) : 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function completedTurnsSinceCompaction(branchEntries: SessionEntry[]): number {
|
|
686
|
+
let start = 0;
|
|
687
|
+
for (let i = branchEntries.length - 1; i >= 0; i -= 1) {
|
|
688
|
+
if (isCompactionEntry(branchEntries[i])) {
|
|
689
|
+
start = i + 1;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
let completed = 0;
|
|
695
|
+
for (let i = start; i < branchEntries.length; i += 1) {
|
|
696
|
+
const entry = branchEntries[i];
|
|
697
|
+
if (isMessageEntry(entry) && entry.message.role === 'assistant') completed += 1;
|
|
698
|
+
}
|
|
699
|
+
return completed;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Resolution of the `keep:N` boundary: scan only the active, uncompacted
|
|
703
|
+
// history from newest to oldest. Kept messages from the prior compaction remain
|
|
704
|
+
// eligible, while history represented by its summary is never selected again.
|
|
606
705
|
function keepBoundaryId(branchEntries: SessionEntry[], keep: number): string | undefined {
|
|
706
|
+
const historyStart = activeHistoryStartIndex(branchEntries);
|
|
607
707
|
let seen = 0;
|
|
608
|
-
for (let i = branchEntries.length - 1; i >=
|
|
708
|
+
for (let i = branchEntries.length - 1; i >= historyStart; i -= 1) {
|
|
609
709
|
const entry = branchEntries[i];
|
|
610
710
|
if (isMessageEntry(entry) && entry.message.role === 'user') {
|
|
611
711
|
seen += 1;
|
|
612
|
-
if (seen === keep)
|
|
613
|
-
return i === 0 ? undefined : entry.id;
|
|
614
|
-
}
|
|
712
|
+
if (seen === keep) return i === historyStart ? undefined : entry.id;
|
|
615
713
|
}
|
|
616
714
|
}
|
|
617
715
|
return undefined;
|
|
618
716
|
}
|
|
619
717
|
|
|
620
|
-
function
|
|
718
|
+
function latestCompactionForRange(
|
|
621
719
|
branchEntries: SessionEntry[],
|
|
622
720
|
untilIndex: number,
|
|
623
721
|
): CompactionEntry | undefined {
|
|
624
|
-
for (let i =
|
|
722
|
+
for (let i = branchEntries.length - 1; i >= 0; i -= 1) {
|
|
625
723
|
const entry = branchEntries[i];
|
|
626
|
-
if (isCompactionEntry(entry))
|
|
724
|
+
if (!isCompactionEntry(entry)) continue;
|
|
725
|
+
if (compactionBoundaryStartIndex(branchEntries, entry) < untilIndex) return entry;
|
|
627
726
|
}
|
|
628
727
|
return undefined;
|
|
629
728
|
}
|
|
@@ -633,17 +732,8 @@ function compactionStartId(
|
|
|
633
732
|
previousCompaction: CompactionEntry,
|
|
634
733
|
untilIndex: number,
|
|
635
734
|
): string | undefined {
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
);
|
|
639
|
-
if (keptIndex >= 0 && keptIndex < untilIndex) return previousCompaction.firstKeptEntryId;
|
|
640
|
-
|
|
641
|
-
const compactionIndex = branchEntries.findIndex((entry) => entry.id === previousCompaction.id);
|
|
642
|
-
if (compactionIndex >= 0 && compactionIndex + 1 < untilIndex) {
|
|
643
|
-
return branchEntries[compactionIndex + 1].id;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
return undefined;
|
|
735
|
+
const boundaryStart = compactionBoundaryStartIndex(branchEntries, previousCompaction);
|
|
736
|
+
return boundaryStart < untilIndex ? branchEntries[boundaryStart]?.id : undefined;
|
|
647
737
|
}
|
|
648
738
|
|
|
649
739
|
function buildCompactRange(
|
|
@@ -653,7 +743,7 @@ function buildCompactRange(
|
|
|
653
743
|
const untilIndex = branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
|
|
654
744
|
if (untilIndex < 0) return undefined;
|
|
655
745
|
|
|
656
|
-
const previousCompaction =
|
|
746
|
+
const previousCompaction = latestCompactionForRange(branchEntries, untilIndex);
|
|
657
747
|
const from = previousCompaction
|
|
658
748
|
? compactionStartId(branchEntries, previousCompaction, untilIndex)
|
|
659
749
|
: undefined;
|
|
@@ -663,6 +753,14 @@ function buildCompactRange(
|
|
|
663
753
|
: { scope: 'lineage', until: firstKeptEntryId };
|
|
664
754
|
}
|
|
665
755
|
|
|
756
|
+
function mergeCompactionSummary(
|
|
757
|
+
previousSummary: string | undefined,
|
|
758
|
+
generatedSummary: string,
|
|
759
|
+
): string {
|
|
760
|
+
const previous = previousSummary?.trim();
|
|
761
|
+
return previous ? `${previous}\n\n${generatedSummary}` : generatedSummary;
|
|
762
|
+
}
|
|
763
|
+
|
|
666
764
|
async function buildGoosedumpCompaction(
|
|
667
765
|
target: string,
|
|
668
766
|
preparation: {
|
|
@@ -691,8 +789,9 @@ async function buildGoosedumpCompaction(
|
|
|
691
789
|
const range = buildCompactRange(branchEntries, firstKeptEntryId);
|
|
692
790
|
if (!range) return undefined;
|
|
693
791
|
|
|
694
|
-
const
|
|
695
|
-
if (!
|
|
792
|
+
const generatedSummary = await goosedumpCompact(target, range, signal);
|
|
793
|
+
if (!generatedSummary) return undefined;
|
|
794
|
+
const summary = mergeCompactionSummary(preparation.previousSummary, generatedSummary);
|
|
696
795
|
|
|
697
796
|
return {
|
|
698
797
|
summary,
|
|
@@ -719,23 +818,30 @@ class NumberInputSubmenu implements Component {
|
|
|
719
818
|
private readonly hint: (text: string) => string;
|
|
720
819
|
private readonly onSubmitValue: (value: number) => void;
|
|
721
820
|
private readonly onCancelEdit: () => void;
|
|
821
|
+
private readonly onClearValue?: () => void;
|
|
722
822
|
private readonly onInvalidValue: (message: string) => void;
|
|
723
823
|
private readonly title: string;
|
|
724
|
-
private readonly currentValue: number;
|
|
824
|
+
private readonly currentValue: number | null;
|
|
725
825
|
private readonly min: number;
|
|
726
826
|
private readonly max: number;
|
|
727
827
|
|
|
728
828
|
constructor(
|
|
729
829
|
title: string,
|
|
730
|
-
currentValue: number,
|
|
830
|
+
currentValue: number | null,
|
|
731
831
|
onSubmit: (value: number) => void,
|
|
732
832
|
onCancel: () => void,
|
|
733
|
-
options?: {
|
|
833
|
+
options?: {
|
|
834
|
+
min?: number;
|
|
835
|
+
max?: number;
|
|
836
|
+
onClear?: () => void;
|
|
837
|
+
onInvalid?: (message: string) => void;
|
|
838
|
+
},
|
|
734
839
|
) {
|
|
735
840
|
this.title = title;
|
|
736
841
|
this.currentValue = currentValue;
|
|
737
842
|
this.onSubmitValue = onSubmit;
|
|
738
843
|
this.onCancelEdit = onCancel;
|
|
844
|
+
this.onClearValue = options?.onClear;
|
|
739
845
|
this.onInvalidValue = options?.onInvalid ?? (() => {});
|
|
740
846
|
this.min = options?.min ?? 0;
|
|
741
847
|
this.max = options?.max ?? 255;
|
|
@@ -747,7 +853,8 @@ class NumberInputSubmenu implements Component {
|
|
|
747
853
|
if (kb.matches(data, 'tui.select.confirm') || data === '\n') {
|
|
748
854
|
const value = this.input.getValue();
|
|
749
855
|
if (value === '') {
|
|
750
|
-
this.
|
|
856
|
+
if (this.onClearValue) this.onClearValue();
|
|
857
|
+
else this.onCancelEdit();
|
|
751
858
|
return;
|
|
752
859
|
}
|
|
753
860
|
if (!/^\d+$/.test(value)) {
|
|
@@ -778,11 +885,17 @@ class NumberInputSubmenu implements Component {
|
|
|
778
885
|
|
|
779
886
|
render(width: number): string[] {
|
|
780
887
|
return [
|
|
781
|
-
this.hint(
|
|
888
|
+
this.hint(
|
|
889
|
+
` ${this.title} · current ${this.currentValue ?? 'inherit'} · ${this.min}-${this.max}`,
|
|
890
|
+
),
|
|
782
891
|
'',
|
|
783
892
|
...this.input.render(width),
|
|
784
893
|
'',
|
|
785
|
-
this.hint(
|
|
894
|
+
this.hint(
|
|
895
|
+
this.onClearValue
|
|
896
|
+
? ' empty inherit · enter submit · esc cancel'
|
|
897
|
+
: ' enter submit · esc cancel',
|
|
898
|
+
),
|
|
786
899
|
];
|
|
787
900
|
}
|
|
788
901
|
}
|
|
@@ -808,26 +921,37 @@ async function runSettingsEditor(ctx: ExtensionContext): Promise<void> {
|
|
|
808
921
|
|
|
809
922
|
function buildSettingItems(): SettingItem[] {
|
|
810
923
|
const global = readScopedGoosedumpSettings(location, 'global');
|
|
811
|
-
const
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
924
|
+
const projectPath = projectSettingsPath(location);
|
|
925
|
+
const projectRaw = readJsonObjectStrict(projectPath).goosedump;
|
|
926
|
+
const projectValue =
|
|
927
|
+
isRecord(projectRaw) &&
|
|
928
|
+
typeof projectRaw.turnsBeforeCompact === 'number' &&
|
|
929
|
+
Number.isSafeInteger(projectRaw.turnsBeforeCompact) &&
|
|
930
|
+
projectRaw.turnsBeforeCompact >= TURNS_BEFORE_COMPACT_MIN &&
|
|
931
|
+
projectRaw.turnsBeforeCompact <= TURNS_BEFORE_COMPACT_MAX
|
|
932
|
+
? projectRaw.turnsBeforeCompact
|
|
933
|
+
: undefined;
|
|
816
934
|
|
|
817
935
|
return (['global', 'project'] as const).map((scope) => ({
|
|
818
936
|
id: `${scope}:turnsBeforeCompact`,
|
|
819
937
|
label: settingLabel(scope, 'turnsBeforeCompact'),
|
|
820
|
-
description:
|
|
821
|
-
|
|
938
|
+
description:
|
|
939
|
+
scope === 'project'
|
|
940
|
+
? 'LLM turns before compaction; empty inherits Global.'
|
|
941
|
+
: 'LLM turns before compaction; 0 disables.',
|
|
942
|
+
currentValue: String(
|
|
943
|
+
scope === 'project' ? (projectValue ?? '') : global.turnsBeforeCompact,
|
|
944
|
+
),
|
|
822
945
|
submenu: (currentValue, subDone) =>
|
|
823
946
|
new NumberInputSubmenu(
|
|
824
947
|
settingLabel(scope, 'turnsBeforeCompact'),
|
|
825
|
-
Number(currentValue),
|
|
948
|
+
currentValue === '' ? null : Number(currentValue),
|
|
826
949
|
(value) => subDone(String(value)),
|
|
827
950
|
() => subDone(),
|
|
828
951
|
{
|
|
829
952
|
min: TURNS_BEFORE_COMPACT_MIN,
|
|
830
953
|
max: TURNS_BEFORE_COMPACT_MAX,
|
|
954
|
+
onClear: scope === 'project' ? () => subDone('') : undefined,
|
|
831
955
|
onInvalid: (message) => ctx.ui.notify(message, 'error'),
|
|
832
956
|
},
|
|
833
957
|
),
|
|
@@ -841,6 +965,19 @@ async function runSettingsEditor(ctx: ExtensionContext): Promise<void> {
|
|
|
841
965
|
getSettingsListTheme(),
|
|
842
966
|
(id, newValue) => {
|
|
843
967
|
const [scope, key] = id.split(':') as [SettingsScope, GoosedumpSettingKey];
|
|
968
|
+
if (scope === 'project' && newValue === '') {
|
|
969
|
+
try {
|
|
970
|
+
clearScopedGoosedumpSetting(location, scope, key);
|
|
971
|
+
settingsList.updateValue(id, '');
|
|
972
|
+
ctx.ui.notify(`${settingLabel(scope, key)} inherits Global`, 'info');
|
|
973
|
+
tui.requestRender();
|
|
974
|
+
} catch (error) {
|
|
975
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
976
|
+
ctx.ui.notify(`goosedump settings error: ${message}`, 'error');
|
|
977
|
+
}
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
844
981
|
const parsed = Number(newValue);
|
|
845
982
|
if (
|
|
846
983
|
!Number.isSafeInteger(parsed) ||
|
|
@@ -848,9 +985,18 @@ async function runSettingsEditor(ctx: ExtensionContext): Promise<void> {
|
|
|
848
985
|
parsed > TURNS_BEFORE_COMPACT_MAX
|
|
849
986
|
)
|
|
850
987
|
return;
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
988
|
+
try {
|
|
989
|
+
writeScopedGoosedumpSetting(location, scope, key, parsed);
|
|
990
|
+
settingsList.updateValue(id, newValue);
|
|
991
|
+
ctx.ui.notify(
|
|
992
|
+
`${settingLabel(scope, key)} = ${formatCompactSetting(parsed)}`,
|
|
993
|
+
'info',
|
|
994
|
+
);
|
|
995
|
+
tui.requestRender();
|
|
996
|
+
} catch (error) {
|
|
997
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
998
|
+
ctx.ui.notify(`goosedump settings error: ${message}`, 'error');
|
|
999
|
+
}
|
|
854
1000
|
tui.requestRender();
|
|
855
1001
|
},
|
|
856
1002
|
() => {
|
|
@@ -1129,47 +1275,33 @@ export function createGoosedumpIntegration(
|
|
|
1129
1275
|
|
|
1130
1276
|
let goosedumpAvailable = false;
|
|
1131
1277
|
let compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
|
|
1132
|
-
let settingsSignature = '';
|
|
1133
1278
|
let turnsRemaining = 0;
|
|
1134
1279
|
let counterTriggered = false;
|
|
1135
1280
|
let compactionQueued = false;
|
|
1136
1281
|
let pendingKeep: number | null = null;
|
|
1137
1282
|
|
|
1138
|
-
function
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
counterTriggered = false;
|
|
1147
|
-
pendingKeep = null;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
function reloadCompactSettings(cwd: string): void {
|
|
1151
|
-
const next = resolveGoosedumpSettings(cwd);
|
|
1152
|
-
const nextSignature = compactSettingsSignature(next);
|
|
1153
|
-
if (nextSignature !== settingsSignature) resetCompactCounters(cwd);
|
|
1283
|
+
function syncCompactCounters(ctx: ExtensionContext, clearPending = false): void {
|
|
1284
|
+
compactSettings = resolveGoosedumpSettings(ctx.cwd);
|
|
1285
|
+
const completedTurns = completedTurnsSinceCompaction(ctx.sessionManager.getBranch());
|
|
1286
|
+
turnsRemaining = Math.max(0, compactSettings.turnsBeforeCompact - completedTurns);
|
|
1287
|
+
counterTriggered =
|
|
1288
|
+
compactSettings.turnsBeforeCompact > 0 &&
|
|
1289
|
+
completedTurns >= compactSettings.turnsBeforeCompact;
|
|
1290
|
+
if (clearPending) pendingKeep = null;
|
|
1154
1291
|
}
|
|
1155
1292
|
|
|
1156
1293
|
function updateStatus(ctx: ExtensionContext): void {
|
|
1157
1294
|
if (!goosedumpAvailable) return;
|
|
1158
1295
|
const active = shouldOverrideDefaultCompaction && compactSettings.turnsBeforeCompact > 0;
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
'
|
|
1162
|
-
|
|
1163
|
-
);
|
|
1296
|
+
const counter = active
|
|
1297
|
+
? Math.max(0, turnsRemaining).toString(16).toUpperCase().padStart(2, '0')
|
|
1298
|
+
: '--';
|
|
1299
|
+
ctx.ui.setStatus('goosedump', ctx.ui.theme.fg('text', `GD: ${counter}`));
|
|
1164
1300
|
}
|
|
1165
1301
|
|
|
1166
1302
|
function noteCompletedTurn(ctx: ExtensionContext): void {
|
|
1167
1303
|
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
|
|
1168
|
-
|
|
1169
|
-
if (compactSettings.turnsBeforeCompact > 0) {
|
|
1170
|
-
turnsRemaining -= 1;
|
|
1171
|
-
if (turnsRemaining <= 0) counterTriggered = true;
|
|
1172
|
-
}
|
|
1304
|
+
syncCompactCounters(ctx);
|
|
1173
1305
|
updateStatus(ctx);
|
|
1174
1306
|
}
|
|
1175
1307
|
|
|
@@ -1470,13 +1602,13 @@ export function createGoosedumpIntegration(
|
|
|
1470
1602
|
if (ctx.hasPendingMessages()) return;
|
|
1471
1603
|
if (!hasCompactableHistory(ctx)) return;
|
|
1472
1604
|
|
|
1473
|
-
|
|
1605
|
+
syncCompactCounters(ctx, true);
|
|
1474
1606
|
updateStatus(ctx);
|
|
1475
1607
|
compactionQueued = true;
|
|
1476
1608
|
ctx.compact({
|
|
1477
1609
|
onComplete: () => {
|
|
1478
1610
|
compactionQueued = false;
|
|
1479
|
-
|
|
1611
|
+
syncCompactCounters(ctx, true);
|
|
1480
1612
|
updateStatus(ctx);
|
|
1481
1613
|
if (resumeAfter && !ctx.hasPendingMessages()) {
|
|
1482
1614
|
pi.sendMessage(
|
|
@@ -1492,7 +1624,7 @@ export function createGoosedumpIntegration(
|
|
|
1492
1624
|
},
|
|
1493
1625
|
onError: (error) => {
|
|
1494
1626
|
compactionQueued = false;
|
|
1495
|
-
|
|
1627
|
+
syncCompactCounters(ctx, true);
|
|
1496
1628
|
updateStatus(ctx);
|
|
1497
1629
|
const message = error instanceof Error ? error.message : String(error);
|
|
1498
1630
|
if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
|
|
@@ -1502,7 +1634,8 @@ export function createGoosedumpIntegration(
|
|
|
1502
1634
|
|
|
1503
1635
|
pi.on('session_start', async (_event, ctx) => {
|
|
1504
1636
|
goosedumpAvailable = checkGoosedump(ctx);
|
|
1505
|
-
|
|
1637
|
+
notifyInvalidSettings(ctx);
|
|
1638
|
+
syncCompactCounters(ctx, true);
|
|
1506
1639
|
updateStatus(ctx);
|
|
1507
1640
|
});
|
|
1508
1641
|
|
|
@@ -1566,7 +1699,7 @@ export function createGoosedumpIntegration(
|
|
|
1566
1699
|
pi.on('session_compact', async (event, ctx) => {
|
|
1567
1700
|
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
|
|
1568
1701
|
|
|
1569
|
-
|
|
1702
|
+
syncCompactCounters(ctx, true);
|
|
1570
1703
|
updateStatus(ctx);
|
|
1571
1704
|
|
|
1572
1705
|
const compactionEntry = event.compactionEntry;
|
|
@@ -1604,7 +1737,7 @@ export function createGoosedumpIntegration(
|
|
|
1604
1737
|
|
|
1605
1738
|
try {
|
|
1606
1739
|
await runSettingsEditor(ctx);
|
|
1607
|
-
|
|
1740
|
+
syncCompactCounters(ctx);
|
|
1608
1741
|
updateStatus(ctx);
|
|
1609
1742
|
} catch (err) {
|
|
1610
1743
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1625,11 +1758,15 @@ export function createGoosedumpIntegration(
|
|
|
1625
1758
|
// the last N user turns verbatim and summarizes everything before; with
|
|
1626
1759
|
// no argument goosedump summarizes up to Pi's token-based cut.
|
|
1627
1760
|
let keep: number | null = null;
|
|
1628
|
-
if (typeof args === 'string') {
|
|
1761
|
+
if (typeof args === 'string' && args.trim()) {
|
|
1629
1762
|
const match = args.trim().match(/^keep:(\d+)$/);
|
|
1630
|
-
if (match)
|
|
1631
|
-
|
|
1632
|
-
|
|
1763
|
+
if (!match) {
|
|
1764
|
+
ctx.ui.notify('Usage: /goose-compact [keep:N], where N >= 1', 'error');
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
keep = Number(match[1]);
|
|
1768
|
+
if (!Number.isSafeInteger(keep) || keep < 1) {
|
|
1769
|
+
ctx.ui.notify('keep:N requires a safe integer N >= 1', 'error');
|
|
1633
1770
|
return;
|
|
1634
1771
|
}
|
|
1635
1772
|
}
|