pi-goosedump 0.12.18 → 0.12.19

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 +13 -11
  2. package/dist/index.ts +70 -69
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -93,17 +93,19 @@ Optional settings live under `goosedump` in:
93
93
  ```
94
94
 
95
95
  `turnsBeforeCompact` is an integer from 0 to 255. `0` disables scheduled
96
- compaction. Each fully settled agent run counts as one turn; intermediate model
97
- responses that call tools do not. The countdown is restored when a session
98
- resumes and appears in the footer with a status dot (green when >25% turns
99
- remain, yellow when ≤25%, red when ≤12.5%) followed by a hexadecimal countdown.
100
- `● GD:0A` means ten turns remain, `● GD:00` is ready to compact, and `● GD:--`
101
- is disabled. The footer adds `queued` while compaction is waiting for the active
102
- agent run to settle and `compacting` while the summary is being built. Pi remains
103
- the sole scheduler for its normal context threshold. Whichever limit is reached
104
- first compacts once, and every successful compaction resets the countdown. A
105
- failed or cancelled compaction leaves the counter armed at zero and is not
106
- retried in the same settlement.
96
+ compaction. Each completed model response counts as one turn, including responses
97
+ that call tools. The countdown is restored when a session resumes and appears in
98
+ the footer with a status dot (green when >25% turns remain, yellow when ≤25%, red
99
+ when ≤12.5%) followed by a hexadecimal countdown. `● GD:0A` means ten turns
100
+ remain, `● GD:00` is ready to compact, and `● GD:--` is disabled. The footer
101
+ adds `queued` while compaction is waiting to start and `compacting` while the
102
+ summary is being built. When the counter hits zero mid tool-loop, compaction
103
+ starts immediately after the current tool batch is persisted and the task is
104
+ resumed automatically. When the counter hits zero on a terminal turn, compaction
105
+ starts after the agent has settled. Pi remains the sole scheduler for its normal
106
+ context threshold. Whichever limit is reached first compacts once, and every
107
+ successful compaction resets the countdown. A failed or cancelled compaction
108
+ leaves the counter armed at zero and retries on the next tool turn or settlement.
107
109
 
108
110
  `summaryMaxTokens` is an integer from 512 to 65536 (default 4096). The effective
109
111
  summary budget is the lower of this value and 80% of Pi's `compaction.reserveTokens`,
package/dist/index.ts CHANGED
@@ -590,26 +590,11 @@ function currentSessionTarget(ctx) {
590
590
  function isCompactionEntry(entry) {
591
591
  return entry.type === "compaction";
592
592
  }
593
- var COMPLETED_AGENT_RUN_ENTRY = "goosedump-completed-agent-run";
594
593
  function isMessageEntry(entry) {
595
594
  return entry.type === "message";
596
595
  }
597
- function isTerminalAssistant(message) {
598
- if (message.role !== "assistant")
599
- return false;
600
- if (message.stopReason !== "stop" && message.stopReason !== "length")
601
- return false;
602
- return !(Array.isArray(message.content) && message.content.some((part) => isRecord(part) && part.type === "toolCall"));
603
- }
604
- function completedAgentRunData(entry) {
605
- if (entry.type !== "custom" || entry.customType !== COMPLETED_AGENT_RUN_ENTRY)
606
- return;
607
- if (!isRecord(entry.data) || entry.data.version !== 1)
608
- return;
609
- const startedAfterEntryId = entry.data.startedAfterEntryId;
610
- if (startedAfterEntryId !== null && typeof startedAfterEntryId !== "string")
611
- return;
612
- return { version: 1, startedAfterEntryId };
596
+ function isCompletedAssistant(message) {
597
+ return message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted";
613
598
  }
614
599
  var SUMMARIZABLE_ENTRY_TYPES = new Set(["message", "custom_message", "branch_summary"]);
615
600
  function piKeepRecentTokens(cwd, includeProject) {
@@ -681,7 +666,7 @@ function activeHistoryStartIndex(branchEntries) {
681
666
  const compaction = latestCompaction(branchEntries);
682
667
  return compaction ? compactionBoundaryStartIndex(branchEntries, compaction) : 0;
683
668
  }
684
- function completedRunsSinceCompaction(branchEntries) {
669
+ function completedTurnsSinceCompaction(branchEntries) {
685
670
  let start = 0;
686
671
  for (let i = branchEntries.length - 1;i >= 0; i -= 1) {
687
672
  if (isCompactionEntry(branchEntries[i])) {
@@ -689,27 +674,10 @@ function completedRunsSinceCompaction(branchEntries) {
689
674
  break;
690
675
  }
691
676
  }
692
- const firstMarkerIndex = branchEntries.findIndex((entry, index) => index >= start && completedAgentRunData(entry) !== undefined);
693
- if (firstMarkerIndex < 0) {
694
- let completed2 = 0;
695
- for (let i = start;i < branchEntries.length; i += 1) {
696
- const entry = branchEntries[i];
697
- if (isMessageEntry(entry) && isTerminalAssistant(entry.message))
698
- completed2 += 1;
699
- }
700
- return completed2;
701
- }
702
- const firstMarker = completedAgentRunData(branchEntries[firstMarkerIndex]);
703
- const boundaryIndex = firstMarker?.startedAfterEntryId ? branchEntries.findIndex((entry) => entry.id === firstMarker.startedAfterEntryId) : start - 1;
704
- const legacyEnd = boundaryIndex >= start ? boundaryIndex + 1 : start;
705
677
  let completed = 0;
706
- for (let i = start;i < legacyEnd; i += 1) {
678
+ for (let i = start;i < branchEntries.length; i += 1) {
707
679
  const entry = branchEntries[i];
708
- if (isMessageEntry(entry) && isTerminalAssistant(entry.message))
709
- completed += 1;
710
- }
711
- for (let i = firstMarkerIndex;i < branchEntries.length; i += 1) {
712
- if (completedAgentRunData(branchEntries[i]) !== undefined)
680
+ if (isMessageEntry(entry) && isCompletedAssistant(entry.message))
713
681
  completed += 1;
714
682
  }
715
683
  return completed;
@@ -877,7 +845,7 @@ async function runSettingsEditor(ctx) {
877
845
  const specs = [
878
846
  {
879
847
  key: "turnsBeforeCompact",
880
- description: "Completed agent runs before compaction; 0 disables.",
848
+ description: "LLM turns before compaction; 0 disables.",
881
849
  min: TURNS_BEFORE_COMPACT_MIN,
882
850
  max: TURNS_BEFORE_COMPACT_MAX
883
851
  },
@@ -1443,8 +1411,8 @@ function createGoosedumpIntegration(options = {}) {
1443
1411
  let operationGeneration = 0;
1444
1412
  let activeSessionFile = null;
1445
1413
  let sessionActive = false;
1446
- let agentRunObserved = false;
1447
- let agentRunStartEntryId = null;
1414
+ let activePi = null;
1415
+ let resumeAfterCompaction = false;
1448
1416
  const rememberQueue = [];
1449
1417
  let rememberRunning = null;
1450
1418
  function enqueueRemember(target, reportError) {
@@ -1482,9 +1450,9 @@ function createGoosedumpIntegration(options = {}) {
1482
1450
  }
1483
1451
  function syncCompactCounters(ctx) {
1484
1452
  compactSettings = resolveGoosedumpSettings(ctx.cwd, projectSettingsTrusted(ctx));
1485
- const completedRuns = completedRunsSinceCompaction(ctx.sessionManager.getBranch());
1486
- turnsRemaining = Math.max(0, compactSettings.turnsBeforeCompact - completedRuns);
1487
- counterTriggered = compactSettings.turnsBeforeCompact > 0 && completedRuns >= compactSettings.turnsBeforeCompact;
1453
+ const completedTurns = completedTurnsSinceCompaction(ctx.sessionManager.getBranch());
1454
+ turnsRemaining = Math.max(0, compactSettings.turnsBeforeCompact - completedTurns);
1455
+ counterTriggered = compactSettings.turnsBeforeCompact > 0 && completedTurns >= compactSettings.turnsBeforeCompact;
1488
1456
  }
1489
1457
  function updateStatus(ctx) {
1490
1458
  if (!goosedumpAvailable)
@@ -1509,13 +1477,44 @@ function createGoosedumpIntegration(options = {}) {
1509
1477
  compactionState = state;
1510
1478
  updateStatus(ctx);
1511
1479
  }
1512
- function noteCompletedRun(ctx) {
1480
+ function noteCompletedTurn(ctx) {
1513
1481
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
1514
1482
  return;
1515
1483
  syncCompactCounters(ctx);
1516
1484
  updateStatus(ctx);
1517
1485
  queueTurnCompaction(ctx, settlementGeneration + 1);
1518
1486
  }
1487
+ function resumeAfterSuccessfulCompaction(ctx, resume) {
1488
+ if (!resume || !activePi || ctx.hasPendingMessages())
1489
+ return;
1490
+ activePi.sendMessage({
1491
+ customType: "goosedump-autocompact-resume",
1492
+ content: "The session was compacted to reduce context size. Continue the task you were working on from where you left off.",
1493
+ display: false
1494
+ }, { triggerTurn: true });
1495
+ }
1496
+ function tryMidRunCounterCompaction(ctx) {
1497
+ if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
1498
+ return false;
1499
+ if (!counterTriggered)
1500
+ return false;
1501
+ if (ctx.hasPendingMessages())
1502
+ return false;
1503
+ if (!hasCompactableHistory(ctx))
1504
+ return false;
1505
+ if (compactionState.phase === "idle") {
1506
+ queueTurnCompaction(ctx, settlementGeneration);
1507
+ } else if (compactionState.phase === "queued") {
1508
+ setCompactionState(ctx, {
1509
+ phase: "queued",
1510
+ request: {
1511
+ ...compactionState.request,
1512
+ eligibleSettlement: Math.min(compactionState.request.eligibleSettlement, settlementGeneration)
1513
+ }
1514
+ });
1515
+ }
1516
+ return startQueuedCompaction(ctx, { resumeAfter: true, allowActiveRun: true });
1517
+ }
1519
1518
  function checkGoosedump(ctx) {
1520
1519
  try {
1521
1520
  execFileSync(process.execPath, [resolveGoosedumpBinary(), "--version"], { stdio: "ignore" });
@@ -1567,7 +1566,7 @@ function createGoosedumpIntegration(options = {}) {
1567
1566
  }
1568
1567
  });
1569
1568
  }
1570
- function startQueuedCompaction(ctx) {
1569
+ function startQueuedCompaction(ctx, options2 = {}) {
1571
1570
  if (compactionState.phase !== "queued")
1572
1571
  return false;
1573
1572
  const request = compactionState.request;
@@ -1575,7 +1574,9 @@ function createGoosedumpIntegration(options = {}) {
1575
1574
  setCompactionState(ctx, { phase: "idle" });
1576
1575
  return false;
1577
1576
  }
1578
- if (!ctx.isIdle() || ctx.hasPendingMessages())
1577
+ if (ctx.hasPendingMessages())
1578
+ return false;
1579
+ if (!options2.allowActiveRun && !ctx.isIdle())
1579
1580
  return false;
1580
1581
  if (!requestHasCompactableHistory(ctx, request.keep)) {
1581
1582
  setCompactionState(ctx, { phase: "idle" });
@@ -1584,6 +1585,8 @@ function createGoosedumpIntegration(options = {}) {
1584
1585
  const operation = ++operationGeneration;
1585
1586
  const generation = request.sessionGeneration;
1586
1587
  const sessionFile = request.sessionFile;
1588
+ const resumeAfter = options2.resumeAfter === true;
1589
+ resumeAfterCompaction = resumeAfter;
1587
1590
  setCompactionState(ctx, {
1588
1591
  phase: "running",
1589
1592
  request,
@@ -1597,12 +1600,20 @@ function createGoosedumpIntegration(options = {}) {
1597
1600
  onComplete: () => {
1598
1601
  if (!runningOperationIsCurrent(operation, generation, sessionFile))
1599
1602
  return;
1603
+ const shouldResume = resumeAfterCompaction;
1604
+ resumeAfterCompaction = false;
1600
1605
  setCompactionState(ctx, { phase: "idle" });
1606
+ syncCompactCounters(ctx);
1607
+ updateStatus(ctx);
1608
+ resumeAfterSuccessfulCompaction(ctx, shouldResume);
1601
1609
  },
1602
1610
  onError: (error) => {
1603
1611
  if (!runningOperationIsCurrent(operation, generation, sessionFile))
1604
1612
  return;
1613
+ resumeAfterCompaction = false;
1605
1614
  setCompactionState(ctx, { phase: "idle" });
1615
+ syncCompactCounters(ctx);
1616
+ updateStatus(ctx);
1606
1617
  const message = error instanceof Error ? error.message : String(error);
1607
1618
  if (ctx.hasUI)
1608
1619
  ctx.ui.notify(`goosedump compaction failed: ${message}`, "error");
@@ -1638,6 +1649,7 @@ function createGoosedumpIntegration(options = {}) {
1638
1649
  return startQueuedCompaction(ctx) ? "started" : "queued";
1639
1650
  }
1640
1651
  function register(pi) {
1652
+ activePi = pi;
1641
1653
  if (shouldRegisterTool) {
1642
1654
  pi.registerTool(defineTool({
1643
1655
  name: "goose_remember",
@@ -1989,6 +2001,8 @@ ${lines.join(`
1989
2001
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
1990
2002
  return;
