pi-codex-compaction 0.2.2 → 0.2.3

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/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.3] - 2026-10-09
10
+
11
+ ### Fixed
12
+
13
+ - Load managed npm installations without a physical Pi AI dependency. Remove host filesystem resolution and internal serializer imports; verify retained output through the configured provider's public serialization boundary with transport disabled.
14
+ - Test the actual packed entry outside the checkout without installing or linking Pi peers, including CLI startup and checkpoint/tool continuation/reload in print, JSON, and RPC modes.
15
+
9
16
  ## [0.2.2] - 2026-10-09
10
17
 
11
18
  ### Fixed
package/README.md CHANGED
@@ -33,6 +33,10 @@ Or load it for one run:
33
33
  pi -e /path/to/pi-mono/packages/pi-codex-compaction
34
34
  ```
35
35
 
36
+ Pi supplies the host packages through its extension loader; this package does
37
+ not install or resolve a separate physical copy of Pi AI. No manual dependency
38
+ installation is required. See Pi's [package dependency contract](https://github.com/earendil-works/pi/blob/v1.1.0/packages/coding-agent/docs/packages.md#declare-dependencies).
39
+
36
40
  Select a GPT-5/GPT-6 model on provider `openai`, API `openai-responses`, at
37
41
  `https://api.openai.com/v1`. No enable command is needed. A previously saved
38
42
  `off` setting on the current session branch remains respected.
@@ -84,6 +88,13 @@ On subsequent requests, it replaces the readable fallback and the retained
84
88
  assistant's serialized items with that checkpoint and output suffix. Later
85
89
  tool results and messages remain.
86
90
 
91
+ Before adopting a checkpoint, the extension verifies the retained assistant
92
+ through the configured provider's public `streamSimple` / `onPayload` boundary.
93
+ This is a local serialization-only operation: it stops before transport, uses no
94
+ real credential, blocks fetch, and adds no model request or usage. It does not
95
+ import Pi's internal serializers or locate host files on disk. A serialization
96
+ failure leaves history intact.
97
+
87
98
  The original transcript stays in the session file. Reload and tree navigation
88
99
  use the selected branch, not a global cache. Turning the mode off stops requesting
89
100
  new checkpoints but still replays a compatible saved checkpoint while the
@@ -171,7 +182,12 @@ incompatible in the public API.
171
182
  Tests use real Pi sessions with mocked transport to cover effective loadouts,
172
183
  streaming, tool continuations, checkpoint ordering, branches, reloads,
173
184
  cancellation, credential changes, context edits, default-on behavior, and
174
- the standard compaction safety net.
185
+ the standard compaction safety net. The loading regression test packs the actual
186
+ npm tarball and loads it outside the checkout with only declared runtime
187
+ dependencies—no physical Pi peers. It uses the shipped bundled host's embedded
188
+ module mapping, not just the ordinary SDK's filesystem aliases. It checks CLI
189
+ flag registration and checkpoint adoption, tool continuation, and reload in
190
+ print, JSON, and RPC modes.
175
191
 
176
192
  A small live subscription test with `gpt-6-astra` verified checkpoint adoption,
177
193
  replay, and synthetic fact recall. At a deliberately low 1,000-token threshold,
@@ -191,6 +207,12 @@ npm test -w packages/pi-codex-compaction
191
207
  npm run -w packages/pi-codex-compaction pack:dry-run
