pi-goosedump 0.12.40 → 0.12.42

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 +4 -3
  2. package/dist/index.ts +179 -120
  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
 
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" };
@@ -800,7 +829,6 @@ class CompactionRuntime {
800
829
  constructor(deps) {
801
830
  this.overrideDefaultCompaction = deps.overrideDefaultCompaction;
802
831
  this.nativeExecutablePath = deps.nativeExecutablePath;
803
- this.isAvailable = deps.isAvailable;
804
832
  this.enqueueRemember = deps.enqueueRemember;
805
833
  }
806
834
  setActivePi(pi) {
@@ -818,7 +846,8 @@ class CompactionRuntime {
818
846
  origin: "manual",
819
847
  sessionGeneration: this.sessionGeneration,
820
848
  sessionFile: this.activeSessionFile,
821
- eligibleSettlement: this.settlementGeneration + 1
849
+ eligibleSettlement: this.settlementGeneration + 1,
850
+ resumeAfter: false
822
851
  }
823
852
  });
824
853
  return this.startQueuedCompaction(ctx) ? "started" : "queued";
@@ -827,13 +856,12 @@ class CompactionRuntime {
827
856
  const projectTrusted = projectSettingsTrusted(ctx);
828
857
  this.compactSettings = resolveGoosedumpSettings(ctx.cwd, projectTrusted);
829
858
  this.piAutoCompactionEnabled = piCompactionEnabled(ctx.cwd, projectTrusted);
859
+ this.piCompactionReserveTokens = piReserveTokens(ctx.cwd, projectTrusted);
830
860
  const completedTurns = completedTurnsSinceCompaction(ctx.sessionManager.getBranch());
831
861
  this.turnsRemaining = Math.max(0, this.compactSettings.turnsBeforeCompact - completedTurns);
832
862
  this.counterTriggered = this.compactSettings.turnsBeforeCompact > 0 && completedTurns >= this.compactSettings.turnsBeforeCompact;
833
863
  }
