pi-goosedump 0.12.16 → 0.12.18

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 +11 -11
  2. package/dist/index.ts +149 -42
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -93,17 +93,17 @@ 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 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 for the active agent run to settle and
102
- `compacting` while the summary is being built. Pi remains the sole scheduler for
103
- its normal context threshold. Whichever limit is reached first compacts once,
104
- and every successful compaction resets the countdown. A failed or cancelled
105
- compaction leaves the counter armed at zero and is not retried in the same
106
- settlement.
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.
107
107
 
108
108
  `summaryMaxTokens` is an integer from 512 to 65536 (default 4096). The effective
109
109
  summary budget is the lower of this value and 80% of Pi's `compaction.reserveTokens`,
package/dist/index.ts CHANGED
@@ -590,11 +590,26 @@ 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";
593
594
  function isMessageEntry(entry) {
594
595
  return entry.type === "message";
595
596
  }
596
- function isCompletedAssistant(message) {
597
- return message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted";
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 };
598
613
  }
599
614
  var SUMMARIZABLE_ENTRY_TYPES = new Set(["message", "custom_message", "branch_summary"]);
600
615
  function piKeepRecentTokens(cwd, includeProject) {
@@ -666,7 +681,7 @@ function activeHistoryStartIndex(branchEntries) {
666
681
  const compaction = latestCompaction(branchEntries);
667
682
  return compaction ? compactionBoundaryStartIndex(branchEntries, compaction) : 0;
668
683
  }
669
- function completedTurnsSinceCompaction(branchEntries) {
684
+ function completedRunsSinceCompaction(branchEntries) {
670
685
  let start = 0;
671
686
  for (let i = branchEntries.length - 1;i >= 0; i -= 1) {
672
687
  if (isCompactionEntry(branchEntries[i])) {
@@ -674,10 +689,27 @@ function completedTurnsSinceCompaction(branchEntries) {
674
689
  break;
675
690
  }
676
691
  }
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;
677
705
  let completed = 0;
678
- for (let i = start;i < branchEntries.length; i += 1) {
706
+ for (let i = start;i < legacyEnd; i += 1) {
679
707
  const entry = branchEntries[i];
680
- if (isMessageEntry(entry) && isCompletedAssistant(entry.message))
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)
681
713
  completed += 1;
682
714
  }
683
715
  return completed;
@@ -845,7 +877,7 @@ async function runSettingsEditor(ctx) {
845
877
  const specs = [
846
878
  {
847
879
  key: "turnsBeforeCompact",
848
- description: "LLM turns before compaction; 0 disables.",
880
+ description: "Completed agent runs before compaction; 0 disables.",
849
881
  min: TURNS_BEFORE_COMPACT_MIN,
850
882
  max: TURNS_BEFORE_COMPACT_MAX
851
883
  },
@@ -1411,7 +1443,8 @@ function createGoosedumpIntegration(options = {}) {
1411
1443
  let operationGeneration = 0;
1412
1444
  let activeSessionFile = null;
1413
1445
  let sessionActive = false;
1414
- let agentEndObserved = true;
1446
+ let agentRunObserved = false;
1447
+ let agentRunStartEntryId = null;
1415
1448
  const rememberQueue = [];
1416
1449
  let rememberRunning = null;
1417
1450
  function enqueueRemember(target, reportError) {
@@ -1449,9 +1482,9 @@ function createGoosedumpIntegration(options = {}) {
1449
1482
  }
1450
1483
  function syncCompactCounters(ctx) {
1451
1484
  compactSettings = resolveGoosedumpSettings(ctx.cwd, projectSettingsTrusted(ctx));
1452
- const completedTurns = completedTurnsSinceCompaction(ctx.sessionManager.getBranch());
1453
- turnsRemaining = Math.max(0, compactSettings.turnsBeforeCompact - completedTurns);
1454
- counterTriggered = compactSettings.turnsBeforeCompact > 0 && completedTurns >= compactSettings.turnsBeforeCompact;
1485
+ const completedRuns = completedRunsSinceCompaction(ctx.sessionManager.getBranch());
1486
+ turnsRemaining = Math.max(0, compactSettings.turnsBeforeCompact - completedRuns);
1487
+ counterTriggered = compactSettings.turnsBeforeCompact > 0 && completedRuns >= compactSettings.turnsBeforeCompact;
1455
1488
  }
1456
1489
  function updateStatus(ctx) {
1457
1490
  if (!goosedumpAvailable)
@@ -1476,7 +1509,7 @@ function createGoosedumpIntegration(options = {}) {
1476
1509
  compactionState = state;
1477
1510
  updateStatus(ctx);
1478
1511
  }
1479
- function noteCompletedTurn(ctx) {
1512
+ function noteCompletedRun(ctx) {
1480
1513
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction)
1481
1514
  return;
1482
1515
  syncCompactCounters(ctx);
@@ -1503,6 +1536,19 @@ function createGoosedumpIntegration(options = {}) {
1503
1536
  function runningOperationIsCurrent(operation, generation, sessionFile) {
1504
1537
  return generation === sessionGeneration && sessionFile === activeSessionFile && compactionState.phase === "running" && compactionState.operation === operation;
1505
1538
  }
1539
+ function releasePiCompactionOnAbort(ctx, signal, operation, generation, sessionFile, request, reason) {
1540
+ signal?.addEventListener("abort", () => {
1541
+ if (!runningOperationIsCurrent(operation, generation, sessionFile))
1542
+ return;
1543
+ setCompactionState(ctx, reason !== "manual" && request && requestIsCurrent(request) ? {
1544
+ phase: "queued",
1545
+ request: {
1546
+ ...request,
1547
+ eligibleSettlement: Math.max(request.eligibleSettlement, settlementGeneration + 2)
1548
+ }
1549
+ } : { phase: "idle" });
1550
+ }, { once: true });
1551
+ }
1506
1552
  function requestHasCompactableHistory(ctx, keep) {
1507
1553
  return keep === null ? hasCompactableHistory(ctx) : keepBoundaryId(ctx.sessionManager.getBranch(), keep) !== undefined;
1508
1554
  }
@@ -1545,7 +1591,7 @@ function createGoosedumpIntegration(options = {}) {
1545
1591
  operation,
1546
1592
  sessionGeneration: generation,
1547
1593
  sessionFile,
1548
- hookActive: false
1594
+ hookClaimed: false
1549
1595
  });
1550
1596
  ctx.compact({
1551
1597
  onComplete: () => {
@@ -1564,6 +1610,17 @@ function createGoosedumpIntegration(options = {}) {
1564
1610
  });
1565
1611
  return true;
1566
1612
  }
1613
+ function deferQueuedCompaction(ctx) {
1614
+ if (compactionState.phase !== "queued")
1615
+ return;
1616
+ const request = compactionState.request;
1617
+ setImmediate(() => {
1618
+ if (compactionState.phase !== "queued" || compactionState.request !== request || !contextIsCurrent(ctx)) {
1619
+ return;
1620
+ }
1621
+ startQueuedCompaction(ctx);
1622
+ });
1623
+ }
1567
1624
  function requestCompaction(ctx, keep) {
1568
1625
  if (compactionState.phase !== "idle")
1569
1626
  return "busy";
@@ -1946,8 +2003,8 @@ ${lines.join(`
1946
2003
  if (compactionState.phase === "idle") {
1947
2004
  queueTurnCompaction(ctx, settlementGeneration);
1948
2005
  }
1949
- if (agentEndObserved && compactionState.phase === "queued" && compactionState.request.eligibleSettlement <= settlementGeneration) {
1950
- startQueuedCompaction(ctx);
2006
+ if (compactionState.phase === "queued" && compactionState.request.eligibleSettlement <= settlementGeneration) {
2007
+ deferQueuedCompaction(ctx);
1951
2008
  }
1952
2009
  }
1953
2010
  pi.on("session_start", async (_event, ctx) => {
@@ -1955,47 +2012,72 @@ ${lines.join(`
1955
2012
  settlementGeneration = 0;
1956
2013
  activeSessionFile = ctx.sessionManager.getSessionFile?.() ?? null;
1957
2014
  sessionActive = true;
1958
- agentEndObserved = true;
2015
+ agentRunObserved = false;
2016
+ agentRunStartEntryId = null;
1959
2017
  compactionState = { phase: "idle" };
1960
2018
  goosedumpAvailable = checkGoosedump(ctx);
1961
2019
  notifyInvalidSettings(ctx);
1962
2020
  syncCompactCounters(ctx);
1963
2021
  updateStatus(ctx);
1964
2022
  queueTurnCompaction(ctx, 1);
2023
+ deferQueuedCompaction(ctx);
1965
2024
  });
1966
2025
  pi.on("session_shutdown", async () => {
1967
2026
  sessionGeneration += 1;
1968
2027
  settlementGeneration = 0;
1969
2028
  activeSessionFile = null;
1970
2029
  sessionActive = false;
1971
- agentEndObserved = true;
2030
+ agentRunObserved = false;
2031
+ agentRunStartEntryId = null;
1972
2032
  compactionState = { phase: "idle" };
1973
2033
  });
1974
- pi.on("agent_start", async () => {
1975
- agentEndObserved = false;
1976
- });
1977
- pi.on("agent_end", async () => {
1978
- agentEndObserved = true;
2034
+ pi.on("session_tree", async (_event, ctx) => {
2035
+ sessionGeneration += 1;
2036
+ settlementGeneration = 0;
2037
+ activeSessionFile = ctx.sessionManager.getSessionFile?.() ?? null;
2038
+ sessionActive = true;
2039
+ agentRunObserved = false;
2040
+ agentRunStartEntryId = null;
2041
+ compactionState = { phase: "idle" };
2042
+ syncCompactCounters(ctx);
2043
+ updateStatus(ctx);
2044
+ queueTurnCompaction(ctx, 1);
2045
+ deferQueuedCompaction(ctx);
1979
2046
  });
1980
- pi.on("turn_end", async (event, ctx) => {
1981
- if (isCompletedAssistant(event.message))
1982
- noteCompletedTurn(ctx);
2047
+ pi.on("agent_start", async (_event, ctx) => {
2048
+ if (!agentRunObserved) {
2049
+ agentRunObserved = true;
2050
+ agentRunStartEntryId = ctx.sessionManager.getBranch().at(-1)?.id ?? null;
2051
+ }
2052
+ if (compactionState.phase !== "running" || compactionState.owner !== "pi")
2053
+ return;
2054
+ const request = compactionState.request;
2055
+ setCompactionState(ctx, request && requestIsCurrent(request) ? {
2056
+ phase: "queued",
2057
+ request: { ...request, eligibleSettlement: settlementGeneration + 1 }
2058
+ } : { phase: "idle" });
1983
2059
  });
1984
2060
  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
+ }
1985
2074
  handleAgentSettled(ctx);
1986
2075
  });
1987
2076
  pi.on("session_before_compact", async (event, ctx) => {
1988
2077
  if (!shouldOverrideDefaultCompaction || !goosedumpAvailable)
1989
2078
  return;
1990
- if (event.customInstructions?.trim()) {
1991
- if (compactionState.phase === "running")
1992
- return { cancel: true };
1993
- if (compactionState.phase === "queued") {
1994
- setCompactionState(ctx, { phase: "idle" });
1995
- }
1996
- return;
1997
- }
1998
- if (event.signal.aborted) {
2079
+ const signal = event.signal;
2080
+ if (signal?.aborted) {
1999
2081
  if (compactionState.phase === "queued" && requestIsCurrent(compactionState.request)) {
2000
2082
  setCompactionState(ctx, event.reason === "manual" ? { phase: "idle" } : {
2001
2083
  phase: "queued",
@@ -2005,6 +2087,22 @@ ${lines.join(`
2005
2087
  }
2006
2088
  });
2007
2089
  }
2090
+ return { cancel: true };
2091
+ }
2092
+ if (event.customInstructions?.trim()) {
2093
+ if (compactionState.phase === "running")
2094
+ return { cancel: true };
2095
+ const operation2 = ++operationGeneration;
2096
+ setCompactionState(ctx, {
2097
+ phase: "running",
2098
+ request: null,
2099
+ owner: "pi",
2100
+ operation: operation2,
2101
+ sessionGeneration,
2102
+ sessionFile: activeSessionFile,
2103
+ hookClaimed: true
2104
+ });
2105
+ releasePiCompactionOnAbort(ctx, signal, operation2, sessionGeneration, activeSessionFile, null, event.reason);
2008
2106
  return;
2009
2107
  }
2010
2108
  let operation;
@@ -2013,7 +2111,7 @@ ${lines.join(`
2013
2111
  let request;
2014
2112
  let owner;
2015
2113
  if (compactionState.phase === "running") {
2016
- if (compactionState.hookActive || compactionState.owner === "pi") {
2114
+ if (compactionState.hookClaimed || compactionState.owner === "pi") {
2017
2115
  return { cancel: true };
2018
2116
  }
2019
2117
  operation = compactionState.operation;
@@ -2021,7 +2119,7 @@ ${lines.join(`
2021
2119
  sessionFile = compactionState.sessionFile;
2022
2120
  request = compactionState.request;
2023
2121
  owner = "extension";
2024
- compactionState = { ...compactionState, hookActive: true };
2122
+ compactionState = { ...compactionState, hookClaimed: true };
2025
2123
  } else {
2026
2124
  request = compactionState.phase === "queued" && requestIsCurrent(compactionState.request) ? compactionState.request : null;
2027
2125
  operation = ++operationGeneration;
@@ -2035,9 +2133,12 @@ ${lines.join(`
2035
2133
  operation,
2036
2134
  sessionGeneration: generation,
2037
2135
  sessionFile,
2038
- hookActive: true
2136
+ hookClaimed: true
2039
2137
  });
2040
2138
  }
2139
+ if (owner === "pi") {
2140
+ releasePiCompactionOnAbort(ctx, signal, operation, generation, sessionFile, request, event.reason);
2141
+ }
2041
2142
  try {
2042
2143
  const { messagesToSummarize, turnPrefixMessages } = event.preparation;
2043
2144
  if ((request?.keep ?? null) === null && messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
@@ -2061,11 +2162,11 @@ ${lines.join(`
2061
2162
  operation,
2062
2163
  sessionGeneration: generation,
2063
2164
  sessionFile,
2064
- hookActive: true
2165
+ hookClaimed: true
2065
2166
  });
2066
2167
  }
2067
2168
  }
2068
- const compaction = await buildGoosedumpCompaction(target, event.preparation, event.branchEntries, effectiveSummaryMaxTokens(ctx.cwd, projectSettingsTrusted(ctx)), keep, event.signal, nativeExecutablePath);
2169
+ const compaction = await buildGoosedumpCompaction(target, event.preparation, event.branchEntries, effectiveSummaryMaxTokens(ctx.cwd, projectSettingsTrusted(ctx)), keep, signal, nativeExecutablePath);
2069
2170
  if (!runningOperationIsCurrent(operation, generation, sessionFile)) {
2070
2171
  return { cancel: true };
2071
2172
  }
@@ -2085,8 +2186,12 @@ ${lines.join(`
2085
2186
  if (!runningOperationIsCurrent(operation, generation, sessionFile)) {
2086
2187
  return { cancel: true };
2087
2188
  }
2088
- if (event.signal.aborted)
2189
+ if (signal?.aborted) {
2190
+ if (owner === "pi" && event.reason === "manual") {
2191
+ setCompactionState(ctx, { phase: "idle" });
2192
+ }
2089
2193
  return { cancel: true };
2194
+ }
2090
2195
  if (ctx.hasUI) {
2091
2196
  const message = err instanceof Error ? err.message : String(err);
2092
2197
  ctx.ui.notify(`goosedump compaction failed; falling back to Pi compaction: ${message}`, "error");
@@ -2094,14 +2199,14 @@ ${lines.join(`
2094
2199
  return;
2095
2200
  } finally {
2096
2201
  if (runningOperationIsCurrent(operation, generation, sessionFile)) {
2097
- setCompactionState(ctx, owner === "pi" && event.reason === "manual" ? { phase: "idle" } : {
2202
+ setCompactionState(ctx, {
2098
2203
  phase: "running",
2099
2204
  request,
2100
2205
  owner,
2101
2206
  operation,
2102
2207
  sessionGeneration: generation,
2103
2208
  sessionFile,
2104
- hookActive: false
2209
+ hookClaimed: true
2105
2210
  });
2106
2211
  }
2107
2212
  }
@@ -2114,7 +2219,9 @@ ${lines.join(`
2114
2219
  if (compactionState.phase === "queued" && compactionState.request.keep !== null && ctx.hasUI) {
2115
2220
  ctx.ui.notify("Queued keep request was superseded by another compaction.", "info");
2116
2221
  }
2117
- compactionState = { phase: "idle" };
2222
+ const extensionOwned = compactionState.phase === "running" && compactionState.owner === "extension";
2223
+ if (!extensionOwned)
2224
+ compactionState = { phase: "idle" };
2118
2225
  syncCompactCounters(ctx);
2119
2226
  updateStatus(ctx);
2120
2227
  if (!goosedumpAvailable)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.12.16",
3
+ "version": "0.12.18",
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.16",
41
+ "@jarkkojs/goosedump": "^0.12.18",
42
42
  "@sinclair/typebox": "^0.34.49"
43
43
  },
44
44
  "devDependencies": {