Skip to content

feat: add pi agent support (@codemem/pi-extension) - #1430

Open
ZeR020 wants to merge 12 commits into
kunickiaj:mainfrom
ZeR020:feat/pi-agent-support
Open

feat: add pi agent support (@codemem/pi-extension)#1430
ZeR020 wants to merge 12 commits into
kunickiaj:mainfrom
ZeR020:feat/pi-agent-support

Conversation

@ZeR020

@ZeR020 ZeR020 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes #1429

Summary

First-class support for the pi coding agent, at the same depth as the opencode integration — plus two pi-only capabilities (compaction-boundary extraction, fork-aware streams).

pi has no native MCP or hooks mechanism; its extension API is the hook surface. This PR adds the fourth client integration following the existing AdapterEvent → raw-events → sweeper → ingest pipeline:

  • @codemem/pi-extension (new npm pi-package): thin client — event ingest, per-turn system-prompt memory injection, all 14 memory_* tools registered natively (no third-party MCP adapter required). Zero native deps inside pi's process (dist imports node builtins only).
  • core: pi event adapter mirroring claude-hooks (source: "pi", deterministic pi:<session>:<entry> ids, flush-only compaction signals) + pi-derived observer config (API-key providers only in v1; OAuth explicitly unsupported-with-status; cheap-model-first; credentials resolved in-memory at point of use, never persisted/logged).
  • viewer-server: dedicated POST /api/pi-hooks (server-side source assertion) + closes the HTTP route gap so all 14 tools work over HTTP (remember, timeline, expand, schema, search_index, explain, distill_candidates).
  • cli: pi-hook-ingest (+spool, boundary flush) / pi-hook-inject, and codemem setup pi target (--pi-only, --pi-mcp adapter opt-in, --pi-extension-path dev mode; idempotent; upgrades stale npm pins without --force).
  • ui/docs: pi in settings/health client enumerations; README + docs updated in the same PR.

Shared store ⟹ multi-agent users get cross-agent memory (pack filters are project-scoped, never agent-scoped).

Attribution hardening

~20 sites silently default an event's source to "opencode"; every pi path passes source: "pi" explicitly and the ingestion route asserts it server-side. Tests assert pi-ingested rows attribute to pi and create zero opencode rows (store + route + CLI levels).

Test plan

  • New packages covered: 38 (adapter) + 21 (observer config) + 15 (routes) + 33 (extension) + 62 (setup/cli) tests — every regression from review has a test that fails against the unfixed code
  • pnpm run lint && pnpm run tsc && pnpm run test && pnpm build green
  • CONTRIBUTING injection suites: pack/pack.eval/memory-inject/claude-hooks 168/168; opencode plugin test:plugin 122/122 (no regressions for existing clients)
  • Live dogfood: pi events ingested with source=pi (0 opencode rows), retry dedupe, remember→inject round-trip, ## codemem memories block
  • Known pre-existing flakes: raw-event-sweeper/raw-event-flush/ingest-pipeline timeout-sensitive tests can fail under heavy load (they pass 179/179 in isolation) — same class exists on main, untouched by this PR

Known limitations (documented)

  • Observer extraction from pi config supports API-key providers only in v1; OAuth-only installs report unconfigured (oauth-only) with guidance instead of silent 401s
  • pi-hook-ingest lock-busy boundary-flush gap in the claude twin is pre-existing upstream behavior; pi's twin handles all paths — the claude fix is left for a follow-up PR
  • codemem setup's settings.json write is JSON (JSONC comments in pi's file are not preserved; .bak backup is written first — same behavior as the existing opencode.jsonc handling)

Copilot AI lite review requested due to automatic review settings August 10, 2026 15:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class pi coding-agent support to codemem as a new client integration, extending the existing raw-event ingest → sweeper → observer → memory pipeline so pi sessions can ingest events, inject pack context per turn, and use the full memory_* tool surface.

Changes:

  • Introduces a new @codemem/pi-extension package (thin pi extension) with HTTP-first ingest/injection and CLI fallback.
  • Adds pi ingestion and tooling support across core + viewer-server + CLI (new /api/pi-hooks, new pi-hook-ingest / pi-hook-inject, pi-aware setup + observer derivation).
  • Updates UI and docs to include pi in supported client enumerations and user-facing guidance; updates release/version automation to include the new package.

Reviewed changes