834
864
  updateStatus(ctx) {
835
- if (!this.isAvailable())
836
- return;
837
865
  const active = this.turnCompactionEnabled();
838
866
  let dotColor = "success";
839
867
  if (active) {
@@ -859,7 +887,7 @@ class CompactionRuntime {
859
887
  this.syncCompactCounters(ctx);
860
888
  this.updateStatus(ctx);
861
889
  this.queueTurnCompaction(ctx, 1);
862
- this.deferQueuedCompaction(ctx);
890
+ this.startQueuedCompaction(ctx);
863
891
  }
864
892
  endSession() {
865
893
  this.sessionGeneration += 1;
@@ -879,12 +907,16 @@ class CompactionRuntime {
879
907
  onTurnEnd(event, ctx) {
880
908
  if (!isCompletedAssistant(event.message))
881
909
  return;
882
- this.noteCompletedTurn(ctx);
883
- this.deferTurnCounterCompaction(ctx, event.toolResults.length > 0);
910
+ const resumeAfter = event.toolResults.length > 0 && !ctx.hasPendingMessages();
911
+ if (this.overrideDefaultCompaction) {
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);
884
918
  }
885
919
  onAgentSettled(ctx) {
886
- if (!this.isAvailable() || !this.overrideDefaultCompaction)
887
- return;
888
920
  this.settlementGeneration += 1;
889
921
  this.syncCompactCounters(ctx);
890
922
  this.updateStatus(ctx);
@@ -900,11 +932,11 @@ class CompactionRuntime {
900
932
  this.queueTurnCompaction(ctx, this.settlementGeneration);
901
933
  }
902
934
  if (this.compactionState.phase === "queued" && this.compactionState.request.eligibleSettlement <= this.settlementGeneration) {
903
- this.deferQueuedCompaction(ctx);
935
+ this.startQueuedCompaction(ctx);
904
936
  }
905
937
  }
906
938
  async onSessionBeforeCompact(event, ctx) {
907
- if (!this.overrideDefaultCompaction || !this.isAvailable())
939
+ if (!this.overrideDefaultCompaction)
908
940
  return;
909
941
  const signal = event.signal;
910
942
  if (signal?.aborted) {
@@ -1048,8 +1080,6 @@ class CompactionRuntime {
1048
1080
  this.compactionState = { phase: "idle" };
1049
1081
  this.resetCompactCounters(ctx);
1050
1082
  this.updateStatus(ctx);
1051
- if (!this.isAvailable())
1052
- return;
1053
1083
  const compactionEntry = event.compactionEntry;
1054
1084
  if (!event.fromExtension || !compactionEntry || !isRecord(compactionEntry.details) || compactionEntry.details.source !== "goosedump") {
1055
1085
  return;
@@ -1074,6 +1104,7 @@ class CompactionRuntime {
1074
1104
  const projectTrusted = projectSettingsTrusted(ctx);
1075
1105
  this.compactSettings = resolveGoosedumpSettings(ctx.cwd, projectTrusted);
1076
1106
  this.piAutoCompactionEnabled = piCompactionEnabled(ctx.cwd, projectTrusted);
1107
+ this.piCompactionReserveTokens = piReserveTokens(ctx.cwd, projectTrusted);
1077
1108
  this.turnsRemaining = this.compactSettings.turnsBeforeCompact;
1078
1109
  this.counterTriggered = false;
1079
1110
  }
@@ -1092,11 +1123,10 @@ class CompactionRuntime {
1092
1123
  this.updateStatus(ctx);
1093
1124
  }
1094
1125
  noteCompletedTurn(ctx) {
1095
- if (!this.isAvailable() || !this.overrideDefaultCompaction)
1126
+ if (!this.overrideDefaultCompaction)
1096
1127
  return;
1097
1128
  this.syncCompactCounters(ctx);
1098
1129
  this.updateStatus(ctx);
1099
- this.queueTurnCompaction(ctx, this.settlementGeneration + 1);
1100
1130
  }
1101
1131
  resumeAfterSuccessfulCompaction(ctx, resume) {
1102
1132
  if (!resume || !this.activePi || ctx.hasPendingMessages())
@@ -1107,21 +1137,33 @@ class CompactionRuntime {
1107
1137
  display: false
1108
1138
  }, { triggerTurn: true });
1109
1139
  }
1110
- deferTurnCounterCompaction(ctx, resumeAfter) {
1111
- if (!this.isAvailable() || !this.turnCompactionEnabled() || !this.counterTriggered)
1140
+ interruptToolLoopForQueuedCompaction(ctx, resumeAfter) {
1141
+ if (!resumeAfter || this.compactionState.phase !== "queued")
1112
1142
  return;
1113
- if (!hasCompactableHistory(ctx))
1143
+ const request = this.compactionState.request;
1144
+ if (!this.requestIsCurrent(request))
1114
1145
  return;
1115
- if (this.compactionState.phase !== "queued" || this.compactionState.request.origin !== "turn") {
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)) {
1116
1155
  return;
1117
1156
  }
1118
- const request = this.compactionState.request;
1119
- setImmediate(() => {
1120
- if (this.compactionState.phase !== "queued" || this.compactionState.request !== request || !this.contextIsCurrent(ctx)) {
1121
- return;
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
1122
1166
  }
1123
- const runStillActive = this.settlementGeneration < request.eligibleSettlement;
1124
- this.startQueuedCompaction(ctx, runStillActive ? { allowActiveRun: true, resumeAfter } : undefined);
1125
1167
  });
1126
1168
  }
1127
1169
  requestIsCurrent(request) {
@@ -1143,7 +1185,7 @@ class CompactionRuntime {
1143
1185
  requestHasCompactableHistory(ctx, keep) {
1144
1186
  return keep === null ? hasCompactableHistory(ctx) : keepBoundaryId(ctx.sessionManager.getBranch(), keep) !== undefined;
1145
1187
  }
1146
- queueTurnCompaction(ctx, eligibleSettlement) {
1188
+ queueTurnCompaction(ctx, eligibleSettlement, resumeAfter = false) {
1147
1189
  if (!this.turnCompactionEnabled() || !this.counterTriggered || this.compactionState.phase !== "idle") {
1148
1190
  return;
1149
1191
  }
@@ -1156,11 +1198,12 @@ class CompactionRuntime {
1156
1198
  origin: "turn",
1157
1199
  sessionGeneration: this.sessionGeneration,
1158
1200
  sessionFile: this.activeSessionFile,
1159
- eligibleSettlement
1201
+ eligibleSettlement,
1202
+ resumeAfter
1160
1203
  }
1161
1204
  });
1162
1205
  }
1163
- startQueuedCompaction(ctx, options = {}) {
1206
+ startQueuedCompaction(ctx) {
1164
1207
  if (this.compactionState.phase !== "queued")
1165
1208
  return false;
1166
1209
  const request = this.compactionState.request;
@@ -1172,7 +1215,7 @@ class CompactionRuntime {
1172
1215
  }
1173
1216
  if (request.origin !== "turn" && ctx.hasPendingMessages())
1174
1217
  return false;
1175
- if (!options.allowActiveRun && !ctx.isIdle())
1218
+ if (!ctx.isIdle())
1176
1219
  return false;
1177
1220
  if (!this.requestHasCompactableHistory(ctx, request.keep)) {
1178
1221
  this.setCompactionState(ctx, { phase: "idle" });
@@ -1181,7 +1224,7 @@ class CompactionRuntime {
1181
1224
  const operation = ++this.operationGeneration;
1182
1225
  const generation = request.sessionGeneration;
1183
1226
  const sessionFile = request.sessionFile;
1184
- const resumeAfter = request.origin === "turn" && options.resumeAfter === true;
1227
+ const resumeAfter = request.resumeAfter;
1185
1228
  this.setCompactionState(ctx, {
1186
1229
  phase: "running",
1187
1230
  request,
@@ -1213,17 +1256,6 @@ class CompactionRuntime {
1213
1256
  });
1214
1257
  return true;
1215
1258
  }
1216
- deferQueuedCompaction(ctx) {
1217
- if (this.compactionState.phase !== "queued")
1218
- return;
1219
- const request = this.compactionState.request;
1220
- setImmediate(() => {
1221
- if (this.compactionState.phase !== "queued" || this.compactionState.request !== request || !this.contextIsCurrent(ctx)) {
1222
- return;
1223
- }
1224
- this.startQueuedCompaction(ctx);
1225
- });
1226
- }
1227
1259
  }
1228
1260
 
1229
1261
  // src/policy.ts
@@ -1377,7 +1409,12 @@ function renderToolRoutingPolicy(options = {}) {
1377
1409
  }
1378
1410
 
1379
1411
  // src/tools.ts
1380
- 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";
1381
1418
  import { Type } from "@sinclair/typebox";
1382
1419
  function historyProviderParam() {
1383
1420
  return Type.Optional(Type.Union(GOOSE_PROVIDERS.map((provider) => Type.Literal(provider)), {
@@ -1412,6 +1449,17 @@ function goosedumpErrorResult(err) {
1412
1449
  const message = err instanceof Error ? err.message : String(err);
1413
1450
  return textResult(`goosedump error: ${message}`);
1414
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
+ }
1415
1463
  function resolveHistoryTarget(ctx, tool, params) {
1416
1464
  const provider = params.provider ?? "pi";
1417
1465
  const target = resolveTarget(ctx, provider, params.session);
@@ -1435,13 +1483,11 @@ function gooseToolMeta(name) {
1435
1483
  };
1436
1484
  }
1437
1485
  var GOOSE_MEMORY_TYPES = ["decision", "fact", "preference", "procedure", "lesson"];
1438
- function defineGooseTool(isAvailable, name, parameters, handler) {
1486
+ function defineGooseTool(name, parameters, handler) {
1439
1487
  return defineTool({
1440
1488
  ...gooseToolMeta(name),
1441
1489
  parameters,
1442
1490
  async execute(_id, params, signal, _onUpdate, ctx) {
1443
- if (!isAvailable())
1444
- return goosedumpUnavailableResult();
1445
1491
  try {
1446
1492
  return await handler(params, signal, ctx);
1447
1493
  } catch (err) {
@@ -1451,8 +1497,8 @@ function defineGooseTool(isAvailable, name, parameters, handler) {
1451
1497
  });
1452
1498
  }
1453
1499
  function registerGooseTools(pi, deps) {
1454
- const { isAvailable, nativeExecutablePath, enqueueRemember, requestCompaction } = deps;
1455
- 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) => {
1456
1502
  const resolved = resolveHistoryTarget(ctx, "goose_remember", params);
1457
1503
  if (!resolved.ok)
1458
1504
  return resolved.result;
@@ -1463,7 +1509,7 @@ function registerGooseTools(pi, deps) {
1463
1509
  });
1464
1510
  return textResult(`Memory learning queued for ${resolved.target}.`);
1465
1511
  }));
1466
- pi.registerTool(defineGooseTool(isAvailable, "goose_recall", Type.Object({
1512
+ pi.registerTool(defineGooseTool("goose_recall", Type.Object({
1467
1513
  query: Type.String({ description: PARAM_DESCRIPTIONS.memoryQuery }),
1468
1514
  type: Type.Optional(Type.Union(GOOSE_MEMORY_TYPES.map((type) => Type.Literal(type)), { description: PARAM_DESCRIPTIONS.memoryType })),
1469
1515
  allProjects: Type.Optional(Type.Boolean({
@@ -1493,7 +1539,7 @@ function registerGooseTools(pi, deps) {
1493
1539
  return textResult(`No durable memory for "${params.query}"`);
1494
1540
  return textResult(formatMemoryHits(hits));
1495
1541
  }));
1496
- pi.registerTool(defineGooseTool(isAvailable, "goose_forget", Type.Object({
1542
+ pi.registerTool(defineGooseTool("goose_forget", Type.Object({
1497
1543
  target: Type.String({
1498
1544
  description: PARAM_DESCRIPTIONS.forgetTarget
1499
1545
  })
@@ -1504,8 +1550,8 @@ function registerGooseTools(pi, deps) {
1504
1550
  const res = await goosedumpMemoryForget(params.target.trim(), signal, nativeExecutablePath);
1505
1551
  return textResult(res);
1506
1552
  }));
1507
- pi.registerTool(defineGooseTool(isAvailable, "goose_memory_status", Type.Object({}), async (_params, signal) => textResult(await goosedumpMemoryStatus(signal, nativeExecutablePath))));
1508
- 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({
1509
1555
  query: Type.String({ description: PARAM_DESCRIPTIONS.searchQuery }),
1510
1556
  ...historyBranchParams(),
1511
1557
  page: Type.Optional(Type.Integer({
@@ -1527,7 +1573,7 @@ function registerGooseTools(pi, deps) {
1527
1573
 
1528
1574
  ${text}`);
1529
1575
  }));
1530
- pi.registerTool(defineGooseTool(isAvailable, "goose_get", Type.Object({
1576
+ pi.registerTool(defineGooseTool("goose_get", Type.Object({
1531
1577
  ids: Type.Array(Type.String(), {
1532
1578
  minItems: 1,
1533
1579
  description: PARAM_DESCRIPTIONS.entryIds
@@ -1541,9 +1587,9 @@ ${text}`);
1541
1587
  if (messages.length === 0) {
1542
1588
  return textResult("No entries found for the given IDs.");
1543
1589
  }
1544
- return textResult(formatMessagesFull(messages));
1590
+ return boundedHistoryResult(formatMessagesFull(messages));
1545
1591
  }));
1546
- pi.registerTool(defineGooseTool(isAvailable, "goose_grep", Type.Object({
1592
+ pi.registerTool(defineGooseTool("goose_grep", Type.Object({
1547
1593
  pattern: Type.String({ description: PARAM_DESCRIPTIONS.pattern }),
1548
1594
  ...historyBranchParams()
1549
1595
  }), async (params, signal, ctx) => {
@@ -1554,9 +1600,9 @@ ${text}`);
1554
1600
  if (messages.length === 0) {
1555
1601
  return textResult(`No matches for "${params.pattern}"`);
1556
1602
  }
1557
- return textResult(formatMessagesCompact(messages));
1603
+ return boundedHistoryResult(formatMessagesCompact(messages));
1558
1604
  }));
1559
- pi.registerTool(defineGooseTool(isAvailable, "goose_compact", Type.Object({
1605
+ pi.registerTool(defineGooseTool("goose_compact", Type.Object({
1560
1606
  keep: Type.Optional(Type.Integer({
1561
1607
  minimum: 1,
1562
1608
  description: PARAM_DESCRIPTIONS.keep
@@ -1571,7 +1617,7 @@ ${text}`);
1571
1617
  }
1572
1618
  return textResult(compactStartedMessage(keep, result === "queued"));
1573
1619
  }));
1574
- pi.registerTool(defineGooseTool(isAvailable, "goose_sessions", Type.Object({
1620
+ pi.registerTool(defineGooseTool("goose_sessions", Type.Object({
1575
1621
  action: Type.Optional(Type.Union([Type.Literal("list"), Type.Literal("delete")], {
1576
1622
  default: "list",
1577
1623
  description: PARAM_DESCRIPTIONS.sessionsAction
@@ -1584,19 +1630,22 @@ ${text}`);
1584
1630
  const action = params.action ?? "list";
1585
1631
  const provider = params.provider ?? "pi";
1586
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);
1587
1635
  if (action === "delete") {
1588
1636
  if (!params.session) {
1589
1637
  return textResult("goose_sessions error: session ID is required for action: delete");
1590
1638
  }
1591
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
+ }
1592
1643
  if (currentTarget && target === currentTarget) {
1593
1644
  return textResult("Cannot delete the active session.");
1594
1645
  }
1595
1646
  await goosedumpRemoveSession(target, signal, nativeExecutablePath);
1596
1647
  return textResult(`Deleted session ${target}`);
1597
1648
  }
1598
- const sessions = await goosedumpListSessions(signal, nativeExecutablePath);
1599
- const cwdSessions = sessions.filter((s) => s.cwd === ctx.cwd && s.provider === provider);
1600
1649
  if (cwdSessions.length === 0) {
1601
1650
  return textResult(`No ${provider} sessions found for ${ctx.cwd}`);
1602
1651
  }
@@ -1900,7 +1949,7 @@ async function runSettingsEditor(ctx) {
1900
1949
  }
1901
1950
  });
1902
1951
  }
1903
- async function runSearchPanel(ctx, query) {
1952
+ async function runSearchPanel(ctx, query, executablePath) {
1904
1953
  const OVERLAY_WIDTH = 84;
1905
1954
  const VIEWPORT = 18;
1906
1955
  const PAGE_SIZE = 5;
@@ -1917,7 +1966,7 @@ async function runSearchPanel(ctx, query) {
1917
1966
  let detailScroll = 0;
1918
1967
  async function runSearch() {
1919
1968
  try {
1920
- const first = await goosedumpSearch(target, query, { scope: "lineage", page: 1 });
1969
+ const first = await goosedumpSearch(target, query, { scope: "lineage", page: 1 }, undefined, executablePath);
1921
1970
  if (first.length === 0) {
1922
1971
  entries = [];
1923
1972
  error = `No results for "${query}"`;
@@ -1927,7 +1976,7 @@ async function runSearchPanel(ctx, query) {
1927
1976
  }
1928
1977
  const all = [...first];
1929
1978
  for (let page = 2;page <= 10; page += 1) {
1930
- const result = await goosedumpSearch(target, query, { scope: "lineage", page });
1979
+ const result = await goosedumpSearch(target, query, { scope: "lineage", page }, undefined, executablePath);
1931
1980
  if (result.length === 0)
1932
1981
  break;
1933
1982
  all.push(...result);
@@ -1941,6 +1990,8 @@ async function runSearchPanel(ctx, query) {
1941
1990
  } catch (err) {
1942
1991
  entries = [];
1943
1992
  error = err instanceof Error ? err.message : String(err);
1993
+ } finally {
1994
+ tui.requestRender();
1944
1995
  }
1945
1996
  }
1946
1997
  if (!target) {
@@ -1950,7 +2001,7 @@ async function runSearchPanel(ctx, query) {
1950
2001
  }
1951
2002
  async function loadDetail(id) {
1952
2003
  try {
1953
- const messages = await goosedumpShow(target, { ids: [id] });
2004
+ const messages = await goosedumpShow(target, { ids: [id] }, undefined, executablePath);
1954
2005
  const content = messages.length > 0 ? messages[0].content : "No content for this entry.";
1955
2006
  detailLines = wrapTextWithAnsi(content, wrapWidth);
1956
2007
  detailScroll = 0;
@@ -1958,6 +2009,8 @@ async function runSearchPanel(ctx, query) {
1958
2009
  } catch (err) {
1959
2010
  const message = err instanceof Error ? err.message : String(err);
1960
2011
  ctx.ui.notify(`goosedump error: ${message}`, "error");
2012
+ } finally {
2013
+ tui.requestRender();
1961
2014
  }
1962
2015
  }
1963
2016
  return {
@@ -2053,7 +2106,6 @@ async function runSearchPanel(ctx, query) {
2053
2106
  if (!entry)
2054
2107
  return;
2055
2108
  loadDetail(entry.id);
2056
- tui.requestRender();
2057
2109
  return;
2058
2110
  }
2059
2111
  },
@@ -2068,6 +2120,21 @@ async function runSearchPanel(ctx, query) {
2068
2120
  }
2069
2121
  });
2070
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
+ }
2071
2138
  async function runSessionsPanel(ctx, executablePath) {
2072
2139
  const OVERLAY_WIDTH = 84;
2073
2140
  const VIEWPORT = 18;
@@ -2116,6 +2183,7 @@ async function runSessionsPanel(ctx, executablePath) {
2116
2183
  let error = null;
2117
2184
  let selected = 0;
2118
2185
  let scroll = 0;
2186
+ let deletionPending = false;
2119
2187
  function rebuildDisplayList() {
2120
2188
  displayList = flattenTree(roots, childrenMap, expandedNodes, 0);
2121
2189
  }
@@ -2132,7 +2200,7 @@ async function runSessionsPanel(ctx, executablePath) {
2132
2200
  sessions = cwdSessions;
2133
2201
  childrenMap = buildChildrenMap(sessions);
2134
2202
  const sessionKeys = new Set(sessions.map((s) => `${s.provider}:${s.id}`));
2135
- roots = sessions.filter((s) => s.parent === null);
2203
+ roots = sessions.filter((s) => s.parent === null || !sessionKeys.has(`${s.provider}:${s.parent}`));
2136
2204
  expandedNodes = new Set([...expandedNodes].filter((key) => sessionKeys.has(key)));
2137
2205
  rebuildDisplayList();
2138
2206
  if (sessions.length === 0) {
@@ -2154,6 +2222,8 @@ async function runSessionsPanel(ctx, executablePath) {
2154
2222
  }
2155
2223
  loadSessions();
2156
2224
  async function deleteSelected() {
2225
+ if (deletionPending)
2226
+ return;
2157
2227
  const row = displayList[selected];
2158
2228
  if (!row)
2159
2229
  return;
@@ -2163,13 +2233,21 @@ async function runSessionsPanel(ctx, executablePath) {
2163
2233
  ctx.ui.notify("Cannot delete the active session.", "error");
2164
2234
  return;
2165
2235
  }
2236
+ deletionPending = true;
2166
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;
2167
2243
  await goosedumpRemoveSession(target, undefined, executablePath);
2168
2244
  ctx.ui.notify(`Deleted session ${s.id}`, "info");
2169
2245
  await loadSessions();
2170
2246
  } catch (err) {
2171
2247
  const message = err instanceof Error ? err.message : String(err);
2172
2248
  ctx.ui.notify(`Failed to delete session: ${message}`, "error");
2249
+ } finally {
2250
+ deletionPending = false;
2173
2251
  }
2174
2252
  }
2175
2253
  return {
@@ -2294,8 +2372,7 @@ async function runSessionsPanel(ctx, executablePath) {
2294
2372
  function createGoosedumpIntegration(options = {}) {
2295
2373
  const shouldRegisterTool = options.registerTool ?? true;
2296
2374
  const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
2297
- const nativeExecutablePath = options.nativeExecutablePath;
2298
- let goosedumpAvailable = false;
2375
+ const nativeExecutablePath = options.nativeExecutablePath ?? resolveNativeGoosedumpBinary();
2299
2376
  const rememberQueue = [];
2300
2377
  let rememberRunning = null;
2301
2378
  function enqueueRemember(target, reportError) {
@@ -2331,32 +2408,22 @@ function createGoosedumpIntegration(options = {}) {
2331
2408
  while (rememberRunning)
2332
2409
  await rememberRunning;
2333
2410
  }
2334
- function checkGoosedump() {
2335
- try {
2336
- execFileSync(process.execPath, [resolveGoosedumpBinary(), "--version"], { stdio: "ignore" });
2337
- return true;
2338
- } catch {
2339
- return false;
2340
- }
2341
- }
2342
2411
  const compaction = new CompactionRuntime({
2343
2412
  overrideDefaultCompaction: shouldOverrideDefaultCompaction,
2344
2413
  nativeExecutablePath,
2345
- isAvailable: () => goosedumpAvailable,
2346
2414
  enqueueRemember
2347
2415
  });
2348
2416
  function register(pi) {
2349
2417
  compaction.setActivePi(pi);
2350
2418
  if (shouldRegisterTool) {
2351
2419
  registerGooseTools(pi, {
2352
- isAvailable: () => goosedumpAvailable,
2353
2420
  nativeExecutablePath,
2354
2421
  enqueueRemember,
2355
2422
  requestCompaction: (ctx, keep) => compaction.requestCompaction(ctx, keep)
2356
2423
  });
2357
2424
  }
2358
2425
  pi.on("before_agent_start", (event) => {
2359
- if (!shouldRegisterTool || !goosedumpAvailable)
2426
+ if (!shouldRegisterTool)
2360
2427
  return;
2361
2428
  return {
2362
2429
  systemPrompt: `${event.systemPrompt}
@@ -2365,10 +2432,6 @@ ${renderToolRoutingPolicy()}`
2365
2432
  };
2366
2433
  });
2367
2434
  pi.on("session_start", async (_event, ctx) => {
2368
- goosedumpAvailable = checkGoosedump();
2369
- if (!goosedumpAvailable) {
2370
- ctx.ui.notify("goosedump was not found. Install with: npm install @jarkkojs/goosedump", "error");
2371
- }
2372
2435
  notifyInvalidSettings(ctx);
2373
2436
  compaction.beginSession(ctx);
2374
2437
  });
@@ -2398,10 +2461,6 @@ ${renderToolRoutingPolicy()}`
2398
2461
  maybePi.registerCommand?.(name, {
2399
2462
  description,
2400
2463
  handler: async (args, ctx) => {
2401
- if (!goosedumpAvailable) {
2402
- ctx.ui.notify(GOOSEDUMP_UNAVAILABLE_MESSAGE, "error");
2403
- return;
2404
- }
2405
2464
  try {
2406
2465
  await handler(args, ctx);
2407
2466
  } catch (err) {
@@ -2443,7 +2502,7 @@ ${renderToolRoutingPolicy()}`
2443
2502
  const query = args.trim() || (await ctx.ui.input("Search query:", ""))?.trim() || "";
2444
2503
  if (query.length === 0)
2445
2504
  return;
2446
- await runSearchPanel(ctx, query);
2505
+ await runSearchPanel(ctx, query, nativeExecutablePath);
2447
2506
  });
2448
2507
  registerGooseCommand("goose-sessions", "List, resume, or delete sessions for the current working directory", async (_args, ctx) => {
2449
2508
  await runSessionsPanel(ctx, nativeExecutablePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.40",
3
+ "version": "0.12.42",
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.40",
41
+ "@jarkkojs/goosedump": "^0.12.42",
42
42
  "@sinclair/typebox": "^0.34.49"
43
43
  },
44
44
  "devDependencies": {