1991
2003
  settlementGeneration += 1;
2004
+ syncCompactCounters(ctx);
2005
+ updateStatus(ctx);
1992
2006
  if (compactionState.phase === "running" && compactionState.owner === "pi") {
1993
2007
  const request = compactionState.request;
1994
2008
  setCompactionState(ctx, request && requestIsCurrent(request) ? {
@@ -2012,8 +2026,7 @@ ${lines.join(`
2012
2026
  settlementGeneration = 0;
2013
2027
  activeSessionFile = ctx.sessionManager.getSessionFile?.() ?? null;
2014
2028
  sessionActive = true;
2015
- agentRunObserved = false;
2016
- agentRunStartEntryId = null;
2029
+ resumeAfterCompaction = false;
2017
2030
  compactionState = { phase: "idle" };
2018
2031
  goosedumpAvailable = checkGoosedump(ctx);
2019
2032
  notifyInvalidSettings(ctx);
@@ -2027,8 +2040,7 @@ ${lines.join(`
2027
2040
  settlementGeneration = 0;
2028
2041
  activeSessionFile = null;
2029
2042
  sessionActive = false;
2030
- agentRunObserved = false;
2031
- agentRunStartEntryId = null;
2043
+ resumeAfterCompaction = false;
2032
2044
  compactionState = { phase: "idle" };
2033
2045
  });
2034
2046
  pi.on("session_tree", async (_event, ctx) => {
@@ -2036,8 +2048,7 @@ ${lines.join(`
2036
2048
  settlementGeneration = 0;
2037
2049
  activeSessionFile = ctx.sessionManager.getSessionFile?.() ?? null;
2038
2050
  sessionActive = true;
2039
- agentRunObserved = false;
2040
- agentRunStartEntryId = null;
2051
+ resumeAfterCompaction = false;
2041
2052
  compactionState = { phase: "idle" };
2042
2053
  syncCompactCounters(ctx);
2043
2054
  updateStatus(ctx);
@@ -2045,10 +2056,6 @@ ${lines.join(`
2045
2056
  deferQueuedCompaction(ctx);
2046
2057
  });
2047
2058
  pi.on("agent_start", async (_event, ctx) => {
2048
- if (!agentRunObserved) {
2049
- agentRunObserved = true;
2050
- agentRunStartEntryId = ctx.sessionManager.getBranch().at(-1)?.id ?? null;
2051
- }
2052
2059
  if (compactionState.phase !== "running" || compactionState.owner !== "pi")
2053
2060
  return;
2054
2061
  const request = compactionState.request;
@@ -2057,20 +2064,13 @@ ${lines.join(`
2057
2064
  request: { ...request, eligibleSettlement: settlementGeneration + 1 }
2058
2065
  } : { phase: "idle" });
2059
2066
  });
2067
+ pi.on("turn_end", async (event, ctx) => {
2068
+ if (isCompletedAssistant(event.message))
2069
+ noteCompletedTurn(ctx);
2070
+ if (event.toolResults.length > 0)
2071
+ tryMidRunCounterCompaction(ctx);
2072
+ });
2060
2073
  pi.on("agent_settled", async (_event, ctx) => {
2061
- if (agentRunObserved) {
2062
- const countRun = shouldOverrideDefaultCompaction && compactSettings.turnsBeforeCompact > 0;
2063
- if (countRun) {
2064
- pi.appendEntry(COMPLETED_AGENT_RUN_ENTRY, {
2065
- version: 1,
2066
- startedAfterEntryId: agentRunStartEntryId
2067
- });
2068
- }
2069
- agentRunObserved = false;
2070
- agentRunStartEntryId = null;
2071
- if (countRun)
2072
- noteCompletedRun(ctx);
2073
- }
2074
2074
  handleAgentSettled(ctx);
2075
2075
  });
2076
2076
  pi.on("session_before_compact", async (event, ctx) => {
@@ -2212,6 +2212,7 @@ ${lines.join(`
2212
2212
  }
2213
2213
  });
2214
2214
  pi.on("session_compact", async (event, ctx) => {
2215
+ resumeAfterCompaction = false;
2215
2216
  if (!shouldOverrideDefaultCompaction)
2216
2217
  return;
2217
2218
  if (!contextIsCurrent(ctx))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.18",
3
+ "version": "0.12.19",
4
4
  "description": "Pi extension for goosedump session search, durable memory, 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.18",
41
+ "@jarkkojs/goosedump": "^0.12.19",
42
42
  "@sinclair/typebox": "^0.34.49"
43
43
  },
44
44
  "devDependencies": {