pi-safe-compact
Resilient context recovery for pi with overflow-safe agentic compaction and guarded continuation after auto-compaction or output-length stops.
Package details
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
- 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.
- 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.
- Programmatic raw scan. Fix an end byte offset, record count, SHA256 and
active-branch ID set. Automatically run
scripts/scan.pyto verify every byte within that snapshot and build a disposable SQLite metadata index. New records appended later are outside the snapshot. - 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 inscratch/. - Validated final submission. Accept only
write_summaryafter 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. - 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
DEGRADEDsummary section anddetails.safeCompactrecord 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.