pi-goosedump 0.12.39 → 0.12.41

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 +6 -5
  2. package/dist/index.ts +201 -107
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -9,10 +9,11 @@ management, and compaction. Tools use the `goose_` prefix.
9
9
  pi install npm:pi-goosedump
10
10
  ```
11
11
 
12
- Bundled native binaries: Linux x64, macOS arm64, Windows x64.
12
+ Bundled native binaries: Linux x64 and arm64, macOS arm64, and Windows x64.
13
13
 
14
- On unsupported platforms, Pi keeps its normal compaction behavior and goosedump
15
- tools report that they are unavailable. Local models download on first use.
14
+ Unsupported platforms are rejected when the extension loads. A missing packaged binary is treated as
15
+ a broken installation instead of falling back to a `goosedump` executable on `PATH`. Local models
16
+ download on first use.
16
17
 
17
18
  ## Tools
18
19
 
@@ -91,8 +92,8 @@ The footer shows the remaining turn count in hexadecimal.
91
92
  - `keep:N` leaves the last N user turns verbatim.
92
93
  - Manual requests wait until Pi is idle.
93
94
  - Memory learning runs in the background after a successful compaction.
94
- - If the turn limit expires during a tool loop, compaction waits until the agent
95
- run settles, so no in-flight operation is aborted.
95
+ - If the turn limit expires during a tool loop, Pi saves the current batch,
96
+ compacts at that turn boundary, and resumes automatically.
96
97
  - Goosedump failures fall back to Pi's compactor.
97
98
 
98
99
  ## License
package/dist/index.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) Jarkko Sakkinen 2026
3
3
 
4
- // src/integration.ts
5
- import { execFileSync } from "node:child_process";
6
-
7
4
  // src/cli.ts
8
5
  import { execFile } from "node:child_process";
9
6
  import { existsSync } from "node:fs";
@@ -34,7 +31,6 @@ var TURNS_BEFORE_COMPACT_MIN = 0;
34
31
  var TURNS_BEFORE_COMPACT_MAX = 255;
35
32
  var SUMMARY_MAX_TOKENS_MIN = 512;
36
33
  var SUMMARY_MAX_TOKENS_MAX = 65536;
37
- var GOOSEDUMP_UNAVAILABLE_MESSAGE = "goosedump is not available. Install with: npm install @jarkkojs/goosedump";
38
34
  var GOOSE_PROVIDERS = [
39
35
  "pi",
40
36
  "claude",
@@ -63,9 +59,6 @@ function missingSessionId(tool, provider = "pi") {
63
59
  }
64
60
  return textResult(`session is required for ${tool} when no current session is available`);
65
61
  }
66
- function goosedumpUnavailableResult() {
67
- return textResult(GOOSEDUMP_UNAVAILABLE_MESSAGE);
68
- }
69
62
  function compactStartedMessage(keep, queued = false) {
70
63
  const action = queued ? "Compaction queued with goosedump" : "Compacting with goosedump";
71
64
  return keep !== null ? `${action}; keeping the last ${keep} user turn${keep === 1 ? "" : "s"}.` : `${action} up to Pi's token-based cut.`;
@@ -181,6 +174,12 @@ function parsePiMessages(output) {
181
174
  role: "user",
182
175
  content: flattenPiContent(entry.content)
183
176
  });
