From 56c1c1714e77cb9e935f132cf87c903678b46978 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Thu, 25 Jun 2026 11:08:26 +0200 Subject: [PATCH] fix: treat missing subsession files as unavailable --- src/server/sessions/piSessionService.test.ts | 24 +++++--------- src/server/sessions/piSessionService.ts | 33 +++++--------------- src/server/sessions/spawnSubsessionTool.ts | 2 +- 3 files changed, 15 insertions(+), 44 deletions(-) diff --git a/src/server/sessions/piSessionService.test.ts b/src/server/sessions/piSessionService.test.ts index 381e971..4c54fde 100644 --- a/src/server/sessions/piSessionService.test.ts +++ b/src/server/sessions/piSessionService.test.ts @@ -1032,7 +1032,7 @@ describe("PiSessionService", () => { } }); - it("hydrates persisted links to archived children without scanning unrelated child headers", async () => { + it("does not hydrate persisted links when the exact child file is unavailable", async () => { const parentFile = "/sessions/parent-1.jsonl"; const parent = fakeRuntime("parent-1", { sessionFile: parentFile, @@ -1043,24 +1043,17 @@ describe("PiSessionService", () => { const service = new PiSessionService(new CapturingSessionEventHub(), { createAgentRuntime: runtimeCreator(parent.runtime), sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() }, - archiveStore: { - ...emptyArchiveStore(), - list: () => Promise.resolve([]), - get: (sessionId) => Promise.resolve(sessionId === "child-1" ? { sessionId: "child-1", cwd: "/workspace-feature", archivedAt: "2026-01-01T00:00:00.000Z", parentSessionPath: parentFile } : undefined), - isArchived: (sessionId) => Promise.resolve(sessionId === "child-1"), - }, + archiveStore: emptyArchiveStore(), heartbeatIntervalMs: 60_000, }); await service.start("/workspace"); - await expect(service.listSubsessions("parent-1")).resolves.toEqual([ - { sessionId: "child-1", cwd: "/workspace-feature", status: "archived" }, - ]); + await expect(service.listSubsessions("parent-1")).resolves.toEqual([]); await service.dispose(); }); - it("does not hydrate parent links without a child file or exact archived child validation", async () => { + it("does not hydrate parent links without a child file", async () => { const parentFile = "/sessions/parent-1.jsonl"; const parent = fakeRuntime("parent-1", { sessionFile: parentFile, @@ -1071,10 +1064,7 @@ describe("PiSessionService", () => { const service = new PiSessionService(new CapturingSessionEventHub(), { createAgentRuntime: runtimeCreator(parent.runtime), sessionManager: { create: () => parent.session.sessionManager, list: () => Promise.resolve([]), listAll: () => Promise.resolve([]), open: () => fakeSessionManager() }, - archiveStore: { - ...emptyArchiveStore(), - get: (sessionId) => Promise.resolve(sessionId === "child" ? { sessionId: "child-fork", cwd: "/workspace-feature", archivedAt: "2026-01-01T00:00:00.000Z", parentSessionPath: parentFile } : undefined), - }, + archiveStore: emptyArchiveStore(), heartbeatIntervalMs: 60_000, }); @@ -1633,7 +1623,7 @@ describe("PiSessionService", () => { await service.dispose(); }); - it("reports an archived child's status in the subsession list", async () => { + it("reports a missing tracked child file as unknown in the subsession list", async () => { const { service } = subsessionService({ allowed: true, cwd: "/workspace-feature" }); await service.start("/workspace"); await service.spawnSubsession({ spawningCwd: "/workspace", parentSessionId: "parent-1", parentSessionFile: "/tmp/parent-1.jsonl", prompt: "go", cwd: "/workspace-feature" }); @@ -1641,7 +1631,7 @@ describe("PiSessionService", () => { await service.archive("child-1"); await expect(service.listSubsessions("parent-1")).resolves.toEqual([ - { sessionId: "child-1", cwd: "/workspace-feature", status: "archived" }, + { sessionId: "child-1", cwd: "/workspace-feature", status: "unknown" }, ]); await service.dispose(); }); diff --git a/src/server/sessions/piSessionService.ts b/src/server/sessions/piSessionService.ts index 552f9b6..35047b9 100644 --- a/src/server/sessions/piSessionService.ts +++ b/src/server/sessions/piSessionService.ts @@ -503,7 +503,7 @@ export class PiSessionService { return { sessionId, cwd: session.sessionManager.getCwd(), - status: await this.subsessionStatus(session), + status: this.subsessionStatus(session), finalText: finalAssistantText(messages), messageCount: messages.length, }; @@ -516,7 +516,7 @@ export class PiSessionService { return { sessionId, cwd: session.sessionManager.getCwd(), - status: await this.subsessionStatus(session), + status: this.subsessionStatus(session), ...view, }; } @@ -645,7 +645,7 @@ export class PiSessionService { private async registerPersistedSubsessionLinks(parentSessionId: string, parentManager: PiSessionManager, parentSessionFile: string | undefined): Promise { // Parent custom links are the authoritative recovery record: verify the - // exact live child file/header or an exact archived child before tracking. + // exact live child file/header before tracking. const entries = parentManager.getEntries?.() ?? parentManager.getBranch(); for (const entry of entries) { const link = parsePersistedParentSubsessionLink(entry); @@ -664,16 +664,8 @@ export class PiSessionService { } private async parentLinkHasValidChildTarget(parentSessionFile: string, link: PersistedParentSubsessionLink): Promise { - if (link.spawnedSessionFile !== undefined && (await sessionFileHeaderMatches(link.spawnedSessionFile, { sessionId: link.spawnedSessionId, parentSessionFile }))) return true; - return this.archivedSubsessionLinkMatchesParent(parentSessionFile, link); - } - - private async archivedSubsessionLinkMatchesParent(parentSessionFile: string, link: PersistedParentSubsessionLink): Promise { - const archived = await this.getArchivedExact(link.spawnedSessionId); - if (archived?.parentSessionPath === undefined) return false; - if (!sessionPathsEqual(archived.parentSessionPath, parentSessionFile)) return false; - if (archived.originalPath !== undefined && link.spawnedSessionFile !== undefined && !sessionPathsEqual(archived.originalPath, link.spawnedSessionFile)) return false; - return true; + return link.spawnedSessionFile !== undefined + && await sessionFileHeaderMatches(link.spawnedSessionFile, { sessionId: link.spawnedSessionId, parentSessionFile }); } private async recoverSubsessionTrackingForOpenedSession(session: PiAgentSession): Promise { @@ -738,9 +730,6 @@ export class PiSessionService { const active = this.activeChildForSubsessionLink(link); if (active !== undefined) return active.runtime.session; - const archived = await this.getArchivedExact(sessionId); - if (archived?.archivePath !== undefined) return (await this.create(this.sessionManager.open(archived.archivePath), archived.cwd)).runtime.session; - if (link.childSessionFile !== undefined) { if (!(await sessionFileHeaderMatches(link.childSessionFile, { sessionId, parentSessionFile: link.parentSessionFile }))) throw new Error("Session not found"); const sessionManager = this.sessionManager.open(link.childSessionFile); @@ -754,10 +743,8 @@ export class PiSessionService { const link = this.subsessionLinks.get(childSessionId); const active = link === undefined ? undefined : this.activeChildForSubsessionLink(link); if (active !== undefined) { - return { cwd: active.runtime.cwd, status: await this.subsessionStatus(active.runtime.session) }; + return { cwd: active.runtime.cwd, status: this.subsessionStatus(active.runtime.session) }; } - const archived = await this.getArchivedExact(childSessionId); - if (archived !== undefined) return { cwd: archived.cwd, status: "archived" }; if (link?.childSessionFile !== undefined && (await sessionFileHeaderMatches(link.childSessionFile, { sessionId: childSessionId, parentSessionFile: link.parentSessionFile }))) { return { cwd: link.cwd ?? "", status: "idle" }; } @@ -765,8 +752,7 @@ export class PiSessionService { return { cwd: "", status: "unknown" }; } - private async subsessionStatus(session: PiAgentSession): Promise { - if (await this.getArchivedExact(session.sessionId) !== undefined) return "archived"; + private subsessionStatus(session: PiAgentSession): SubsessionStatus { if (this.hasActiveWork(session)) return "working"; if (this.activities.get(session.sessionId)?.phase === "error") return "error"; return "idle"; @@ -1238,11 +1224,6 @@ export class PiSessionService { return archived; } - private async getArchivedExact(sessionId: string): Promise { - const archived = await this.archiveStore.get(sessionId); - return archived?.sessionId === sessionId ? archived : undefined; - } - private activeForLookup(ref: PiSessionLookup): ActiveSession | undefined { const sessionId = sessionIdFromLookup(ref); const exact = this.active.get(sessionId); diff --git a/src/server/sessions/spawnSubsessionTool.ts b/src/server/sessions/spawnSubsessionTool.ts index 5a47665..9297396 100644 --- a/src/server/sessions/spawnSubsessionTool.ts +++ b/src/server/sessions/spawnSubsessionTool.ts @@ -3,7 +3,7 @@ import { defineTool } from "@earendil-works/pi-coding-agent"; import type { TranscriptContentKind, TranscriptEntry, TranscriptRole, TranscriptView } from "./subsessionTranscript.js"; /** Lifecycle phase of a tracked subsession as seen by its parent. */ -export type SubsessionStatus = "working" | "idle" | "error" | "archived" | "unknown"; +export type SubsessionStatus = "working" | "idle" | "error" | "unknown"; export interface SpawnSubsessionResult { sessionId: string;