pi-safe-compact

Resilient context recovery for pi with overflow-safe agentic compaction and guarded continuation after auto-compaction or output-length stops.

Packages

Package details

extension

Install pi-safe-compact from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-safe-compact
Package
pi-safe-compact
Version
0.6.3
Published
Sep 8, 2026
Downloads
603/mo · 66/wk
Author
primp9053
License
MIT
Types
extension
Size
131.8 KB
Dependencies
0 dependencies · 4 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-safe-compact

Two-stage context compaction with an independent, append-only raw archive. Requires Pi 0.84.4 (peer range ^0.84.4) and Python 3.10+ on the destination host for agentic review. No Python packages are required.

0.6.3: explicit first-stage cancellation

When an in-flight first-stage draft is aborted, its status and persisted diagnostic say compaction was cancelled, not that summary generation failed. No partial checkpoint is committed. Pi Desktop runtime 0.1.142 additionally settles the accepted Stop before a compact RPC rejection can commit a failed terminal outcome.

0.6.2: model-sized generation budget and bounded length recovery

Each first-stage request uses min(selectedModel.maxTokens, 65536) output tokens (coefficient 1.0), including history, split-turn prefixes and split/reduce calls. This allowance includes reasoning; it is not a target length for the visible summary. Invalid/missing model limits fall back to 16384. The budget follows the model snapshot taken when compaction begins, independently of Pi's saved trigger settings. Pi Desktop's managed runtime also uses the same formula for the automatic-compaction reserve, reevaluated after model switches without changing the settings file. Installing only the npm extension does not change native Pi's automatic-compaction threshold.

An output length response cannot become a checkpoint, even if its partial text looks complete. Up to three output-length splits per history/prefix reduction may recover by summarizing both chronological halves and merging their complete results. This shares the existing overall 63-request/depth-12 input-overflow bound; it does not multiply that bound or silently lower the selected effort. Recovery is announced. Persistent length failure retains the original context and records a pi-safe-compact-diagnostic custom entry outside model context, so Desktop history rebuilds can display the actual root cause.

0.6.1: inherit the session's current model and effort

At the start of each compaction, capture the session's current model and thinking level. Switching either before Compact takes effect immediately for that compact. The preliminary draft, split-turn history/prefix summaries, all overflow split/reduce requests, and the fresh reviewer use the same snapshot. They do not reuse the model selected when the extension/session was first loaded, nor reread a different model after a lengthy scan or draft.

First-stage requests now use the configured provider's streamSimple mapping, like the main agent. The raw ModelRegistry.complete API does not translate the portable reasoning level; on Responses it could send reasoning.effort=none even when the session was using high/xhigh reasoning, causing a provider 400. Explicit off is still off; this extension never silently replaces the user's effort with low/medium. Provider-specific supported-level mapping is Pi's own. Checkpoint details record the provider/model ID and selected thinking level, without credentials. Authentication headers and auth-specific endpoints remain in memory and are preserved for summarization.

0.6.0: two data layers

Pi's own JSONL, fork copying, tree navigation and retained messages are unchanged.

For each persisted Pi session, this extension maintains:

<session-file>.safe-compact/
  raw.jsonl       # one COMPLETE JSON object per physical line
  pending/       # recoverable capture/commit spools; not a second transcript
  writer.lock

Each raw record contains the original Pi entry at payload.entry, its identity, parent, role, provenance, byte counts and SHA256 values. Exact captured tool bytes are base64 strings in payload.data on the same line. They are not chunk records, external permanent attachments, or truncated previews. Streaming serialization and extraction avoid constructing a giant JSON string. Base64 costs about 33% extra space.

The archive is on the execution host: an SSH session writes on SSH, a WSL session in WSL, a Windows session on Windows. Desktop does not transfer the archive just to show a transcript. Forks may copy the parent's raw archive into their independent file; the review snapshot filters by the actual active Pi branch.

