@henryqw/pi-auto-compact 0.1.1 → 0.1.2

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.
package/README.md CHANGED
@@ -8,7 +8,19 @@ Pi extension that compacts context before it reaches 50% of current model contex
8
8
  pi install npm:@henryqw/pi-auto-compact
9
9
  ```
10
10
 
11
- Restart Pi after installation. Remove with:
11
+ Disable Pi's built-in auto-compaction in `~/.pi/agent/settings.json`:
12
+
13
+ ```json
14
+ {
15
+ "compaction": {
16
+ "enabled": false
17
+ }
18
+ }
19
+ ```
20
+
21
+ Restart Pi after installation or settings changes. Trusted project settings in `.pi/settings.json` must not override `compaction.enabled` back to `true`. Manual `/compact` remains available.
22
+
23
+ Remove with:
12
24
 
13
25
  ```bash
14
26
  pi remove npm:@henryqw/pi-auto-compact
@@ -16,13 +28,14 @@ pi remove npm:@henryqw/pi-auto-compact
16
28
 
17
29
  ## Behavior
18
30
 
31
+ - Refuses activation with an error when Pi's effective `compaction.enabled` setting is not `false`; competing automatic compactors can start duplicate summaries.
19
32
  - Checks `turn_start`, tool-call `turn_end`, `context`, and resumed/forked `session_start`.
20
33
  - Uses Pi's default `ctx.compact()` summary and session persistence.
21
34
  - Keeps newest 15% as temporary emergency context while compaction runs.
22
35
  - Sends a follow-up message after compaction so task execution continues.
23
- - Uses fixed 50% threshold; no configuration needed.
36
+ - Uses fixed 50% threshold; no extension-specific configuration.
24
37
 
25
- `ctx.compact()` aborts current low-level run. Extension starts new run after compaction with the current task resume message.
38
+ `ctx.compact()` aborts current low-level run. Extension hides that empty internal abort message, then starts new run with current task resume message. Other aborts and provider errors remain visible.
26
39
 
27
40
  ## Development
28
41
 
@@ -32,4 +45,4 @@ npm run pack:check
32
45
  npm run test:live
