feat(sessions): persist shared unread state

This commit is contained in:
Federico Jaramillo Martinez
2026-07-20 19:36:16 +02:00
parent a20a8c8c09
commit 115d74e79a
37 changed files with 4272 additions and 66 deletions
+24 -6
View File
@@ -12,6 +12,7 @@ import { PiSessionService } from "./sessions/piSessionService.js";
import { createPiSessionManagerGateway } from "./sessions/piSessionManagerGateway.js";
import { registerSessionRoutes } from "./sessions/sessionRoutes.js";
import { SessionNotificationStore } from "./sessions/sessionNotificationStore.js";
import { FileSessionUnreadPersistence, SessionUnreadStore } from "./sessions/sessionUnreadStore.js";
import { ProjectScopedSpawnTargetResolver } from "./sessions/spawnTargetResolver.js";
import { ProjectService } from "./projects/projectService.js";
import { ProjectStore } from "./storage/projectStore.js";
@@ -40,6 +41,13 @@ await runSessionDaemonStartup({
async createRuntime() {
const eventHub = new SessionEventHub();
const notificationStore = new SessionNotificationStore();
const unreadStore = new SessionUnreadStore({
persistence: new FileSessionUnreadPersistence(),
onPersistenceError(operation, error) {
app.log.error({ err: error, operation }, "session unread persistence failed");
},
});
await unreadStore.load();
const workspaceActivity = new WorkspaceActivityService(eventHub);
const auth = await AuthService.create({ agentDir: activeAgentProfile.dir, logger: app.log });
const spawnTargets = config.spawnSessions
@@ -53,6 +61,7 @@ await runSessionDaemonStartup({
...(spawnTargets === undefined ? {} : { spawnTargets }),
subsessionsEnabled: spawnTargets !== undefined && config.subsessions,
notificationStore,
unreadStore,
sessionManager: createPiSessionManagerGateway({
agentDir: activeAgentProfile.dir,
env: daemonEnvironment,
@@ -65,7 +74,7 @@ await runSessionDaemonStartup({
...getPiWebRuntimeComponent("sessiond", SESSIOND_RUNTIME_CAPABILITIES),
activeAgentProfile,
});
return { eventHub, workspaceActivity, auth, sessions, terminals, activeAgentProfile, runtimeComponent };
return { eventHub, workspaceActivity, auth, sessions, terminals, unreadStore, activeAgentProfile, runtimeComponent };
},
registerRoutes({ eventHub, workspaceActivity, auth, sessions, terminals, runtimeComponent }) {
registerWorkspaceActivityRoutes(app, workspaceActivity);
@@ -88,16 +97,25 @@ await runSessionDaemonStartup({
app.get("/runtime", () => runtimeComponent);
},
async listen({ auth, sessions, terminals }) {
async listen({ auth, sessions, terminals, unreadStore }) {
let shuttingDown = false;
async function shutdown(signal: NodeJS.Signals): Promise<void> {
if (shuttingDown) return;
shuttingDown = true;
app.log.info({ signal }, "shutting down session daemon");
terminals.dispose();
auth.dispose();
await sessions.dispose();
await app.close();
const attempt = async (operation: string, run: () => void | Promise<void>): Promise<void> => {
try {
await run();
} catch (error: unknown) {
process.exitCode = 1;
app.log.error({ err: error, operation }, "session daemon shutdown operation failed");
}
};
await attempt("dispose terminals", () => { terminals.dispose(); });
await attempt("dispose auth", () => { auth.dispose(); });
await attempt("dispose sessions", () => sessions.dispose());
await attempt("flush session unread state", () => unreadStore.flush());
await attempt("close server", () => app.close());
}
process.once("SIGINT", (signal) => { void shutdown(signal); });