What “complete” means

  • Full user/assistant/Pi tool-result entries received by the session are recorded.
  • Wrapped built-in Bash and PowerShell capture emitted subprocess stdout/stderr before Pi's display truncation, in observed arrival order.
  • Built-in Read captures the actual source bytes before selection, resizing or truncation. This can be wider than explicit Read offset/limit; the original tool arguments remain the authority for the requested range.
  • Grep/Find capture actual subprocess output. A search stopped at its own result limit has NOT searched the whole project. Unproduced matches cannot be archived.
  • Ls captures directory entries before its display limit.
  • Existing custom/sandbox overrides are not replaced. Their returned result is recorded, but the extension cannot reconstruct bytes already discarded inside their implementation. Such records are marked returned-only; producer completeness unknown.
  • Imported history may already be truncated. It is explicitly marked historical; the extension cannot recover output that was never captured.
  • Cancellation/crashes cannot preserve data a producer never emitted. Interrupted captures are marked incomplete/outcome unknown, never fabricated as successful tool results. Images and binary bytes are preserved, not promised to be semantically understood by the summarizer.

IO tees are scoped with AsyncLocalStorage to the wrapped built-in executions; unrelated model/auth/config IO is not captured. Public tool definitions retain Pi's own limits, command prefixes, shell settings and active tool selection.

Compaction workflow

  1. Retain recent context. Aim to keep the latest 10 context messages and latest user turn, within an adaptive token budget. Never start inside a tool-call/result exchange or cross a previous compaction floor. K is a target, not permission to exceed the model window.
  2. First-stage draft. Summarize Pi-visible material (tool text is explicitly preview-truncated here, never in raw.jsonl). Split-turn history and prefix are both covered. On input overflow or bounded output-length recovery, split the evidence, summarize both halves, then reduce them chronologically. Do not drop oldest messages. Work is bounded to 63 requests/depth 12 per reduction. Empty, length, error and cancelled outputs cannot become checkpoints. If no complete first-stage draft is available, cancel compaction and preserve original context.
  3. Programmatic raw scan. Fix an end byte offset, record count, SHA256 and active-branch ID set. Automatically run scripts/scan.py to verify every byte within that snapshot and build a disposable SQLite metadata index. New records appended later are outside the snapshot.
  4. Fresh reviewing agent. No extensions, skills, prompt templates, context files or recursive compaction. It receives the draft and snapshot reference, not another giant raw-file copy. It must page through the entire latest user request with read_current_request, then selectively inspect relevant history and tool output using supplied Python tools or its own code in scratch/.
  5. Validated final submission. Accept only write_summary after complete latest-request paging, valid active-branch references, required headings and current-request checks. Bare text, interrupted tool arguments and incomplete outputs are not accepted. These deterministic checks are not a proof of semantic completeness.
  6. Explicit fallback. Missing Python, exhausted post-reminder grace, invalid/stale summary or other second-stage failure uses the complete first-stage draft. A warning, persisted DEGRADED summary section and details.safeCompact record state the reason. User cancellation does not commit a fallback checkpoint.

Both stages' system prompts ask for a complete summary under 64k (65,536) tokens as a soft concision target, not a size quota or truncation rule. This also applies to first-stage updates, split-turn prefixes and split/reduce calls. Latest user requirements, key facts, constraints and unfinished work take priority over shortening the summary. No post-generation clipping or length-only rejection is added; the model's actual output/context capacity still applies.

The final context is the summary plus Pi's retained messages, not all raw history reinserted into the model window. The latest request gets an authoritative verbatim guard, bounded to 32,000 characters with an explicit truncation marker when needed. The full request remains in raw.jsonl and the reviewer workspace.

Reviewer workspace scripts

Each disposable workspace contains inputs.json, current-user-request.txt, one-pass-summary.txt, scripts/ and scratch/. The prompt documents:

  • scan.py: full byte/checksum scan, active-branch index and coverage report.
  • read_messages.py: recent IDs, bounded entry pages, full verified entry export.
  • search.py: streaming literal search over active entry JSON.
  • tool_output.py: provenance, bounded preview, verified complete capture export.
  • check_summary.py: headings/reference checks; not semantic validation.

Use --help for each script. Exports must stay under scratch/, cannot overwrite existing files, and appear under the requested name only after successful verification. Never use line-based cat/tail/json.loads on huge raw records. The shell allows Python analysis, but prompt restrictions are not an OS sandbox.

Failure and recovery

New captured records are fsynced before spools are removed. Historical imports may batch writes because their durable Pi source remains available.

A raw write/integrity/locking failure latches a visible error, aborts the current operation and blocks later provider/tool/input work through Pi's cancellation and tool hooks. Hidden continuations also stop. Unsubmitted input must be retried after recovery; do not assume it reached the model.

At restart, complete pending commits replay idempotently. An incomplete final raw line can be repaired; corruption in committed data is not silently erased. Capture manifests reconnect spools to actual Pi tool results. If a crash preceded the Pi result, an archive-only interrupted-capture record preserves available bytes without inventing an execution outcome.