Copilot reviewed 52 out of 54 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tsconfig.json Adds the new packages/pi-extension project reference.
scripts/release-version.test.mjs Updates release-version test fixtures and expected changed-file count to include pi-extension.
scripts/release-version.mjs Adds packages/pi-extension/package.json to required markers, version reading, and version bump writes.
README.md Documents pi support, setup flow, ingest/injection/tools behavior, and caveats.
pnpm-workspace.yaml Prevents running @google/genai build script (pi transitive dev-only dependency).
packages/viewer-server/src/routes/raw-events.ts Adds POST /api/pi-hooks ingestion endpoint with server-side source: "pi" assertion.
packages/viewer-server/src/index.ts Wires memoryToolRoutes into the viewer server so memory tools work over HTTP.
packages/ui/src/tabs/settings/data/value-helpers.ts Adds a centralized agent-client source list including pi and a formatter for copy strings.
packages/ui/src/tabs/settings/data/value-helpers.test.ts Tests the pi-inclusive client enumeration + formatted list output.
packages/ui/src/tabs/settings/data/model-accessors.ts Updates observer model description copy to mention pi-derived defaults.
packages/ui/src/tabs/settings/components/ObserverPanel.tsx Updates observer connection copy/tooltips to include pi and pi-derived behavior notes.
packages/ui/src/tabs/settings/components/ObserverPanel.test.tsx Ensures observer panel copy mentions pi and pi-derived settings.
packages/ui/src/tabs/health/render/health-overview.ts Updates health recommendations copy to include pi and updates retry guidance.
packages/pi-extension/vite.config.ts Adds build/test config for the new pi extension package with thin-client bundling constraints.
packages/pi-extension/tsconfig.json TypeScript project config for the new pi extension package.
packages/pi-extension/src/viewer.ts Implements viewer discovery/auto-start plus ingest availability probing/backoff.
packages/pi-extension/src/payloads.ts Defines pi-hook payload builders and injection block formatting (CLI parity).
packages/pi-extension/src/payloads.test.ts Validates payload ↔ core adapter mapping and injection header parity.
packages/pi-extension/src/learn.ts Adds the memory_learn prompt contract payload for the pi tool surface.
packages/pi-extension/src/index.ts Main extension factory: wiring handlers, ingest/inject logic, tool registration, boundary flush behavior.
packages/pi-extension/src/config.ts Loads pi extension config from codemem config + env, with JSONC stripping.
packages/pi-extension/src/config.test.ts Tests config loading + env override behavior.
packages/pi-extension/src/client.ts HTTP-first client with CLI fallback for ingest, pack fetching, and tool calls.
packages/pi-extension/README.md Package-level pi extension documentation (install, config, lifecycle, caveats).
packages/pi-extension/package.json Declares @codemem/pi-extension package metadata, build/test scripts, and peer deps.
packages/pi-extension/LICENSE Adds MIT license file for the new package.
packages/core/src/pi-observer-config.ts Implements derivation of observer settings from pi config (API-key providers only, cheap-first).
packages/core/src/pi-hooks.ts Adds pi payload → AdapterEvent mapping, envelope building, and flush-only boundary signals.
packages/core/src/observer-client.ts Adds pi-derived observer fields and in-memory pi auth fallback into the observer auth cascade.
packages/core/src/observer-client.test.ts Adds tests for pi-derived observer auth and ensures secrets aren’t promoted/persisted.
packages/core/src/observer-auth.ts Extends auth resolution cascade to include a pi token source.
packages/core/src/observer-auth.test.ts Tests the new pi-token position in the auth cascade and precedence.
packages/core/src/index.ts Re-exports pi hooks + pi observer config APIs/types from core.
packages/cli/src/index.ts Registers new pi-hook-ingest and pi-hook-inject commands.
packages/cli/src/commands/setup.ts Adds pi setup target, optional MCP adapter support, and pi-based observer derivation wiring.
packages/cli/src/commands/setup-pi.test.ts Adds comprehensive tests for pi setup behavior, idempotency, and no-secret persistence.
packages/cli/src/commands/setup-config.ts Adds JSON writer with backup support for setup flows.
packages/cli/src/commands/pi-hook-inject.ts Implements pi-hook-inject for formatted system-prompt injection blocks (fail-open).
packages/cli/src/commands/pi-hook-inject.test.ts Tests injection behavior, fallback behavior, framing, truncation, and logging.
packages/cli/src/commands/pi-hook-ingest.ts Implements pi-hook-ingest with HTTP-first, direct DB fallback, spooling, and boundary flush.
packages/cli/src/commands/pi-hook-ingest-spool.ts Adds spool + lock implementation for durable pi ingest with backlog draining.
docs/versioning.md Adds @codemem/pi-extension to the shared versioning policy and release bump list.
docs/user-guide.md Documents pi observer derivation behavior and OAuth-only limitations.
docs/plugin-reference.md Adds pi extension installation/configuration and surface parity documentation.
docs/architecture.md Updates architecture docs and diagrams to include pi extension flow and support tier.
.github/workflows/release.yml Publishes @codemem/pi-extension as part of the release workflow.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +500 to +523
const opencodeSessionId = envelope.session_stream_id;
// Assert/normalize source server-side — never trust a caller-supplied source.
const source = "pi";
const strippedPayload = stripPrivateObj(envelope.payload) as Record<string, unknown>;

