From ec0ca131200724967ed1b09b4286822c304dc834 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Sun, 12 Jul 2026 22:52:59 +0200 Subject: [PATCH] fix: store session archives under PI_WEB_DATA_DIR --- .changeset/honor-archive-data-dir.md | 5 +++ docs/config.md | 13 +++++- .../sessions/sessionArchiveStore.test.ts | 44 +++++++++++++++++-- src/server/sessions/sessionArchiveStore.ts | 8 +++- 4 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 .changeset/honor-archive-data-dir.md diff --git a/.changeset/honor-archive-data-dir.md b/.changeset/honor-archive-data-dir.md new file mode 100644 index 0000000..705e87b --- /dev/null +++ b/.changeset/honor-archive-data-dir.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Store session archive metadata and archived session files under `PI_WEB_DATA_DIR` when configured. diff --git a/docs/config.md b/docs/config.md index 1d8dfee..b06bb9b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -109,7 +109,7 @@ Rows with JSON key `—` are runtime-only environment variables, not config-file | Project config version | `version` | — | Project | Project-local only; must be `1` when present | Next project-config read | | **Runtime-only environment variables** | | | | | | | Global config file path | — | `PI_WEB_CONFIG` (`XDG_CONFIG_HOME` affects the default path) | Process/env | Selects the global config file; not a project config | Restart services/processes after changing env | -| Managed data directory | — | `PI_WEB_DATA_DIR` | Process/env | Not supported locally | Restart services before changing; moves managed state location | +| Managed data directory | — | `PI_WEB_DATA_DIR` | Process/env | Not supported locally | Restart web/API and session daemon before changing; relocates managed state, including session archives | | Session daemon socket | — | `PI_WEB_SESSIOND_SOCKET` | Web/API + session daemon env | Not supported locally | Restart daemon and web/API; both must match | | Session daemon TCP port | — | `PI_WEB_SESSIOND_PORT` | Session daemon env | Not supported locally | Restart session daemon; set `PI_WEB_SESSIOND_URL` for web/API too | | Session daemon TCP host | — | `PI_WEB_SESSIOND_HOST` | Session daemon env | Not supported locally | Restart session daemon | @@ -122,6 +122,17 @@ Rows with JSON key `—` are runtime-only environment variables, not config-file ## Key details +### Managed data directory + +`PI_WEB_DATA_DIR` selects the machine-global root for PI WEB-managed state. It defaults to `~/.pi-web`. Session archival uses this layout: + +- `$PI_WEB_DATA_DIR/archived-sessions.json` for the archive index. +- `$PI_WEB_DATA_DIR/archived-sessions/` for archived session files. + +The session daemon owns both archive locations. Restart the session daemon after setting or changing `PI_WEB_DATA_DIR`; restart the web/API process as well so its other managed-state stores use the same root. + +PI WEB does not automatically migrate existing managed state. Archives created by earlier releases while a custom `PI_WEB_DATA_DIR` was already set may remain under `~/.pi-web` and will not appear automatically after upgrading. Stop the session daemon and back up both locations before reconciling them manually. The archive index stores absolute `archivePath` values, so update those values if archived files are moved to a different path. + ### External path access `pathAccess.allowedPaths` grants PI WEB's file explorer and absolute `@` path completions access to specific filesystem roots outside the current workspace. diff --git a/src/server/sessions/sessionArchiveStore.test.ts b/src/server/sessions/sessionArchiveStore.test.ts index c11f579..83bdc8c 100644 --- a/src/server/sessions/sessionArchiveStore.test.ts +++ b/src/server/sessions/sessionArchiveStore.test.ts @@ -1,17 +1,55 @@ import { constants } from "node:fs"; import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; -import { tmpdir } from "node:os"; -import { afterEach, describe, expect, it } from "vitest"; -import { SessionArchiveStore } from "./sessionArchiveStore.js"; +import { homedir, tmpdir } from "node:os"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { defaultSessionArchiveFilePath, SessionArchiveStore } from "./sessionArchiveStore.js"; const tempRoots: string[] = []; +describe("defaultSessionArchiveFilePath", () => { + it("uses PI_WEB_DATA_DIR when configured", () => { + expect(defaultSessionArchiveFilePath({ PI_WEB_DATA_DIR: "managed-state" }, "/tmp/pi-web")).toBe(resolve("/tmp/pi-web", "managed-state", "archived-sessions.json")); + }); + + it("preserves the ~/.pi-web default when PI_WEB_DATA_DIR is unset", () => { + expect(defaultSessionArchiveFilePath({}, "/tmp/pi-web")).toBe(join(homedir(), ".pi-web", "archived-sessions.json")); + }); +}); + describe("SessionArchiveStore", () => { afterEach(async () => { + vi.unstubAllEnvs(); await Promise.all(tempRoots.splice(0).map((path) => rm(path, { recursive: true, force: true }))); }); + it("stores its default index and archived files under PI_WEB_DATA_DIR", async () => { + const root = await mkdtemp(join(tmpdir(), "pi-web-archive-data-dir-")); + tempRoots.push(root); + const dataDir = join(root, "managed-state"); + const activeDir = join(root, "active"); + await mkdir(activeDir, { recursive: true }); + const sourcePath = join(activeDir, "2026-01-01_managed.jsonl"); + await writeFile(sourcePath, "session contents\n", "utf8"); + vi.stubEnv("PI_WEB_DATA_DIR", dataDir); + + const store = new SessionArchiveStore(); + const record = await store.archive({ + sessionId: "managed", + cwd: "/workspace", + path: sourcePath, + created: "2026-01-01T00:00:00.000Z", + modified: "2026-01-01T00:01:00.000Z", + messageCount: 1, + firstMessage: "hello", + }); + + const archivePath = join(dataDir, "archived-sessions", "2026-01-01_managed.jsonl"); + expect(record.archivePath).toBe(archivePath); + await expect(readFile(archivePath, "utf8")).resolves.toBe("session contents\n"); + await expect(readFile(join(dataDir, "archived-sessions.json"), "utf8")).resolves.toContain('"sessionId": "managed"'); + }); + it("moves archived session files out of the active session directory and restores them", async () => { const root = await mkdtemp(join(tmpdir(), "pi-web-archive-")); tempRoots.push(root); diff --git a/src/server/sessions/sessionArchiveStore.ts b/src/server/sessions/sessionArchiveStore.ts index cefbb0f..53085cd 100644 --- a/src/server/sessions/sessionArchiveStore.ts +++ b/src/server/sessions/sessionArchiveStore.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import { constants } from "node:fs"; import { access, copyFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; import { basename, dirname, join } from "node:path"; -import { homedir } from "node:os"; +import { piWebDataDir } from "../../config.js"; import { canonicalizeStoredCwd } from "../workingDirectory.js"; export interface ArchiveSessionInput { @@ -35,11 +35,15 @@ interface ArchiveFile { sessions: ArchivedSessionRecord[]; } +export function defaultSessionArchiveFilePath(env: NodeJS.ProcessEnv = process.env, cwd = process.cwd()): string { + return join(piWebDataDir(env, cwd), "archived-sessions.json"); +} + export class SessionArchiveStore { private operationQueue: Promise = Promise.resolve(); constructor( - private readonly filePath = join(homedir(), ".pi-web", "archived-sessions.json"), + private readonly filePath = defaultSessionArchiveFilePath(), private readonly archiveDir = join(dirname(filePath), "archived-sessions"), ) {}