Skip to content

feat(pi): add full lifecycle hooks to pi integration#604

Open
paulodearaujo wants to merge 1 commit into
rohitg00:mainfrom
paulodearaujo:feat/pi-full-lifecycle-hooks
Open

feat(pi): add full lifecycle hooks to pi integration#604
paulodearaujo wants to merge 1 commit into
rohitg00:mainfrom
paulodearaujo:feat/pi-full-lifecycle-hooks

Conversation

@paulodearaujo
Copy link
Copy Markdown

@paulodearaujo paulodearaujo commented May 22, 2026

Add the missing lifecycle hooks to the pi integration, bringing it from
3 hooks to 6 — matching Codex CLI parity.

Problem

The pi integration only used session_start, before_agent_start, and
agent_end. Pi's extension API exposes 29 lifecycle events, but most
were unused. As a result:

  • ~90% of observations were lost — only one generic observe per agent
    turn instead of per-tool-call granularity.
  • No session/start call — the server never loaded project profiles.
  • No session/end — no summarization or consolidation on shutdown.
  • No pre-compact hook — observations were lost when pi compacted
    context.

Changes

Hook API call Before After
session_start POST /session/start ❌ health check only ✅ notifies server
before_agent_start POST /smart-search ✅ (unchanged)
tool_result POST /observe ✅ per tool call
session_before_compact POST /summarize
agent_end POST /observe ✅ (unchanged)
session_shutdown POST /summarize + POST /session/end

Additional improvements:

  • Fix deprecated import: @mariozechner/pi-coding-agent
    @earendil-works/pi-coding-agent
  • Client-side SHA-256 dedup: 5-minute window mirrors the server's
    dedup, avoids unnecessary HTTP round-trips during rapid tool bursts
  • Extract observePayload() helper: reduces repetition across
    observe calls
  • Await shutdown calls: session_shutdown awaits both HTTP calls
    sequentially instead of fire-and-forget, preventing data loss when the
    process exits immediately after the hook returns

How to verify

  1. Start agentmemory: agentmemory
  2. Copy integrations/pi/ to ~/.pi/agent/extensions/agentmemory/
  3. Start pi, run a few tool calls, then quit
  4. Check the viewer at http://localhost:3113 — observations should
    appear for each tool call, not just one per agent turn
  5. Session end and summarize should fire on quit

Note on system prompt injection

The before_agent_start hook still mutates the system prompt per turn
(same as before this PR). A follow-up could explore one-shot injection
via pi.sendMessage at session_start to preserve prefix cache, but
that is a separate concern and out of scope here.

Summary by CodeRabbit

  • New Features

    • Added lifecycle hooks for session start/end, agent start/end, tool results, and pre-compact processing.
    • Image scrubbing replaces embedded image payloads with placeholders before sending observations.
    • Optional request timeout for agent memory calls.
  • Improvements

    • Client-side deduplication prevents repeated observation submissions within a 5‑minute window and prunes old entries.
    • Unified observation envelope for more consistent posting, improved session synchronization, and background summarization triggers.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

@paulodearaujo is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

The PI extension import is updated; helpers for base URL normalization, SHA‑256 hashing, image-scrubbing, and client-side deduplication (5‑minute window) are added. callAgentMemory gains timeout support. Session and tool lifecycle hooks are reorganized to post deduplicated observations, trigger background context/summarize, and sequence summarize then session/end on shutdown.

Changes

PI Extension Lifecycle and Deduplication

