Add session archiving

This commit is contained in:
Federico Jaramillo Martinez
2026-05-07 21:06:11 +02:00
parent b5b1814383
commit 402d5b17aa
11 changed files with 272 additions and 25 deletions
+37 -11
View File
@@ -13,6 +13,7 @@ import type { ClientCommand, ClientCommandResult, ClientMessagePage, ClientSessi
import type { SessionEventHub } from "../realtime/sessionEventHub.js";
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
import { SessionCommandService } from "./sessionCommandService.js";
import { SessionArchiveStore } from "./sessionArchiveStore.js";
import type { ActiveSession } from "./sessionRuntimeStore.js";
function noop(): void {
@@ -24,6 +25,7 @@ export class PiSessionService {
private readonly activities = new Map<string, { phase: "active" | "idle" | "error"; label: string; detail?: string; at: string }>();
private readonly heartbeat: NodeJS.Timeout;
private readonly commandService: SessionCommandService;
private readonly archiveStore = new SessionArchiveStore();
private readonly agentDir = getAgentDir();
private readonly authStorage = AuthStorage.create();
private readonly modelRegistry = ModelRegistry.create(this.authStorage);
@@ -46,17 +48,22 @@ export class PiSessionService {
}
async list(cwd: string): Promise<ClientSession[]> {
const sessions = await SessionManager.list(cwd);
return sessions.map((s) => ({
id: s.id,
path: s.path,
cwd: s.cwd,
...(s.name === undefined ? {} : { name: s.name }),
created: s.created.toISOString(),
modified: s.modified.toISOString(),
messageCount: s.messageCount,
firstMessage: s.firstMessage,
}));
const [sessions, archivedRecords] = await Promise.all([SessionManager.list(cwd), this.archiveStore.list()]);
const archivedById = new Map(archivedRecords.filter((record) => record.cwd === cwd).map((record) => [record.sessionId, record]));
return sessions.map((s) => {
const archived = archivedById.get(s.id);
return {
id: s.id,
path: s.path,
cwd: s.cwd,
...(s.name === undefined ? {} : { name: s.name }),
created: s.created.toISOString(),
modified: s.modified.toISOString(),
messageCount: s.messageCount,
firstMessage: s.firstMessage,
...(archived === undefined ? {} : { archived: true, archivedAt: archived.archivedAt }),
};
});
}
async start(cwd: string): Promise<ClientSession> {
@@ -104,6 +111,7 @@ export class PiSessionService {
}
async prompt(sessionId: string, text: string, streamingBehavior?: "steer" | "followUp"): Promise<void> {
await this.assertWritable(sessionId);
const session = await this.getOrOpen(sessionId);
const behavior = session.isStreaming || session.isCompacting ? streamingBehavior ?? "followUp" : undefined;
this.publishActivity(session, session.isCompacting ? "message queued during compaction" : behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
@@ -115,6 +123,7 @@ export class PiSessionService {
}
async shell(sessionId: string, text: string): Promise<void> {
await this.assertWritable(sessionId);
const active = await this.getActive(sessionId);
const { session } = active.runtime;
const isExcluded = text.startsWith("!!");
@@ -149,13 +158,26 @@ export class PiSessionService {
}
async runCommand(sessionId: string, text: string): Promise<ClientCommandResult> {
await this.assertWritable(sessionId);
return this.commandService.run(sessionId, text);
}
async respondToCommand(sessionId: string, requestId: string, value: string): Promise<ClientCommandResult> {
await this.assertWritable(sessionId);
return this.commandService.respond(sessionId, requestId, value);
}
async archive(sessionId: string): Promise<void> {
const session = await this.getOrOpen(sessionId);
if (session.isStreaming || session.isCompacting || session.isBashRunning || session.pendingMessageCount > 0) throw new Error("Stop current session activity before archiving");
await this.archiveStore.archive(sessionId, session.sessionManager.getCwd());
this.stop(sessionId);
}
async restore(sessionId: string): Promise<void> {
await this.archiveStore.restore(sessionId);
}
async abort(sessionId: string): Promise<void> {
const active = this.active.get(sessionId);
if (active) await active.runtime.session.abort();
@@ -170,6 +192,10 @@ export class PiSessionService {
this.activities.delete(sessionId);
}
private async assertWritable(sessionId: string): Promise<void> {
if (await this.archiveStore.isArchived(sessionId)) throw new Error("Archived sessions are read-only. Restore the session to continue.");
}
private async getOrOpen(sessionId: string): Promise<AgentSession> {
return (await this.getActive(sessionId)).runtime.session;
}