Skip to content

Commit 2f8e5e8

Browse files
fix(pi): add session summary project fallback
1 parent 743f2d0 commit 2f8e5e8

9 files changed

Lines changed: 112 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This project follows [Conventional Commits](https://www.conventionalcommits.org/
99
Full release notes with changelogs per version live on the **[GitHub Releases page](https://github.com/Gentleman-Programming/engram/releases)**.
1010

1111
GoReleaser generates them automatically from commits, filtering by type:
12+
1213
- `feat:` / `fix:` / `refactor:` / `chore:` commits appear in the release notes
1314
- `docs:` / `test:` / `ci:` commits are excluded from the generated changelog
1415

@@ -22,6 +23,8 @@ Breaking changes are always marked with a `type:breaking-change` label and docum
2223

2324
### Pi package (`pi-engram`)
2425

26+
- **fix(plugin):** allow `mem_session_summary` to accept an explicit `project` fallback when automatic project detection is unavailable.
27+
- **fix(plugin):** fall back to local `.engram/config.json` and surface a clearer version-mismatch diagnostic when the running Engram server lacks `/project/current`.
2528
- **feat(plugin):** add `gentle-engram` package for Pi marketplace installs, with HTTP event capture, Memory Protocol prompt injection, safe `engram mcp` launcher config, and `pi-engram init` setup helper.
2629

2730
### Cloud dashboard visual parity (`cloud-dashboard-visual-parity`)
@@ -63,6 +66,7 @@ The `project` argument has been removed from the JSON schemas of 7 MCP write too
6366
**After:** the project is auto-detected from the server's working directory (cwd). Any `project` argument sent by the LLM is silently discarded.
6467

6568
**Migration:**
69+
6670
- Remove `project` from write tool calls in your agent's memory protocol.
6771
- Use `mem_current_project` (new tool) to inspect which project Engram will use before writing.
6872
- If the cwd is ambiguous (multiple git repos), Engram returns a structured error with `available_projects`. Navigate to one of the repos before writing.

docs/AGENT-SETUP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ Install Engram's Pi package, the MCP adapter, and Pi MCP config:
3535
engram setup pi
3636
```
3737

38-
`engram setup pi` runs `pi install npm:gentle-engram@0.1.5` and `pi install npm:pi-mcp-adapter`, then ensures Pi settings contain both packages and writes `mcpServers.engram` in the Pi agent MCP config when no Engram server is already configured. Existing `mcpServers.engram` entries are preserved.
38+
`engram setup pi` runs `pi install npm:gentle-engram@0.1.7` and `pi install npm:pi-mcp-adapter`, then ensures Pi settings contain both packages and writes `mcpServers.engram` in the Pi agent MCP config when no Engram server is already configured. Existing `mcpServers.engram` entries are preserved.
3939

4040
Manual equivalent:
4141

4242
```bash
43-
pi install npm:gentle-engram@0.1.5
43+
pi install npm:gentle-engram@0.1.7
4444
pi install npm:pi-mcp-adapter
4545
pi-engram init
4646
```

internal/setup/setup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const claudeCodeMarketplace = "Gentleman-Programming/engram"
7777

7878
const openCodeSubagentStatuslinePlugin = "opencode-subagent-statusline"
7979

80-
const piGentleEngramPackage = "npm:gentle-engram@0.1.5"
80+
const piGentleEngramPackage = "npm:gentle-engram@0.1.7"
8181
const piMCPAdapterPackage = "npm:pi-mcp-adapter"
8282

8383
// claudeCodeMCPTools are the MCP tool permission names for the agent profile

internal/setup/setup_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ func TestInstallPiInstallsPackagesAndWritesConfig(t *testing.T) {
315315
if result.Agent != "pi" || result.Destination != agentDir || result.Files != 2 {
316316
t.Fatalf("unexpected install result: %#v", result)
317317
}
318-
wantCommands := []string{"pi install npm:gentle-engram@0.1.5", "pi install npm:pi-mcp-adapter"}
318+
wantCommands := []string{"pi install npm:gentle-engram@0.1.7", "pi install npm:pi-mcp-adapter"}
319319
if !reflect.DeepEqual(commands, wantCommands) {
320320
t.Fatalf("unexpected pi install commands: got %#v want %#v", commands, wantCommands)
321321
}
@@ -330,7 +330,7 @@ func TestInstallPiInstallsPackagesAndWritesConfig(t *testing.T) {
330330
if err := json.Unmarshal(settingsRaw, &settings); err != nil {
331331
t.Fatalf("parse settings: %v", err)
332332
}
333-
for _, pkg := range []string{"npm:gentle-engram@0.1.5", "npm:pi-mcp-adapter"} {
333+
for _, pkg := range []string{"npm:gentle-engram@0.1.7", "npm:pi-mcp-adapter"} {
334334
if !slices.Contains(settings.Packages, pkg) {
335335
t.Fatalf("expected settings packages to include %q, got %#v", pkg, settings.Packages)
336336
}
@@ -398,7 +398,7 @@ func TestInstallPiPreservesExistingEngramMCPServer(t *testing.T) {
398398
if err != nil {
399399
t.Fatalf("read settings after install: %v", err)
400400
}
401-
if !strings.Contains(string(settingsRaw), "npm:existing") || !strings.Contains(string(settingsRaw), "npm:gentle-engram@0.1.5") || !strings.Contains(string(settingsRaw), "npm:pi-mcp-adapter") {
401+
if !strings.Contains(string(settingsRaw), "npm:existing") || !strings.Contains(string(settingsRaw), "npm:gentle-engram@0.1.7") || !strings.Contains(string(settingsRaw), "npm:pi-mcp-adapter") {
402402
t.Fatalf("expected settings packages to be preserved and extended, got %s", settingsRaw)
403403
}
404404
}
@@ -409,7 +409,7 @@ func TestInstallPiCommandFailure(t *testing.T) {
409409
return []byte("boom"), errors.New("exit 1")
410410
}
411411
_, err := Install("pi")
412-
if err == nil || !strings.Contains(err.Error(), "install npm:gentle-engram@0.1.5") {
412+
if err == nil || !strings.Contains(err.Error(), "install npm:gentle-engram@0.1.7") {
413413
t.Fatalf("expected pi install error, got %v", err)
414414
}
415415
}

plugin/pi/README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Engram includes a terminal UI for browsing sessions, observations, prompts, proj
7878
## Quick start
7979

8080
```bash
81-
pi install npm:gentle-engram@0.1.5
81+
pi install npm:gentle-engram@0.1.7
8282
pi install npm:pi-mcp-adapter
8383
pi-engram init
8484
```
@@ -189,7 +189,7 @@ If the binary is missing, Pi keeps running and memory degrades instead of crashi
189189

190190
`pi-engram init` writes Pi-owned config in the Pi agent directory:
191191

192-
- `settings.json`: ensures `npm:pi-mcp-adapter` and `npm:gentle-engram@0.1.5` are declared.
192+
- `settings.json`: ensures `npm:pi-mcp-adapter` and `npm:gentle-engram@0.1.7` are declared.
193193
- `mcp.json`: adds an `engram` MCP server that launches `engram mcp --tools=agent` through a safe Node wrapper with `directTools: false`, so MCP remains available through the gateway without duplicating Pi-native `mem_*` tools.
194194

195195
Existing `mcpServers.engram` entries are preserved unless you pass `--force`:
@@ -210,7 +210,7 @@ The HTTP event-capture path mirrors Engram's normal project detection order as c
210210
4. single child git repo name
211211
5. current directory basename
212212

213-
MCP tool calls still use Engram core's canonical project resolver at call time. For critical repos or monorepos, prefer an explicit `.engram/config.json`:
213+
MCP tool calls still use Engram core's canonical project resolver at call time. Pi-native tool calls ask the Engram HTTP server for `/project/current`; if that route is missing on an older running server, the adapter falls back to the nearest local `.engram/config.json` and returns a version-mismatch warning. For critical repos or monorepos, prefer an explicit `.engram/config.json`:
214214

215215
```json
216216
{
@@ -220,12 +220,14 @@ MCP tool calls still use Engram core's canonical project resolver at call time.
220220

221221
## Troubleshooting
222222

223-
| Symptom | Fix |
224-
| ----------------------------------------- | ---------------------------------------------------------------- |
225-
| `mem_*` tools are missing | Install `pi-mcp-adapter`, run `pi-engram init`, then restart Pi. |
226-
| Pi cannot find `engram` | Set `ENGRAM_BIN=/absolute/path/to/engram`. |
227-
| Session capture should use another server | Set `ENGRAM_URL=http://host:7437`. |
228-
| Existing MCP config was not replaced | Run `pi-engram init --force`. |
223+
| Symptom | Fix |
224+
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
225+
| `mem_*` tools are missing | Install `pi-mcp-adapter`, run `pi-engram init`, then restart Pi. |
226+
| Pi cannot find `engram` | Set `ENGRAM_BIN=/absolute/path/to/engram`. |
227+
| Session capture should use another server | Set `ENGRAM_URL=http://host:7437`. |
228+
| Existing MCP config was not replaced | Run `pi-engram init --force`. |
229+
| `mem_current_project` reports `/project/current` unsupported | Restart or upgrade the running `engram serve`; check `ENGRAM_URL`/`ENGRAM_BIN`. If `.engram/config.json` exists, Pi uses it as a temporary fallback. |
230+
| `mem_session_summary` cannot detect a project | Ask the user which project should receive the summary, then retry `mem_session_summary` with `project: "name"`. |
229231

230232
## Next steps
231233

plugin/pi/cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
33
import { homedir } from "node:os";
44
import { dirname, join } from "node:path";
55

6-
const PACKAGE_NAME = "npm:gentle-engram@0.1.5";
6+
const PACKAGE_NAME = "npm:gentle-engram@0.1.7";
77
const MCP_ADAPTER_PACKAGE = "npm:pi-mcp-adapter";
88
const HELP = `pi-engram
99
@@ -12,7 +12,7 @@ Usage:
1212
1313
Creates Pi's Engram MCP config in the Pi agent dir and ensures pi-mcp-adapter
1414
is declared in settings.json. The Pi extension itself is loaded by installing
15-
the package with: pi install npm:gentle-engram@0.1.5
15+
the package with: pi install npm:gentle-engram@0.1.7
1616
`;
1717

1818
const MCP_LAUNCHER =

plugin/pi/index.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import { spawn, type ChildProcess } from "node:child_process";
10-
import { existsSync } from "node:fs";
11-
import { basename, resolve } from "node:path";
10+
import { existsSync, readFileSync } from "node:fs";
11+
import { basename, dirname, resolve } from "node:path";
1212
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1313
import { Text } from "@earendil-works/pi-tui";
1414
import { Type } from "typebox";
@@ -78,6 +78,8 @@ call \`mem_search\`, then \`mem_get_observation\` for full content.
7878
7979
Before ending a session or saying "done", call \`mem_session_summary\`
8080
with Goal, Instructions, Discoveries, Accomplished, Next Steps, and Relevant Files.
81+
If \`mem_session_summary\` fails because Engram cannot detect a project, ask the user
82+
which project should receive the summary, then retry with \`project: "<name>"\`.
8183
8284
### AFTER COMPACTION
8385
@@ -116,6 +118,9 @@ interface MigrationBody {
116118

117119
interface CurrentProjectResponse {
118120
project?: string;
121+
project_source?: string;
122+
project_path?: string;
123+
cwd?: string;
119124
available_projects?: string[] | null;
120125
warning?: string;
121126
error_hint?: string;
@@ -186,6 +191,46 @@ async function bestEffortEngramFetch<TResponse = unknown>(path: string, opts: Fe
186191
}
187192
}
188193

194+
function detectLocalConfigProject(cwd: string): CurrentProjectResponse | undefined {
195+
let current = resolve(cwd || ".");
196+
while (true) {
197+
const configPath = `${current}/.engram/config.json`;
198+
if (existsSync(configPath)) {
199+
try {
200+
const parsed = JSON.parse(readFileSync(configPath, "utf8")) as { project_name?: unknown };
201+
const projectName = typeof parsed.project_name === "string" ? parsed.project_name.trim() : "";
202+
if (projectName) {
203+
return {
204+
project: projectName,
205+
project_source: "config",
206+
project_path: current,
207+
cwd,
208+
warning: `Engram server at ${ENGRAM_URL} does not support /project/current; using ${configPath}. Upgrade or restart Engram for canonical project detection.`,
209+
};
210+
}
211+
return {
212+
cwd,
213+
error_hint: `${configPath} exists but project_name is missing or empty. Fix the config or pass project explicitly.`,
214+
};
215+
} catch (error) {
216+
const message = error instanceof Error ? error.message : String(error);
217+
return { cwd, error_hint: `Could not read ${configPath}: ${message}` };
218+
}
219+
}
220+
221+
const parent = dirname(current);
222+
if (parent === current) return undefined;
223+
current = parent;
224+
}
225+
}
226+
227+
function projectCurrentUnsupportedError(cwd: string): CurrentProjectResponse {
228+
return {
229+
cwd,
230+
error_hint: `Engram server at ${ENGRAM_URL} does not support /project/current. Upgrade or restart the running Engram server, verify ENGRAM_URL/ENGRAM_BIN, or pass project explicitly to project-capable memory tools.`,
231+
};
232+
}
233+
189234
async function ensureSessionBestEffort(sessionId: string, sessionProject = project): Promise<void> {
190235
try {
191236
await ensureSession(sessionId, sessionProject);
@@ -273,8 +318,14 @@ async function ensureSession(sessionId: string, sessionProject = project): Promi
273318

274319
async function detectServerProject(cwd: string): Promise<CurrentProjectResponse | undefined> {
275320
for (let attempt = 0; attempt < 5; attempt += 1) {
276-
const detected = await bestEffortEngramFetch<CurrentProjectResponse>(`/project/current${queryString({ cwd })}`);
277-
if (detected) return detected;
321+
try {
322+
const detected = await engramFetch<CurrentProjectResponse>(`/project/current${queryString({ cwd })}`);
323+
if (detected) return detected;
324+
} catch (error) {
325+
if (error instanceof EngramHttpError && error.status === 404) {
326+
return detectLocalConfigProject(cwd) || projectCurrentUnsupportedError(cwd);
327+
}
328+
}
278329
if (attempt < 4) await wait(200);
279330
}
280331
return undefined;
@@ -394,6 +445,7 @@ const MEMORY_TOOL_SCHEMAS: Record<string, ReturnType<typeof Type.Object>> = {
394445
mem_session_summary: Type.Object({
395446
content: Type.String({ description: "Full session summary" }),
396447
session_id: optionalString("Session ID"),
448+
project: optionalString("Optional project to use when automatic detection is unavailable"),
397449
}),
398450
mem_context: Type.Object({
399451
project: optionalString("Filter by project"),
@@ -534,16 +586,16 @@ async function callMemoryTool(toolName: string, params: Record<string, unknown>,
534586
body: { session_id: activeSessionId, content: params.content, project: activeProject },
535587
});
536588
case "mem_session_summary":
537-
requireResolvedProject();
538-
await ensureSession(activeSessionId);
589+
if (!requestedProject) requireResolvedProject();
590+
await ensureSession(activeSessionId, activeProject);
539591
return engramFetch("/observations", {
540592
method: "POST",
541593
body: {
542594
session_id: activeSessionId,
543595
type: "session_summary",
544596
title: "Session summary",
545597
content: params.content,
546-
project,
598+
project: activeProject,
547599
scope: "project",
548600
},
549601
});
@@ -558,8 +610,17 @@ async function callMemoryTool(toolName: string, params: Record<string, unknown>,
558610
method: "POST",
559611
body: { summary: params.summary || "" },
560612
});
561-
case "mem_current_project":
562-
return engramFetch(`/project/current${queryString({ cwd: params.cwd || ctx.cwd })}`);
613+
case "mem_current_project": {
614+
const cwd = String(params.cwd || ctx.cwd);
615+
try {
616+
return await engramFetch(`/project/current${queryString({ cwd })}`);
617+
} catch (error) {
618+
if (error instanceof EngramHttpError && error.status === 404) {
619+
return detectLocalConfigProject(cwd) || projectCurrentUnsupportedError(cwd);
620+
}
621+
throw error;
622+
}
623+
}
563624
case "mem_doctor":
564625
return engramFetch(`/doctor${queryString({ project: params.project, check: params.check, cwd: params.project ? undefined : ctx.cwd })}`);
565626
case "mem_capture_passive":

plugin/pi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gentle-engram",
3-
"version": "0.1.5",
3+
"version": "0.1.7",
44
"description": "Persistent memory for Pi agents — one local-or-cloud brain shared across sessions, compactions, and MCP agents",
55
"type": "module",
66
"license": "MIT",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import assert from "node:assert/strict";
2+
import { readFileSync } from "node:fs";
3+
import { test } from "node:test";
4+
5+
const source = readFileSync(new URL("../index.ts", import.meta.url), "utf8");
6+
7+
test("mem_session_summary accepts explicit project fallback", () => {
8+
assert.match(source, /mem_session_summary: Type\.Object\(\{[\s\S]*project: optionalString\("Optional project to use when automatic detection is unavailable"\)/);
9+
assert.match(source, /case "mem_session_summary":[\s\S]*if \(!requestedProject\) requireResolvedProject\(\);[\s\S]*ensureSession\(activeSessionId, activeProject\)[\s\S]*project: activeProject/);
10+
});
11+
12+
test("project detection 404 falls back to local config or diagnostic", () => {
13+
assert.match(source, /function detectLocalConfigProject\(cwd: string\)/);
14+
assert.match(source, /project_name/);
15+
assert.match(source, /error\.status === 404[\s\S]*detectLocalConfigProject\(cwd\) \|\| projectCurrentUnsupportedError\(cwd\)/);
16+
assert.match(source, /does not support \/project\/current/);
17+
});

0 commit comments

Comments
 (0)