Layer / File(s) Summary
Dependency and helper foundation
integrations/pi/index.ts
ExtensionAPI import is switched to @earendil-works/pi-coding-agent. Adds getBaseUrl() for URL normalization, a SHA-256 helper, image-detection/strip utilities, extends callAgentMemory to accept timeoutMs and wires AbortSignal.timeout, and introduces in-memory deduplication state with a 5-minute window and pruning.
Unreachable messages and formatting
integrations/pi/index.ts
Replaces hardcoded localhost in unreachable/health messages with the computed getBaseUrl() and repositions nearby section comments.
Tools and Session lifecycle wiring
integrations/pi/index.ts
Adds a shared observePayload envelope builder. Updates session_start to prefer session-file-derived sessionId and only post after health check; before_agent_start now observes prompts (with timeout), runs smart-search and builds recall formatting; tool_result, session_before_compact, and agent_end post deduplicated, image-scrubbed observations; session_shutdown waits for in-flight POSTs, runs summarize, then posts session/end with sessionId, project, cwd, and timestamp.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop through hooks and payload streams,
Hashing echoes into silent dreams,
I scrub the images, prune the noise,
Post once, not twice — tidy joys,
Then nudge a gentle summarize.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(pi): add full lifecycle hooks to pi integration' accurately and specifically describes the main change: extending the pi integration with additional lifecycle hooks to achieve feature parity.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@integrations/pi/index.ts`:
- Around line 332-348: The background observe posts started from the
pi.on("tool_result") (and the similar agent_end handler) can still be in-flight
when session_shutdown summarizes/closes; track and await these before
finalizing. Create a shared pendingObserves Set/array used by
callAgentMemory("observe") callers: when you call callAgentMemory(...) from the
tool_result and agent_end handlers, push the returned Promise into
pendingObserves and remove it on settle; in session_shutdown (and any
summary/close function) await Promise.all([...pendingObserves]) (or drain the
set) before proceeding to summarize/close to ensure no observe requests are
dropped. Ensure the same pattern is applied to the other observe emitters
mentioned.
- Around line 399-420: The session_shutdown handler awaits
callAgentMemory("summarize", ...) and callAgentMemory("session/end", ...)
without a timeout or AbortSignal, so PI shutdown can hang; modify the handler in
integrations/pi/index.ts (the pi.on("session_shutdown", ...) block) to create an
AbortController with a bounded timeout (e.g., setTimeout to controller.abort
after N ms), pass controller.signal into callAgentMemory (and update
callAgentMemory to accept and forward an AbortSignal to fetch if it doesn't
already), and ensure you abort/clear the timer after the requests complete (use
Promise.race or Promise.allSettled to await both with the signal) so the
shutdown path always finishes promptly even if the server never responds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c26e5dd2-47d5-4eae-975b-bc9f53c00168

📥 Commits

Reviewing files that changed from the base of the PR and between bc64107 and 225716c.

📒 Files selected for processing (1)
  • integrations/pi/index.ts

Comment thread integrations/pi/index.ts
Comment thread integrations/pi/index.ts
@paulodearaujo paulodearaujo force-pushed the feat/pi-full-lifecycle-hooks branch from 225716c to 1e847e3 Compare May 22, 2026 14:55
Wire tool_result, session_shutdown, session_before_compact, and
session/start hooks that pi's extension API already supports but
were not used. Fix deprecated @mariozechner import.

Before: 3 hooks (session_start, before_agent_start, agent_end).
After:  6 hooks matching Codex parity.

Changes:
- tool_result: POST /observe per tool call with dedup
- session_shutdown: await POST /summarize + /session/end
- session_before_compact: POST /summarize before context loss
- session_start: POST /session/start for project profile loading
- Fix import to @earendil-works/pi-coding-agent
- Extract observePayload() helper to reduce repetition
- Add client-side SHA-256 dedup (5min window, mirrors server)
@paulodearaujo paulodearaujo force-pushed the feat/pi-full-lifecycle-hooks branch from 1e847e3 to f4a4a47 Compare May 22, 2026 14:57
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
integrations/pi/index.ts (1)

357-373: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Drain in-flight observe posts before final summarize/session/end.

tool_result and agent_end enqueue background observe calls, but session_shutdown can finalize before they settle. That can drop the last observations or summarize without them.

