pi-goosedump 0.12.66 → 0.12.69

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 (2) hide show
  1. package/dist/index.ts +225 -97
  2. package/package.json +2 -2
package/dist/index.ts CHANGED
@@ -7,7 +7,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
7
7
  // src/cli.ts
8
8
  import { Type as Type2 } from "typebox";
9
9
  import { Value as Value2 } from "typebox/value";
10
- import { execFile } from "node:child_process";
10
+ import { spawn } from "node:child_process";
11
11
  import { accessSync, constants as fsConstants, existsSync, readFileSync as readFileSync2, statSync } from "node:fs";
12
12
  import { createRequire } from "node:module";
13
13
  import { dirname, isAbsolute, join } from "node:path";
@@ -167,7 +167,8 @@ var GOOSE_PROVIDERS = [
167
167
  var SUMMARIZABLE_ENTRY_TYPES = new Set(["message", "custom_message", "branch_summary"]);
168
168
 
169
169
  // src/util.ts
170
- import { readFileSync } from "node:fs";
170
+ import { closeSync, openSync, readFileSync, readSync } from "node:fs";
171
+ import { StringDecoder } from "node:string_decoder";
171
172
  function isRecord(value) {
172
173
  return typeof value === "object" && value !== null && !Array.isArray(value);
173
174
  }
@@ -187,14 +188,56 @@ function compactStartedMessage(keep, queued = false) {
187
188
  const action = queued ? "Compaction queued with goosedump" : "Compacting with goosedump";
188
189
  return keep !== null ? `${action}; keeping the last ${keep} user turn${keep === 1 ? "" : "s"}.` : `${action} up to Pi's token-based cut.`;
189
190
  }
191
+ var FIRST_LINE_BUFFER_SIZE = 4096;
192
+ var MAX_FIRST_LINE_BYTES = 64 * 1024;
190
193
  function readFirstLine(path) {
194
+ let fd = null;
191
195
  try {
192
- const content = readFileSync(path, "utf8");
193
- const newline = content.indexOf(`
196
+ fd = openSync(path, "r");
197
+ const buffer = Buffer.alloc(FIRST_LINE_BUFFER_SIZE);
198
+ const decoder = new StringDecoder("utf8");
199
+ let totalText = "";
200
+ let offset = 0;
201
+ while (offset < MAX_FIRST_LINE_BYTES) {
202
+ const bytesRead = readSync(fd, buffer, 0, FIRST_LINE_BUFFER_SIZE, offset);
203
+ if (bytesRead === 0)
204
+ break;
205
+ offset += bytesRead;
206
+ const chunk = decoder.write(buffer.subarray(0, bytesRead));
207
+ const newline = chunk.indexOf(`
194
208
  `);
195
- return newline < 0 ? content : content.slice(0, newline);
196
- } catch {
209
+ if (newline >= 0) {
210
+ totalText += chunk.slice(0, newline);
211
+ return totalText.endsWith("\r") ? totalText.slice(0, -1) : totalText;
212
+ }
213
+ totalText += chunk;
214
+ if (bytesRead < FIRST_LINE_BUFFER_SIZE)
215
+ break;
216
+ }
217
+ totalText += decoder.end();
218
+ const finalNewline = totalText.indexOf(`
219
+ `);
220
+ const line = finalNewline >= 0 ? totalText.slice(0, finalNewline) : totalText;
221
+ if (line.length > 0) {
222
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
223
+ }
197
224
  return null;
225
+ } catch {
226
+ try {
227
+ const content = readFileSync(path, "utf8");
228
+ const newline = content.indexOf(`
229
+ `);
230
+ const line = newline < 0 ? content : content.slice(0, newline);
231
+ return line.length > 0 ? line.endsWith("\r") ? line.slice(0, -1) : line : null;
232
+ } catch {
233
+ return null;
234
+ }
235
+ } finally {
236
+ if (fd !== null) {
237
+ try {
238
+ closeSync(fd);
239
+ } catch {}
240
+ }
198
241
  }
199
242
  }
200
243
  function formatTarget(provider, id) {
@@ -470,18 +513,66 @@ function resolveNativeGoosedumpBinary(environment = process.env) {
470
513
  }
471
514
  }
472
515
  function runGoosedumpAsync(args, signal, executablePath = resolveNativeGoosedumpBinary()) {
516
+ if (signal?.aborted)
517
+ return Promise.reject(signal.reason);
473
518
  return new Promise((resolve, reject) => {
474
- execFile(executablePath, args, {
475
- encoding: "utf-8",
476
- maxBuffer: 16 * 1024 * 1024,
477
- signal,
478
- ...process.platform === "linux" ? { env: { ...process.env, APPIMAGE_EXTRACT_AND_RUN: "1" } } : {}
479
- }, (err, stdout, stderr) => {
480
- if (err) {
481
- const message = stderr.trim() || err.message;
482
- reject(new Error(message));
519
+ const processGroup = process.platform !== "win32";
520
+ const output = { stdout: "", stderr: "" };
521
+ const bytes = { stdout: 0, stderr: 0 };
522
+ let child;
523
+ let failure;
524
+ const abort = () => {
525
+ if (child?.pid === undefined)
526
+ return;
527
+ try {
528
+ if (processGroup)
529
+ process.kill(-child.pid, "SIGTERM");
530
+ else
531
+ child.kill("SIGTERM");
532
+ } catch (error) {
533
+ if (error.code !== "ESRCH") {
534
+ failure = new Error("failed to stop goosedump process", { cause: error });
535
+ }
536
+ }
537
+ };
538
+ const capture = (stream, chunk) => {
539
+ if (failure)
540
+ return;
541
+ bytes[stream] += Buffer.byteLength(chunk);
542
+ if (bytes[stream] > 16 * 1024 * 1024) {
543
+ failure = new Error(`goosedump ${stream} exceeded the 16 MiB buffer limit`);
544
+ abort();
545
+ } else {
546
+ output[stream] += chunk;
547
+ }
548
+ };
549
+ signal?.addEventListener("abort", abort, { once: true });
550
+ try {
551
+ child = spawn(executablePath, args, {
552
+ detached: processGroup,
553
+ signal,
554
+ stdio: ["ignore", "pipe", "pipe"],
555
+ ...process.platform === "linux" ? { env: { ...process.env, APPIMAGE_EXTRACT_AND_RUN: "1" } } : {}
556
+ });
557
+ } catch (error) {
558
+ signal?.removeEventListener("abort", abort);
559
+ reject(error);
560
+ return;
561
+ }
562
+ child.stdout?.setEncoding("utf8");
563
+ child.stderr?.setEncoding("utf8");
564
+ child.stdout?.on("data", (chunk) => capture("stdout", chunk));
565
+ child.stderr?.on("data", (chunk) => capture("stderr", chunk));
566
+ child.on("error", (error) => {
567
+ failure ??= error;
568
+ });
569
+ child.once("close", (code, exitSignal) => {
570
+ signal?.removeEventListener("abort", abort);
571
+ if (failure || code !== 0) {
572
+ const message = output.stderr.trim() || failure?.message || `goosedump exited with ${exitSignal ?? `code ${code}`}`;
573
+ reject(new Error(message, { cause: failure }));
483
574
  } else {
484
- resolve({ stdout, stderr });
575
+ resolve(output);
485
576
  }
486
577
  });
487
578
  });
@@ -597,7 +688,12 @@ async function goosedumpCompact(target, options, signal, executablePath) {
597
688
  return { summary: output.stdout.trim(), warning: output.stderr.trim() };
598
689
  }
599
690
  function currentSessionTarget(ctx) {
600
- const sessionFile = ctx.sessionManager.getSessionFile();
691
+ const sessionManager = ctx.sessionManager;
692
+ const inMemoryId = sessionManager?.getSessionId?.() ?? sessionManager?.getHeader?.()?.id;
693
+ if (typeof inMemoryId === "string" && inMemoryId.trim().length > 0) {
694
+ return formatTarget("pi", inMemoryId.trim());
695
+ }
696
+ const sessionFile = sessionManager?.getSessionFile?.();
601
697
  if (!sessionFile)
602
698
  return null;
603
699
  const line = readFirstLine(sessionFile);
@@ -605,8 +701,8 @@ function currentSessionTarget(ctx) {
605
701
  return null;
606
702
  try {
607
703
  const entry = JSON.parse(line);
608
- if (entry.type === "session" && typeof entry.id === "string") {
609
- return formatTarget("pi", entry.id);
704
+ if (entry.type === "session" && typeof entry.id === "string" && entry.id.trim().length > 0) {
705
+ return formatTarget("pi", entry.id.trim());
610
706
  }
611
707
  } catch {
612
708
  return null;
@@ -618,7 +714,7 @@ function currentSessionTarget(ctx) {
618
714
  import { DEFAULT_COMPACTION_SETTINGS as DEFAULT_COMPACTION_SETTINGS2 } from "@earendil-works/pi-coding-agent";
619
715
 
620
716
  // src/compaction.ts
621
- import { findCutPoint } from "@earendil-works/pi-coding-agent";
717
+ import { findCutPoint, sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
622
718
 
623
719
  // src/settings.ts
624
720
  import { DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
@@ -733,9 +829,6 @@ function writeJsonObjectAtomically(path, value) {
733
829
  throw error;
734
830
  }
735
831
  }
736
- function readGoosedumpSettingsFrom(path) {
737
- return pickValidGoosedumpSettings(readJsonObject(path).goosedump);
738
- }
739
832
  function goosedumpSettingValidationMessage(key) {
740
833
  const spec = GOOSEDUMP_SETTING_SPECS[key];
741
834
  if (spec.kind === "boolean")
@@ -756,13 +849,33 @@ function validateGoosedumpSettings(path) {
756
849
  }
757
850
  }
758
851
  }
759
- function resolveGoosedumpSettings(cwd, includeProject) {
760
- return {
852
+ var isNumberValue = (value) => typeof value === "number";
853
+ var isBooleanValue = (value) => typeof value === "boolean";
854
+ function resolveWorkspaceSettings(cwd, includeProject) {
855
+ const globalObj = readJsonObject(globalSettingsPath());
856
+ const projectObj = includeProject ? readJsonObject(projectSettingsPath(cwd)) : undefined;
857
+ const goosedump = {
761
858
  ...DEFAULT_GOOSEDUMP_SETTINGS,
762
- ...readGoosedumpSettingsFrom(globalSettingsPath()),
763
- ...includeProject ? readGoosedumpSettingsFrom(projectSettingsPath(cwd)) : {}
859
+ ...pickValidGoosedumpSettings(globalObj.goosedump),
860
+ ...projectObj ? pickValidGoosedumpSettings(projectObj.goosedump) : {}
861
+ };
862
+ const projectCompaction = isRecord(projectObj?.compaction) ? projectObj.compaction : undefined;
863
+ const globalCompaction = isRecord(globalObj.compaction) ? globalObj.compaction : undefined;
864
+ const enabled = (isBooleanValue(projectCompaction?.enabled) ? projectCompaction.enabled : undefined) ?? (isBooleanValue(globalCompaction?.enabled) ? globalCompaction.enabled : undefined) ?? DEFAULT_COMPACTION_SETTINGS.enabled;
865
+ const keepRecentTokens = (isNumberValue(projectCompaction?.keepRecentTokens) ? projectCompaction.keepRecentTokens : undefined) ?? (isNumberValue(globalCompaction?.keepRecentTokens) ? globalCompaction.keepRecentTokens : undefined) ?? DEFAULT_COMPACTION_SETTINGS.keepRecentTokens;
866
+ const reserveTokens = (isNumberValue(projectCompaction?.reserveTokens) ? projectCompaction.reserveTokens : undefined) ?? (isNumberValue(globalCompaction?.reserveTokens) ? globalCompaction.reserveTokens : undefined) ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
867
+ return {
868
+ goosedump,
869
+ compaction: {
870
+ enabled,
871
+ keepRecentTokens,
872
+ reserveTokens
873
+ }
764
874
  };
765
875
  }
876
+ function resolveGoosedumpSettings(cwd, includeProject) {
877
+ return resolveWorkspaceSettings(cwd, includeProject).goosedump;
878
+ }
766
879
  function notifyInvalidSettings(ctx) {
767
880
  const paths = [
768
881
  globalSettingsPath(),
@@ -819,30 +932,13 @@ function clearScopedGoosedumpSetting(cwd, scope, key) {
819
932
  function formatCompactSetting(value) {
820
933
  return value === 0 ? "disabled" : String(value);
821
934
  }
822
- var isNumberValue = (value) => typeof value === "number";
823
- var isBooleanValue = (value) => typeof value === "boolean";
824
- function piCompactionValue(cwd, includeProject, key, is, fallback) {
825
- const paths = includeProject ? [projectSettingsPath(cwd), globalSettingsPath()] : [globalSettingsPath()];
826
- for (const path of paths) {
827
- const compaction = readJsonObject(path).compaction;
828
- const value = isRecord(compaction) ? compaction[key] : undefined;
829
- if (is(value))
830
- return value;
831
- }
832
- return fallback;
833
- }
834
935
  function piKeepRecentTokens(cwd, includeProject) {
835
- return piCompactionValue(cwd, includeProject, "keepRecentTokens", isNumberValue, DEFAULT_COMPACTION_SETTINGS.keepRecentTokens);
836
- }
837
- function piCompactionEnabled(cwd, includeProject) {
838
- return piCompactionValue(cwd, includeProject, "enabled", isBooleanValue, DEFAULT_COMPACTION_SETTINGS.enabled);
839
- }
840
- function piReserveTokens(cwd, includeProject) {
841
- return piCompactionValue(cwd, includeProject, "reserveTokens", isNumberValue, DEFAULT_COMPACTION_SETTINGS.reserveTokens);
936
+ return resolveWorkspaceSettings(cwd, includeProject).compaction.keepRecentTokens;
842
937
  }
843
938
  function effectiveSummaryMaxTokens(cwd, includeProject) {
844
- const configured = resolveGoosedumpSettings(cwd, includeProject).summaryMaxTokens;
845
- const reserveTokens = piReserveTokens(cwd, includeProject);
939
+ const settings = resolveWorkspaceSettings(cwd, includeProject);
940
+ const configured = settings.goosedump.summaryMaxTokens;
941
+ const reserveTokens = settings.compaction.reserveTokens;
846
942
  if (reserveTokens <= 0)
847
943
  return configured;
848
944
  return Math.max(1, Math.min(configured, Math.floor(reserveTokens * 0.8)));
@@ -858,6 +954,11 @@ function isMessageEntry(entry) {
858
954
  function isCompletedAssistant(message) {
859
955
  return message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted";
860
956
  }
957
+ function hasCompactionMessage(entry) {
958
+ if (entry.type === "compaction")
959
+ return false;
960
+ return sessionEntryToContextMessages(entry).length > 0;
961
+ }
861
962
  function hasCompactableHistory(ctx) {
862
963
  const branch = ctx.sessionManager.getBranch();
863
964
  if (branch.length === 0)
@@ -868,17 +969,10 @@ function hasCompactableHistory(ctx) {
868
969
  const cut = findCutPoint(branch, boundaryStart, branch.length, piKeepRecentTokens(ctx.cwd, projectSettingsTrusted(ctx)));
869
970
  if (!branch[cut.firstKeptEntryIndex]?.id)
870
971
  return false;
871
- const historyEnd = cut.isSplitTurn ? cut.turnStartIndex : cut.firstKeptEntryIndex;
872
- for (let i = boundaryStart;i < historyEnd; i += 1) {
873
- if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type))
972
+ for (let i = boundaryStart;i < cut.firstKeptEntryIndex; i += 1) {
973
+ if (hasCompactionMessage(branch[i]))
874
974
  return true;
875
975
  }
876
- if (cut.isSplitTurn) {
877
- for (let i = cut.turnStartIndex;i < cut.firstKeptEntryIndex; i += 1) {
878
- if (SUMMARIZABLE_ENTRY_TYPES.has(branch[i].type))
879
- return true;
880
- }
881
- }
882
976
  return false;
883
977
  }
884
978
  function latestCompaction(branchEntries) {
@@ -901,16 +995,11 @@ function activeHistoryStartIndex(branchEntries) {
901
995
  return compaction ? compactionBoundaryStartIndex(branchEntries, compaction) : 0;
902
996
  }
903
997
  function completedTurnsSinceCompaction(branchEntries) {
904
- let start = 0;
905
- for (let i = branchEntries.length - 1;i >= 0; i -= 1) {
906
- if (isCompactionEntry(branchEntries[i])) {
907
- start = i + 1;
908
- break;
909
- }
910
- }
911
998
  let completed = 0;
912
- for (let i = start;i < branchEntries.length; i += 1) {
999
+ for (let i = branchEntries.length - 1;i >= 0; i -= 1) {
913
1000
  const entry = branchEntries[i];
1001
+ if (isCompactionEntry(entry))
1002
+ break;
914
1003
  if (isMessageEntry(entry) && isCompletedAssistant(entry.message))
915
1004
  completed += 1;
916
1005
  }
@@ -929,26 +1018,21 @@ function keepBoundaryId(branchEntries, keep) {
929
1018
  }
930
1019
  return;
931
1020
  }
932
- function latestCompactionForRange(branchEntries, untilIndex) {
933
- for (let i = branchEntries.length - 1;i >= 0; i -= 1) {
934
- const entry = branchEntries[i];
935
- if (!isCompactionEntry(entry))
936
- continue;
937
- if (compactionBoundaryStartIndex(branchEntries, entry) < untilIndex)
938
- return entry;
939
- }
940
- return;
941
- }
942
- function compactionStartId(branchEntries, previousCompaction, untilIndex) {
943
- const boundaryStart = compactionBoundaryStartIndex(branchEntries, previousCompaction);
944
- return boundaryStart < untilIndex ? branchEntries[boundaryStart]?.id : undefined;
945
- }
946
1021
  function buildCompactRange(branchEntries, firstKeptEntryId) {
947
1022
  const untilIndex = branchEntries.findIndex((entry) => entry.id === firstKeptEntryId);
948
1023
  if (untilIndex < 0)
949
1024
  return;
950
- const previousCompaction = latestCompactionForRange(branchEntries, untilIndex);
951
- const from = previousCompaction ? compactionStartId(branchEntries, previousCompaction, untilIndex) : undefined;
1025
+ let from;
1026
+ for (let i = Math.min(untilIndex - 1, branchEntries.length - 1);i >= 0; i -= 1) {
1027
+ const entry = branchEntries[i];
1028
+ if (!isCompactionEntry(entry))
1029
+ continue;
1030
+ const boundaryStart = compactionBoundaryStartIndex(branchEntries, entry);
1031
+ if (boundaryStart < untilIndex) {
1032
+ from = branchEntries[boundaryStart]?.id;
1033
+ break;
1034
+ }
1035
+ }
952
1036
  return from && from !== firstKeptEntryId ? { scope: "lineage", from, until: firstKeptEntryId } : { scope: "lineage", until: firstKeptEntryId };
953
1037
  }
954
1038
  async function buildGoosedumpCompaction(target, preparation, branchEntries, summaryMaxTokens, keep, signal, executablePath) {
@@ -993,6 +1077,11 @@ function isCompactionCancellation(error) {
993
1077
  const code = error.code;
994
1078
  return error.name === "AbortError" || code === "ABORT_ERR" || isCompactionCancellationMessage(error.message);
995
1079
  }
1080
+ function isSessionTooSmallError(error) {
1081
+ if (!(error instanceof Error))
1082
+ return false;
1083
+ return /nothing to compact|session too small|already compacted/i.test(error.message);
1084
+ }
996
1085
 
997
1086
  class CompactionRuntime {
998
1087
  overrideDefaultCompaction;
@@ -1004,6 +1093,7 @@ class CompactionRuntime {
1004
1093
  piCompactionReserveTokens = DEFAULT_COMPACTION_SETTINGS2.reserveTokens;
1005
1094
  turnsRemaining = 0;
1006
1095
  counterTriggered = false;
1096
+ smallSessionDisarmed = false;
1007
1097
  compactionState = { phase: "idle" };
1008
1098
  sessionGeneration = 0;
1009
1099
  settlementGeneration = 0;
@@ -1023,6 +1113,7 @@ class CompactionRuntime {
1023
1113
  this.activePi = pi;
1024
1114
  }
1025
1115
  requestCompaction(ctx, keep) {
1116
+ this.smallSessionDisarmed = false;
1026
1117
  if (this.compactionState.phase !== "idle")
1027
1118
  return "busy";
1028
1119
  if (!this.requestHasCompactableHistory(ctx, keep))
@@ -1042,14 +1133,17 @@ class CompactionRuntime {
1042
1133
  }
1043
1134
  syncCompactCounters(ctx) {
1044
1135
  const projectTrusted = projectSettingsTrusted(ctx);
1045
- this.compactSettings = resolveGoosedumpSettings(ctx.cwd, projectTrusted);
1046
- this.piAutoCompactionEnabled = piCompactionEnabled(ctx.cwd, projectTrusted);
1047
- this.piCompactionReserveTokens = piReserveTokens(ctx.cwd, projectTrusted);
1136
+ const resolved = resolveWorkspaceSettings(ctx.cwd, projectTrusted);
1137
+ this.compactSettings = resolved.goosedump;
1138
+ this.piAutoCompactionEnabled = resolved.compaction.enabled;
1139
+ this.piCompactionReserveTokens = resolved.compaction.reserveTokens;
1048
1140
  const completedTurns = completedTurnsSinceCompaction(ctx.sessionManager.getBranch());
1049
1141
  this.turnsRemaining = Math.max(0, this.compactSettings.turnsBeforeCompact - completedTurns);
1050
1142
  this.counterTriggered = this.compactSettings.turnsBeforeCompact > 0 && completedTurns >= this.compactSettings.turnsBeforeCompact;
1051
1143
  }
1052
1144
  updateStatus(ctx) {
1145
+ if (!ctx.hasUI)
1146
+ return;
1053
1147
  const active = this.turnCompactionEnabled();
1054
1148
  let dotColor = "success";
1055
1149
  if (active) {
@@ -1060,10 +1154,17 @@ class CompactionRuntime {
1060
1154
  dotColor = "warning";
1061
1155
  }
1062
1156
  }
1063
- const dot = ctx.ui.theme.fg(dotColor, "●");
1064
1157
  const counter = active ? Math.max(0, this.turnsRemaining).toString(16).toUpperCase().padStart(2, "0") : "--";
1065
1158
  const activity = this.compactionState.phase === "queued" ? " queued" : this.compactionState.phase === "running" ? " compacting" : "";
1066
- const label = ctx.ui.theme.fg("text", `${ctx.ui.theme.bold?.("GD") ?? "GD"}:${counter}${activity}`);
1159
+ let dot = "●";
1160
+ let label = `GD:${counter}${activity}`;
1161
+ try {
1162
+ if (ctx.ui?.theme) {
1163
+ dot = ctx.ui.theme.fg(dotColor, "●");
1164
+ const boldGd = ctx.ui.theme.bold?.("GD") ?? "GD";
1165
+ label = ctx.ui.theme.fg("text", `${boldGd}:${counter}${activity}`);
1166
+ }
1167
+ } catch {}
1067
1168
  ctx.ui.setStatus("goosedump", `${dot} ${label}`);
1068
1169
  }
1069
1170
  beginSession(ctx) {
@@ -1072,6 +1173,7 @@ class CompactionRuntime {
1072
1173
  this.activeSessionFile = ctx.sessionManager.getSessionFile?.() ?? null;
1073
1174
  this.sessionActive = true;
1074
1175
  this.compactionState = { phase: "idle" };
1176
+ this.smallSessionDisarmed = false;
1075
1177
  this.expectedTurnBoundaryAbort = false;
1076
1178
  this.syncCompactCounters(ctx);
1077
1179
  this.updateStatus(ctx);
@@ -1084,6 +1186,7 @@ class CompactionRuntime {
1084
1186
  this.activeSessionFile = null;
1085
1187
  this.sessionActive = false;
1086
1188
  this.compactionState = { phase: "idle" };
1189
+ this.smallSessionDisarmed = false;
1087
1190
  this.expectedTurnBoundaryAbort = false;
1088
1191
  }
1089
1192
  onAgentStart(ctx) {
@@ -1308,11 +1411,13 @@ class CompactionRuntime {
1308
1411
  }
1309
1412
  resetCompactCounters(ctx) {
1310
1413
  const projectTrusted = projectSettingsTrusted(ctx);
1311
- this.compactSettings = resolveGoosedumpSettings(ctx.cwd, projectTrusted);
1312
- this.piAutoCompactionEnabled = piCompactionEnabled(ctx.cwd, projectTrusted);
1313
- this.piCompactionReserveTokens = piReserveTokens(ctx.cwd, projectTrusted);
1414
+ const resolved = resolveWorkspaceSettings(ctx.cwd, projectTrusted);
1415
+ this.compactSettings = resolved.goosedump;
1416
+ this.piAutoCompactionEnabled = resolved.compaction.enabled;
1417
+ this.piCompactionReserveTokens = resolved.compaction.reserveTokens;
1314
1418
  this.turnsRemaining = this.compactSettings.turnsBeforeCompact;
1315
1419
  this.counterTriggered = false;
1420
+ this.smallSessionDisarmed = false;
1316
1421
  }
1317
1422
  turnCompactionEnabled() {
1318
1423
  return this.overrideDefaultCompaction && this.piAutoCompactionEnabled && this.compactSettings.turnsBeforeCompact > 0;
@@ -1331,6 +1436,7 @@ class CompactionRuntime {
1331
1436
  noteCompletedTurn(ctx) {
1332
1437
  if (!this.overrideDefaultCompaction)
1333
1438
  return;
1439
+ this.smallSessionDisarmed = false;
1334
1440
  this.syncCompactCounters(ctx);
1335
1441
  this.updateStatus(ctx);
1336
1442
  }
@@ -1390,14 +1496,19 @@ class CompactionRuntime {
1390
1496
  }, { once: true });
1391
1497
  }
1392
1498
  requestHasCompactableHistory(ctx, keep) {
1393
- return keep === null ? hasCompactableHistory(ctx) : keepBoundaryId(ctx.sessionManager.getBranch(), keep) !== undefined;
1499
+ if (!hasCompactableHistory(ctx))
1500
+ return false;
1501
+ return keep === null || keepBoundaryId(ctx.sessionManager.getBranch(), keep) !== undefined;
1394
1502
  }
1395
1503
  queueTurnCompaction(ctx, eligibleSettlement, resumeAfter = false) {
1396
- if (!this.turnCompactionEnabled() || !this.counterTriggered || this.compactionState.phase !== "idle") {
1504
+ if (!this.turnCompactionEnabled() || !this.counterTriggered || this.smallSessionDisarmed || this.compactionState.phase !== "idle") {
1397
1505
  return;
1398
1506
  }
1399
- if (!hasCompactableHistory(ctx))
1507
+ if (!hasCompactableHistory(ctx)) {
1508
+ this.counterTriggered = false;
1509
+ this.smallSessionDisarmed = true;
1400
1510
  return;
1511
+ }
1401
1512
  this.setCompactionState(ctx, {
1402
1513
  phase: "queued",
1403
1514
  request: {
@@ -1425,6 +1536,10 @@ class CompactionRuntime {
1425
1536
  if (!ctx.isIdle())
1426
1537
  return false;
1427
1538
  if (!this.requestHasCompactableHistory(ctx, request.keep)) {
1539
+ if (request.origin === "turn") {
1540
+ this.counterTriggered = false;
1541
+ this.smallSessionDisarmed = true;
1542
+ }
1428
1543
  this.setCompactionState(ctx, { phase: "idle" });
1429
1544
  return false;
1430
1545
  }
@@ -1453,8 +1568,12 @@ class CompactionRuntime {
1453
1568
  return;
1454
1569
  this.setCompactionState(ctx, { phase: "idle" });
1455
1570
  this.syncCompactCounters(ctx);
1571
+ if (isSessionTooSmallError(error)) {
1572
+ this.counterTriggered = false;
1573
+ this.smallSessionDisarmed = true;
1574
+ }
1456
1575
  this.updateStatus(ctx);
1457
- if (isCompactionCancellation(error))
1576
+ if (isCompactionCancellation(error) || isSessionTooSmallError(error))
1458
1577
  return;
1459
1578
  const message = error instanceof Error ? error.message : String(error);
1460
1579
  if (ctx.hasUI)
@@ -1576,13 +1695,22 @@ var MEMORY_ACTIVITY_STATUS_KEY = "goosedump-memory-activity";
1576
1695
  function showMemoryActivity(ctx) {
1577
1696
  if (!ctx.hasUI)
1578
1697
  return;
1579
- const dot = ctx.ui.theme.fg("accent", "●");
1580
- const label = ctx.ui.theme.fg("muted", "remembering");
1698
+ let dot = "●";
1699
+ let label = "remembering";
1700
+ try {
1701
+ if (ctx.ui?.theme) {
1702
+ dot = ctx.ui.theme.fg("accent", "●");
1703
+ label = ctx.ui.theme.fg("muted", "remembering");
1704
+ }
1705
+ } catch {}
1581
1706
  ctx.ui.setStatus(MEMORY_ACTIVITY_STATUS_KEY, `${dot} ${label}`);
1582
1707
  }
1583
1708
  function hideMemoryActivity(ctx) {
1584
- if (ctx.hasUI)
1585
- ctx.ui.setStatus(MEMORY_ACTIVITY_STATUS_KEY, undefined);
1709
+ if (ctx.hasUI) {
1710
+ try {
1711
+ ctx.ui.setStatus(MEMORY_ACTIVITY_STATUS_KEY, undefined);
1712
+ } catch {}
1713
+ }
1586
1714
  }
1587
1715
 
1588
1716
  // src/policy.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.66",
3
+ "version": "0.12.69",
4
4
  "description": "Pi extension for goosedump session search, durable memory, session management, and compaction",
5
5
  "keywords": [
6
6
  "compaction",
@@ -37,7 +37,7 @@
37
37
  "ci:test": "npm test"
38
38
  },
39
39
  "dependencies": {
40
- "@jarkkojs/goosedump-api": "^0.12.66"
40
+ "@jarkkojs/goosedump-api": "^0.12.69"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@earendil-works/pi-coding-agent": "^0.84.2",