pi-goosedump 0.9.3 → 0.9.5
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 +202 -135
- package/package.json +10 -7
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 {
|
|
@@ -368,6 +373,43 @@ function resolveTarget(
|
|
|
368
373
|
return null;
|
|
369
374
|
}
|
|
370
375
|
|
|
376
|
+
const GOOSE_PROVIDERS = ['pi', 'claude', 'codex', 'crush', 'gemini', 'goose', 'opencode'] as const;
|
|
377
|
+
|
|
378
|
+
type HistoryOperation = 'search' | 'read' | 'grep';
|
|
379
|
+
|
|
380
|
+
const HISTORY_TARGET_GUIDELINE =
|
|
381
|
+
'Use the current Pi session by default; non-Pi providers require an explicit session ID.';
|
|
382
|
+
|
|
383
|
+
function historyProviderParam(operation: HistoryOperation) {
|
|
384
|
+
return Type.Optional(
|
|
385
|
+
Type.Union(
|
|
386
|
+
GOOSE_PROVIDERS.map((provider) => Type.Literal(provider)),
|
|
387
|
+
{
|
|
388
|
+
default: 'pi',
|
|
389
|
+
description: `Source provider whose sessions to ${operation}. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.`,
|
|
390
|
+
},
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function historySessionParam() {
|
|
396
|
+
return Type.Optional(
|
|
397
|
+
Type.String({
|
|
398
|
+
description:
|
|
399
|
+
'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function historyScopeParam() {
|
|
405
|
+
return Type.Optional(
|
|
406
|
+
Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
|
|
407
|
+
default: 'lineage',
|
|
408
|
+
description: 'Scope: lineage (current branch) or all entries in the session',
|
|
409
|
+
}),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
371
413
|
async function goosedumpCompact(
|
|
372
414
|
target: string,
|
|
373
415
|
options: { scope?: string; from?: string; until: string },
|
|
@@ -449,6 +491,56 @@ function isMessageEntry(entry: SessionEntry): entry is SessionEntry & {
|
|
|
449
491
|
return entry.type === 'message';
|
|
450
492
|
}
|
|
451
493
|
|
|
494
|
+
// Entry types that sessionEntryToContextMessages turns into context messages;
|
|
495
|
+
// only these count toward "is there anything to summarize".
|
|
496
|
+
const SUMMARIZABLE_ENTRY_TYPES = new Set(['message', 'custom_message', 'branch_summary']);
|
|
497
|
+
|
|
498
|
+
// Pi's compaction keepRecentTokens setting, project scope over global, matching
|
|
499
|
+
// SettingsManager's merge order.
|
|
500
|
+
function piKeepRecentTokens(cwd: string): number {
|
|
501
|
+
for (const path of [projectSettingsPath(cwd), globalSettingsPath()]) {
|
|
502
|
+
const compaction = readJsonObject(path).compaction;
|
|
503
|
+
if (isRecord(compaction) && typeof compaction.keepRecentTokens === 'number') {
|
|
504
|
+
return compaction.keepRecentTokens;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return DEFAULT_COMPACTION_SETTINGS.keepRecentTokens;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Predict whether ctx.compact() would throw "Nothing to compact" or "Already
|
|
511
|
+
// compacted": mirrors prepareCompaction's guards over the active lineage so
|
|
512
|
+
// plugin-triggered compaction can skip the call instead of surfacing pi's
|
|
513
|
+
// error toast (and needlessly aborting the agent).
|
|
514
|
+
function hasCompactableHistory(ctx: ExtensionContext): boolean {
|
|
515
|
+
const branch = ctx.sessionManager.getBranch();
|
|
516
|
+
if (branch.length === 0) return false;
|
|
517
|
+
if (branch[branch.length - 1].type === 'compaction') return false;
|
|
518
|
+
|
|
519
|
+
let boundaryStart = 0;
|
|
520
|
+
for (let i = branch.length - 1; i >= 0; i -= 1) {
|
|
521
|
+
const entry = branch[i];
|
|
522
|
+
if (isCompactionEntry(entry)) {
|
|
523
|
+
const keptIndex = branch.findIndex((e) => e.id === entry.firstKeptEntryId);
|
|
524
|
+
boundaryStart = keptIndex >= 0 ? keptIndex : i + 1;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const cut = findCutPoint(branch, boundaryStart, branch.length, piKeepRecentTokens(ctx.cwd));
|
|
530
|
+
if (!branch[cut.firstKeptEntryIndex]?.id) return false;
|
|
531
|
+
|
|
532
|
+
const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
|
|
533
|
+
for (let i = boundaryStart; i < historyEnd; i += 1) {
|
|
534
|
+
if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type)) return true;
|
|
535
|
+
}
|
|
536
|
+
if (cut.isSplitTurn) {
|
|
537
|
+
for (let i = cut.turnStartIndex; i < cut.firstKeptEntryIndex; i += 1) {
|
|
538
|
+
if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type)) return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
452
544
|
// Resolution of the `keep:N` boundary: scan the active lineage from the newest
|
|
453
545
|
// entry backward, count user turns (those whose message role is `user`), and
|
|
454
546
|
// return the id of the Nth-from-last user entry — the earliest entry to keep.
|
|
@@ -595,12 +687,16 @@ class NumberInputSubmenu implements Component {
|
|
|
595
687
|
handleInput(data: string): void {
|
|
596
688
|
const kb = getKeybindings();
|
|
597
689
|
if (kb.matches(data, 'tui.select.confirm') || data === '\n') {
|
|
598
|
-
const
|
|
599
|
-
if (
|
|
690
|
+
const value = this.input.getValue();
|
|
691
|
+
if (value === '') {
|
|
600
692
|
this.onCancelEdit();
|
|
601
693
|
return;
|
|
602
694
|
}
|
|
603
|
-
|
|
695
|
+
if (!/^\d+$/.test(value)) {
|
|
696
|
+
this.onInvalidValue(`Enter a whole number between ${this.min} and ${this.max}`);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const parsed = Number(value);
|
|
604
700
|
if (!Number.isSafeInteger(parsed) || parsed < this.min || parsed > this.max) {
|
|
605
701
|
this.onInvalidValue(`Enter a whole number between ${this.min} and ${this.max}`);
|
|
606
702
|
return;
|
|
@@ -1017,29 +1113,6 @@ export function createGoosedumpIntegration(
|
|
|
1017
1113
|
if (turnsRemaining <= 0) counterTriggered = true;
|
|
1018
1114
|
}
|
|
1019
1115
|
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
1116
|
}
|
|
1044
1117
|
|
|
1045
1118
|
function checkGoosedump(ctx?: ExtensionContext): boolean {
|
|
@@ -1061,18 +1134,28 @@ export function createGoosedumpIntegration(
|
|
|
1061
1134
|
// command and the goose_compact tool). Stashes a turn-based keep override and
|
|
1062
1135
|
// fires Pi's compaction flow; the session_before_compact hook reads pendingKeep
|
|
1063
1136
|
// and resets it on use, completion, or error.
|
|
1064
|
-
function requestCompaction(
|
|
1137
|
+
function requestCompaction(
|
|
1138
|
+
ctx: ExtensionContext,
|
|
1139
|
+
keep: number | null,
|
|
1140
|
+
): 'started' | 'busy' | 'nothing-to-compact' {
|
|
1141
|
+
if (compactionQueued) return 'busy';
|
|
1142
|
+
if (!hasCompactableHistory(ctx)) return 'nothing-to-compact';
|
|
1143
|
+
|
|
1144
|
+
compactionQueued = true;
|
|
1065
1145
|
pendingKeep = keep;
|
|
1066
1146
|
ctx.compact({
|
|
1067
1147
|
onComplete: () => {
|
|
1148
|
+
compactionQueued = false;
|
|
1068
1149
|
pendingKeep = null;
|
|
1069
1150
|
},
|
|
1070
1151
|
onError: (error) => {
|
|
1152
|
+
compactionQueued = false;
|
|
1071
1153
|
pendingKeep = null;
|
|
1072
1154
|
const message = error instanceof Error ? error.message : String(error);
|
|
1073
1155
|
if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
|
|
1074
1156
|
},
|
|
1075
1157
|
});
|
|
1158
|
+
return 'started';
|
|
1076
1159
|
}
|
|
1077
1160
|
|
|
1078
1161
|
function register(pi: ExtensionAPI): void {
|
|
@@ -1083,46 +1166,18 @@ export function createGoosedumpIntegration(
|
|
|
1083
1166
|
label: 'goose_search',
|
|
1084
1167
|
description:
|
|
1085
1168
|
'Search coding agent session history by ranked relevance. Returns compact entry overviews with entry IDs; use goose_get to expand specific IDs to full content. Defaults to the current Pi session.',
|
|
1086
|
-
promptSnippet:
|
|
1087
|
-
'goose_search({ query, provider?, session?, scope?, page? }) - rank messages by query relevance; defaults to current Pi session',
|
|
1169
|
+
promptSnippet: 'Search session history by ranked relevance',
|
|
1088
1170
|
promptGuidelines: [
|
|
1089
1171
|
"Use goose_search to find relevant context in this or another coding agent's session history.",
|
|
1090
1172
|
'Start with compact ranked results, then use goose_get to expand interesting entry IDs to full content.',
|
|
1091
1173
|
'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
|
|
1092
|
-
|
|
1174
|
+
HISTORY_TARGET_GUIDELINE,
|
|
1093
1175
|
],
|
|
1094
1176
|
parameters: Type.Object({
|
|
1095
1177
|
query: Type.String({ description: 'Search query to rank messages by relevance' }),
|
|
1096
|
-
provider:
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
Type.Literal('pi'),
|
|
1100
|
-
Type.Literal('claude'),
|
|
1101
|
-
Type.Literal('codex'),
|
|
1102
|
-
Type.Literal('crush'),
|
|
1103
|
-
Type.Literal('gemini'),
|
|
1104
|
-
Type.Literal('goose'),
|
|
1105
|
-
Type.Literal('opencode'),
|
|
1106
|
-
],
|
|
1107
|
-
{
|
|
1108
|
-
default: 'pi',
|
|
1109
|
-
description:
|
|
1110
|
-
'Source provider whose sessions to search. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
|
|
1111
|
-
},
|
|
1112
|
-
),
|
|
1113
|
-
),
|
|
1114
|
-
session: Type.Optional(
|
|
1115
|
-
Type.String({
|
|
1116
|
-
description:
|
|
1117
|
-
'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
|
|
1118
|
-
}),
|
|
1119
|
-
),
|
|
1120
|
-
scope: Type.Optional(
|
|
1121
|
-
Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
|
|
1122
|
-
default: 'lineage',
|
|
1123
|
-
description: 'Scope: lineage (current branch) or all entries in the session',
|
|
1124
|
-
}),
|
|
1125
|
-
),
|
|
1178
|
+
provider: historyProviderParam('search'),
|
|
1179
|
+
session: historySessionParam(),
|
|
1180
|
+
scope: historyScopeParam(),
|
|
1126
1181
|
page: Type.Optional(
|
|
1127
1182
|
Type.Integer({
|
|
1128
1183
|
minimum: 1,
|
|
@@ -1166,47 +1221,21 @@ export function createGoosedumpIntegration(
|
|
|
1166
1221
|
label: 'goose_get',
|
|
1167
1222
|
description:
|
|
1168
1223
|
'Expand specific entry IDs (from goose_search or goose_grep) to their full content. Defaults to the current Pi session.',
|
|
1169
|
-
promptSnippet:
|
|
1170
|
-
'goose_get({ ids, provider?, session? }) - expand entry IDs to full content; defaults to current Pi session',
|
|
1224
|
+
promptSnippet: 'Expand session entries by ID to full content',
|
|
1171
1225
|
promptGuidelines: [
|
|
1172
1226
|
'Use goose_get to expand entry IDs returned by goose_search or goose_grep into their full content.',
|
|
1173
1227
|
'Pass only the specific IDs you need; full transcripts are expensive.',
|
|
1228
|
+
'Default scope is "lineage" (current branch); use scope: "all" to retrieve entries from all session branches.',
|
|
1229
|
+
HISTORY_TARGET_GUIDELINE,
|
|
1174
1230
|
],
|
|
1175
1231
|
parameters: Type.Object({
|
|
1176
1232
|
ids: Type.Array(Type.String(), {
|
|
1177
1233
|
minItems: 1,
|
|
1178
1234
|
description: 'Entry IDs to expand (show full content)',
|
|
1179
1235
|
}),
|
|
1180
|
-
provider:
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
Type.Literal('pi'),
|
|
1184
|
-
Type.Literal('claude'),
|
|
1185
|
-
Type.Literal('codex'),
|
|
1186
|
-
Type.Literal('crush'),
|
|
1187
|
-
Type.Literal('gemini'),
|
|
1188
|
-
Type.Literal('goose'),
|
|
1189
|
-
Type.Literal('opencode'),
|
|
1190
|
-
],
|
|
1191
|
-
{
|
|
1192
|
-
default: 'pi',
|
|
1193
|
-
description:
|
|
1194
|
-
'Source provider whose sessions to read. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
|
|
1195
|
-
},
|
|
1196
|
-
),
|
|
1197
|
-
),
|
|
1198
|
-
session: Type.Optional(
|
|
1199
|
-
Type.String({
|
|
1200
|
-
description:
|
|
1201
|
-
'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
|
|
1202
|
-
}),
|
|
1203
|
-
),
|
|
1204
|
-
scope: Type.Optional(
|
|
1205
|
-
Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
|
|
1206
|
-
default: 'lineage',
|
|
1207
|
-
description: 'Scope: lineage (current branch) or all entries in the session',
|
|
1208
|
-
}),
|
|
1209
|
-
),
|
|
1236
|
+
provider: historyProviderParam('read'),
|
|
1237
|
+
session: historySessionParam(),
|
|
1238
|
+
scope: historyScopeParam(),
|
|
1210
1239
|
}),
|
|
1211
1240
|
async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
|
|
1212
1241
|
if (!goosedumpAvailable) return goosedumpUnavailableResult();
|
|
@@ -1239,45 +1268,18 @@ export function createGoosedumpIntegration(
|
|
|
1239
1268
|
label: 'goose_grep',
|
|
1240
1269
|
description:
|
|
1241
1270
|
'Filter session messages by glob pattern (e.g. *rand*). Returns compact entry overviews with entry IDs; use goose_get to expand. Defaults to the current Pi session.',
|
|
1242
|
-
promptSnippet:
|
|
1243
|
-
'goose_grep({ pattern, provider?, session?, scope? }) - filter messages by glob; defaults to current Pi session',
|
|
1271
|
+
promptSnippet: 'Filter session history by glob pattern',
|
|
1244
1272
|
promptGuidelines: [
|
|
1245
1273
|
'Use goose_grep to filter session messages by glob pattern when you need exact substring/glob matching instead of ranked relevance.',
|
|
1246
1274
|
'Expand interesting entry IDs with goose_get.',
|
|
1247
1275
|
'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
|
|
1276
|
+
HISTORY_TARGET_GUIDELINE,
|
|
1248
1277
|
],
|
|
1249
1278
|
parameters: Type.Object({
|
|
1250
1279
|
pattern: Type.String({ description: 'Glob pattern to filter messages (e.g. *rand*)' }),
|
|
1251
|
-
provider:
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
Type.Literal('pi'),
|
|
1255
|
-
Type.Literal('claude'),
|
|
1256
|
-
Type.Literal('codex'),
|
|
1257
|
-
Type.Literal('crush'),
|
|
1258
|
-
Type.Literal('gemini'),
|
|
1259
|
-
Type.Literal('goose'),
|
|
1260
|
-
Type.Literal('opencode'),
|
|
1261
|
-
],
|
|
1262
|
-
{
|
|
1263
|
-
default: 'pi',
|
|
1264
|
-
description:
|
|
1265
|
-
'Source provider whose sessions to grep. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
|
|
1266
|
-
},
|
|
1267
|
-
),
|
|
1268
|
-
),
|
|
1269
|
-
session: Type.Optional(
|
|
1270
|
-
Type.String({
|
|
1271
|
-
description:
|
|
1272
|
-
'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
|
|
1273
|
-
}),
|
|
1274
|
-
),
|
|
1275
|
-
scope: Type.Optional(
|
|
1276
|
-
Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
|
|
1277
|
-
default: 'lineage',
|
|
1278
|
-
description: 'Scope: lineage (current branch) or all entries in the session',
|
|
1279
|
-
}),
|
|
1280
|
-
),
|
|
1280
|
+
provider: historyProviderParam('grep'),
|
|
1281
|
+
session: historySessionParam(),
|
|
1282
|
+
scope: historyScopeParam(),
|
|
1281
1283
|
}),
|
|
1282
1284
|
async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
|
|
1283
1285
|
if (!goosedumpAvailable) return goosedumpUnavailableResult();
|
|
@@ -1310,8 +1312,7 @@ export function createGoosedumpIntegration(
|
|
|
1310
1312
|
label: 'goose_compact',
|
|
1311
1313
|
description:
|
|
1312
1314
|
"Trigger a goosedump compaction of the current session now, optionally keeping the last N user turns verbatim while summarizing everything before. Compaction runs asynchronously via Pi's compaction flow.",
|
|
1313
|
-
promptSnippet:
|
|
1314
|
-
"goose_compact({ keep? }) - compact now; keep last N user turns (turn-based), omit to compact up to Pi's cut",
|
|
1315
|
+
promptSnippet: 'Compact session history; optionally keep recent user turns',
|
|
1315
1316
|
promptGuidelines: [
|
|
1316
1317
|
'Use goose_compact to compact the session proactively with goosedump when context is large or nearing limits.',
|
|
1317
1318
|
"Pass keep:N (N >= 1) to keep the last N user turns verbatim and summarize everything before them; omit to compact up to Pi's token-based cut.",
|
|
@@ -1330,7 +1331,11 @@ export function createGoosedumpIntegration(
|
|
|
1330
1331
|
if (!goosedumpAvailable) return goosedumpUnavailableResult();
|
|
1331
1332
|
|
|
1332
1333
|
const keep = params.keep ?? null;
|
|
1333
|
-
requestCompaction(ctx, keep);
|
|
1334
|
+
const result = requestCompaction(ctx, keep);
|
|
1335
|
+
if (result === 'busy') return textResult('Compaction already in progress.');
|
|
1336
|
+
if (result === 'nothing-to-compact') {
|
|
1337
|
+
return textResult('Nothing to compact (session too small).');
|
|
1338
|
+
}
|
|
1334
1339
|
return textResult(
|
|
1335
1340
|
keep !== null
|
|
1336
1341
|
? `Compacting with goosedump; keeping the last ${keep} user turn${keep === 1 ? '' : 's'}.`
|
|
@@ -1341,14 +1346,62 @@ export function createGoosedumpIntegration(
|
|
|
1341
1346
|
);
|
|
1342
1347
|
}
|
|
1343
1348
|
|
|
1349
|
+
// Fire counter-driven compaction. ctx.compact() uses manual compaction
|
|
1350
|
+
// semantics and aborts the current run, so a mid-run trigger (resumeAfter)
|
|
1351
|
+
// re-triggers the agent with a synthetic continue message once the
|
|
1352
|
+
// compaction lands. If the session is still too small to compact, the
|
|
1353
|
+
// counter stays armed and retries at a later turn_end/agent_end.
|
|
1354
|
+
function maybeAutoCompact(ctx: ExtensionContext, resumeAfter: boolean): void {
|
|
1355
|
+
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
|
|
1356
|
+
if (!counterTriggered || compactionQueued) return;
|
|
1357
|
+
if (ctx.hasPendingMessages()) return;
|
|
1358
|
+
if (!hasCompactableHistory(ctx)) return;
|
|
1359
|
+
|
|
1360
|
+
resetCompactCounters(ctx.cwd);
|
|
1361
|
+
updateStatus(ctx);
|
|
1362
|
+
compactionQueued = true;
|
|
1363
|
+
ctx.compact({
|
|
1364
|
+
onComplete: () => {
|
|
1365
|
+
compactionQueued = false;
|
|
1366
|
+
resetCompactCounters(ctx.cwd);
|
|
1367
|
+
updateStatus(ctx);
|
|
1368
|
+
if (resumeAfter && !ctx.hasPendingMessages()) {
|
|
1369
|
+
pi.sendMessage(
|
|
1370
|
+
{
|
|
1371
|
+
customType: 'goosedump-autocompact-resume',
|
|
1372
|
+
content:
|
|
1373
|
+
'The session was compacted to reduce context size. Continue the task you were working on from where you left off.',
|
|
1374
|
+
display: false,
|
|
1375
|
+
},
|
|
1376
|
+
{ triggerTurn: true },
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
onError: (error) => {
|
|
1381
|
+
compactionQueued = false;
|
|
1382
|
+
resetCompactCounters(ctx.cwd);
|
|
1383
|
+
updateStatus(ctx);
|
|
1384
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1385
|
+
if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
|
|
1386
|
+
},
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1344
1390
|
pi.on('session_start', async (_event, ctx) => {
|
|
1345
1391
|
goosedumpAvailable = checkGoosedump(ctx);
|
|
1346
1392
|
resetCompactCounters(ctx.cwd);
|
|
1347
1393
|
updateStatus(ctx);
|
|
1348
1394
|
});
|
|
1349
1395
|
|
|
1350
|
-
pi.on('turn_end', async (
|
|
1396
|
+
pi.on('turn_end', async (event, ctx) => {
|
|
1351
1397
|
noteCompletedTurn(ctx);
|
|
1398
|
+
// A turn with tool results means the agent loop would keep going; compact
|
|
1399
|
+
// between turns and resume so the run's progress is not lost.
|
|
1400
|
+
if (event.toolResults.length > 0) maybeAutoCompact(ctx, true);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
1404
|
+
maybeAutoCompact(ctx, false);
|
|
1352
1405
|
});
|
|
1353
1406
|
|
|
1354
1407
|
pi.on('session_before_compact', async (event, ctx) => {
|
|
@@ -1371,11 +1424,9 @@ export function createGoosedumpIntegration(
|
|
|
1371
1424
|
keep,
|
|
1372
1425
|
);
|
|
1373
1426
|
if (compaction && 'cancel' in compaction) {
|
|
1374
|
-
pendingKeep = null;
|
|
1375
1427
|
if (ctx.hasUI) ctx.ui.notify(compaction.reason, 'info');
|
|
1376
1428
|
return { cancel: true };
|
|
1377
1429
|
}
|
|
1378
|
-
pendingKeep = null;
|
|
1379
1430
|
return compaction ? { compaction } : undefined;
|
|
1380
1431
|
} catch (err) {
|
|
1381
1432
|
if (ctx.hasUI) {
|
|
@@ -1386,15 +1437,26 @@ export function createGoosedumpIntegration(
|
|
|
1386
1437
|
);
|
|
1387
1438
|
}
|
|
1388
1439
|
return undefined;
|
|
1440
|
+
} finally {
|
|
1441
|
+
pendingKeep = null;
|
|
1389
1442
|
}
|
|
1390
1443
|
});
|
|
1391
1444
|
|
|
1392
1445
|
pi.on('session_compact', async (event, ctx) => {
|
|
1393
|
-
if (!goosedumpAvailable || !
|
|
1446
|
+
if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
|
|
1394
1447
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1448
|
+
resetCompactCounters(ctx.cwd);
|
|
1449
|
+
updateStatus(ctx);
|
|
1397
1450
|
|
|
1451
|
+
const compactionEntry = event.compactionEntry;
|
|
1452
|
+
if (
|
|
1453
|
+
!event.fromExtension ||
|
|
1454
|
+
!compactionEntry ||
|
|
1455
|
+
!isRecord(compactionEntry.details) ||
|
|
1456
|
+
compactionEntry.details.source !== 'goosedump'
|
|
1457
|
+
) {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1398
1460
|
const tokensBefore = compactionEntry.tokensBefore;
|
|
1399
1461
|
const summaryLen = compactionEntry.summary.length;
|
|
1400
1462
|
const tokenEstimate = Math.ceil(summaryLen / 4);
|
|
@@ -1451,7 +1513,12 @@ export function createGoosedumpIntegration(
|
|
|
1451
1513
|
}
|
|
1452
1514
|
}
|
|
1453
1515
|
|
|
1454
|
-
requestCompaction(ctx, keep);
|
|
1516
|
+
const result = requestCompaction(ctx, keep);
|
|
1517
|
+
if (result === 'busy') {
|
|
1518
|
+
ctx.ui.notify('Compaction already in progress', 'info');
|
|
1519
|
+
} else if (result === 'nothing-to-compact') {
|
|
1520
|
+
ctx.ui.notify('Nothing to compact (session too small)', 'info');
|
|
1521
|
+
}
|
|
1455
1522
|
},
|
|
1456
1523
|
});
|
|
1457
1524
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goosedump",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Pi extension for goosedump-backed session-history search, entry expansion, and compaction",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"compaction",
|
|
@@ -21,13 +21,15 @@
|
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
23
|
"scripts": {
|
|
24
|
-
"fmt": "oxfmt index.ts package.json tsconfig.json .oxfmtrc.json README.md",
|
|
25
|
-
"lint": "oxlint index.ts",
|
|
24
|
+
"fmt": "oxfmt index.ts tests package.json tsconfig.json .oxfmtrc.json README.md",
|
|
25
|
+
"lint": "oxlint index.ts tests",
|
|
26
26
|
"check": "tsc --noEmit",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"all": "npm run fmt && npm run lint && npm run check && npm test",
|
|
29
|
+
"ci:fmt": "oxfmt --check index.ts tests package.json tsconfig.json .oxfmtrc.json README.md",
|
|
29
30
|
"ci:lint": "npm run lint",
|
|
30
|
-
"ci:check": "npm run check"
|
|
31
|
+
"ci:check": "npm run check",
|
|
32
|
+
"ci:test": "npm test"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
@@ -39,7 +41,8 @@
|
|
|
39
41
|
"@types/node": "^24.0.0",
|
|
40
42
|
"oxfmt": "^0.53.0",
|
|
41
43
|
"oxlint": "^1.68.0",
|
|
42
|
-
"typescript": "^5.0.0"
|
|
44
|
+
"typescript": "^5.0.0",
|
|
45
|
+
"vitest": "^4.1.10"
|
|
43
46
|
},
|
|
44
47
|
"peerDependencies": {
|
|
45
48
|
"@earendil-works/pi-coding-agent": "^0.78.0"
|