Suggested minimal patch
+  const pendingObserves = new Set<Promise<unknown>>();
+
+  function trackObserve<T>(promise: Promise<T>): Promise<T> {
+    pendingObserves.add(promise);
+    void promise.finally(() => pendingObserves.delete(promise));
+    return promise;
+  }
+
   pi.on("tool_result", (event) => {
@@
-    void callAgentMemory("observe", {
+    void trackObserve(callAgentMemory("observe", {
       body: observePayload({
         tool_name: event.toolName,
         tool_input: input.slice(0, 8000),
         tool_output: output.slice(0, 8000),
         is_error: event.isError ?? false,
       }),
       timeoutMs: 3000,
-    });
+    }));
   });
@@
-    void callAgentMemory("observe", {
+    void trackObserve(callAgentMemory("observe", {
       body: observePayload({
         tool_name: "conversation",
         tool_input: lastPrompt.slice(0, 8000),
         tool_output: assistantText.slice(0, 8000),
       }),
       timeoutMs: 3000,
-    });
+    }));
   });
@@
   pi.on("session_shutdown", async () => {
     if (!lastHealthOk) return;
+    await Promise.allSettled([...pendingObserves]);
 
     await callAgentMemory("summarize", {
       body: { sessionId },
       timeoutMs: 120_000,
     });

Also applies to: 400-415, 424-440

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/pi/index.ts` around lines 357 - 373, The background observe
calls started in the pi.on("tool_result") handler (and similar handlers around
agent_end/session_shutdown) can still be in-flight when session finalization
runs, so add a simple inflight tracker: create a Set or array (e.g.,
inflightObserves) and when you call callAgentMemory("observe", ...) from the
observed places (tool_result, agent_end), push the resulting Promise into
inflightObserves and attach .finally() to remove it; then update the
session_shutdown / summarize / session_end flow to await
Promise.race([Promise.all(inflightObserves), timeoutPromise(ms)]) or otherwise
drain the Set with a short timeout to ensure the last observes complete before
finalizing; reference the event handler pi.on("tool_result"),
callAgentMemory("observe"), observePayload, and the session_shutdown/agent_end
summarization entry points when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@integrations/pi/index.ts`:
- Around line 357-373: The background observe calls started in the
pi.on("tool_result") handler (and similar handlers around
agent_end/session_shutdown) can still be in-flight when session finalization
runs, so add a simple inflight tracker: create a Set or array (e.g.,
inflightObserves) and when you call callAgentMemory("observe", ...) from the
observed places (tool_result, agent_end), push the resulting Promise into
inflightObserves and attach .finally() to remove it; then update the
session_shutdown / summarize / session_end flow to await
Promise.race([Promise.all(inflightObserves), timeoutPromise(ms)]) or otherwise
drain the Set with a short timeout to ensure the last observes complete before
finalizing; reference the event handler pi.on("tool_result"),
callAgentMemory("observe"), observePayload, and the session_shutdown/agent_end
summarization entry points when making these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 753c3a36-e693-4679-bd07-9a82a06e67fb

📥 Commits

Reviewing files that changed from the base of the PR and between 225716c and 1e847e3.

📒 Files selected for processing (1)
  • integrations/pi/index.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
integrations/pi/index.ts (1)

102-126: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make helper timeouts opt-out, not opt-in.

timeoutMs only protects callers that remember to pass it. In this file, interactive paths like memory_search and memory_save still omit it, so a stalled agentmemory request can hang PI indefinitely. A bounded default here is safer, and the long-running hooks can still override it explicitly.

Suggested fix
 async function callAgentMemory<T>(
   pathname: string,
   options?: {
     method?: "GET" | "POST";
     body?: unknown;
     baseUrl?: string;
     timeoutMs?: number;
   },
 ): Promise<T | null> {
   const baseUrl = options?.baseUrl?.replace(/\/+$/, "") || getBaseUrl();
   const method = options?.method || "POST";
+  const timeoutMs = options?.timeoutMs ?? 5000;
   const url = `${baseUrl}/agentmemory/${pathname.replace(/^\/+/, "")}`;
   const headers: Record<string, string> = {};
   const secret = process.env.AGENTMEMORY_SECRET;
   guardPlaintextBearerAuth(baseUrl, secret);
   if (options?.body !== undefined) headers["Content-Type"] = "application/json";
   if (secret) headers.Authorization = `Bearer ${secret}`;
@@
     const response = await fetch(url, {
       method,
       headers,
       body: options?.body !== undefined ? JSON.stringify(options.body) : undefined,
-      signal: options?.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined,
+      signal: AbortSignal.timeout(timeoutMs),
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/pi/index.ts` around lines 102 - 126, callAgentMemory currently
leaves requests unbounded unless callers pass timeoutMs, which lets interactive
paths hang; change it to apply a sensible default timeout when
options?.timeoutMs is undefined (e.g., set const timeout = options?.timeoutMs ??
DEFAULT_TIMEOUT_MS) and pass AbortSignal.timeout(timeout) into the fetch call so
callers (memory_search, memory_save, etc.) can still override by providing
timeoutMs; keep existing logic for headers, baseUrl and
guardPlaintextBearerAuth, and choose a constant DEFAULT_TIMEOUT_MS near other
service timeouts in the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@integrations/pi/index.ts`:
- Around line 311-319: The background call to callAgentMemory("session/start")
(with sessionId/currentProject) can race with before_agent_start/smart-search
and cause first-turn recall to miss newly-loaded profiles; change this to await
the call (or otherwise wait for its completion) instead of firing it void with
an 800ms timeout so session/context initialization finishes before
before_agent_start runs — update the callAgentMemory invocation near the
lastHealthOk branch to await the promise (and remove or extend the timeout) or
gate before_agent_start on the callAgentMemory promise so the server has
finished loading profiles/context before first-turn recall executes.
- Around line 332-354: The health check (refreshStatus(ctx)) must run before
blocking agentmemory calls so an outage doesn't cause repeated turn latency; in
before_agent_start call refreshStatus(ctx) prior to invoking
trackPost/callAgentMemory("observe") and before awaiting
callAgentMemory("smart-search"), and if refreshStatus indicates the backend is
unhealthy, skip or short-circuit the smart-search/recall logic (i.e., avoid
awaiting callAgentMemory and set recallBlock = ""), keeping the rest of the flow
(lastPrompt, results/recallBlock usage) unchanged.

---

Outside diff comments:
In `@integrations/pi/index.ts`:
- Around line 102-126: callAgentMemory currently leaves requests unbounded
unless callers pass timeoutMs, which lets interactive paths hang; change it to
apply a sensible default timeout when options?.timeoutMs is undefined (e.g., set
const timeout = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS) and pass
AbortSignal.timeout(timeout) into the fetch call so callers (memory_search,
memory_save, etc.) can still override by providing timeoutMs; keep existing
logic for headers, baseUrl and guardPlaintextBearerAuth, and choose a constant
DEFAULT_TIMEOUT_MS near other service timeouts in the codebase.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ecebc51-4519-45d3-8118-58436d374682

📥 Commits

Reviewing files that changed from the base of the PR and between 1e847e3 and f4a4a47.

📒 Files selected for processing (1)
  • integrations/pi/index.ts

Comment thread integrations/pi/index.ts
Comment on lines +311 to +319
if (lastHealthOk) {
void callAgentMemory("session/start", {
body: {
sessionId,
project: currentProject,
cwd: currentProject,
},
timeoutMs: 800,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await session/start before relying on first-turn recall.

This hook is supposed to initialize session context, but it is fired in the background with an 800 ms timeout. before_agent_start can race ahead and query smart-search before the server has finished loading profiles/context, so the first recall block can miss the very data this PR is adding.

Suggested fix
     if (lastHealthOk) {
-      void callAgentMemory("session/start", {
+      await callAgentMemory("session/start", {
         body: {
           sessionId,
           project: currentProject,
           cwd: currentProject,
         },
-        timeoutMs: 800,
+        timeoutMs: 3000,
       });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (lastHealthOk) {
void callAgentMemory("session/start", {
body: {
sessionId,
project: currentProject,
cwd: currentProject,
},
timeoutMs: 800,
});
if (lastHealthOk) {
await callAgentMemory("session/start", {
body: {
sessionId,
project: currentProject,
cwd: currentProject,
},
timeoutMs: 3000,
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/pi/index.ts` around lines 311 - 319, The background call to
callAgentMemory("session/start") (with sessionId/currentProject) can race with
before_agent_start/smart-search and cause first-turn recall to miss newly-loaded
profiles; change this to await the call (or otherwise wait for its completion)
instead of firing it void with an 800ms timeout so session/context
initialization finishes before before_agent_start runs — update the
callAgentMemory invocation near the lastHealthOk branch to await the promise
(and remove or extend the timeout) or gate before_agent_start on the
callAgentMemory promise so the server has finished loading profiles/context
before first-turn recall executes.

Comment thread integrations/pi/index.ts
Comment on lines +332 to 354
// Observe the prompt so the server knows what was asked.
trackPost(callAgentMemory("observe", {
body: {
hookType: "prompt_submit",
sessionId,
project: currentProject,
cwd: currentProject,
timestamp: new Date().toISOString(),
data: { prompt: lastPrompt },
},
timeoutMs: 3000,
}));

const result = await callAgentMemory<{ results?: SmartSearchResult[] }>("smart-search", {
body: { query: lastPrompt, limit: 5 },
timeoutMs: 3000,
});
const results = result?.results || [];
const recallBlock = results.length
? [
"Relevant long-term memory from agentmemory:",
formatSearchResults(results),
].join("\n")
? ["Relevant long-term memory from agentmemory:", formatSearchResults(results)].join("\n")
: "";

await refreshStatus(ctx);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh health before the blocking recall path.

before_agent_start starts observe and then awaits smart-search before it checks health. When agentmemory is down, every turn pays the search timeout first and only marks the backend unhealthy afterward, which turns an outage into repeated prompt-start latency.

Suggested fix
   pi.on("before_agent_start", async (event, ctx) => {
     currentProject = event.systemPromptOptions.cwd || process.cwd();
     lastPrompt = event.prompt?.trim() || "";
     if (!lastPrompt) return;
+
+    await refreshStatus(ctx);
+    if (!lastHealthOk) {
+      return {
+        systemPrompt: [event.systemPrompt, TOOL_GUIDANCE].filter(Boolean).join("\n\n"),
+      };
+    }
 
     // Observe the prompt so the server knows what was asked.
     trackPost(callAgentMemory("observe", {
@@
     const results = result?.results || [];
     const recallBlock = results.length
       ? ["Relevant long-term memory from agentmemory:", formatSearchResults(results)].join("\n")
       : "";
 
-    await refreshStatus(ctx);
     return {
       systemPrompt: [event.systemPrompt, TOOL_GUIDANCE, recallBlock].filter(Boolean).join("\n\n"),
     };
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/pi/index.ts` around lines 332 - 354, The health check
(refreshStatus(ctx)) must run before blocking agentmemory calls so an outage
doesn't cause repeated turn latency; in before_agent_start call
refreshStatus(ctx) prior to invoking trackPost/callAgentMemory("observe") and
before awaiting callAgentMemory("smart-search"), and if refreshStatus indicates
the backend is unhealthy, skip or short-circuit the smart-search/recall logic
(i.e., avoid awaiting callAgentMemory and set recallBlock = ""), keeping the
rest of the flow (lastPrompt, results/recallBlock usage) unchanged.

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.

1 participant