fix: retain snapshots for removed workspaces
This commit is contained in:
@@ -72,6 +72,15 @@ export function sessionRoutes(
|
||||
return typeof runner.withPrincipal === "function" ? runner.withPrincipal(principal) : runner;
|
||||
};
|
||||
|
||||
/** Include retained historical descriptors so removed workspaces remain resumable. */
|
||||
const sessionRevisions = async () => {
|
||||
const registry = d.workspaceRegistry as Partial<WorkspaceRegistry>;
|
||||
if (typeof registry.listRetainedSnapshots === "function") {
|
||||
return await registry.listRetainedSnapshots();
|
||||
}
|
||||
return await d.workspaceRegistry.list();
|
||||
};
|
||||
|
||||
const isNotFound = (error: unknown) =>
|
||||
/not found|non trovata|inesistente|404/i.test(error instanceof Error ? error.message : String(error));
|
||||
|
||||
@@ -107,7 +116,7 @@ export function sessionRoutes(
|
||||
};
|
||||
let revisions: Awaited<ReturnType<typeof d.workspaceRegistry.list>>;
|
||||
try {
|
||||
revisions = await d.workspaceRegistry.list();
|
||||
revisions = await sessionRevisions();
|
||||
} catch (registryError) {
|
||||
// Sessions created before revision pinning still live under the installation's legacy
|
||||
// default config. Keep that compatibility path available when a fresh installation has
|
||||
@@ -394,11 +403,15 @@ export function sessionRoutes(
|
||||
// Admin RLS is deliberately disabled for a normal 'mine' listing.
|
||||
const scopedPrincipal = scope === "mine" ? { ...principal, isAdmin: false } : principal;
|
||||
const runner = runnerFor(scopedPrincipal);
|
||||
const revisions = await d.workspaceRegistry.list();
|
||||
const revisions = await sessionRevisions();
|
||||
const lists = await Promise.all(revisions
|
||||
.filter((revision) => revision.state === "operational")
|
||||
.map((revision) => runner.sessionList(revision.snapshotPath) as Promise<SessionRow[]>));
|
||||
const list = lists.flat();
|
||||
const sessions = new Map<string, SessionRow>();
|
||||
for (const row of lists.flat()) {
|
||||
if (!sessions.has(row.id)) sessions.set(row.id, row);
|
||||
}
|
||||
const list = [...sessions.values()];
|
||||
// Only an administrator-visible complete list (or the single local principal) is safe
|
||||
// input for retention. A remote per-user view can never discard another principal's pin.
|
||||
const reconcileSnapshotRetention = (d.workspaceRegistry as Partial<WorkspaceRegistry>).reconcileSnapshotRetention;
|
||||
|
||||
@@ -141,6 +141,31 @@ export class WorkspaceRegistry {
|
||||
return (await this.activeState()).revisions;
|
||||
}
|
||||
|
||||
/**
|
||||
* List every intact retained snapshot, current snapshots first. Session discovery and
|
||||
* retention use this rather than only the active revision so removing a workspace from
|
||||
* Git cannot strand a resumable session that still pins one of its older descriptors.
|
||||
*/
|
||||
async listRetainedSnapshots(): Promise<WorkspaceRevision[]> {
|
||||
await this.repository.ensureLayout();
|
||||
return await this.lock.run(async () => {
|
||||
try {
|
||||
const active = await this.activeState();
|
||||
const revisions = [...active.revisions];
|
||||
const entries = await readdir(this.repository.snapshotsPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.isSymbolicLink() || !/^[0-9a-f]{40}$/.test(entry.name)) continue;
|
||||
if (entry.name === active.head) continue;
|
||||
const state = await this.snapshotState(entry.name);
|
||||
revisions.push(...state.revisions.filter((revision) => revision.state === "operational"));
|
||||
}
|
||||
return revisions;
|
||||
} catch (error) {
|
||||
throw workspaceError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async read(id: string): Promise<{ workspace: WorkspaceDescriptor; revision: WorkspaceRevision }> {
|
||||
const state = await this.activeState();
|
||||
const revision = state.revisions.find((candidate) => candidate.id === id);
|
||||
@@ -550,6 +575,19 @@ export class WorkspaceRegistry {
|
||||
return JSON.parse(await readFile(path, "utf8"));
|
||||
}
|
||||
|
||||
private async snapshotState(head: string): Promise<ActiveState> {
|
||||
const manifest = await this.readSnapshotManifest(safeCommit(head));
|
||||
if (this.isLegacySnapshotManifest(manifest)) {
|
||||
const state = await this.deriveStateFromLegacyRevisions(manifest);
|
||||
await this.migrateLegacySnapshotManifest(state, manifest);
|
||||
return state;
|
||||
}
|
||||
const state = manifest as ActiveState;
|
||||
this.assertActiveState(state);
|
||||
await this.assertSnapshotIntegrity(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
private async assertSnapshotIntegrity(state: ActiveState): Promise<void> {
|
||||
const directory = join(this.repository.snapshotsPath, state.head);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user