pi-portia
Pi-native spatial project memory extension backed by SQLite.
Package details
Install pi-portia from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-portia- Package
pi-portia- Version
1.1.0- Published
- May 22, 2026
- Downloads
- 213/mo · 14/wk
- Author
- vihu89
- License
- MIT
- Types
- extension
- Size
- 281.3 KB
- Dependencies
- 1 dependency · 3 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Pi Portia
Pi-native spatial project memory for agents.
Portia is a project-local, inspectable memory layer backed by SQLite. It stores pointers, gotchas, decisions, invariants, purpose, patterns, and plans that help future agents re-perceive code faster. It does not replace reading source files.
Status
Beta.
Portia is usable for day-to-day local project memory. The core workflows are implemented and validated. In the v1.x line, the documented public surface follows Semantic Versioning; renderer wording, diagnostics, ranking weights, and maintenance ergonomics may continue to evolve in compatible minor or patch releases.
Implemented now:
- SQLite database creation and migrations using
better-sqlite3 - project-local DB at
.pi/portia/portia.sqlite /portia-status/portia-doctor/portia-reindex [dry-run]/portia-sense <path> [query]/portia-listwith structured filters, configurable page limits, and cursor pagination/portia-search <query>with safe FTS5 search, ranking, snippets, filters, and cursor pagination/portia-inspect <id>/portia-repair <id> <stale|delete|reactivate> <reason>/portia-delete <id> <reason>soft-delete convenience command/portia-trailspheromone trail browserportia_senseread-only toolportia_recordwrite/proposal toolportia_listread-only toolportia_searchread-only toolportia_doctorread-only toolportia_inspectread-only toolportia_repairwrite/proposal tool- turn-local autopilot guidance and bounded context injection
- automatic pheromone trace capture for exposed/followed/validated memories
- conservative pheromone-aware retrieval ranking with visible
PHEROMONEsignals - generated search-term expansion for code paths and camelCase identifiers
- documented SQLite data-location and portability model (no built-in backup/import/export workflow planned for v1)
- parser, renderer, tool-schema, and migration fixture test coverage
- changelog plus release, semver, migration, and package checklist documentation
Near-term hardening roadmap:
- additional search/list/autopilot polish for long-session ergonomics
- release audit and smoke testing on each publish
Deferred until measured need:
- vector search
- trigram accelerator for substring fallback
- cloud/remote sync
- automatic broad search on every agent turn
- rich TUI dashboards
Installation
Install from npm with Pi:
pi install npm:pi-portia
Alternatively, install directly from GitHub:
pi install git:github.com/vihu/pi-portia
Then restart Pi, or run /reload in an existing session if your Pi version supports extension reloads.
For local development from a checkout:
git clone https://github.com/vihu/pi-portia.git
cd pi-portia
npm install
pi -e .
To use a local checkout globally without publishing/installing from GitHub, add its absolute path to Pi settings or run:
pi install /absolute/path/to/pi-portia
Storage
Portia uses a project-local SQLite database:
.pi/portia/portia.sqlite
The database is the complete Portia data store for the checkout. It is intended to be shared by all agents working in the same checkout and may still be excluded from Git by a global ignore rule. Portia does not add a separate backup/export/import layer for v1; if you need to move a project memory store, move or copy this SQLite file, preferably while Pi is not writing to it or using normal SQLite-safe copy practices.
Usage
Portia includes a small autopilot layer. On each agent turn it can add turn-local guidance, a bounded Portia Project Context pack selected from existing memories by prompt/path, and an intent-gated historical recall assist for prompts that clearly ask about prior decisions, evidence, or memory history. This should make Portia useful during normal work without adding persistent boilerplate messages to the session.
You can still run explicit commands:
/portia-status
/portia-doctor
/portia-reindex dry-run
/portia-reindex
/portia-sense src/auth token expiry
/portia-list
/portia-list all
/portia-list kind decision
/portia-list scope src/auth
/portia-list query autopilot
/portia-list limit 50
/portia-list scope src/auth cursor <nextCursor>
/portia-list cursor <nextCursor> query autopilot
/portia-search portia search limits
/portia-search query max sense results
/portia-search kind decision search limits
/portia-search scope src limit 50 fts
/portia-search match any order updated query /portia-list
/portia-search scope src limit 50 cursor <nextCursor> query fts
/portia-inspect <memory-id>
/portia-trails
/portia-trails recent
/portia-trails memory <memory-id>
/portia-repair <memory-id> delete Temporary test memory; safe to hide from active retrieval.
/portia-delete <memory-id> Temporary test memory; safe to hide from active retrieval.
Tool/command quick reference:
| API | Use for | Notes |
|---|---|---|
portia_sense / /portia-sense |
bounded path/task context | compact output for agent context; not for exhaustive browsing |
portia_search / /portia-search |
explicit keyword search | safe FTS5 queries, snippets, filters, and cursor pagination |
portia_list / /portia-list |
structured inventory/audit browsing | status/kind/scope/query filters, configurable limits, and cursor pagination |
portia_doctor / /portia-doctor |
database health diagnostics | read-only checks for schema, FTS, triggers, search terms, and orphaned rows |
/portia-reindex |
search index maintenance | command-only; recomputes search_terms and rebuilds FTS when write policy allows |
portia_inspect / /portia-inspect |
full details for one memory | provenance, event history, and pheromone summary |
portia_record |
write or propose durable memories | honors writePolicy/workerWritePolicy |
portia_repair / /portia-repair |
soft-repair memory status | marks stale/deleted/active without physical deletion |
For more detailed agent guidance, see docs/agent-usage.md.
portia_sense returns compact memories with ids, scopes, kinds, and retrieval signals. Use it for bounded path/task context before unfamiliar work. Treat the output as pointers to re-read source files and commands, not as complete ground truth. When pheromones are enabled, reinforced memories may receive a bounded PHEROMONE boost, but only after they were already selected by normal proximity/dependency/FTS candidate generation.
Use portia_search//portia-search for explicit keyword search across memories, especially in long sessions where portia_sense is intentionally too bounded. Search is the right tool for prior decisions, old validation notes, package names, error strings, and broad concept recall. Search supports status, kind, scope, ordering, match mode, substring fallback, configurable page limits, and opaque cursor pagination. Use the returned nextCursor with the same query and filters to continue browsing additional pages; cursors validate against the original query/filter fingerprint and do not store the full query.
Autopilot can also assist search-shaped turns. In default autoSearchMode: "assist", medium-confidence historical prompts receive a concrete portia_search suggestion, while high-confidence prompts can receive a tiny Portia Historical Recall Preview generated from internal search. The preview is bounded and pointer-only; it omits DB paths, cursor metadata, no-hit sections, and full provenance. Routine path/edit/test prompts should continue to rely on portia_sense and should not receive search preview noise.
Search query text is plain input, not raw FTS syntax. Portia quotes search terms before sending them to SQLite FTS5, so code-like literals such as /portia-list, src/config.ts, foo:bar, -6, and words like AND/OR are treated safely instead of as operators. Default matchMode is all; use match any for broader recall or match phrase for an exact phrase. Generated search_terms help component searches find code/camelCase text such as maxSenseResults from max sense results.
Use portia_list//portia-list for structured inventory browsing/auditing. List keeps query as a simple case-insensitive substring inventory filter over memory title, body, scope, kind, and provenance; use portia_search for FTS relevance ranking and snippets. List output includes page metadata and nextCursor when more rows are available. Repeat the same status/scope/kind/query filters with the cursor to continue browsing. In slash-command syntax, put cursor <nextCursor> before query <text> because query consumes the rest of the command.
Use portia_inspect//portia-inspect to view one memory with provenance, event history, and a compact pheromone summary, and portia_repair//portia-repair to soft-mark memories stale, deleted, or active again via reactivate. Repair keeps rows and appends memory events; it does not physically delete records. /portia-delete <id> <reason> is a shorter human-facing alias for soft deletion. Use /portia-trails to inspect reinforced, weak, recent, or per-memory pheromone traces.
The main agent can call portia_record after verified durable project findings, for example:
Record a Portia memory: scope src/auth, kind gotcha, title Auth fixtures, body Login tests require seeded user fixtures; read tests/auth before changing auth behavior.
portia_record writes immediately only when the effective write policy is write. In readonly and current confirm mode, it returns a structured proposal and does not persist a memory. It blocks exact duplicate active memories by default (duplicatePolicy: "blockExact"), can return related-memory warnings, and accepts supersedesId to create a replacement memory while atomically marking the old active memory superseded.
Use sourceType and sourceRef for provenance. When promoting an observational-memory fact, set sourceType to observation or reflection and put the observation/reflection id in sourceRef.
Use portia_doctor//portia-doctor for read-only health diagnostics. Doctor checks the schema version, expected tables/columns, FTS availability and row consistency, FTS maintenance triggers, null search_terms, orphaned event/pheromone/trace/edge rows, foreign-key integrity, and DB path. It reports warnings/errors only; it does not mutate the database.
The FTS index is maintained by SQLite triggers. Schema migrations rebuild the external-content FTS index when indexed columns change, including the generated search_terms column used for code/camelCase search expansion. Use /portia-reindex dry-run to preview search maintenance and /portia-reindex to recompute all generated search_terms and rebuild memory_fts. Reindex honors the effective write policy; if policy is not write, it reports what would happen without applying changes.
Settings
Global settings live in Pi's agent settings file. Project settings live in .pi/settings.json and override global settings.
{
"portia": {
"enabled": true,
"dbPath": ".pi/portia/portia.sqlite",
"writePolicy": "confirm",
"workerWritePolicy": "readonly",
"maxSenseResults": 12,
"searchDefaultLimit": 30,
"searchMaxResults": 250,
"listDefaultLimit": 30,
"listMaxResults": 250,
"enableDependencyScan": true,
"enableFts": true,
"enableVectors": false,
"autoPromptGuidance": true,
"autoRecordGuidance": true,
"autoSense": true,
"autoSenseMaxResults": 5,
"autoSenseMaxChars": 2500,
"autoSearchMode": "assist",
"autoSearchMaxResults": 5,
"autoSearchMaxChars": 900,
"enablePheromones": true,
"pheromoneRanking": true,
"pheromoneHalfLifeDays": 30,
"pheromoneMaxBoost": 25,
"pheromoneFollowWeight": 1,
"pheromoneSuccessWeight": 2,
"pheromoneFailureWeight": -0.4,
"pheromoneIgnoredWeight": 0,
"pheromoneWorkerPolicy": "off",
"traceRetentionDays": 180,
},
}
Environment override:
PORTIA_MODE=readonly # force read-only/proposal-only behavior
PORTIA_MODE=off # disable Portia tools/commands
Default public behavior is conservative: writePolicy defaults to confirm, which currently returns a proposal. If you want the main agent to record durable memories without asking every time, set:
{
"portia": {
"writePolicy": "write",
},
"pi-fork": {
"environment": { "PORTIA_MODE": "readonly" },
},
"pi-minimal-subagent": {
"environment": { "PORTIA_MODE": "readonly" },
},
}
That gives the main session automatic Portia writes while fork/subagent child Pi processes remain proposal-only.
Autopilot settings:
autoPromptGuidance: add turn-local Portia guidance to the system promptautoRecordGuidance: includeportia_recordguidance in that prompt sectionautoSense: internally retrieve a bounded context pack for each turnautoSenseMaxResults: max memories in that pack, capped at 12autoSenseMaxChars: max rendered pack size, capped at 12000autoSearchMode: historical recall assist mode, one ofoff,suggest,assist, orcontext; defaultassistautoSearchMaxResults: max search hits in automatic preview/suggestion limits, default5, capped at 12autoSearchMaxChars: max rendered automatic search preview size, default900, capped at 8000
Mode behavior:
| Mode | Behavior |
|---|---|
off |
No search-intent suggestion and no internal historical search preview. |
suggest |
Detected historical/broad-memory prompts append a concrete portia_search suggestion only; no internal search is performed. |
assist |
Default middle ground: medium-confidence prompts get the suggestion only; high-confidence prior-decision/provenance/audit prompts get a tiny internal preview plus the suggestion. |
context |
Stronger opt-in: detected medium/high confidence prompts can receive compact internal search preview plus the suggestion. |
Autopilot does not run a background summarizer or silently write semantic memories by itself. It makes the agent more likely to sense, search, and record intentionally.
Search and browse settings:
maxSenseResults: default maximum forportia_sense; capped at 50 so context retrieval stays boundedsearchDefaultLimit: default page size forportia_search; default30searchMaxResults: maximum acceptedportia_searchpage size; default250, absolute cap500listDefaultLimit: default page size forportia_list; default30listMaxResults: maximum acceptedportia_listpage size; default250, absolute cap500
Pheromone settings:
enablePheromones: record behavioral traces and summary pheromone strengthpheromoneRanking: allow bounded pheromone boosts inportia_senseranking; set false for debug/dark-mode operationpheromoneHalfLifeDays: lazy decay half-life for stored strengthpheromoneMaxBoost: maximum rank points a positive pheromone can contributepheromoneFollowWeight: weight when an exposed memory's scope/source is read or editedpheromoneSuccessWeight: additional weight when validation passes after following a memorypheromoneFailureWeight: weak negative weight when validation fails after following a memorypheromoneIgnoredWeight: weight for exposed-but-unfollowed memories; default0pheromoneWorkerPolicy:off,low, orwritebehavior when the effective write policy is readonlytraceRetentionDays: retention horizon for raw trace events
Pheromones adjust salience of existing active memories. They do not create new semantic memories automatically.
Development
pi-portia supports Node.js 22 or newer; CI validates Node 22 and 24.
See CHANGELOG.md for release notes and docs/release.md for the release checklist, semver policy, and migration/data-location policy.
npm run typecheck
npm test
pi -e .
If pi-portia is also installed globally, avoid duplicate tool registration during local smoke tests by loading only the explicit checkout:
PI_OFFLINE=1 pi --no-extensions -e . --no-session -p "/portia-status"