Run /raw-status to inspect the location/status. On failure, stop the session, fix disk space/permissions or investigate the reported corruption, preserve raw.jsonl and pending/, then reload/restart. Do not delete a live writer lock. A dead-PID lock can be reclaimed; a live/ambiguous lock fails closed. Two writers to the same archive are not allowed. Power loss, hardware faults and non-cooperative custom providers cannot be given an absolute preservation guarantee.

Desktop Reload performs an explicit idle writer handoff before starting its replacement process. While parked, the old process cannot perform model/tool work. A failed replacement is terminated before the old writer is resumed.

In-memory/--no-session operation is refused by the archival extension. The isolated reviewer intentionally has no extension and can use an in-memory session.

Continuation and resource limits

Manual compact remains idle. Automatic post-turn compaction may queue one hidden continuation only while the original run is active, no other message is pending, and Pi is not already retrying. Output-length recovery similarly defers to pending work. Context-starved tiny-output responses do not trigger a length loop. After three consecutive output-length continuations, stop and ask for inspection.

Optional configuration at ~/.pi/agent/extensions/pi-safe-compact.json:

{
  "pythonCommand": "/absolute/path/to/python3",
  "maxTurns": 500,
  "maxRuntimeMs": 600000,
  "graceTurns": 10,
  "graceTimeMs": 180000,
  "keepRecentMessages": 10,
  "maxVerbatimUserChars": 32000
}

pythonCommand is one executable path, not a shell command. Otherwise try python3, then python; never install Python during compaction. The executable availability probe has a 10-second timeout. The mandatory full raw scan has no time cutoff and remains cancellable with Stop.

maxTurns and maxRuntimeMs are soft finish-reminder thresholds, despite their legacy names. Defaults are 500 reviewer turns and 10 minutes. One turn is one reviewer assistant response plus its tool results, not a source conversation turn. The clock starts when model review starts, after the raw scan and setup. The initial prompt asks the reviewer to finish within these targets. Reaching either threshold queues one nudge via Pi's steering mechanism: stop optional exploration and submit write_summary immediately, completing any mandatory latest-request paging/checks first and stating uncertainty honestly. The reminder is delivered at the next safe turn boundary; it does not interrupt an in-flight model response or tool. Each threshold nudges at most once, even if both are crossed.

The first nudge starts a hard grace period: 10 further reviewer turns or 3 minutes, whichever runs out first (graceTurns / graceTimeMs). Further turns are counted after Pi delivers that reminder, so an in-flight pre-reminder response does not consume a grace turn. Wall time starts when the reminder is queued, allowing cancellation of a stuck in-flight model/tool even if it has not yet received the nudge. A second reminder does not reset or extend either limit. A validated summary submitted on the final allowed turn wins; otherwise the reviewer is aborted and the complete first-stage summary is used, with the specific grace-exhaustion reason visibly and durably marked DEGRADED.

Old explicit threshold/grace values remain supported. Stop still cancels the whole compaction without committing a fallback checkpoint. Actual provider, tool, or validation failures can also cause fallback; a reminder never bypasses validation and cannot guarantee that the model will obey. The mandatory raw scan still runs to completion or user cancellation before model review begins.

Optional maxRetainedTokens overrides the adaptive retention budget. The default is 25% of usable model context after two summary reserves, capped at 256,000 and never below Pi's keepRecentTokens. Oversized turns remain in the archive.

Install and test

pi install ./pi-safe-compact -l
npm ci
npm run check
npm test
npm pack

Tests use temporary sessions and local fake providers, not live user sessions or paid models. They cover original retention regressions, real Pi SDK compaction, archive-write failure blocking later provider requests, interrupted recovery, branch isolation, binary/Unicode integrity, and a 24 MiB capture scanned with a 96 MiB Python address-space limit. Two historical-session tests require PI_SAFE_COMPACT_REAL_SESSION_FIXTURE; absent fixtures are reported as skipped. Linux-specific disk-failure/memory-limit tests are run on WSL/Linux.

Pi Desktop bundles this source as its default compaction extension. Its destination entry path checks configured npm extensions before create/load/fork/reload and uses immutable package generations. A newer installed safe-compact can supersede the bundled version; known duplicate handlers are removed. Running sessions are not hot reloaded. See the root Desktop README for update behavior.

MIT.