Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 33 additions & 18 deletions src/discovery/adapters/pi.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ import {

/**
* Pi writes standalone sessions under
* `~/.pi/agent/sessions/<escaped-cwd>/<ISO-ts>_<uuid>.jsonl`. BB's Pi bridge writes the
* same JSONL shape directly under `<bb-data-dir>/pi-bridge-sessions/`.
* `~/.pi/agent/sessions/<escaped-cwd>/<ISO-ts>_<uuid>.jsonl`. omp (Oh My Pi) uses the
* same JSONL shape under `~/.omp/agent/sessions/` and honors `PI_CODING_AGENT_DIR`, but
* prepends a fixed-width `{type:"title"}` record, so the `{type:"session", cwd, id}`
* entry is line 2 there. omp also writes subagent transcripts one level deeper, at
* `<escaped-cwd>/<session-id>/<Name>.jsonl`. BB's Pi bridge writes the same JSONL shape
* directly under `<bb-data-dir>/pi-bridge-sessions/`.
*
* Line 1 is `{type:"session", cwd, id}`. Entries form a parent/child tree but arrive in
* Entries form a parent/child tree but arrive in
* order, so a linear read is faithful. `model_change` / `thinking_level_change` records
* give the model actually used. No remote is recorded - dead worktrees reach tier 3 only.
* give the model actually used (`modelId` on pi, `model` on omp). No remote is
* recorded - dead worktrees reach tier 3 only.
*/

export const name = "pi";
Expand Down Expand Up @@ -49,6 +54,7 @@ function realpathOrResolve(value) {
function storeSpecs() {
const specs = [
{ path: storeRoot(), direct: false, nested: true },
{ path: home(".omp", "agent", "sessions"), direct: false, nested: true },
{ path: home(".bb", "pi-bridge-sessions"), direct: true, nested: false },
];
const piAgentDir = expandEnvPath(process.env.PI_CODING_AGENT_DIR);
Expand Down Expand Up @@ -84,7 +90,13 @@ export function enumerate() {
for (const spec of storeSpecs()) {
const files = [
...(spec.direct ? listFiles(spec.path, ".jsonl") : []),
...(spec.nested ? listDirs(spec.path).flatMap((dir) => listFiles(dir, ".jsonl")) : []),
...(spec.nested
? listDirs(spec.path).flatMap((dir) => [
...listFiles(dir, ".jsonl"),
// omp nests subagent transcripts one level below the session files.
...listDirs(dir).flatMap((sub) => listFiles(sub, ".jsonl")),
])
: []),
];
for (const file of files) {
const key = realpathOrResolve(file);
Expand All @@ -99,18 +111,21 @@ export function enumerate() {
}

export function classify(candidate) {
const [first] = readHeadLines(candidate.path, 1);
const entry = first && parseJsonLine(first);
if (!entry || entry.type !== "session" || !entry.cwd) return null;
return {
id: entry.id || path.basename(candidate.path, ".jsonl"),
cwd: entry.cwd,
gitBranch: null,
remotes: [],
startedAt: entry.timestamp ? Date.parse(entry.timestamp) : candidate.mtimeMs,
model: null,
interactionSignals: emptyInteractionSignals(),
};
// omp prepends a fixed-width title record, so the session entry is not always line 1.
for (const line of readHeadLines(candidate.path, 8)) {
const entry = parseJsonLine(line);
if (!entry || entry.type !== "session" || !entry.cwd) continue;
return {
id: entry.id || path.basename(candidate.path, ".jsonl"),
cwd: entry.cwd,
gitBranch: null,
remotes: [],
startedAt: entry.timestamp ? Date.parse(entry.timestamp) : candidate.mtimeMs,
model: null,
interactionSignals: emptyInteractionSignals(),
};
}
return null;
}

export function read(ref) {
Expand All @@ -120,7 +135,7 @@ export function read(ref) {

for (const entry of entries) {
if (entry.type === "model_change") {
model = entry.modelId || model;
model = entry.modelId || entry.model || model;
continue;
}
if (entry.type !== "message" || !entry.message) continue;
Expand Down
34 changes: 33 additions & 1 deletion test/adapters.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,28 @@ test("pi adapter reads the session header and drops thinking blocks", () => {
assert.equal(toolCall.result, "nothing to commit");
});

test("pi adapter classifies omp sessions past the title record and reads model", () => {
const file = path.join(FIXTURES, "omp-session.jsonl");
const descriptor = pi.classify(candidateFor(file));
assert.equal(descriptor.id, "omp-5678");
assert.equal(descriptor.cwd, "/repo/demo");

const { events, model } = pi.read({ path: file });
assert.equal(model, "cursor/composer-2.5", "omp model_change carries model, not modelId");
const [toolCall] = tools(events);
assert.equal(toolCall.name, "bash");
assert.equal(toolCall.result, "nothing to commit");
});

function writeOmpSession(file, { id, cwd }) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(
file,
`${JSON.stringify({ type: "title", v: 1, title: "", updatedAt: "2026-08-27T00:00:00.000Z", pad: " " })}\n` +
`${JSON.stringify({ type: "session", version: 3, id, timestamp: "2026-08-27T00:00:00.000Z", cwd })}\n`,
);
}

function writePiSession(file, { id, cwd }) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(
Expand Down Expand Up @@ -195,6 +217,14 @@ test("pi adapter enumerates standalone and BB-managed session roots without dupl
id: "standalone",
cwd: "/repo/demo",
});
writeOmpSession(path.join(fakeHome, ".omp", "agent", "sessions", "-repo-demo", "omp-standalone.jsonl"), {
id: "omp-standalone",
cwd: "/repo/demo",
});
writeOmpSession(path.join(fakeHome, ".omp", "agent", "sessions", "-repo-demo", "omp-standalone", "Subagent.jsonl"), {
id: "omp-subagent",
cwd: "/repo/demo",
});
writePiSession(path.join(piAgentDir, "sessions", "-repo-demo", "custom-agent.jsonl"), {
id: "custom-agent",
cwd: "/repo/demo",
Expand Down Expand Up @@ -228,8 +258,10 @@ test("pi adapter enumerates standalone and BB-managed session roots without dupl
"custom-session.jsonl",
"default-bb.jsonl",
"direct-override.jsonl",
"omp-standalone.jsonl",
"standalone.jsonl",
],
"Subagent.jsonl",
].sort(),
);
});

Expand Down
6 changes: 6 additions & 0 deletions test/fixtures/omp-session.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{"type":"title","v":1,"title":"","updatedAt":"2026-08-03T08:00:00.000Z","pad":" "}
{"type":"session","version":3,"id":"omp-5678","timestamp":"2026-08-03T08:00:00.000Z","cwd":"/repo/demo"}
{"type":"model_change","id":"m1","parentId":null,"timestamp":"2026-08-03T08:00:00.100Z","model":"cursor/composer-2.5","resolvedModelIsFallback":false}
{"type":"message","id":"e1","parentId":"m1","timestamp":"2026-08-03T08:00:01.000Z","message":{"role":"user","content":[{"type":"text","text":"Add the changelog entry."}]}}
{"type":"message","id":"e2","parentId":"e1","timestamp":"2026-08-03T08:00:02.000Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"internal reasoning that must be dropped"},{"type":"text","text":"Editing CHANGELOG.md."},{"type":"toolCall","id":"tc1","name":"bash","arguments":{"command":"git status"}}]}}
{"type":"message","id":"e3","parentId":"e2","timestamp":"2026-08-03T08:00:03.000Z","message":{"role":"toolResult","toolCallId":"tc1","content":[{"type":"text","text":"nothing to commit"}]}}
Loading