Skip to content

Commit 97e324a

Browse files
committed
fix(pi): refresh-primers used a nonexistent Pi session API; share one resolver
The Pi primer raw provider probed SessionManager.listSessions + loadEntriesFromFile, neither of which exists in pi-coding-agent's public API (listing is SessionManager.listAll; entries come from readFileSync + parseSessionEntries). The identical drift was fixed for the retrospective provider in #201 but this second copy kept the dead lookup: the factory converts the throw into a null provider, so scheduled refresh-primers silently ran closed-book on Pi, and dream-run telemetry recorded 'Pi session APIs unavailable' for runs on hosts whose long-running Pi process predated #201. Extract the API lookup into one shared resolver (pi-session-api.ts) consumed by both providers, so a future pi-coding-agent drift breaks exactly one place. Add default-path tests against the actually installed package — the providers' dependency-injected unit tests could never catch a broken default lookup, which is exactly how this survived #201.
1 parent 8844f19 commit 97e324a

4 files changed

Lines changed: 98 additions & 68 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/// <reference types="bun-types" />
2+
3+
import { describe, expect, it } from "bun:test";
4+
import { loadDefaultPiSessionApi } from "./pi-session-api";
5+
6+
/**
7+
* These tests exercise the DEFAULT resolution path against the actually
8+
* installed pi-coding-agent package. The Pi session-listing API drifted once
9+
* (`SessionManager.listSessions` never existed publicly) and the providers'
10+
* dependency-injected unit tests could not catch it — every test supplied its
11+
* own listSessions stub, so the broken default lookup only failed at runtime
12+
* inside the dreamer. This is the missing coverage: if pi-coding-agent renames
13+
* or removes the session APIs again, this fails in CI instead of silently
14+
* degrading retrospective/refresh-primers.
15+
*/
16+
describe("loadDefaultPiSessionApi", () => {
17+
it("resolves the session APIs from the installed pi-coding-agent", async () => {
18+
const api = await loadDefaultPiSessionApi();
19+
expect(typeof api.listSessions).toBe("function");
20+
expect(typeof api.loadEntriesFromFile).toBe("function");
21+
});
22+
23+
it("parses JSONL session entries through the resolved loader", async () => {
24+
const api = await loadDefaultPiSessionApi();
25+
const { mkdtempSync, writeFileSync } = await import("node:fs");
26+
const { tmpdir } = await import("node:os");
27+
const { join } = await import("node:path");
28+
29+
const dir = mkdtempSync(join(tmpdir(), "pi-session-api-test-"));
30+
const file = join(dir, "session.jsonl");
31+
const entry = {
32+
type: "message",
33+
id: "e1",
34+
message: {
35+
role: "user",
36+
timestamp: 123,
37+
content: [{ type: "text", text: "hello" }],
38+
},
39+
};
40+
writeFileSync(file, `${JSON.stringify(entry)}\n`);
41+
42+
const entries = await api.loadEntriesFromFile(file);
43+
expect(Array.isArray(entries)).toBe(true);
44+
expect(entries.length).toBeGreaterThan(0);
45+
});
46+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { readFileSync } from "node:fs";
2+
3+
/**
4+
* Shared resolver for pi-coding-agent's session APIs, used by every Pi dreamer
5+
* provider that reads historical JSONL sessions (retrospective, refresh-primers).
6+
*
7+
* ONE resolver on purpose: the session-listing API drifted once already
8+
* (`SessionManager.listSessions` never existed publicly; listing is
9+
* `SessionManager.listAll`, and `loadEntriesFromFile` is not exported — entries
10+
* come from `readFileSync` + `parseSessionEntries`). When each provider carried
11+
* its own copy of this lookup, one copy got fixed and the other kept probing the
12+
* nonexistent API, so its feature silently degraded. Any future Pi API drift
13+
* should break exactly one resolver and one test.
14+
*/
15+
export interface PiSessionApi {
16+
listSessions: (sessionDir?: string) => unknown[] | Promise<unknown[]>;
17+
loadEntriesFromFile: (filePath: string) => unknown[] | Promise<unknown[]>;
18+
}
19+
20+
const PI_CODING_AGENT_MODULE = "@earendil-works/pi-coding-agent";
21+
22+
export async function loadDefaultPiSessionApi(): Promise<PiSessionApi> {
23+
const mod = (await import(/* @vite-ignore */ PI_CODING_AGENT_MODULE)) as {
24+
SessionManager?: {
25+
listAll?: (sessionDir?: string) => unknown[] | Promise<unknown[]>;
26+
};
27+
loadEntriesFromFile?: (filePath: string) => unknown[] | Promise<unknown[]>;
28+
parseSessionEntries?: (content: string) => unknown[];
29+
};
30+
const listSessions = mod.SessionManager?.listAll;
31+
if (typeof listSessions !== "function") {
32+
throw new Error(
33+
"Pi session APIs unavailable: expected SessionManager.listAll on pi-coding-agent",
34+
);
35+
}
36+
// loadEntriesFromFile is NOT part of pi-coding-agent's public API — fall back
37+
// to readFileSync + parseSessionEntries (both exported).
38+
const loadEntriesFromFile: PiSessionApi["loadEntriesFromFile"] =
39+
mod.loadEntriesFromFile ??
40+
((filePath: string) => {
41+
const content = readFileSync(filePath, "utf8");
42+
return mod.parseSessionEntries?.(content) ?? [];
43+
});
44+
return {
45+
listSessions: listSessions.bind(mod.SessionManager),
46+
loadEntriesFromFile,
47+
};
48+
}

packages/pi-plugin/src/dreamer/primer-raw-provider-pi.ts

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RawMessageProvider } from "@magic-context/core/hooks/magic-context/read-session-chunk";
22
import type { RawMessage } from "@magic-context/core/hooks/magic-context/read-session-raw";
33
import { convertEntriesToRawMessages } from "../read-session-pi";
4+
import { loadDefaultPiSessionApi } from "./pi-session-api";
45

56
/**
67
* Pi `primerRawProviderFactory`: resolve a historical session id to a
@@ -18,8 +19,6 @@ export interface PiPrimerRawProviderDeps {
1819
sessionDir?: string;
1920
}
2021

21-
const PI_CODING_AGENT_MODULE = "@earendil-works/pi-coding-agent";
22-
2322
interface PiSessionInfoLike {
2423
id?: unknown;
2524
path?: unknown;
@@ -41,7 +40,7 @@ export function createPiPrimerRawProviderFactory(
4140
loadEntriesFromFile: deps.loadEntriesFromFile,
4241
};
4342
}
44-
resolved ??= loadDefaultPiSessionDeps();
43+
resolved ??= loadDefaultPiSessionApi();
4544
return resolved;
4645
};
4746

@@ -75,30 +74,3 @@ export function createPiPrimerRawProviderFactory(
7574
}
7675
};
7776
}
78-
79-
async function loadDefaultPiSessionDeps(): Promise<
80-
Required<
81-
Pick<PiPrimerRawProviderDeps, "listSessions" | "loadEntriesFromFile">
82-
>
83-
> {
84-
const mod = (await import(/* @vite-ignore */ PI_CODING_AGENT_MODULE)) as {
85-
SessionManager?: {
86-
listSessions?: (sessionDir?: string) => unknown[] | Promise<unknown[]>;
87-
};
88-
loadEntriesFromFile?: (filePath: string) => unknown[] | Promise<unknown[]>;
89-
};
90-
const listSessions = mod.SessionManager?.listSessions;
91-
const loadEntriesFromFile = mod.loadEntriesFromFile;
92-
if (
93-
typeof listSessions !== "function" ||
94-
typeof loadEntriesFromFile !== "function"
95-
) {
96-
throw new Error(
97-
"Pi session APIs unavailable: expected SessionManager.listSessions and loadEntriesFromFile",
98-
);
99-
}
100-
return {
101-
listSessions: listSessions.bind(mod.SessionManager),
102-
loadEntriesFromFile,
103-
};
104-
}

