pi-portia
v1.0.0
Published
Pi-native spatial project memory extension backed by SQLite.
Maintainers
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, but the public tool/command surface and maintenance workflows may still change before v1.0.0.
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
V1 hardening roadmap:
- additional search/list polish for long-session ergonomics
- v1 release-candidate audit and smoke testing
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-portiaAlternatively, install directly from GitHub:
pi install git:github.com/vihu/pi-portiaThen 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-portiaStorage
Portia uses a project-local SQLite database:
.pi/portia/portia.sqliteThe 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 and a bounded Portia Project Context pack selected from existing memories by prompt/path. 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.
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,
"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/commandsDefault 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 12000
Autopilot does not run a background summarizer or silently write semantic memories by itself. It makes the agent more likely to sense 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"