177
+ } else if (entry.type === "compaction") {
178
+ messages.push({
179
+ id: entry.id ?? "",
180
+ role: "compactionSummary",
181
+ content: entry.summary ?? ""
182
+ });
184
183
  } else if (entry.type === "branch_summary") {
185
184
  messages.push({
186
185
  id: entry.id ?? "",
@@ -283,33 +282,40 @@ ${m.content}`).join(`
283
282
 
284
283
  // src/cli.ts
285
284
  var require2 = createRequire(import.meta.url);
286
- function resolveGoosedumpBinary() {
287
- return require2.resolve("@jarkkojs/goosedump/bin/goosedump.js");
288
- }
289
- function resolveNativeGoosedumpBinary() {
290
- const target = `${process.platform}-${process.arch}`;
291
- let packageName;
292
- let binaryName = "goosedump";
285
+ function nativeGoosedumpPackage(target) {
293
286
  switch (target) {
294
287
  case "linux-x64":
295
- packageName = "@jarkkojs/goosedump-linux-x64";
296
- break;
288
+ return { packageName: "@jarkkojs/goosedump-linux-x64", binaryName: "goosedump" };
289
+ case "linux-arm64":
290
+ return { packageName: "@jarkkojs/goosedump-linux-arm64", binaryName: "goosedump" };
297
291
  case "darwin-arm64":
298
- packageName = "@jarkkojs/goosedump-darwin-arm64";
299
- break;
292
+ return { packageName: "@jarkkojs/goosedump-darwin-arm64", binaryName: "goosedump" };
300
293
  case "win32-x64":
301
- packageName = "@jarkkojs/goosedump-win32-x64";
302
- binaryName = "goosedump.exe";
303
- break;
294
+ return { packageName: "@jarkkojs/goosedump-win32-x64", binaryName: "goosedump.exe" };
304
295
  default:
305
296
  throw new Error(`unsupported platform architecture: ${target}`);
306
297
  }
307
- const wrapperRequire = createRequire(resolveGoosedumpBinary());
308
- const packageDir = dirname(wrapperRequire.resolve(`${packageName}/package.json`));
309
- const binaryPath = join(packageDir, "bin", binaryName);
310
- if (!existsSync(binaryPath))
311
- throw new Error(`binary not found: ${binaryPath}`);
312
- return binaryPath;
298
+ }
299
+ function resolveGoosedumpBinary() {
300
+ try {
301
+ return require2.resolve("@jarkkojs/goosedump/bin/goosedump.js");
302
+ } catch (cause) {
303
+ throw new Error("broken pi-goosedump installation: cannot resolve the @jarkkojs/goosedump dependency", { cause });
304
+ }
305
+ }
306
+ function resolveNativeGoosedumpBinary() {
307
+ const target = `${process.platform}-${process.arch}`;
308
+ const { packageName, binaryName } = nativeGoosedumpPackage(target);
309
+ try {
310
+ const wrapperRequire = createRequire(resolveGoosedumpBinary());
311
+ const packageDir = dirname(wrapperRequire.resolve(`${packageName}/package.json`));
312
+ const binaryPath = join(packageDir, "bin", binaryName);
313
+ if (!existsSync(binaryPath))
314
+ throw new Error(`binary not found: ${binaryPath}`);
315
+ return binaryPath;
316
+ } catch (cause) {
317
+ throw new Error(`broken pi-goosedump installation: cannot resolve the packaged binary ${packageName} for ${target}`, { cause });
318
+ }
313
319
  }
314
320
  function runGoosedumpAsync(args, signal, executablePath = resolveNativeGoosedumpBinary()) {
315
321
  return new Promise((resolve, reject) => {
@@ -384,12 +390,16 @@ async function goosedumpMemoryStatus(signal, executablePath) {
384
390
  async function goosedumpListSessions(signal, executablePath) {
385
391
  const args = ["session", "list", "--json"];
386
392
  const output = await runGoosedumpAsync(args, signal, executablePath);
393
+ let parsed;
387
394
  try {
388
- const parsed = JSON.parse(output.stdout);
389
- return Array.isArray(parsed) ? parsed : [];
390
- } catch {
391
- return [];
395
+ parsed = JSON.parse(output.stdout);
396
+ } catch (error) {
397
+ throw new Error("invalid goosedump session list JSON", { cause: error });
398
+ }
399
+ if (!Array.isArray(parsed)) {
400
+ throw new Error("invalid goosedump session list JSON: expected an array");
392
401
  }
402
+ return parsed;
393
403
  }
394
404
  async function goosedumpRemoveSession(target, signal, executablePath) {
395
405
  const args = ["session", "remove", target, "--yes"];
@@ -520,13 +530,32 @@ function readJsonObjectStrict(path) {
520
530
  throw new Error(`${path} must contain a JSON object`);
521
531
  return parsed;
522
532
  }
533
+ function replaceFileAtomically(temporaryPath, path) {
534
+ if (process.platform !== "win32" || !existsSync2(path)) {
535
+ renameSync(temporaryPath, path);
536
+ return;
537
+ }
538
+ const backupPath = `${path}.${randomUUID()}.bak`;
539
+ renameSync(path, backupPath);
540
+ try {
541
+ renameSync(temporaryPath, path);
542
+ } catch (error) {
543
+ try {
544
+ renameSync(backupPath, path);
545
+ } catch {}
546
+ throw error;
547
+ }
548
+ try {
549
+ unlinkSync(backupPath);
550
+ } catch {}
551
+ }
523
552
  function writeJsonObjectAtomically(path, value) {
524
553
  mkdirSync(dirname2(path), { recursive: true });
525
554
  const temporaryPath = `${path}.${randomUUID()}.tmp`;
526
555
  try {
527
556
  writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}
528
557
  `, { flag: "wx" });
529
- renameSync(temporaryPath, path);
558
+ replaceFileAtomically(temporaryPath, path);
530
559
  } catch (error) {
531
560
  try {
532
561
  unlinkSync(temporaryPath);
@@ -784,10 +813,10 @@ function isCompactionCancellation(error) {
784
813
  class CompactionRuntime {
785
814
  overrideDefaultCompaction;
786
815
  nativeExecutablePath;
787
- isAvailable;
788
816
  enqueueRemember;
789
817
  compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
790
818
  piAutoCompactionEnabled = DEFAULT_COMPACTION_SETTINGS2.enabled;
819
+ piCompactionReserveTokens = DEFAULT_COMPACTION_SETTINGS2.reserveTokens;
791
820
  turnsRemaining = 0;
792
821
  counterTriggered = false;
793
822
  compactionState = { phase: "idle" };
@@ -796,12 +825,15 @@ class CompactionRuntime {
796
825
  operationGeneration = 0;
797
826
  activeSessionFile = null;
798
827
  sessionActive = false;
828
+ activePi = null;
799
829
  constructor(deps) {
800
830
  this.overrideDefaultCompaction = deps.overrideDefaultCompaction;
801
831
  this.nativeExecutablePath = deps.nativeExecutablePath;
802
- this.isAvailable = deps.isAvailable;
803
832
  this.enqueueRemember = deps.enqueueRemember;
804
833
  }
834
+ setActivePi(pi) {
835
+ this.activePi = pi;
836
+ }
805
837
  requestCompaction(ctx, keep) {
806
838
  if (this.compactionState.phase !== "idle")
807
839
  return "busy";
@@ -814,7 +846,8 @@ class CompactionRuntime {
814
846
  origin: "manual",
815
847
  sessionGeneration: this.sessionGeneration,
816
848
  sessionFile: this.activeSessionFile,
817
- eligibleSettlement: this.settlementGeneration + 1
849
+ eligibleSettlement: this.settlementGeneration + 1,
850
+ resumeAfter: false
818
851
  }
819
852
  });
820
853
  return this.startQueuedCompaction(ctx) ? "started" : "queued";
@@ -823,13 +856,12 @@ class CompactionRuntime {
823
856
  const projectTrusted = projectSettingsTrusted(ctx);
824
857
  this.compactSettings = resolveGoosedumpSettings(ctx.cwd, projectTrusted);
825
858
  this.piAutoCompactionEnabled = piCompactionEnabled(ctx.cwd, projectTrusted);
859
+ this.piCompactionReserveTokens = piReserveTokens(ctx.cwd, projectTrusted);
826
860
  const completedTurns = completedTurnsSinceCompaction(ctx.sessionManager.getBranch());
827
861
  this.turnsRemaining = Math.max(0, this.compactSettings.turnsBeforeCompact - completedTurns);
828
862
  this.counterTriggered = this.compactSettings.turnsBeforeCompact > 0 && completedTurns >= this.compactSettings.turnsBeforeCompact;
829
863
  }
830
864
  updateStatus(ctx) {
831
- if (!this.isAvailable())
832
- return;
833
865
  const active = this.turnCompactionEnabled();
834
866
  let dotColor = "success";
835
867
  if (active) {
@@ -855,7 +887,7 @@ class CompactionRuntime {
855
887
  this.syncCompactCounters(ctx);
856
888
  this.updateStatus(ctx);
857
889
  this.queueTurnCompaction(ctx, 1);
858
- this.deferQueuedCompaction(ctx);
890
+ this.startQueuedCompaction(ctx);
859
891
  }
860
892
  endSession() {
861
893
  this.sessionGeneration += 1;
@@ -873,12 +905,18 @@ class CompactionRuntime {
873
905
  this.setCompactionState(ctx, request ? this.requeueRequest(request, this.settlementGeneration + 1) : { phase: "idle" });
874
906
  }
875
907
  onTurnEnd(event, ctx) {
876
- if (isCompletedAssistant(event.message))
908
+ if (!isCompletedAssistant(event.message))
909
+ return;
910
+ const resumeAfter = event.toolResults.length > 0 && !ctx.hasPendingMessages();
911
+ if (this.overrideDefaultCompaction) {
877
912
  this.noteCompletedTurn(ctx);
913
+ const eligibleSettlement = this.settlementGeneration + 1;
914
+ this.queueTurnCompaction(ctx, eligibleSettlement, resumeAfter);
915
+ this.queueThresholdCompaction(ctx, eligibleSettlement, resumeAfter);
916
+ }
917
+ this.interruptToolLoopForQueuedCompaction(ctx, resumeAfter);
878
918
  }
879
919
  onAgentSettled(ctx) {
880
- if (!this.isAvailable() || !this.overrideDefaultCompaction)
881
- return;
882
920
  this.settlementGeneration += 1;
883
921
  this.syncCompactCounters(ctx);
884
922
  this.updateStatus(ctx);
@@ -894,11 +932,11 @@ class CompactionRuntime {
894
932
  this.queueTurnCompaction(ctx, this.settlementGeneration);
895
933
  }
896
934
  if (this.compactionState.phase === "queued" && this.compactionState.request.eligibleSettlement <= this.settlementGeneration) {
897
- this.deferQueuedCompaction(ctx);
935
+ this.startQueuedCompaction(ctx);
898
936
  }
899
937
  }
900
938
  async onSessionBeforeCompact(event, ctx) {
901
- if (!this.overrideDefaultCompaction || !this.isAvailable())
939
+ if (!this.overrideDefaultCompaction)
902
940
  return;
903
941
  const signal = event.signal;
904
942
  if (signal?.aborted) {
@@ -1042,8 +1080,6 @@ class CompactionRuntime {
1042
1080
  this.compactionState = { phase: "idle" };
1043
1081
  this.resetCompactCounters(ctx);
1044
1082
  this.updateStatus(ctx);
1045
- if (!this.isAvailable())
1046
- return;
1047
1083
  const compactionEntry = event.compactionEntry;
1048
1084
  if (!event.fromExtension || !compactionEntry || !isRecord(compactionEntry.details) || compactionEntry.details.source !== "goosedump") {
1049
1085
  return;
@@ -1068,6 +1104,7 @@ class CompactionRuntime {
1068
1104
  const projectTrusted = projectSettingsTrusted(ctx);
1069
1105
  this.compactSettings = resolveGoosedumpSettings(ctx.cwd, projectTrusted);
1070
1106
  this.piAutoCompactionEnabled = piCompactionEnabled(ctx.cwd, projectTrusted);
1107
+ this.piCompactionReserveTokens = piReserveTokens(ctx.cwd, projectTrusted);
1071
1108
  this.turnsRemaining = this.compactSettings.turnsBeforeCompact;
1072
1109
  this.counterTriggered = false;
1073
1110
  }
@@ -1086,11 +1123,48 @@ class CompactionRuntime {
1086
1123
  this.updateStatus(ctx);
1087
1124
  }
1088
1125
  noteCompletedTurn(ctx) {
1089
- if (!this.isAvailable() || !this.overrideDefaultCompaction)
1126
+ if (!this.overrideDefaultCompaction)
1090
1127
  return;
1091
1128
  this.syncCompactCounters(ctx);
1092
1129
  this.updateStatus(ctx);
1093
- this.queueTurnCompaction(ctx, this.settlementGeneration + 1);
1130
+ }
1131
+ resumeAfterSuccessfulCompaction(ctx, resume) {
1132
+ if (!resume || !this.activePi || ctx.hasPendingMessages())
1133
+ return;
1134
+ this.activePi.sendMessage({
1135
+ customType: "goosedump-autocompact-resume",
1136
+ content: "The session was compacted to reduce context size. Continue the task you were working on from where you left off.",
1137
+ display: false
1138
+ }, { triggerTurn: true });
1139
+ }
1140
+ interruptToolLoopForQueuedCompaction(ctx, resumeAfter) {
1141
+ if (!resumeAfter || this.compactionState.phase !== "queued")
1142
+ return;
1143
+ const request = this.compactionState.request;
1144
+ if (!this.requestIsCurrent(request))
1145
+ return;
1146
+ this.compactionState = {
1147
+ phase: "queued",
1148
+ request: { ...request, resumeAfter: true }
1149
+ };
1150
+ ctx.abort();
1151
+ }
1152
+ queueThresholdCompaction(ctx, eligibleSettlement, resumeAfter) {
1153
+ const usage = ctx.getContextUsage();
1154
+ if (!this.piAutoCompactionEnabled || this.piCompactionReserveTokens <= 0 || usage?.tokens === null || usage?.tokens === undefined || usage.tokens < usage.contextWindow - this.piCompactionReserveTokens || this.compactionState.phase !== "idle" || !hasCompactableHistory(ctx)) {
1155
+ return;
1156
+ }
1157
+ this.setCompactionState(ctx, {
1158
+ phase: "queued",
1159
+ request: {
1160
+ keep: null,
1161
+ origin: "threshold",
1162
+ sessionGeneration: this.sessionGeneration,
1163
+ sessionFile: this.activeSessionFile,
1164
+ eligibleSettlement,
1165
+ resumeAfter
1166
+ }
1167
+ });
1094
1168
  }
1095
1169
  requestIsCurrent(request) {
1096
1170
  return request.sessionGeneration === this.sessionGeneration && request.sessionFile === this.activeSessionFile;
@@ -1111,7 +1185,7 @@ class CompactionRuntime {
1111
1185
  requestHasCompactableHistory(ctx, keep) {
1112
1186
  return keep === null ? hasCompactableHistory(ctx) : keepBoundaryId(ctx.sessionManager.getBranch(), keep) !== undefined;
1113
1187
  }
1114
- queueTurnCompaction(ctx, eligibleSettlement) {
1188
+ queueTurnCompaction(ctx, eligibleSettlement, resumeAfter = false) {
1115
1189
  if (!this.turnCompactionEnabled() || !this.counterTriggered || this.compactionState.phase !== "idle") {
1116
1190
  return;
1117
1191
  }
@@ -1124,7 +1198,8 @@ class CompactionRuntime {
1124
1198
  origin: "turn",
1125
1199
  sessionGeneration: this.sessionGeneration,
1126
1200
  sessionFile: this.activeSessionFile,
1127
- eligibleSettlement
1201
+ eligibleSettlement,
1202
+ resumeAfter
1128
1203
  }
1129
1204
  });
1130
1205
  }
@@ -1138,7 +1213,7 @@ class CompactionRuntime {
1138
1213
  this.setCompactionState(ctx, { phase: "idle" });
1139
1214
  return false;
1140
1215
  }
1141
- if (ctx.hasPendingMessages())
1216
+ if (request.origin !== "turn" && ctx.hasPendingMessages())
1142
1217
  return false;
1143
1218
  if (!ctx.isIdle())
1144
1219
  return false;
@@ -1149,6 +1224,7 @@ class CompactionRuntime {
1149
1224
  const operation = ++this.operationGeneration;
1150
1225
  const generation = request.sessionGeneration;
1151
1226
  const sessionFile = request.sessionFile;
1227
+ const resumeAfter = request.resumeAfter;
1152
1228
  this.setCompactionState(ctx, {
1153
1229
  phase: "running",
1154
1230
  request,
@@ -1163,6 +1239,7 @@ class CompactionRuntime {
1163
1239
  if (!this.runningOperationIsCurrent(operation, generation, sessionFile))
1164
1240
  return;
1165
1241
  this.setCompactionState(ctx, { phase: "idle" });
1242
+ this.resumeAfterSuccessfulCompaction(ctx, resumeAfter);
1166
1243
  },
1167
1244
  onError: (error) => {
1168
1245
  if (!this.runningOperationIsCurrent(operation, generation, sessionFile))
@@ -1179,17 +1256,6 @@ class CompactionRuntime {
1179
1256
  });
1180
1257
  return true;
1181
1258
  }
1182
- deferQueuedCompaction(ctx) {
1183
- if (this.compactionState.phase !== "queued")
1184
- return;
1185
- const request = this.compactionState.request;
1186
- setImmediate(() => {
1187
- if (this.compactionState.phase !== "queued" || this.compactionState.request !== request || !this.contextIsCurrent(ctx)) {
1188
- return;
1189
- }
1190
- this.startQueuedCompaction(ctx);
1191
- });
1192
- }
1193
1259
  }
1194
1260
 
1195
1261
  // src/policy.ts
@@ -1343,7 +1409,12 @@ function renderToolRoutingPolicy(options = {}) {
1343
1409
  }
1344
1410
 
1345
1411
  // src/tools.ts
1346
- import { defineTool } from "@earendil-works/pi-coding-agent";
1412
+ import {
1413
+ DEFAULT_MAX_BYTES,
1414
+ DEFAULT_MAX_LINES,
1415
+ defineTool,
1416
+ truncateHead
1417
+ } from "@earendil-works/pi-coding-agent";
1347
1418
  import { Type } from "@sinclair/typebox";
1348
1419
  function historyProviderParam() {
1349
1420
  return Type.Optional(Type.Union(GOOSE_PROVIDERS.map((provider) => Type.Literal(provider)), {
@@ -1378,6 +1449,17 @@ function goosedumpErrorResult(err) {
1378
1449
  const message = err instanceof Error ? err.message : String(err);
1379
1450
  return textResult(`goosedump error: ${message}`);
1380
1451
  }
1452
+ function boundedHistoryResult(text) {
1453
+ const output = truncateHead(text, {
1454
+ maxBytes: DEFAULT_MAX_BYTES,
1455
+ maxLines: DEFAULT_MAX_LINES
1456
+ });
1457
+ if (!output.truncated)
1458
+ return textResult(text);
1459
+ return textResult(`${output.content}
1460
+
1461
+ [Output truncated: showing ${output.outputLines} of ${output.totalLines} lines ` + `and ${output.outputBytes} of ${output.totalBytes} bytes. ` + "Use goose_get with fewer IDs or a narrower goose_grep pattern.]");
1462
+ }
1381
1463
  function resolveHistoryTarget(ctx, tool, params) {
1382
1464
  const provider = params.provider ?? "pi";
1383
1465
  const target = resolveTarget(ctx, provider, params.session);
@@ -1401,13 +1483,11 @@ function gooseToolMeta(name) {
1401
1483
  };
1402
1484
  }
1403
1485
  var GOOSE_MEMORY_TYPES = ["decision", "fact", "preference", "procedure", "lesson"];
1404
- function defineGooseTool(isAvailable, name, parameters, handler) {
1486
+ function defineGooseTool(name, parameters, handler) {
1405
1487
  return defineTool({
1406
1488
  ...gooseToolMeta(name),
1407
1489
  parameters,
1408
1490
  async execute(_id, params, signal, _onUpdate, ctx) {
1409
- if (!isAvailable())
1410
- return goosedumpUnavailableResult();
1411
1491
  try {
1412
1492
  return await handler(params, signal, ctx);
1413
1493
  } catch (err) {
@@ -1417,8 +1497,8 @@ function defineGooseTool(isAvailable, name, parameters, handler) {
1417
1497
  });
1418
1498
  }
1419
1499
  function registerGooseTools(pi, deps) {
1420
- const { isAvailable, nativeExecutablePath, enqueueRemember, requestCompaction } = deps;
1421
- pi.registerTool(defineGooseTool(isAvailable, "goose_remember", Type.Object({ ...historyTargetParams() }), async (params, _signal, ctx) => {
1500
+ const { nativeExecutablePath, enqueueRemember, requestCompaction } = deps;
1501
+ pi.registerTool(defineGooseTool("goose_remember", Type.Object({ ...historyTargetParams() }), async (params, _signal, ctx) => {
1422
1502
  const resolved = resolveHistoryTarget(ctx, "goose_remember", params);
1423
1503
  if (!resolved.ok)
1424
1504
  return resolved.result;
@@ -1429,7 +1509,7 @@ function registerGooseTools(pi, deps) {
1429
1509
  });
1430
1510
  return textResult(`Memory learning queued for ${resolved.target}.`);
1431
1511
  }));
1432
- pi.registerTool(defineGooseTool(isAvailable, "goose_recall", Type.Object({
1512
+ pi.registerTool(defineGooseTool("goose_recall", Type.Object({
1433
1513
  query: Type.String({ description: PARAM_DESCRIPTIONS.memoryQuery }),
1434
1514
  type: Type.Optional(Type.Union(GOOSE_MEMORY_TYPES.map((type) => Type.Literal(type)), { description: PARAM_DESCRIPTIONS.memoryType })),
1435
1515
  allProjects: Type.Optional(Type.Boolean({
@@ -1459,7 +1539,7 @@ function registerGooseTools(pi, deps) {
1459
1539
  return textResult(`No durable memory for "${params.query}"`);
1460
1540
  return textResult(formatMemoryHits(hits));
1461
1541
  }));
1462
- pi.registerTool(defineGooseTool(isAvailable, "goose_forget", Type.Object({
1542
+ pi.registerTool(defineGooseTool("goose_forget", Type.Object({
1463
1543
  target: Type.String({
1464
1544
  description: PARAM_DESCRIPTIONS.forgetTarget
1465
1545
  })
@@ -1470,8 +1550,8 @@ function registerGooseTools(pi, deps) {
1470
1550
  const res = await goosedumpMemoryForget(params.target.trim(), signal, nativeExecutablePath);
1471
1551
  return textResult(res);
1472
1552
  }));
1473
- pi.registerTool(defineGooseTool(isAvailable, "goose_memory_status", Type.Object({}), async (_params, signal) => textResult(await goosedumpMemoryStatus(signal, nativeExecutablePath))));
1474
- pi.registerTool(defineGooseTool(isAvailable, "goose_search", Type.Object({
1553
+ pi.registerTool(defineGooseTool("goose_memory_status", Type.Object({}), async (_params, signal) => textResult(await goosedumpMemoryStatus(signal, nativeExecutablePath))));
1554
+ pi.registerTool(defineGooseTool("goose_search", Type.Object({
1475
1555
  query: Type.String({ description: PARAM_DESCRIPTIONS.searchQuery }),
1476
1556
  ...historyBranchParams(),
1477
1557
  page: Type.Optional(Type.Integer({
@@ -1493,7 +1573,7 @@ function registerGooseTools(pi, deps) {
1493
1573
 
1494
1574
  ${text}`);
1495
1575
  }));
1496
- pi.registerTool(defineGooseTool(isAvailable, "goose_get", Type.Object({
1576
+ pi.registerTool(defineGooseTool("goose_get", Type.Object({
1497
1577
  ids: Type.Array(Type.String(), {
1498
1578
  minItems: 1,
1499
1579
  description: PARAM_DESCRIPTIONS.entryIds
@@ -1507,9 +1587,9 @@ ${text}`);
1507
1587
  if (messages.length === 0) {
1508
1588
  return textResult("No entries found for the given IDs.");
1509
1589
  }
1510
- return textResult(formatMessagesFull(messages));
1590
+ return boundedHistoryResult(formatMessagesFull(messages));
1511
1591
  }));
1512
- pi.registerTool(defineGooseTool(isAvailable, "goose_grep", Type.Object({
1592
+ pi.registerTool(defineGooseTool("goose_grep", Type.Object({
1513
1593
  pattern: Type.String({ description: PARAM_DESCRIPTIONS.pattern }),
1514
1594
  ...historyBranchParams()
1515
1595
  }), async (params, signal, ctx) => {
@@ -1520,9 +1600,9 @@ ${text}`);
1520
1600
  if (messages.length === 0) {
1521
1601
  return textResult(`No matches for "${params.pattern}"`);
1522
1602
  }
1523
- return textResult(formatMessagesCompact(messages));
1603
+ return boundedHistoryResult(formatMessagesCompact(messages));
1524
1604
  }));
1525
- pi.registerTool(defineGooseTool(isAvailable, "goose_compact", Type.Object({
1605
+ pi.registerTool(defineGooseTool("goose_compact", Type.Object({
1526
1606
  keep: Type.Optional(Type.Integer({
1527
1607
  minimum: 1,
1528
1608
  description: PARAM_DESCRIPTIONS.keep
@@ -1537,7 +1617,7 @@ ${text}`);
1537
1617
  }
1538
1618
  return textResult(compactStartedMessage(keep, result === "queued"));
1539
1619
  }));
1540
- pi.registerTool(defineGooseTool(isAvailable, "goose_sessions", Type.Object({
1620
+ pi.registerTool(defineGooseTool("goose_sessions", Type.Object({
1541
1621
  action: Type.Optional(Type.Union([Type.Literal("list"), Type.Literal("delete")], {
1542
1622
  default: "list",
1543
1623
  description: PARAM_DESCRIPTIONS.sessionsAction
@@ -1550,19 +1630,22 @@ ${text}`);
1550
1630
  const action = params.action ?? "list";
1551
1631
  const provider = params.provider ?? "pi";
1552
1632
  const currentTarget = currentSessionTarget(ctx);
1633
+ const sessions = await goosedumpListSessions(signal, nativeExecutablePath);
1634
+ const cwdSessions = sessions.filter((s) => s.cwd === ctx.cwd && s.provider === provider);
1553
1635
  if (action === "delete") {
1554
1636
  if (!params.session) {
1555
1637
  return textResult("goose_sessions error: session ID is required for action: delete");
1556
1638
  }
1557
1639
  const target = formatTarget(provider, params.session);
1640
+ if (!cwdSessions.some((session) => session.id === params.session)) {
1641
+ return textResult(`Cannot delete session ${target}: not found for ${ctx.cwd}.`);
1642
+ }
1558
1643
  if (currentTarget && target === currentTarget) {
1559
1644
  return textResult("Cannot delete the active session.");
1560
1645
  }
1561
1646
  await goosedumpRemoveSession(target, signal, nativeExecutablePath);
1562
1647
  return textResult(`Deleted session ${target}`);
1563
1648
  }
1564
- const sessions = await goosedumpListSessions(signal, nativeExecutablePath);
1565
- const cwdSessions = sessions.filter((s) => s.cwd === ctx.cwd && s.provider === provider);
1566
1649
  if (cwdSessions.length === 0) {
1567
1650
  return textResult(`No ${provider} sessions found for ${ctx.cwd}`);
1568
1651
  }
@@ -1866,7 +1949,7 @@ async function runSettingsEditor(ctx) {
1866
1949
  }
1867
1950
  });
1868
1951
  }
1869
- async function runSearchPanel(ctx, query) {
1952
+ async function runSearchPanel(ctx, query, executablePath) {
1870
1953
  const OVERLAY_WIDTH = 84;
1871
1954
  const VIEWPORT = 18;
1872
1955
  const PAGE_SIZE = 5;
@@ -1883,7 +1966,7 @@ async function runSearchPanel(ctx, query) {
1883
1966
  let detailScroll = 0;
1884
1967
  async function runSearch() {
1885
1968
  try {
1886
- const first = await goosedumpSearch(target, query, { scope: "lineage", page: 1 });
1969
+ const first = await goosedumpSearch(target, query, { scope: "lineage", page: 1 }, undefined, executablePath);
1887
1970
  if (first.length === 0) {
1888
1971
  entries = [];
1889
1972
  error = `No results for "${query}"`;
@@ -1893,7 +1976,7 @@ async function runSearchPanel(ctx, query) {
1893
1976
  }
1894
1977
  const all = [...first];
1895
1978
  for (let page = 2;page <= 10; page += 1) {
1896
- const result = await goosedumpSearch(target, query, { scope: "lineage", page });
1979
+ const result = await goosedumpSearch(target, query, { scope: "lineage", page }, undefined, executablePath);
1897
1980
  if (result.length === 0)
1898
1981
  break;
1899
1982
  all.push(...result);
@@ -1907,6 +1990,8 @@ async function runSearchPanel(ctx, query) {
1907
1990
  } catch (err) {
1908
1991
  entries = [];
1909
1992
  error = err instanceof Error ? err.message : String(err);
1993
+ } finally {
1994
+ tui.requestRender();
1910
1995
  }
1911
1996
  }
1912
1997
  if (!target) {
@@ -1916,7 +2001,7 @@ async function runSearchPanel(ctx, query) {
1916
2001
  }
1917
2002
  async function loadDetail(id) {
1918
2003
  try {
1919
- const messages = await goosedumpShow(target, { ids: [id] });
2004
+ const messages = await goosedumpShow(target, { ids: [id] }, undefined, executablePath);
1920
2005
  const content = messages.length > 0 ? messages[0].content : "No content for this entry.";
1921
2006
  detailLines = wrapTextWithAnsi(content, wrapWidth);
1922
2007
  detailScroll = 0;
@@ -1924,6 +2009,8 @@ async function runSearchPanel(ctx, query) {
1924
2009
  } catch (err) {
1925
2010
  const message = err instanceof Error ? err.message : String(err);
1926
2011
  ctx.ui.notify(`goosedump error: ${message}`, "error");
2012
+ } finally {
2013
+ tui.requestRender();
1927
2014
  }
1928
2015
  }
1929
2016
  return {
@@ -2019,7 +2106,6 @@ async function runSearchPanel(ctx, query) {
2019
2106
  if (!entry)
2020
2107
  return;
2021
2108
  loadDetail(entry.id);
2022
- tui.requestRender();
2023
2109
  return;
2024
2110
  }
2025
2111
  },
@@ -2034,6 +2120,21 @@ async function runSearchPanel(ctx, query) {
2034
2120
  }
2035
2121
  });
2036
2122
  }
2123
+ function countSessionDescendants(childrenMap, target) {
2124
+ const seen = new Set([target]);
2125
+ const pending = [...childrenMap.get(target) ?? []];
2126
+ while (pending.length > 0) {
2127
+ const session = pending.pop();
2128
+ if (!session)
2129
+ continue;
2130
+ const key = `${session.provider}:${session.id}`;
2131
+ if (seen.has(key))
2132
+ continue;
2133
+ seen.add(key);
2134
+ pending.push(...childrenMap.get(key) ?? []);
2135
+ }
2136
+ return seen.size - 1;
2137
+ }
2037
2138
  async function runSessionsPanel(ctx, executablePath) {
2038
2139
  const OVERLAY_WIDTH = 84;
2039
2140
  const VIEWPORT = 18;
@@ -2082,6 +2183,7 @@ async function runSessionsPanel(ctx, executablePath) {
2082
2183
  let error = null;
2083
2184
  let selected = 0;
2084
2185
  let scroll = 0;
2186
+ let deletionPending = false;
2085
2187
  function rebuildDisplayList() {
2086
2188
  displayList = flattenTree(roots, childrenMap, expandedNodes, 0);
2087
2189
  }
@@ -2098,7 +2200,7 @@ async function runSessionsPanel(ctx, executablePath) {
2098
2200
  sessions = cwdSessions;
2099
2201
  childrenMap = buildChildrenMap(sessions);
2100
2202
  const sessionKeys = new Set(sessions.map((s) => `${s.provider}:${s.id}`));
2101
- roots = sessions.filter((s) => s.parent === null);
2203
+ roots = sessions.filter((s) => s.parent === null || !sessionKeys.has(`${s.provider}:${s.parent}`));
2102
2204
  expandedNodes = new Set([...expandedNodes].filter((key) => sessionKeys.has(key)));
2103
2205
  rebuildDisplayList();
2104
2206
  if (sessions.length === 0) {
@@ -2120,6 +2222,8 @@ async function runSessionsPanel(ctx, executablePath) {
2120
2222
  }
2121
2223
  loadSessions();
2122
2224
  async function deleteSelected() {
2225
+ if (deletionPending)
2226
+ return;
2123
2227
  const row = displayList[selected];
2124
2228
  if (!row)
2125
2229
  return;
@@ -2129,13 +2233,21 @@ async function runSessionsPanel(ctx, executablePath) {
2129
2233
  ctx.ui.notify("Cannot delete the active session.", "error");
2130
2234
  return;
2131
2235
  }
2236
+ deletionPending = true;
2132
2237
  try {
2238
+ const descendants = countSessionDescendants(childrenMap, target);
2239
+ const confirmed = await ctx.ui.confirm("Permanently delete session?", `Permanently remove ${target} and all descendants (${descendants} shown)? This cannot be undone.`);
2240
+ tui.requestRender();
2241
+ if (!confirmed)
2242
+ return;
2133
2243
  await goosedumpRemoveSession(target, undefined, executablePath);
2134
2244
  ctx.ui.notify(`Deleted session ${s.id}`, "info");
2135
2245
  await loadSessions();
2136
2246
  } catch (err) {
2137
2247
  const message = err instanceof Error ? err.message : String(err);
2138
2248
  ctx.ui.notify(`Failed to delete session: ${message}`, "error");
2249
+ } finally {
2250
+ deletionPending = false;
2139
2251
  }
2140
2252
  }
2141
2253
  return {
@@ -2260,8 +2372,7 @@ async function runSessionsPanel(ctx, executablePath) {
2260
2372
  function createGoosedumpIntegration(options = {}) {
2261
2373
  const shouldRegisterTool = options.registerTool ?? true;
2262
2374
  const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
2263
- const nativeExecutablePath = options.nativeExecutablePath;
2264
- let goosedumpAvailable = false;
2375
+ const nativeExecutablePath = options.nativeExecutablePath ?? resolveNativeGoosedumpBinary();
2265
2376
  const rememberQueue = [];
2266
2377
  let rememberRunning = null;
2267
2378
  function enqueueRemember(target, reportError) {
@@ -2297,31 +2408,22 @@ function createGoosedumpIntegration(options = {}) {
2297
2408
  while (rememberRunning)
2298
2409
  await rememberRunning;
2299
2410
  }
2300
- function checkGoosedump() {
2301
- try {
2302
- execFileSync(process.execPath, [resolveGoosedumpBinary(), "--version"], { stdio: "ignore" });
2303
- return true;
2304
- } catch {
2305
- return false;
2306
- }
2307
- }
2308
2411
  const compaction = new CompactionRuntime({
2309
2412
  overrideDefaultCompaction: shouldOverrideDefaultCompaction,
2310
2413
  nativeExecutablePath,
2311
- isAvailable: () => goosedumpAvailable,
2312
2414
  enqueueRemember
2313
2415
  });
2314
2416
  function register(pi) {
2417
+ compaction.setActivePi(pi);
2315
2418
  if (shouldRegisterTool) {
2316
2419
  registerGooseTools(pi, {
2317
- isAvailable: () => goosedumpAvailable,
2318
2420
  nativeExecutablePath,
2319
2421
  enqueueRemember,
2320
2422
  requestCompaction: (ctx, keep) => compaction.requestCompaction(ctx, keep)
2321
2423
  });
2322
2424
  }
2323
2425
  pi.on("before_agent_start", (event) => {
2324
- if (!shouldRegisterTool || !goosedumpAvailable)
2426
+ if (!shouldRegisterTool)
2325
2427
  return;
2326
2428
  return {
2327
2429
  systemPrompt: `${event.systemPrompt}
@@ -2330,10 +2432,6 @@ ${renderToolRoutingPolicy()}`
2330
2432
  };
2331
2433
  });
2332
2434
  pi.on("session_start", async (_event, ctx) => {
2333
- goosedumpAvailable = checkGoosedump();
2334
- if (!goosedumpAvailable) {
2335
- ctx.ui.notify("goosedump was not found. Install with: npm install @jarkkojs/goosedump", "error");
2336
- }
2337
2435
  notifyInvalidSettings(ctx);
2338
2436
  compaction.beginSession(ctx);
2339
2437
  });
@@ -2363,10 +2461,6 @@ ${renderToolRoutingPolicy()}`
2363
2461
  maybePi.registerCommand?.(name, {
2364
2462
  description,
2365
2463
  handler: async (args, ctx) => {
2366
- if (!goosedumpAvailable) {
2367
- ctx.ui.notify(GOOSEDUMP_UNAVAILABLE_MESSAGE, "error");
2368
- return;
2369
- }
2370
2464
  try {
2371
2465
  await handler(args, ctx);
2372
2466
  } catch (err) {
@@ -2408,7 +2502,7 @@ ${renderToolRoutingPolicy()}`
2408
2502
  const query = args.trim() || (await ctx.ui.input("Search query:", ""))?.trim() || "";
2409
2503
  if (query.length === 0)
2410
2504
  return;
2411
- await runSearchPanel(ctx, query);
2505
+ await runSearchPanel(ctx, query, nativeExecutablePath);
2412
2506
  });
2413
2507
  registerGooseCommand("goose-sessions", "List, resume, or delete sessions for the current working directory", async (_args, ctx) => {
2414
2508
  await runSessionsPanel(ctx, nativeExecutablePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.39",
3
+ "version": "0.12.41",
4
4
  "description": "Pi extension for goosedump session search, durable memory, session management, and compaction",
5
5
  "keywords": [
6
6
  "compaction",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@earendil-works/pi-tui": "^0.82.1",
41
- "@jarkkojs/goosedump": "^0.12.39",
41
+ "@jarkkojs/goosedump": "^0.12.41",
42
42
  "@sinclair/typebox": "^0.34.49"
43
43
  },
44
44
  "devDependencies": {