从官方 Git 仓库导入 chandra447/pi-hermes-memory 在 tag v0.9.10 (df624056a2e8c68bc89ba0a81e1c120856ec9665,2026-10-02)的源码快照。 本次仅导入源码,尚未接入组合包:package.json、加载顺序、依赖与权限策略 均未改动。 - 原样导入 src/、tests/、docs/、scripts/、README、CHANGELOG、PLAN、 AGENTS 与 LICENSE;该快照共 156 个上游文件。 - 剔除嵌套 .git、上游 .github 工作流、.ralph/、.claude/、node_modules、 覆盖率与构建产物。 - 新增中文 UPSTREAM.md,记录来源,以及启用前必须完成的待决事项: 在 pi-compaction 之前注册加载顺序、better-sqlite3 版本与根 allowScripts 对齐、strip-ansi 解析确认。 - 说明选择 tag 而非 main 的原因:导入时上游 main 已领先至 24fcd8ad26fc27badb8d7dbaf8034645de07b893(2026-10-11),选 tag 是为 对应一个已发布、可复现的版本。 尚未接线。启用前还需反转 tests/extension-load.test.ts 中"不得加载 Hermes"的断言,并同步 pi-tool-search 分组、权限策略与文档。 验证:快照 156 个上游文件全部导入,目录内仅多出 UPSTREAM.md 一份; 已确认与上游 main 内容存在差异,证明取的是 tag 而非 main。
60 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Changed
- Opt-in chunked subprocess consolidation (
consolidationChunking, defaultfalse): stores aboveconsolidationChunkChars(new, default4000) consolidate in bounded rounds — a head slice of entries per child run, reloading from disk between rounds, so a killed run resumes from partial progress instead of restarting. The rounds share one overall time budget (consolidationTimeoutMs, same worst case as the single call this replaces), the loop stops at the target's capacity goal (failure memory is a 2× tier and is no longer over-merged down to the slice size), a slice that cannot shrink is skipped rather than blocking later slices, A store that fits one prompt runs a single unscoped decisive round with the whole remaining store; a child exiting 0 without shrinking is reported as an honest partial instead of silent success, and an over-chunk store already within its capacity goal is a clean no-op. Partial runs reportpartial: truewith the reason and are surfaced by the warning path, the store's add-retry error, and/memory-consolidate; disappearances outside a round's slice (rogue child deletions, legitimate cross-slice dedup, or concurrent writers — indistinguishable to the parent) are detected and reported rather than resurrected, because re-adding through the cap-enforcing add path cannot converge and resurrection ping-pongs across triggers. An over-chunk store already within its capacity goal is a clean no-op. Single-shot behavior below the threshold is unchanged.
Added
-
Usage-hit tracking — the promotion gate for consolidation (
usageHitTrackingEnabled/consolidationUsageSignals, both defaulttrue):memory_searchnow records a per-entry recall counter (hit_count) and last-recall date (last_hit_at), giving consolidation the usage signal neither the write nor the read path could see before — the prompt's "entries older than 30 days without recent references" rule finally has data behind it. Recording is one batched counter update per search and is invisible to search results, ranking, and prompts. Consolidation (direct and subprocess transports, auto and manual) appends a "Usage Signals" section listing the presented entries' recall counts — both halves capped at 20 lines (tracked entries ranked by hits descending, remainders folded into summary lines), framed as tie-breakers: well-recalled entries are load-bearing (prefer keep/merge), never-recalled entries are weaker keep candidates. The section only appears once at least one presented entry has actually been recalled, so stores without tracking history keep byte-identical prompts./memory-insightsgains a usage section (top-recalled entries, tracked/total counts). Counters live on the memory row itself — they die with the entry, need no pruning, and survive the legacy schema rebuild and corruption-recovery copies. -
One-time provider misconfiguration notice (#197): when a review or consolidation completion comes back with an empty answer channel and the whole payload parked in the thinking channel, the extension logs a single warning per session naming the provider/model, explaining that a server-side thinking default is swallowing the answer and that the thinking-channel recovery is best-effort, and pointing the user to fix the provider (
Ask your Pi to fix this as well.). Normal answers, redacted-only completions, and fully empty completions never trigger it. -
Opt-in memory lifecycle timings:
PI_TIMING=1now reports startup synchronization and loading, backfill and live-index callbacks, shutdown flush/index/wait/close, and database open/integrity-check/checkpoint durations. Normal runs produce no timing output. -
Opt-in SQLite session retention (#183): new
sessionRetentionDayssetting (default0, disabled). When set to a positive value, sessions whose JSONL source file has not been updated within the window are pruned from SQLite at startup — rows only, the JSONL files themselves are never deleted — and both the deferred backfill and/memory-index-sessionsskip files outside the window, so pruned sessions stay pruned instead of being re-indexed on every startup. With the default0, pruning is fully disabled and startup keeps the legacy count-only backfill preflight.
Changed
- Natural-language search now filters common stop words (#184): session and memory searches drop function words (
the,is,what, …) before FTS5 matching, improving precision for natural-language queries. Intent-bearing words (can,need,off,like,get, …) are deliberately kept. A query made up entirely of stop words no longer returns an empty result — it degrades to a scoped literal substring search over the original terms. - Startup markdown→SQLite reconcile is gated per scope (#227):
session-start.persistence-syncpreviously parsed every markdown file and re-upserted every entry into SQLite on every launch, measured at 2.1–7.2s on large stores. Each markdown scope now carries a fingerprint (extension_metadata, written in the same transaction as the reconcile), so an unchanged scope skips the per-entry SQL and an unchanged store's startup cost drops to a directory walk plus a few indexed reads per scope. The fingerprint binds markdown bytes and a row count, not the mirror's content, so mirror drift that preserves the count — a hand-edited row, or a search index that lost rows — now waits for a repair instead of being rewritten on every launch;/memory-sync-markdownis the forced full repair for both. New and deleted project dirs are still detected by the walk, and a corruption rebuild re-mirrors markdown exactly once. - Compact flush runs on one shared window (#225): the pre-compaction flush previously handed its full 30-second timeout to the direct transport and then again to the subprocess fallback, so a failing direct call re-sent the same large prompt to the subprocess and could spend ~60s saving nothing, silently. The flush now owns a single wall-clock window — new
flushCompactTimeoutMssetting (default60000, raised from the hardcoded30000); the direct transport runs first and the subprocess receives only the remaining time. Escape still cancels immediately, and the shutdown flush keeps its own shorter silent cap. - Skill discovery guidance now matches how Pi indexes skills (#244, #249):
skill_manage's parameter hints, tool description, one-shot example, and prompt guidelines had left the indexeddescriptionas a what-summary while the "when to use" text went into the body — where Pi never looks at discovery time, so a correct skill could silently never load. The guidance now states thatdescriptionis the only indexed field, demonstrates trigger signals in the one-shot example, and keepswhen_to_usefor the expanded scenario and boundaries; the README section and example frontmatter teach the same rule. A behavioural canary (tests/skill-discovery-contract.test.ts) pins the index contract through Pi's publicloadSkills/formatSkillsForPromptAPI, so a change in how Pi indexes skills fails in CI instead of silently degrading discovery.
Fixed
-
Direct review works for providers with ambient credentials: Pi resolves
amazon-bedrockwith an AWS profile or SSO session to request auth with no API key and no credential header, because the AWS SDK signs the request itself. The direct transport treated that asno_auth, so every review, flush, correction save, and consolidation fell back to thepi -psubprocess. The auth check now also accepts a provider that Pi reports as configured (modelRegistry.hasConfiguredAuth), the same check Pi runs before a prompt. OAuth providers that resolve to no credentials still reportno_auth. -
Background memory jobs work on OpenCode (#250, reported by @fallleave001): the direct transport calls
completeSimpleitself, so it bypassed the session plumbing Pi applies to its own completions, and the OpenCode gateway rejected every review, flush, and correction-save completion with400 missing_session_idbefore the model was reached — losing the memory write, then spending an extrapi -pspawn on the fallback. The transport now sendsx-opencode-sessionwith the real Pi session id fromctx.sessionManager.getSessionId(), scoped per model to theopencodeandopencode-goproviders and to theopencode.aihost, alongside thex-opencode-clientheader Pi sends. Other providers are untouched, an operator-configured session header is never overwritten, and a missing session id degrades to the previous behaviour rather than throwing. -
FTS5 index errors during markdown sync are surfaced instead of hidden (#184): when the FTS5 search index is broken, memory writes still succeed in Markdown, but the tool result and
/memory-sync-markdownnow report the degraded search state together with the repair command instead of showing a silent zero-count success. Genuine database corruption is unaffected by the fallback and now recovers through the markdown sync path too: reconciliation runs insideDatabaseManager.withCorruptionRecovery(), so a corruptsessions.dbis quarantined, rebuilt, and the sync retried instead of failing every memory write. -
A failed compact flush is now visible (#225): when the flush cannot save before compaction — budget spent, child process killed by its watchdog, or a child error — the TUI shows one warning naming
flushCompactTimeoutMsinstead of failing silently. The watchdog-killed child path was invisible before. SettingflushCompactTimeoutMsto0or lower now skips the flush silently (an explicit disable, not a failure), and non-finite values fall back to the default. -
Rapid Markdown memory writes now reuse a recent recovery snapshot (#214): successive overwrites within one hour publish atomically without creating another
.recovery-*sidecar, while slower writes still take a fresh snapshot for crash recovery. -
Policy-only memory writes bypass the Markdown character cap (#218): SQLite is the query authority in policy-only mode, so adds, replacements, and atomic mutation plans can exceed the Markdown export limit without triggering automatic consolidation. Legacy-inject mode keeps its existing caps, grace windows, auto-consolidation, failure limits, and FIFO eviction.
-
Policy-only memory usage is reported as a count, not a percentage (#248, #252, #258): policy-only mode intentionally does not apply the Markdown character cap, so a capacity percentage named a ceiling nothing was holding the store to — and, clamped at 100%, it read
100% — <count>/<limit> charson every write of an over-cap store. The write tool result and the renderedMEMORY,USER PROFILE, andPROJECT MEMORYheaders now render${current} charsthrough oneusageLabel(current, limit)helper gated on the samecapEnforcedpredicate the write gates use, so the report and the gates cannot drift apart again; enforced modes keep the previous percentage strings byte-identical. -
The memory policy no longer forces the rendered system prompt (#251):
before_agent_startreturned a fullsystemPrompt, which Pi documents as replacing the complete prompt for the turn, so the structured sections were flattened into opaque text — a section-aware extension (pi-doppelclauderewrites the named sections and refuses to send when its wording survives) or provider lost the sections it keys on, and its guard fired on every turn. The policy now appends through the structured prompt options, which Pi re-renders from on 0.86+; 0.80.6–0.85.x keep the previous replacement path, because there the session applies only a returnedsystemPromptand never reads the options back. -
Corruption rebuilds no longer carry markdown-scope fingerprints (#227): the recovery copies coerce invalid values instead of dropping rows, so a copied fingerprint could agree with coerced rows while content drifted; a rebuilt database now re-mirrors markdown exactly once instead of trusting copied fingerprints.
-
SQLite open integrity scans can be disabled (#194): set
quickCheckOnOpentofalseto skip the asynchronous startupPRAGMA quick_check; operation-time corruption recovery remains enabled.
Fixed
-
memory_searchacceptstarget: "project"(#212): the tool schema only declaredmemory/user/failureastargetfilter values, whilememory_addwrites withtarget: "project"and search results label those very entries[target=project](the valuememory_replace/memory_removethen require) — so filtering by the mutation target shown in the results failed schema validation ("Validation failed for tool") before the handler ever ran, and the only workaround (project: "<name>") required knowing the project name up front.target: "project"now passes validation and resolves totarget = 'memory' AND project IS NOT NULL, exactly the entries whose displayed mutation target isproject; project-attributed failures remain labeled[target=failure]and are not matched. The injected memory-policy guidance now lists the fourth accepted value. -
Clean process exit on Node 24+ (#193, reported by @vanneswong):
better-sqlite312.x inherits Node's rawnode::ObjectWrap, whose destructor callsRemoveEnvironmentCleanupHookafter Node has torn down its Environment — so on Node 24 (observed on Linux aarch64) Pi could abort with SIGABRT and a non-zero exit code at every shutdown, after all work completed. The dependency moves tobetter-sqlite3@^13.0.3, the first N-API/node-addon-api release, which removes that crash path. The public API this extension uses is unchanged; the declared engines (>=22) cover every Pi-supported runtime. -
Snapshot retention converges for dormant memory stores (#202): retention caps for
.recovery-*/.retired-*sidecar files only ran insidesaveToDisk(), so a project store that stopped being written kept its snapshots past the count, age, and byte limits forever (one reported store held 171 recovery snapshots against a cap of 32). Eachsession_startnow sweeps the global store and every discovered project store with the identical per-file policy under the same mutation lock; enumeration skips symlinks, hidden directories, and nested paths. -
Reload no longer waits for an unnecessary LLM memory flush: Pi preserves the current session during
/reload, but the shutdown handler ignoredevent.reasonand awaited direct completion plus possible subprocess fallback after six user turns. Reload now skips both transports; quit and session-replacement reasons keep the existing flush policy. -
memory_searchranks by relevance instead of recency (#118): FTS5 matches were ordered bylast_referenced DESCalone, so a long note that mentions a query term once outranked a short memory that is entirely about it, purely because it had been touched more recently. The match now joinsmemory_ftsand orders bybm25(memory_fts)ascending withlast_referenced DESCas the tie-break, so the densest match wins and recency only decides between equally relevant rows. Result sets are unchanged; only their order is.
Performance
- The process-incarnation probe no longer runs at extension load (#245):
AtomicLockCoordinatorprobed its own incarnation at module scope, so merely importing the module — which happens on every Pi launch, before any hermes code runs — paid aspawnSyncfor a lock most sessions never take. On Windows that ispowershell.exe, measured at 515ms of the 2108mspi-hermes-memorymodule import (PI_TIMING=1, six extensions loaded). The probe is now memoized and deferred to the first coordinator construction;AtomicLockCoordinator.shared()already builds the coordinator on first lock use and the session-start path takes no lock, so the same profile drops to 1466ms with no change in locking behavior. Separately, the 500mstimeoutmeans the probe always returnsnullwhere PowerShell takes longer than that to start, which this change does not address.
[0.9.4] - 2026-08-08
Added
- Standing instructions: user-authored rules that are always in context (#121, designed with @jagaliano): in
policy-onlymode a persisted constraint only takes effect if the model chooses to callmemory_searchbefore the action the constraint would have prevented. For a prohibition that is exactly the moment the model has no reason to look, so the recall path is structurally unable to enforce prohibitions, independent of model quality. It works fine for preferences, where a wrong guess is cheap./memory-pin <rule>writes to a new~/.pi/agent/pi-hermes-memory/STANDING.mdthat is injected into every session, in every memory mode, in its own<standing-instructions>block after the memory policy. Background review, consolidation and the correction detector all write throughMemoryStoreand never touch this file, so a model-generated memory has no path into the trusted block — provenance is a property of the storage rather than of a flag. Hard cap of 20 entries / 2000 characters, independent ofmemoryCharLimitanduserCharLimit; a hand-edited file over the cap is truncated at injection and the omission is stated inside the block rather than dropped silently. Every pin goes through the samescanContent()scan as any memory write./memory-pinalso takeslist,remove <n>andclear;/memory-preview-contextshows the block and flags over-budget entries. SetstandingInstructionsEnabled: falseto drop the store and the command.
Fixed
- Consolidation lock contention no longer hard-fails memory writes (#144, reported by @Frank-Opus): while any session held a consolidation lock, every capacity-exceeding memory write in every session failed with
Consolidation already in progress ... Skipping duplicate subprocess, for the fullstaleMswindow. Three defects: the lock gave up on the first collision whileacquireMarkdownMutationLockalready polled with a deadline;staleMswastimeoutMs + 30s(210s) against a worst-case hold of2 x timeoutMs(360s), so a healthy consolidator could be robbed mid-run and two children could write the same store; and contention was reported as a failure. Acquisition now polls with a bounded 5s window, the holder heartbeats its lease every 10s (AtomicLockCoordinator.renew(), token-fenced) sostaleMsdrops to a fixed 45s — a wedged holder is reclaimed 4.5x faster while a slow-but-healthy one cannot lose its lease at all — and contention returnsdeferredwith "retry in a moment" rather than a broken-consolidation error. After waiting out contention the target is re-read from disk, and if the session ahead already freed space the redundant LLM pass is skipped. Lock rows whose holder process is gone are now garbage-collected; previously a storage identity that never returned leaked its row forever. - Provider auth is resolved against fresh credentials on every direct call (#139, reported by @avinashkanaujiya): Pi's
AuthStorageparsesauth.jsononce in its constructor and only reloads it when an OAuth refresh fails, andExtensionRunnerhands every event the sameModelRegistrysingleton — so anapi_keycredential was frozen for the process lifetime. A key rotated on disk mid-session (for example by@lnilluv/pi-opencode-go-rotationafter a weekly limit) stayed invisible, and every direct memory completion kept presenting the revoked key while everything else in the user's setup had already picked up the new one. Credentials are now re-read before each direct completion, and a provider auth rejection mid-request triggers one retry with a refreshed key — but only when the key actually changed, so a genuine auth failure still falls through to the subprocess transport. A malformedauth.jsondegrades to the already-loaded credentials instead of taking the review path down. Known gap:!command-backed keys remain cached in the SDK's owncommandResultCache, which an extension has no supported way to clear. - Documented git install commands actually work (#140, fixed by @xz-dev):
pi install git:github:<owner>/<repo>parses but resolves tohttps://github:<owner>/<repo>, takinggithubas the hostname and the owner as the port, and fails atgit clone.AGENTS.mdanddocs/PUBLISHING.mdadditionally used a baregithub:shorthand that Pi'sparseGitUrlrejects outright. - Memory mutations now use schema-safe action tools (#151):
memory_add,memory_replace, andmemory_removeexpose the fields each operation requires, so malformed calls fail validation before they can enter mutation logic. - Test runs now have a bounded per-file timeout (#155):
TEST_TIMEOUTdefaults to 120 seconds, detects GNU/macOS timeout utilities when available, reports the exact hanging test file, and preserves the existing fallback when no timeout utility is installed. - Correction detection now saves the latest user correction (#156): recent user messages are scanned newest-first so a later correction cannot be replaced by an earlier unrelated user message.
- Project memory follows the active session project (#157): project stores, SQLite synchronization, and project-aware handlers rebind when a session changes project, preserving the correct project scope.
Changed
- Minimum supported Pi corrected to 0.80.1, and now verified in CI:
peerDependenciesdeclared>=0.74.0, butsrc/handlers/review-memory-ops.tsimports@earendil-works/pi-ai/compat, a subpath that does not exist before 0.80.1 — so every user on 0.74 through 0.79.x gotERR_PACKAGE_PATH_NOT_EXPORTEDand no extension at all, whilepackage.jsontold them they were supported. The floor is corrected, and a newmin-sdkCI job installs exactly the declared minimum and type-checks against it, so the advertised range can no longer drift from reality. The regularcheckjob could never catch this: it installs whatever the devDependency range resolves to, which is always new enough.
Upgrading
- Nothing changes until you pin something. With no
STANDING.mdon disk — every existing install — the injected system prompt is byte-identical to 0.9.2.policy-onlystill means stored Markdown memories stay out of the prompt; what it no longer means is "nothing but policy", since anything you explicitly pin is now always present. That is a change to a previously documented guarantee, so it is stated here rather than left to be discovered. - The peer floor bump cannot block an install. Pi's package manager never reads
peerDependencies; the range is advisory metadata. Users below 0.80.1 were already unable to load the extension at all. - During the upgrade window only, a 0.9.2 session and an upgraded session consolidating the same target concurrently can overlap: the old build does not heartbeat, so the new one may reclaim its lease after 45s. Markdown writes stay serialized by the mutation lock, so the worst case is that one redundant consolidation's result is superseded — no entry loss or corruption. It resolves as soon as every session is on the new build.
[0.9.2] - 2026-07-30
Fixed
- Auto-consolidation no longer dies on a timeout the manual path never had (#136, reported by @jasonrr):
consolidationTimeoutMsdefaulted to 60s, but/memory-consolidatequietly floored itself at 180s while the over-capacity auto path passed the raw 60s straight to api -pchild. A consolidation pays child-process boot plus a full LLM turn, so on stock defaults the auto path was killed mid-run every time — 100% failure on slower models or near-full memory, while manual runs of the same memory succeeded. The default is now 180000 and both paths use the configured value verbatim; the manual floor that hid the mismatch is gone. A configured value below the default is still honored, butloadConfigwarns at startup that it is likely to be killed mid-run. - Auto-consolidation failures are surfaced instead of swallowed (#135, reported by @jasonrr):
triggerConsolidationreturns a specific reason for every failure mode — lock contention, spawn failure, non-zero exit, timeout kill — andMemoryStorediscarded all of it in a barecatch {}, returning the unchangedMemory at N/M chars...capacity error. Every mode looked identical to a memory that was simply full, and the auto path was undiagnosable from outside. The reason is now appended to the tool result (... Auto-consolidation attempted but failed: <reason>) and logged once per failure. A consolidation that ran successfully but freed no space is reported distinctly (Auto-consolidation ran but did not free enough space.), as is a reload failure after a successful consolidation.
[0.9.1] - 2026-07-27
Changed
- Global skills return to
~/.pi/agent/pi-hermes-memory/skills/(#126, reported by @kdejaeger): 0.9.0 moved them into Pi's own~/.pi/agent/skills/to eliminate a shadowing bug. That fixed the bug by deleting one of the two directories, and took a deliberate design property with it — skills this extension curates from your sessions were mixed in with skills you installed yourself, so you could no longer wipe or audit ours without picking through yours. Worse,skill_manage deletegained the ability to remove hand-written skills it never created. Skills are separated by who writes them again. skill_manage deletecan no longer target Pi's global skills root. It reaches only this extension's own global directory and the active project's. Skills you installed yourself are out of its blast radius.
Fixed
- Silent write-loss on shadowed global skill names, fixed at the source (#125): Pi keys skills by name, first-loaded wins, and its own
~/.pi/agent/skills/outranks anything an extension contributes viaresources_discover. Creating a global skill under a name already present there wrote a file Pi never loads and reported success. Rather than removing one of the directories,skill_managenow refuses such a write and names both paths, so the shadowed state cannot be created in the first place. Project scope is unaffected — only the global root collides.
Upgrading
If you ran 0.9.0, the skills it relocated into ~/.pi/agent/skills/ stay there and keep working. They are not moved back automatically: 0.9.0's migration renamed each skill and then removed its source directory without recording a manifest, and skill frontmatter carries no provenance field, so there is no reliable way to tell which skills in Pi's root came from this extension and which are yours. Moving files out of your global root on a guess would be a worse version of the mistake being corrected here. Move any you want back by hand, or open an issue and a confirmation-gated /memory-skills reclaim will be added.
[0.9.0] - 2026-07-26
Changed
- Global skills moved to
~/.pi/agent/skills/.skill_managewithscope: "global"previously wrote to a private~/.pi/agent/pi-hermes-memory/skills/. Existing global skills are relocated automatically on first run. Because that directory is Pi's own user-skills root,/memory-skillsnow also lists skills you wrote by hand or that another tool installed there. See the #125/#126 entry below for why the private directory could not stay. - Project identity follows the Git repository, not the working directory name. Running Pi from a linked worktree or a subdirectory of a repository now resolves to the repository's project scope. Directories that already exist under the old cwd-basename identity keep being used, so nothing is orphaned on upgrade.
Fixed
- Git worktrees each became their own project (#120): project-scoped memory and skills were keyed off
path.basename(cwd), so running Pi in a linked worktree wrote toprojects-memory/<worktree-name>/. Deleting the worktree after merging stranded that memory and those skills where the main checkout could never see them. Inside a Git repository the project name is now the repository root's basename — resolved from the worktree's.gitpointer and itscommondir, with nogitsubprocess — so every worktree and every subdirectory of a repository shares one identity. An existingprojects-memory/<cwd-basename>/store still wins over a newly derived repository name, so upgrading never orphans memory written under the old identity. - Global skills were permanently shadowed by Pi's own skills root (#125, #126):
skill_managewithscope: "global"wrote to~/.pi/agent/pi-hermes-memory/skills/, which the extension then contributed as an extra scan root. Pi keys skills by name, first-loaded wins, and its own~/.pi/agent/skills/is auto-discovered at higher precedence — so any name present in both places produced a collision warning every session and silently ignored our copy, makingskill_manageedits a no-op against the skill the agent actually loaded. Global skills now live in~/.pi/agent/skills/alongside every other user skill, and only the project skills directory is contributed viaresources_discover. Existing skills are moved out of the private directory on first run; a name that already exists in Pi's root is left untouched and reported rather than clobbered.SKILL_TOOL_DESCRIPTIONnow states the on-disk path for each scope.
Performance
- Session start no longer churns SQLite connections (#124, thanks @Fraguinha):
AtomicLockCoordinatoropened and closed a fresh connection for everytryAcquire/isCurrentOwner/release— each open running PRAGMA setup plusCREATE TABLE IF NOT EXISTS, each close triggering a WAL checkpoint — andsyncMarkdownMemoriesToSqliteconstructed a new coordinator per file. On a 2.5 MBsessions.dbthis accounted for 16.7s of better-sqlite3 CPU per/newor/resume, now 3.7s. Connections are cached per coordinator and coordinators are shared per database path, so every lock site (markdown mutations, consolidation, corruption recovery, legacy-root migration) reuses one connection instead of leaking one per call.
[0.8.3] - 2026-07-26
Tagged but never published to npm; its fixes ship in 0.9.0.
Fixed
- Compiled Pi failed to load the extension at all (#117):
src/extension-root-migration.tsresolvedbetter-sqlite3at module scope, andsrc/index.tsimports it during extension load. Compiled Pi runs on Bun, whose resolver cannot findbetter-sqlite3(nor its transitivebindings) from an on-disk extension file, so every session started withFailed to load extension: ResolveMessage: Cannot find package 'better-sqlite3'and the whole extension was dead. The legacy-root migration now resolves SQLite lazily and routes throughbun:sqliteunder Bun, matching whatdb.tsandatomic-lock-coordinator.tsalready did. Those two also defer their native load out of module scope, so an ABI mismatch now surfaces as an actionable in-session error (the recovery guidance added in 0.8.2) instead of a fatal extension-load failure. memory_searchsilently missed natural-language queries with uppercase connectors (#122, #127): a query likeDO NOT USE FIND /was classified as raw FTS5 syntax because of the uppercaseNOT, the unquoted/then failed to parse, and the parse error was flattened into "no memories found" with the safe fallback skipped — while the same query in lowercase worked. Bothmemory_searchandsession_searchnow retry as quoted natural language when (and only when) the raw query fails to parse, preservingtarget/project/limitfilters on the retry. Intentional operator queries such aspnpm OR AESTkeep their exact semantics.
[0.8.2] - 2026-07-21
Fixed
- Silent memory data loss after external
MEMORY.mdedits (#112, #113): manual truncate/cpraces could returnsuccess: truewith stale usage while the live Markdown file was empty and the SQLite search mirror was wiped. Mutations now treat disk as source of truth, refresh usage/entry counts from disk, retry post-publish fingerprint mismatches, and always reconcile SQLite via the existing mutation observer (including failed writes). Exhausted external-write conflicts point at/memory-sync-markdown. - Immediate
database is lockedunder concurrent Pi writers (#110, #113):DatabaseManagernow setsPRAGMA busy_timeout = 5000so short overlapping writers serialize instead of failing immediately. - Homebrew Pi
better-sqlite3ABI mismatch (#111, #114): when the native addon was compiled for a different Node ABI than the runtime hosting Pi, load failures are detected, one automaticnpm rebuild better-sqlite3is attempted against the current Node, and remaining failures return a clear recovery path (including brew/npm guidance). skill_managepatch content format corruption (#107, #115): free-form LLM patch payloads (JSON arrays/objects, empty bodies, injected##headers) could wipe or splice skill sections. Patch now accepts structured section fields (procedure_steps/pitfalls/verification_steps/when_to_use), coerces JSON string arrays into Markdown lists, and rejects unsafe payloads. Prompts steer structured patch and preferupdatefor multi-section rewrites.
[0.8.1] - 2026-07-14
Fixed
- Published package exposed unusable
check/testscripts (#108):npm run checkandnpm testare source-checkout-only (they need TypeScript,tsconfig.json, andtests/, none of which ship in the production tarball). Running them afterpi installnow fails with a clear message pointing at a git clone +npm install, instead of cryptictsc: not found/tests/run-all.sh: not founderrors. Runtime extension behavior is unchanged; CI still runs these scripts before publish.
[0.8.0] - 2026-07-12
Fixed
--no-extensionssilently stripped provider auth adapters from child subprocesses (#94): childpi -pinvocations pass--no-extensionsto skip loading settings.json packages, which also dropped OAuth billing adapters (e.g.pi-claude-oauth-adapter) — causing Claude Pro/Max subscription requests from consolidation/flush/correction/review subprocesses to silently rebill as paid "extra usage". Sibling packages matching the*-oauth-adapter/*-auth-adapternaming convention (including scoped packages) are now auto-detected via theirpackage.jsonpi.extensionsmanifest and re-added to child invocations automatically — no hardcoded adapter list, so a futurexai-oauth-adapteror similar is picked up without a code change. A newchildExtensionPathsconfig option lets users explicitly re-add any other extension.- Sensitive prompt payload leaked via argv (#95): child
pi -psubprocess invocations (background review fallback, session flush, correction save, consolidation) now write the review/flush/consolidation prompt — which includes current memory, user profile, project memory, and recent transcript — to a private temp file (0600, per-invocation temp directory) and pass it via@<path>instead of embedding the raw text on the command line. Prevents the prompt from being captured by AV/EDR command-line logging (e.g. Windows Defender Protection History) orps-style process listings. The temp file is deleted immediately after the child process exits. - Corrupt-DB recovery fork-storm (#96):
DatabaseManagercorruption recovery is now gated behind a cross-process advisory lock — concurrent processes wait instead of independently quarantining and rebuilding — with a circuit breaker after repeated recovery failures and bounded backup/temp-file retention. A lock held by a process that's alive but wedged (blocked I/O, suspended) is now reclaimable once its lease exceeds a staleness threshold, and the destructive rebuild rename verifies it still holds the lease immediately before publishing so a resumed stale holder aborts instead of racing the current owner. - Duplicate consolidation subprocesses and unserialized memory mutations (#97, #99): concurrent writers to the same Markdown target (add/replace/remove, consolidation, session flush) are now serialized through a token-owned lock, and a content-fingerprint check immediately before every publish detects and safely retries around external/concurrent edits instead of silently overwriting them. Duplicate consolidation subprocess spawns for the same target are also deduplicated.
- Orphaned SQLite search rows never self-heal (#98): the SQLite
memory_searchmirror now reconciles against Markdown (the source of truth) on every mutation, automatically pruning rows for entries that were removed, deduplicated, or rewritten in Markdown instead of accumulating permanent orphans that only manualsqlite3surgery could clear. The reconciliation pass covers whole scopes (project or target) whose Markdown file was deleted entirely, not just individually-removed entries. - Unbounded
session_searchoutput could blow the model's context window (#100): a single indexed message could produce a multi-million-character tool result. Legacysession_searchsnippets are now bounded to 1,200 characters by default (configurable 100–4,000 via a newsnippetCharsargument), and every response path — including zero-result and error responses — enforces a hard 50 KiB aggregate ceiling. Truncation is reported viaoutputChars/outputTruncated/truncatedCountmetadata instead of duplicating the full oversized text into the tool'sdetailspayload. - Runaway child
piprocess trees could hang indefinitely (#105, #106): Pi's own child-process timeout only sendsSIGTERMand considers the child handled once the signal is sent, not once it exits — a child that traps or ignoresSIGTERM(e.g. during an unbounded invalid memory-tool retry loop) could run forever, and only the direct child was ever targeted, not its descendants. Every non-interactive childpiinvocation (review, flush, correction, consolidation) now runs behind a bundled process-tree watchdog that enforces the timeout independently, escalates from gracefulSIGTERMto a hard, whole-treeSIGKILL(POSIX process groups;taskkill /T /Fon Windows), and routes cancellation through a private marker file instead of forwardingAbortSignaldirectly into the watchdog process. - Background review silently dropped a turn on a transient direct-transport failure: unlike flush, correction detection, and consolidation, the background-review handler didn't catch exceptions from the in-process direct completion path, so a throw skipped the subprocess fallback entirely instead of falling back like its siblings. Found during post-merge verification; now wrapped in the same try/catch pattern as the other three handlers.
session_search'ssnippetCharsargument accepted non-finite input:snippetChars: NaNbypassed the numeric clamp (unlike the adjacentlimitargument, which already guarded against this) and silently produced an empty snippet instead of falling back to the default. Found during post-merge verification; now guarded the same way aslimit.
Changed
- In-process direct transport extended to flush, correction save, and consolidation: session flush, correction detection, and the manual
/memory-consolidatecommand now try the same in-processcompleteSimple()transport background review already defaulted to, falling back to api -psubprocess only on failure. Since there's no subprocess on the common path, this also structurally sidesteps both the argv-leak (#95) and auth-adapter-stripping (#94) issues rather than just working around them —ctx.modelRegistryalready carries whatever OAuth/billing headers any installed adapter registered, regardless of vendor. The automatic over-capacity consolidator (MemoryStore's internalsetConsolidatorhook) stays subprocess-only by design, since it has noExtensionContextaccess. Consolidation additionally requires a direct-mode result to have actually freed space (appliedCount > 0) before skipping the subprocess fallback, since an empty result there is a functional failure, not a normal outcome.
[0.7.22] - 2026-06-28
Fixed
- Release dependency alignment: aligned direct
@earendil-works/pi-aiand@earendil-works/pi-tuipackage ranges with@earendil-works/pi-coding-agent0.80.2 sonpm installstays lockfile-clean and TypeScript sees a single compatible Pi TUI type surface in CI.
[0.7.21] - 2026-06-28
Fixed
- Custom Pi session directory indexing (#90):
/memory-index-sessionsnow respectsPI_CODING_AGENT_SESSION_DIR, so users with custom session storage do not need symlinks. - Legacy
sessions.dbproject-column migration (#91): older SQLite indexes missingprojectcolumns onsessionsormemoriesnow migrate automatically and backfillsessions.projectfrom the sessioncwd, fixing repeatedno such column: projecterrors after upgrades.
[0.7.20] - 2026-06-25
Fixed
- Self-healing corrupt SQLite database (#86):
DatabaseManager.open()now runsPRAGMA quick_checkbefore and after schema initialization and performs a lossless row-level rebuild (read readable rows → fresh DB →foreign_key_check+quick_check→ atomic swap → quarantine original) when corruption is detected, falling back to recreate-empty only if the rebuild fails. A newwithCorruptionRecovery()wrapper catchesSQLITE_CORRUPT/"malformed" errors on writes (including themessage_endlive indexer andsession_shutdownupsert paths) and heals + retries once, so a torn DB self-heals instead of logging⚠️ Live session indexing failedon every turn indefinitely. Also restoreswal_autocheckpointto SQLite's default (1000, was 100) to narrow the torn-write window. - Graceful close on corrupt handle:
DatabaseManager.close()now wrapsthis.db.close()in a try/catch so the recoveryclose()→ reopen path doesn't propagate a throw from a corrupt database handle.
[0.7.19] - 2026-06-23
Fixed
- Bounded startup session backfill (#83, #84): startup no longer synchronously parses the full session history. The
session_startbackfill now does a stat-only discovery pass and only parses files without matching stored size/mtime metadata, capped at 50 files per startup, instead of callingindexAllSessionson every Pi launch. Eliminates multi-second startup stalls for users with large session archives. - Newest-first backfill ordering (#85):
indexChangedSessionsnow sorts changed files by modification time descending before applying the per-startup cap, so recently crashed sessions are indexed on the next startup instead of waiting behind old historical files. - Shutdown metadata sync (#85): the
session_shutdownhandler now upsertssession_filesmetadata after indexing, so stored size/mtime reflects the final on-disk state. Prevents every startup from re-parsing recently-closed sessions whose metadata had gone stale.
Fixed
- Failure-memory consolidation (#68):
failures.mdnow participates in both automatic and manual consolidation flows, so full failure memory no longer gets stuck in a persistent overflow state while other core memory targets can recover.
Changed
- Skill tool naming (#66): the procedural-skill management tool is now exposed as
skill_manageto make its purpose explicit and reduce accidental use as a generic skill-discovery tool.
[0.7.13] - 2026-05-27
Fixed
- Memory store maintenance for issue #52 (#59, #52): failure memories now respect configured caps and exact dedupe,
memory removecan accept the formatted text copied directly frommemory_searchresults, and successful memory mutations return concise metadata instead of dumping large entry lists.
Changed
- Failure-memory writes now use the same guarded add path as other memory targets, so overflow handling and validation stay consistent across stores.
- The SQLite memory mirror now normalizes pasted search-result text the same way as the Markdown source of truth, keeping remove/replace behavior aligned.
[0.7.12] - 2026-05-27
Fixed
- FTS query normalization for
memory_searchandsession_search(#58): multi-word natural-language queries now behave like term-wise searches instead of accidental exact-phrase matches, while explicit quoted phrases and valid FTS operators still work. - Project memory auto-consolidation (#51): project-scoped memory writes now follow the same auto-consolidation retry flow as the global store when limits are hit.
- WAL growth controls on SQLite connections (#56): SQLite setup now bounds WAL growth with
wal_autocheckpoint,journal_size_limit, and a best-effort checkpoint on close.
Changed
- Main now includes the merged WAL follow-up from #56 together with its underlying runtime change commit (
761400d) and the later search-fix merge from #58.
[0.7.4] - 2026-05-13
Added
- Configurable correction detection patterns: Strong, weak, and negative correction patterns plus weak-pattern directive words can now be overridden with optional config fields. Omitted fields preserve the existing defaults.
Tests
- Config loading tests now use an injected temporary config path instead of writing to
~/.pi/agent/hermes-memory-config.json.
[0.7.3] - 2026-05-12
Added
- Configurable memory policy prompt (#26):
policy-onlymode now supportsmemoryPolicyStyle(full,compact,custom, ornone) andmemoryPolicyCustomText. The defaultfullstyle preserves the existing v0.7 policy prompt behavior.
Fixed
- Bun runtime SQLite compatibility (#27, #25, #24): Added a runtime fallback from
better-sqlite3tobun:sqliteinsrc/store/db.tsso memory and search features do not crash when loaded in Bun contexts. - Safer DB initialization across runtimes (#27): PRAGMA setup now consistently enables
journal_mode=WALandforeign_keys=ONfor each connection, and legacy target-constraint migration handling is hardened to avoid partial schema updates.
[0.7.2] - 2026-05-11
Fixed
- Searchable project-memory backfill: Startup now runs the same Markdown-to-SQLite sync used by
/memory-sync-markdownafter migrating legacy project folders. This makes memories in~/.pi/agent/projects-memory/<project>/MEMORY.mdsearchable viamemory_searchautomatically, including entries copied forward from the old~/.pi/agent/<project>/MEMORY.mdlayout. - Project-scoped correction search: Correction/failure memories captured while a project is active are now synced into SQLite with that project scope, so
memory_searchcan retrieve them using the project filter. - Explicit project writes:
target="project"now routes to the projectMEMORY.mdtarget explicitly before mirroring the entry into SQLite.
Tests
- Added coverage proving new-layout project Markdown is indexed into SQLite and returned by
memory_search. - Added coverage for project-scoped correction memory sync and explicit project target routing.
[0.7.1] - 2026-05-11
Fixed
- Legacy project memory migration: Users upgrading from the old
~/.pi/agent/<project>/MEMORY.mdlayout now keep their existing project memories. On startup, legacy project memory files are copied or merged into~/.pi/agent/projects-memory/<project>/MEMORY.mdwithout deleting the old folders. - Markdown backfill compatibility:
/memory-sync-markdownnow scans both the newprojects-memory/<project>layout and legacy~/.pi/agent/<project>project folders, so existing project memories can still be imported into SQLite search.
Tests
- Added migration coverage for copy, merge/dedupe, skip behavior, and legacy project backfill.
[0.7.0] - 2026-05-11
Added
- Policy-only memory prompt by default: The system prompt now appends a compact
<memory-policy>instead of dumping full Markdown memory, project memory, recent failures, and the skill index into every new session. - Legacy injection escape hatch: Set
memoryMode: "legacy-inject"to restore the previous full prompt-injection behavior for users who rely on it. - Prompt context builder: Centralized prompt assembly in
buildPromptContext()with tests for policy-only and legacy modes. - Expanded
/memory-preview-context: Shows the active policy-only prompt by default, or the full legacy memory/skill blocks when legacy mode is enabled. - v0.7 docs and task plan: Added the token-aware memory policy plan and future retrieval/router phases.
Changed
- Memory is described and handled as searchable context, not always-on authority.
- The memory policy now accurately reflects current tool behavior:
memory_searchsearches durable user, global, project-scoped, and failure memories.session_searchsearches indexed past conversation messages.skillsupportslist,view,create,patch,edit, anddelete.
- Category-filter guidance now avoids missing ordinary user/project/global memories; category filters are reserved for categorized failure/lesson memories.
- README, roadmap, in-app learning guide, Mermaid diagrams, and generated SVGs now describe policy-only as the default and
legacy-injectas opt-in. - Content scanner warnings now mention search and legacy prompt injection instead of implying all memory is always injected.
Preserved From Recent PRs
- Project-scoped memory remains under
~/.pi/agent/projects-memory/<project>/. - Windows-safe atomic writes still use temp files next to their target files and
fs.rm()cleanup. reviewRecentMessagesandflushRecentMessagesremain configurable and independently applied.
Tests
- 362 automated tests across 23 test files.
- Added policy prompt tests covering default policy-only behavior, legacy prompt assembly, accurate memory tool guidance, and stale wording regressions.
[0.6.5] - 2026-05-03
Fixed
- Background review no longer blocks interactive chat (#10): The
turn_endhandler now spawns the review subprocess as fire-and-forget instead ofawait-ing it.reviewInProgressis reset immediately so the next review cycle can proceed. Notifications are delivered asynchronously via.then(). - Auto-review errors silenced on Windows (#9): The auto-review error notification (
[hermes] auto-review failed (exit=...)) has been removed. Auto-review is best-effort — subprocess failures (non-zero exits, timeouts, spawn errors) are silently ignored. The next review cycle will retry naturally.
[0.2.0] - 2026-04-26
Added
Procedural Skills (skill tool)
- New
skilltool with actions:create,view,patch,edit,delete - Skills stored as SKILL.md files in
~/.pi/agent/memory/skills/ - Progressive disclosure — skill index (name + description only) injected into system prompt, full content loaded on demand via
skill view - Auto-extraction after complex tasks (8+ tool calls using 2+ distinct tool types in a single turn)
- Rate limited to 1 auto-extraction per session
- All skill writes pass through the same content scanner as memory writes
- New
/memory-skillscommand to list all agent-created skills
Auto-Consolidation
- When
add()would exceed the character limit, automatically trigger consolidation instead of returning an error - Consolidation spawns a one-shot
pi.exec()process that merges related entries and removes outdated ones - Parent process reloads from disk after consolidation to stay in sync with changes
- New
/memory-consolidatecommand for manual consolidation trigger - Configurable via
autoConsolidatesetting (default:true)
Correction Detection
- Detect user corrections in real-time and trigger immediate memory save
- Two-pass pattern filter:
- Strong patterns (always trigger): "don't do that", "I said...", "please don't...", "that's not what I..."
- Weak patterns (need directive clause): "no, use yarn" triggers, "no worries" does not
- Negative patterns (suppress false positives): "no worries", "actually looks great", "no problem", "stop there"
- Rate limited to 1 correction save per 3 turns
- Configurable via
correctionDetectionsetting (default:true)
Tool-Call-Aware Nudge
- Background review now triggers based on tool call count OR turn count, whichever comes first
- Counts
toolCallblocks from the session branch atturn_endtime - Default: triggers at 15 tool calls (configurable via
nudgeToolCalls) - Both turn and tool-call counters reset after each review
Updated Background Review Prompt
COMBINED_REVIEW_PROMPTnow explicitly references theskilltool- Tells the agent to use
createfor new skills andpatchfor updating existing ones - Single review pass can save both memories and skills
Changed
MemoryStore.add()is now async (returnsPromise<MemoryResult>) to support consolidation- Consolidator injected via
setConsolidator()to avoid circular imports - Background review counts tool calls from session branch instead of relying on events
Configuration
New settings in ~/.pi/agent/hermes-memory-config.json:
| Setting | Default | Description |
|---|---|---|
autoConsolidate |
true |
Auto-merge when memory hits capacity |
correctionDetection |
true |
Detect user corrections and save immediately |
nudgeToolCalls |
15 |
Tool calls before background review triggers |
Tests
- 218 total tests (up from 119 in v0.1.0)
- 99 new tests covering: auto-consolidation (9), correction detection (35), tool-call nudge (6), skill store (27), skill tool (10), skill auto-trigger (6)
Files Changed
New files (7 source + 6 test):
src/store/skill-store.ts— SkillStore class with CRUD, frontmatter parsing, progressive disclosuresrc/tools/skill-tool.ts—skillLLM tool registration and executesrc/handlers/auto-consolidate.ts— Consolidation trigger and/memory-consolidatecommandsrc/handlers/correction-detector.ts— Two-pass correction detection and immediate savesrc/handlers/skill-auto-trigger.ts— Auto-extract skills after complex taskssrc/handlers/skills-command.ts—/memory-skillscommandtests/handlers/auto-consolidate.test.tstests/handlers/correction-detector.test.tstests/handlers/skill-auto-trigger.test.tstests/store/skill-store.test.tstests/tools/skill-tool.test.ts
Modified files (8):
src/index.ts— Wire all new handlers, tools, commands, and system prompt injectionsrc/types.ts— New interfaces (ConsolidationResult,SkillIndex,SkillDocument,SkillResult) + config fieldssrc/constants.ts— New prompts (CONSOLIDATION_PROMPT,CORRECTION_SAVE_PROMPT,SKILL_TOOL_DESCRIPTION), correction patterns, updatedCOMBINED_REVIEW_PROMPTsrc/config.ts— Parse new config fields (autoConsolidate,correctionDetection,nudgeToolCalls)src/store/memory-store.ts—add()async,setConsolidator()injection, reload-after-consolidationsrc/tools/memory-tool.ts—await store.add()src/handlers/background-review.ts— Tool-call counting, OR trigger logictests/store/memory-store.test.ts— Alladd()calls migrated toawait, new config fields in test fixtures
[0.1.0] - 2026-04-20
Added
- Persistent memory via
MEMORY.md+USER.mdwith§delimiter - Real-time
memorytool (add / replace / remove) for the LLM - Content scanning: prompt injection, role hijacking, secret exfiltration, invisible unicode
- Background learning loop (every N turns via
pi.exec) - Session flush before compaction and shutdown
/memory-insightscommand- Frozen snapshot injection into system prompt (preserves Pi's prompt cache)
- Atomic writes (temp + rename)
- 119 automated tests, 0 type errors