192
208
  ```
193
209
 
210
+ Before releasing, also install the candidate tarball outside the checkout with
211
+ host-peer installation suppressed, then load it through the installed Pi CLI.
212
+ `pi -ne -e /path/to/installed/package --offline --help` must show both
213
+ `--server-compaction` flags without loading errors or host-dependency warnings.
214
+ Never add Pi peers to the candidate directory to make this smoke test pass.
215
+
194
216
  For an approved TUI smoke test, cross the configured threshold in a synthetic
195
217
  session, then check facts from earlier turns and a tool continuation. Check replay
196
218
  after `/reload`, mode-off, and branch navigation. Successful adoption should
package/SECURITY.md CHANGED
@@ -49,8 +49,13 @@ a bounded transcript excerpt, not a complete summary. The original history and
49
49
  fallback remain subject to the user's Pi session-file permissions and retention.
50
50
 
51
51
  Replay checks the canonical retained assistant and serialized output
52
- fingerprints. Omitted, edited, or transformed messages fall back rather than
53
- restoring stale raw content. As with Pi text summaries, edits to already-compacted
52
+ fingerprints. Before adoption, serialization uses the configured provider's
53
+ public payload hook and aborts before transport. It uses a non-credential
54
+ placeholder, a fetch function that always throws, and zero retries; it does not
55
+ resolve real authentication or send an additional inference request. Missing
56
+ hooks, serialization errors, or an attempted fetch prevent adoption. No host
57
+ filesystem lookup or private serializer imports are required. Omitted, edited,
58
+ or transformed messages fall back rather than restoring stale raw content. As with Pi text summaries, edits to already-compacted
54
59
  source history cannot alter an opaque checkpoint. Legacy checkpoint details are
55
60
  not migrated or replayed across provider/authentication boundaries.
56
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codex-compaction",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Keep Pi sessions flowing with automatic server-side compaction on public OpenAI Responses.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,10 +1,5 @@
1
1
  import { createHash, createHmac, randomUUID } from "node:crypto";
2
- import type { Model } from "@earendil-works/pi-ai";
3
- // Resolve before importing: Pi's jiti root alias also matches subpaths.
4
- const aiEntry = import.meta.resolve("@earendil-works/pi-ai");
5
- const { convertResponsesMessages } = await import(new URL("./api/openai-responses-shared.js", aiEntry).href) as typeof import("@earendil-works/pi-ai/api/openai-responses-shared");
6
- const { createGrammarToolInputProperties } = await import(new URL("./api/constrained-sampling.js", aiEntry).href) as typeof import("@earendil-works/pi-ai/api/constrained-sampling");
7
- const { getCurrentSystemMessage, normalizeContext } = await import(new URL("./utils/transcript.js", aiEntry).href) as typeof import("@earendil-works/pi-ai/utils/transcript");
2
+ import { getCurrentSystemMessage, normalizeContext, type AssistantMessage, type Model } from "@earendil-works/pi-ai";
8
3
  import {
9
4
  convertToLlm,
10
5
  serializeConversation,
@@ -185,6 +180,41 @@ function fallbackSummary(entries: SessionEntry[], ctx: ExtensionContext): string
185
180
  return `[${KIND}:${randomUUID()}]\nProvider checkpoint; the following is only a bounded readable fallback, not a full summary.\n${excerpt}`;
186
181
  }
187
182
 
183
+ /**
184
+ * Ask the configured provider to serialize, then stop at its public onPayload
185
+ * boundary. Never resolve host files or reproduce its private serializer.
186
+ * The dummy credential, throwing fetch and zero retries prohibit inference,
187
+ * even if a future host accidentally continues past the payload callback.
188
+ */
189
+ async function serializedAssistant(ctx: ExtensionContext, message: AssistantMessage): Promise<unknown[] | undefined> {
190
+ if (!supports(ctx.model) || ctx.signal?.aborted) return;
191
+ const provider = ctx.modelRegistry.getProvider(ctx.model.provider);
192
+ if (!provider) return;
193
+ const system = getCurrentSystemMessage(ctx.sessionManager.buildSessionProjection().messages);
194
+ const controller = new AbortController();
195
+ const stop = new Error("Serialization only; no provider request allowed");
196
+ let input: unknown[] | undefined;
197
+ let fetchAttempted = false;
198
+ const result = await provider.streamSimple(ctx.model, normalizeContext({
199
+ messages: [...(system ? [system] : []), message],
200
+ }), {
201
+ // Not an API key. No real credential is resolved or sent by this dry run.
202
+ apiKey: "serialization-only",
203
+ signal: controller.signal,
204
+ cacheRetention: "none",
205
+ maxRetries: 0,
206
+ fetch: async () => { fetchAttempted = true; throw stop; },
207
+ onPayload: (payload) => {
208
+ if (record(payload) && Array.isArray(payload.input) && bytes(payload.input) <= MAX_BYTES) {
209
+ input = structuredClone(payload.input);
210
+ }
211
+ controller.abort();
212
+ throw stop;
213
+ },
214
+ }).result();
215
+ return result.stopReason === "aborted" && !fetchAttempted && !ctx.signal?.aborted ? input : undefined;
216
+ }
217
+
188
218
  /** Public Pi hooks only. No provider replacement, synthetic model turn, or private registry access. */
189
219
  export function registerAutomaticCompaction(pi: ExtensionAPI): void {
190
220
  pi.registerFlag("server-compaction", { type: "boolean", default: true,
@@ -332,7 +362,7 @@ export function registerAutomaticCompaction(pi: ExtensionAPI): void {
332
362
  attempt.items.clear();
333
363
  });
334
364
 
335
- pi.on("turn_end", (event, ctx) => {
365
+ pi.on("turn_end", async (event, ctx) => {
336
366
  const current = attempt;
337
367
  attempt = undefined;
338
368
  inTurn = false;
@@ -364,13 +394,8 @@ export function registerAutomaticCompaction(pi: ExtensionAPI): void {
364
394
  // output before discarding any history. In particular, message_end redaction
365
395
  // must not later be undone by replaying an unredacted raw suffix.
366
396
  try {
367
- const system = getCurrentSystemMessage(ctx.sessionManager.buildSessionProjection().messages);
368
- const normalized = convertResponsesMessages(ctx.model,
369
- normalizeContext({ messages: [...(system ? [system] : []), event.message] }), new Set(["openai"]), {
370
- includeSystemPrompt: false,
371
- grammarToolInputProperties: createGrammarToolInputProperties(
372
- system?.toolsAdded ?? [], ctx.model.compat?.supportsOpenAIGrammarTools ?? false),
373
- });
397
+ const normalized = await serializedAssistant(ctx, event.message);
398
+ if (!normalized) return;
374
399
  const actualHashes = normalized.map(outputHash).filter((value) => value !== undefined);
375
400
  if (hash(actualHashes) !== hash(hashes)) return;
376
401
  } catch { return; }