const inserted = store.recordRawEvent({
opencodeSessionId,
source,
eventId: envelope.event_id,
eventType: envelope.event_type,
payload: strippedPayload,
tsWallMs: envelope.ts_wall_ms,
});

store.updateRawEventSessionMeta({
opencodeSessionId,
source,
cwd: envelope.cwd,
project: envelope.project,
startedAt: envelope.started_at,
lastSeenTsWallMs: envelope.ts_wall_ms,
});

nudgeSweeper(sweeper, [opencodeSessionId], source);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation, applied: the local binding is now named sessionStreamId and the store API field keeps its opencodeSessionId name explicitly, so the pi route no longer implies an opencode origin. Commit aee5090.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 341b77040a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/pi-extension/src/tools.ts Outdated
Comment on lines +77 to +81
const objStart = trimmed.lastIndexOf("{");
const arrStart = trimmed.lastIndexOf("[");
const start = Math.max(objStart, arrStart);
if (start < 0) return trimmed;
return JSON.parse(trimmed.slice(start));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse CLI fallback JSON from the top-level value

When the viewer is unreachable and a native pi tool falls back to the CLI, normal JSON outputs that contain nested objects (for example {"items":[{"id":1}]} or [{"id":1}]) fail here: lastIndexOf("{") selects the inner object rather than the start of the top-level JSON value, so JSON.parse(trimmed.slice(start)) throws and the tool returns an error instead of the CLI result. This makes the documented CLI fallback unusable for most memory tools whenever HTTP is down.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed: parseCliJson now scans candidate JSON value starts in document order and returns the first suffix that parses, so nested shapes like the items-with-inner-object example work with viewer down. Added regression tests for the exact nested-object case plus logs-before-JSON. Commit aee5090.