packages/pi-plugin/src/dreamer/retrospective-raw-provider-pi.ts

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { readFileSync } from "node:fs";
21
import { resolve } from "node:path";
32

43
import type {
@@ -7,6 +6,7 @@ import type {
76
RetrospectiveRawProvider,
87
RetrospectiveSinceRead,
98
} from "@magic-context/core/features/magic-context/dreamer/retrospective-raw-provider";
9+
import { loadDefaultPiSessionApi } from "./pi-session-api";
1010

1111
interface PiSessionInfoLike {
1212
id?: unknown;
@@ -34,8 +34,6 @@ export interface PiRetrospectiveRawProviderDeps {
3434
loadEntriesFromFile?: (filePath: string) => unknown[] | Promise<unknown[]>;
3535
}
3636

37-
const PI_CODING_AGENT_MODULE = "@earendil-works/pi-coding-agent";
38-
3937
export class PiRetrospectiveRawProvider implements RetrospectiveRawProvider {
4038
private readonly sessionPathById = new Map<string, string>();
4139
private resolvedDefaultDeps: Promise<
@@ -144,7 +142,7 @@ export class PiRetrospectiveRawProvider implements RetrospectiveRawProvider {
144142
loadEntriesFromFile: this.deps.loadEntriesFromFile,
145143
};
146144
}
147-
this.resolvedDefaultDeps ??= loadDefaultPiSessionDeps();
145+
this.resolvedDefaultDeps ??= loadDefaultPiSessionApi();
148146
return this.resolvedDefaultDeps;
149147
}
150148
}
@@ -184,37 +182,3 @@ function extractPiTextContent(content: unknown): string {
184182
})
185183
.join("\n");
186184
}
187-
188-
async function loadDefaultPiSessionDeps(): Promise<
189-
Required<
190-
Pick<PiRetrospectiveRawProviderDeps, "listSessions" | "loadEntriesFromFile">
191-
>
192-
> {
193-
const mod = (await import(/* @vite-ignore */ PI_CODING_AGENT_MODULE)) as {
194-
SessionManager?: {
195-
listAll?: (sessionDir?: string) => unknown[] | Promise<unknown[]>;
196-
};
197-
loadEntriesFromFile?: (filePath: string) => unknown[] | Promise<unknown[]>;
198-
parseSessionEntries?: (content: string) => unknown[];
199-
};
200-
const listSessions = mod.SessionManager?.listAll;
201-
if (typeof listSessions !== "function") {
202-
throw new Error(
203-
"Pi session APIs unavailable: expected SessionManager.listAll on pi-coding-agent",
204-
);
205-
}
206-
// loadEntriesFromFile is NOT part of pi-coding-agent's public API —
207-
// fall back to readFileSync + parseSessionEntries (both exported).
208-
const loadEntriesFromFile: (
209-
filePath: string,
210-
) => unknown[] | Promise<unknown[]> =
211-
mod.loadEntriesFromFile ??
212-
((filePath: string) => {
213-
const content = readFileSync(filePath, "utf8");
214-
return mod.parseSessionEntries?.(content) ?? [];
215-
});
216-
return {
217-
listSessions: listSessions.bind(mod.SessionManager),
218-
loadEntriesFromFile,
219-
};
220-
}

0 commit comments

Comments
 (0)