33
46
  ```
34
47
 
35
- `test:live` uses real Pi plus authenticated model access. It sets a temporary 12K context window, sends a large prompt, and verifies compaction, automatic resume, persisted resume message, and assistant response. Set `PI_AUTO_COMPACT_AUTH_FILE` when auth is not at `~/.pi/agent/auth.json`.
48
+ `test:live` uses real Pi plus authenticated model access. It disables Pi's built-in auto-compaction, sets a temporary 12K context window, sends a large prompt, and verifies extension compaction, automatic resume, persisted resume message, and assistant response. Set `PI_AUTO_COMPACT_AUTH_FILE` when auth is not at `~/.pi/agent/auth.json`.
@@ -1,4 +1,8 @@
1
- import { estimateTokens } from "@earendil-works/pi-coding-agent";
1
+ import {
2
+ estimateTokens,
3
+ getAgentDir,
4
+ SettingsManager,
5
+ } from "@earendil-works/pi-coding-agent";
2
6
  import type {
3
7
  ExtensionAPI,
4
8
  ExtensionContext,
@@ -20,6 +24,10 @@ const COMPACT_THRESHOLD_PERCENT = 50;
20
24
  // Emergency context guard keeps recent messages while default compaction runs.
21
25
  const KEEP_RECENT_PERCENT = 15;
22
26
  const RESUME_MESSAGE = "Auto-compact ran. Continue the current task.";
27
+ const COMPACTION_ABORT_ERROR = "This operation was aborted";
28
+ const ACTIVATION_ERROR =
29
+ "pi-auto-compact failed to activate: Pi built-in auto-compaction is enabled. " +
30
+ "Set compaction.enabled to false in Pi settings, then restart Pi.";
23
31
 
24
32
  /** Estimate current request size using same estimator Pi uses. */
25
33
  function estimateTotalTokens(messages: AgentMessage[]): number {
@@ -75,13 +83,17 @@ function hasToolCall(message: AgentMessage): boolean {
75
83
  }
76
84
 
77
85
  export default function (pi: ExtensionAPI) {
86
+ let active = false;
78
87
  // Prevent turn_start, turn_end, and context from starting duplicate summaries.
79
88
  let compactionPending = false;
89
+ let compactionAbortExpected = false;
80
90
 
81
91
  const runCompaction = (ctx: ExtensionContext) => {
92
+ compactionAbortExpected = Boolean(ctx.signal && !ctx.signal.aborted);
82
93
  ctx.compact({
83
94
  onComplete: () => {
84
95
  compactionPending = false;
96
+ compactionAbortExpected = false;
85
97
  // Pi may flush queued input during compaction_end. Wait one macrotask
86
98
  // before checking idle, otherwise follow-up can race that flush.
87
99
  setImmediate(() => {
@@ -90,12 +102,13 @@ export default function (pi: ExtensionAPI) {
90
102
  },
91
103
  onError: () => {
92
104
  compactionPending = false;
105
+ compactionAbortExpected = false;
93
106
  },
94
107
  });
95
108
  };
96
109
 
97
110
  const compactIfNeeded = (ctx: ExtensionContext) => {
98
- if (compactionPending) return;
111
+ if (!active || compactionPending) return;
99
112
 
100
113
  const usage = ctx.getContextUsage();
101
114
  if (usage?.percent == null || usage.percent <= COMPACT_THRESHOLD_PERCENT) return;
@@ -104,6 +117,25 @@ export default function (pi: ExtensionAPI) {
104
117
  runCompaction(ctx);
105
118
  };
106
119
 
120
+ // Hide only empty abort produced when ctx.compact() cancels active run.
121
+ pi.on("message_end", (event, ctx) => {
122
+ const message = event.message;
123
+ if (
124
+ !compactionPending ||
125
+ !compactionAbortExpected ||
126
+ !ctx.signal?.aborted ||
127
+ message.role !== "assistant" ||
128
+ message.stopReason !== "error" ||
129
+ message.errorMessage !== COMPACTION_ABORT_ERROR ||
130
+ message.content.some((part) => part.type !== "text" || part.text !== "")
131
+ ) return;
132
+
133
+ compactionAbortExpected = false;
134
+ return {
135
+ message: { ...message, stopReason: "stop", errorMessage: undefined },
136
+ };
137
+ });
138
+
107
139
  // Do not use agent_settled here: long tool loops may cross threshold before
108
140
  // the full run settles. These hooks inspect every provider-turn boundary.
109
141
  // Pre-turn catches resumed/queued work before provider request starts.
@@ -118,7 +150,7 @@ export default function (pi: ExtensionAPI) {
118
150
  // Runs before every provider request. Temporary truncation protects request
119
151
  // size while asynchronous default compaction summarizes persisted history.
120
152
  pi.on("context", (event, ctx) => {
121
- if (compactionPending) return;
153
+ if (!active || compactionPending) return;
122
154
 
123
155
  const contextWindow = ctx.getContextUsage()?.contextWindow ?? ctx.model?.contextWindow ?? 0;
124
156
  const estimatedTokens = estimateTotalTokens(event.messages);
@@ -137,10 +169,15 @@ export default function (pi: ExtensionAPI) {
137
169
  return { messages: truncated };
138
170
  });
139
171
 
140
- // Resume/fork can load an already-large session before first turn.
172
+ // Pi's built-in automatic compaction competes with this extension. Refuse
173
+ // activation unless effective global/project settings disable it.
141
174
  pi.on("session_start", (event, ctx) => {
142
- if (event.reason === "resume" || event.reason === "fork") {
143
- compactIfNeeded(ctx);
144
- }
175
+ active = !SettingsManager.create(ctx.cwd, getAgentDir(), {
176
+ projectTrusted: ctx.isProjectTrusted(),
177
+ }).getCompactionEnabled();
178
+ if (!active) throw new Error(ACTIVATION_ERROR);
179
+
180
+ // Resume/fork can load an already-large session before first turn.
181
+ if (event.reason === "resume" || event.reason === "fork") compactIfNeeded(ctx);
145
182
  });
146
183
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-auto-compact",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Proactively compact Pi context at 50% and resume the current task.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "test": "node --test test/*.test.ts",
23
23
  "test:live": "PI_AUTO_COMPACT_LIVE=1 node --test test/live.test.ts",
24
- "typecheck": "tsc --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/auto-compact.ts test/live.test.ts",
24
+ "typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/auto-compact.ts test/*.test.ts",
25
25
  "pack:check": "npm pack --dry-run"
26
26
  },
27
27
  "peerDependencies": {