Comment thread packages/pi-extension/src/tools.ts Outdated
Comment on lines +824 to +829
const body = {
...(params as Record<string, unknown>),
project:
projectOrClient(params as Record<string, unknown>, client) ??
(params as { project?: string }).project,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not add project when distilling all projects

When the caller sets all_projects: true, this still fills project from the current pi session via projectOrClient, so the HTTP route receives both all_projects and project and rejects the request, and the CLI fallback similarly builds distill -p <project> -A. In the common case where client.project is set, the pi memory_distill_candidates tool cannot actually run across all projects.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against the copy in memory-tools.ts:332, which rejects project combined with all_projects, so this was a real failure mode when client.project was set. buildDistillBody now drops project whenever all_projects is requested, on both the HTTP body and the CLI fallback args. Regression tests cover client.project being set. Commit aee5090.

@ZeR020

ZeR020 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

@kunickiaj Could you please take a look at #1430 when you have a moment? It has been open since August 10; latest main is merged and CI is green. Happy to address any feedback. Thank you.

@kunickiaj kunickiaj left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting substantial work into this. I’m open to supporting pi, especially if you’re willing to remain the primary maintainer for the integration.

I can’t merge this in its current form, though. This is not only about the 12.6k-line size: review found several correctness and credential-handling blockers, detailed inline.

The main blockers are:

  • A pi API key can be offered to a different observer provider, potentially sending one provider’s credential to another provider’s endpoint.
  • File-context enrichment can replace string tool output instead of appending to it.
  • Compaction/shutdown extraction receives a seven-second timeout despite performing retries, spool draining, and an LLM call.
  • Injection framing is detected using a substring of untrusted memory content.
  • Project resolution uses the cwd basename while ingestion uses the Git root, silently breaking retrieval in monorepos and subdirectories.
  • The successful boundary path flushes before draining the spooled backlog.
  • The new package version is already out of alignment with the release set.

There are also some important follow-ups:

  • The new memory-tool POST routes should reuse the bounded JSON parser and validate filter types.
  • The spool/locking implementation should be shared rather than adding a third copy of concurrency-sensitive code.
  • MCP/viewer memory access and schema behavior should share core primitives to avoid authorization and contract drift.
  • The retrieval-ledger behavior for pi’s preferred HTTP path needs an explicit decision.

I’m also asking that this be converted into a stack rather than continuing as one PR. The existing commits already roughly follow suitable boundaries, so this should preserve the work rather than require starting over. A reasonable stack would be:

  1. Shared spool/memory-access refactors
  2. Core pi event adapter
  3. Viewer routes
  4. Pi observer configuration/auth
  5. CLI ingest/inject
  6. Pi extension lifecycle/client
  7. Native tools
  8. Setup, UI, docs, and release wiring

Graphite or GitHub’s native stacked-PR workflow is fine. Each layer should remain independently reviewable and testable.

Local validation at 898ccbdd:

  • Build passed
  • TypeScript passed
  • Biome passed
  • Targeted tests passed: 292/292 after generating viewer assets
  • pnpm run release:version -- check failed because @codemem/pi-extension is 0.40.1 while the release set is 0.40.2

Once the stack is created and the blocking findings are addressed, I’m happy to review it layer by layer.

// cfg.observerApiKey (that would look "explicit" and could be persisted
// by callers of toConfig()). Only used as a lower-priority cascade source.
if (!this._apiKey) {
this._piApiKey = resolvePiApiKeyForObserver();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: resolvePiApiKeyForObserver() is not provider-scoped. _piApiKey is subsequently offered to the custom, Anthropic, and OpenAI auth branches. If pi selects an API key for provider A while codemem explicitly configures provider B, provider A’s key can be transmitted to provider B’s endpoint.

Please resolve the credential for the effective observer provider and return it only when the pi provider matches. Add a regression test with an explicit Anthropic/OpenAI observer and an unrelated pi provider key.

try {
const block = await fetchFileContextBlock(client, path, ctx.signal);
if (block) {
const existing = Array.isArray(event.content) ? [...event.content] : [];

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: when event.content is a string, existing becomes [], so the read result is replaced by the memory block. This package’s serializer already handles string content, and pi’s tool result contract permits text content.

Normalize string content into a text item before appending, and add a regression test asserting that the original file contents remain present.

const { stdout } = await this.execCodemem(["pi-hook-ingest"], {
stdin: JSON.stringify(payload),
signal,
timeoutMs: this.config.httpTimeoutMs + 2000,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: boundary events always use this CLI path, but the default timeout is only seven seconds. The command may perform HTTP retries, lock acquisition, spool draining, and flushRawEvents, including an observer LLM request. It can therefore receive SIGTERM during the extraction that shutdown/compaction is specifically intended to preserve.

Boundary events need a substantially larger or independently configured timeout, plus a test demonstrating that a long-running flush is not killed.

if (!packOrBlock.trim()) return;

// pi-hook-inject already returns the full ## block; HTTP pack is bare pack_text.
const block = packOrBlock.includes("## codemem memories")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this treats a substring inside recalled memory content as proof that the transport returned a preformatted block. A memory that happens to contain ## codemem memories bypasses formatPiInjectionBlock() and therefore the reference-data safety framing.

Please have fetchPackText() return a discriminated result such as { text, preformatted } instead of inferring transport state from content.

}

/** Best-effort project label from cwd basename (viewer accepts explicit project). */
static projectFromCwd(cwd: string): string | null {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: ingestion resolves the project from the Git root, but this derives retrieval scope from the cwd basename. Running pi from repo/packages/api can write memories under repo and query them under api, producing silently empty injection and tool results.

Please match core’s project resolution semantics, including walking to the repository root, and add a nested-cwd regression test.

// 1. Unlocked HTTP attempt — fast path when the viewer is up.
const httpResult = await httpIngest(payload, opts.host, port);
if (httpResult.ok) {
await flushOnBoundaryIfRequested();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the successful HTTP path flushes before draining the backlog. For a shutdown/compaction event, spooled earlier events are inserted only after the final extraction, leaving the session tail unprocessed.

Drain the backlog before flushOnBoundaryIfRequested(). The locked retry path already uses the correct ordering.

@@ -0,0 +1,69 @@
{
"name": "@codemem/pi-extension",
"version": "0.40.1",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pnpm run release:version -- check currently fails because this package is 0.40.1 while the managed release set is 0.40.2. Please align it before the release-wiring layer lands.

const store = getStore();
let body: unknown;
try {
body = await c.req.json();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new POST routes use unbounded c.req.json() even though raw-events.ts already has a bounded JSON parser. Please reuse the bounded parser for remember, expand, explain, and distill_candidates, and validate filter value types before passing them to SQLite.

@ZeR020

ZeR020 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@kunickiaj Thank you for the review.

I split the work into the eight stacked PRs you asked for, rebased onto current main (0.41). This PR stays open; it is not the merge path.

Please start at #1473 (layer 1; mergeable). Layers 2–8 are on the fork for now, because GitHub cannot base a PR on kunickiaj:stack/…:

After #1473 merges I will retarget layer 2 at this repo’s main, so each later layer lands here one at a time.

The seven blockers are fixed in the layer that owns them, with tests. I will do what I can on the pi side and fix issues or bugs in this integration. I cannot guarantee I will maintain it forever.

Happy to walk through any layer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add pi agent support

3 participants